前面兩篇文章所使用的范例都是傳輸字符串,有的時(shí)候我們可能會(huì)想在服務(wù)端和客戶端之間傳遞文件。比如,考慮這樣一種情況,假如客戶端顯示了一個(gè)菜單,當(dāng)我們輸入S1、S2或S3(S為Send縮寫(xiě))時(shí),分別向服務(wù)端發(fā)送文件Client01.jpg、Client02.jpg、Client03.jpg;當(dāng)我們輸入R1、R2或R3時(shí)(R為Receive縮寫(xiě)),則分別從服務(wù)端接收文件Server01.jpg、Server02.jpg、Server03.jpg。那么,我們?cè)撊绾瓮瓿蛇@件事呢?此時(shí)可能有這樣兩種做法:
類似于FTP協(xié)議,服務(wù)端開(kāi)辟兩個(gè)端口,并持續(xù)對(duì)這兩個(gè)端口偵聽(tīng):一個(gè)用于接收字符串,類似于FTP的控制端口,它接收各種命令(接收或發(fā)送文件);一個(gè)用于傳輸數(shù)據(jù),也就是發(fā)送和接收文件。
服務(wù)端只開(kāi)辟一個(gè)端口,用于接收字符串,我們稱之為控制端口。當(dāng)接到請(qǐng)求之后,根據(jù)請(qǐng)求內(nèi)容在客戶端開(kāi)辟一個(gè)端口專用于文件傳輸,并在傳輸結(jié)束后關(guān)閉端口。
現(xiàn)在我們只關(guān)注于上面的數(shù)據(jù)端口,回憶一下在第二篇中我們所總結(jié)的,可以得出:當(dāng)我們使用上面的方法一時(shí),服務(wù)端的數(shù)據(jù)端口可以為多個(gè)客戶端的多次請(qǐng)求服務(wù);當(dāng)我們使用方法二時(shí),服務(wù)端只為一個(gè)客戶端的一次請(qǐng)求服務(wù),但是因?yàn)槊看握?qǐng)求都會(huì)重新開(kāi)辟端口,所以實(shí)際上還是相當(dāng)于可以為多個(gè)客戶端的多次請(qǐng)求服務(wù)。同時(shí),因?yàn)樗粸橐淮握?qǐng)求服務(wù),所以我們?cè)跀?shù)據(jù)端口上傳輸文件時(shí)無(wú)需采用異步傳輸方式。但在控制端口我們?nèi)匀恍枰褂卯惒椒绞健?/p>
從上面看出,第一種方式要好得多,但是我們將采用第二種方式。至于原因,你可以回顧一下Part.1(基本概念和操作)中關(guān)于聊天程序模式的講述,因?yàn)榻酉聛?lái)一篇文章我們將創(chuàng)建一個(gè)聊天程序,而這個(gè)聊天程序采用第三種模式,所以本文的練習(xí)實(shí)際是對(duì)下一篇的一個(gè)鋪墊。
1.訂立協(xié)議
1.1發(fā)送文件
我們先看一下發(fā)送文件的情況,如果我們想將文件client01.jpg由客戶端發(fā)往客戶端,那么流程是什么:
客戶端開(kāi)辟數(shù)據(jù)端口用于偵聽(tīng),并獲取端口號(hào),假設(shè)為8005。
假設(shè)客戶端輸入了S1,則發(fā)送下面的控制字符串到服務(wù)端:[file=Client01.jpg, mode=send, port=8005]。
服務(wù)端收到以后,根據(jù)客戶端ip和端口號(hào)與該客戶端建立連接。
客戶端偵聽(tīng)到服務(wù)端的連接,開(kāi)始發(fā)送文件。
傳送完畢后客戶端、服務(wù)端分別關(guān)閉連接。
此時(shí),我們訂立的發(fā)送文件協(xié)議為:[file=Client01.jpg, mode=send, port=8005]。但是,由于它是一個(gè)普通的字符串,在上一篇中,我們采用了正則表達(dá)式來(lái)獲取其中的有效值,但這顯然不是一種好辦法。因此,在本文及下一篇文章中,我們采用一種新的方式來(lái)編寫(xiě)協(xié)議:XML。對(duì)于上面的語(yǔ)句,我們可以寫(xiě)成這樣的XML:
<protocol><file name="client01.jpg" mode="send" port="8005" /></protocol>這樣我們?cè)诜?wù)端就會(huì)好處理得多,接下來(lái)我們來(lái)看一下接收文件的流程及其協(xié)議。
NOTE:這里說(shuō)發(fā)送、接收文件是站在客戶端的立場(chǎng)說(shuō)的,當(dāng)客戶端發(fā)送文件時(shí),對(duì)于服務(wù)器來(lái)收,則是接收文件。
1.2接收文件
接收文件與發(fā)送文件實(shí)際上完全類似,區(qū)別只是由客戶端向網(wǎng)絡(luò)流寫(xiě)入數(shù)據(jù),還是由服務(wù)端向網(wǎng)絡(luò)流寫(xiě)入數(shù)據(jù)。
客戶端開(kāi)辟數(shù)據(jù)端口用于偵聽(tīng),假設(shè)為8006。
假設(shè)客戶端輸入了R1,則發(fā)送控制字符串:<protocol><file name="Server01.jpg" mode="receive" port="8006" /></protocol>到服務(wù)端。
服務(wù)端收到以后,根據(jù)客戶端ip和端口號(hào)與該客戶端建立連接。
客戶端建立起與服務(wù)端的連接,服務(wù)端開(kāi)始網(wǎng)絡(luò)流中寫(xiě)入數(shù)據(jù)。
傳送完畢后服務(wù)端、客戶端分別關(guān)閉連接。
2.協(xié)議處理類的實(shí)現(xiàn)
和上面一章一樣,在開(kāi)始編寫(xiě)實(shí)際的服務(wù)端客戶端代碼之前,我們首先要編寫(xiě)處理協(xié)議的類,它需要提供這樣兩個(gè)功能:1、方便地幫我們獲取完整的協(xié)議信息,因?yàn)榍懊嫖覀冋f(shuō)過(guò),服務(wù)端可能將客戶端的多次獨(dú)立請(qǐng)求拆分或合并。比如,客戶端連續(xù)發(fā)送了兩條控制信息到服務(wù)端,而服務(wù)端將它們合并了,那么則需要先拆開(kāi)再分別處理。2、方便地獲取我們所想要的屬性信息,因?yàn)閰f(xié)議是XML格式,所以還需要一個(gè)類專門對(duì)XML進(jìn)行處理,獲得字符串的屬性值。
2.1 ProtocalHandler輔助類
我們先看下ProtocalHandler,它與上一篇中的RequestHandler作用相同。需要注意的是必須將它聲明為實(shí)例的,而非靜態(tài)的,這是因?yàn)槊總€(gè)TcpClient都需要對(duì)應(yīng)一個(gè)ProtocalHandler,因?yàn)樗鼉?nèi)部維護(hù)的patialProtocal不能共享,在協(xié)議發(fā)送不完整的情況下,這個(gè)變量用于臨時(shí)保存被截?cái)嗟淖址?/p>
public class ProtocolHandler {? private string partialProtocal; // 保存不完整的協(xié)議 public ProtocolHandler() {? ??????? partialProtocal = "";?????? ??? }? public string[] GetProtocol(string input) {? return GetProtocol(input, null);? ??? }? // 獲得協(xié)議 private string[] GetProtocol(string input, List<string> outputList) {? if (outputList == null)? ??????????? outputList = new List<string>();? if (String.IsNullOrEmpty(input))? return outputList.ToArray();? if (!String.IsNullOrEmpty(partialProtocal))? ??????????? input = partialProtocal + input;? string pattern = "(^<protocol>.*?</protocol>)";? // 如果有匹配,說(shuō)明已經(jīng)找到了,是完整的協(xié)議 if (Regex.IsMatch(input, pattern)) {? // 獲取匹配的值 string match = Regex.Match(input, pattern).Groups[0].Value;? ??????????? outputList.Add(match);? ??????????? partialProtocal = "";? // 縮短input的長(zhǎng)度 ??????????? input = input.Substring(match.Length);? // 遞歸調(diào)用 ??????????? GetProtocol(input, outputList);? ??????? } else {? // 如果不匹配,說(shuō)明協(xié)議的長(zhǎng)度不夠, // 那么先緩存,然后等待下一次請(qǐng)求 ??????????? partialProtocal = input;? ??????? }? return outputList.ToArray();? ??? }? }? 因?yàn)楝F(xiàn)在它已經(jīng)不是本文的重點(diǎn)了,所以我就不演示對(duì)于它的測(cè)試了,本文所附帶的代碼中含有它的測(cè)試代碼(我在ProtocolHandler中添加了一個(gè)靜態(tài)類Test())。
2.2 FileRequestType枚舉和FileProtocol結(jié)構(gòu)
因?yàn)閄ML是以字符串的形式在進(jìn)行傳輸,為了方便使用,我們最好構(gòu)建一個(gè)強(qiáng)類型來(lái)對(duì)它們進(jìn)行操作,這樣會(huì)方便很多。我們首先可以定義FileRequestMode枚舉,它代表是發(fā)送還是接收文件:
public enum FileRequestMode {? ??? Send = 0,? ??? Receive? }? 接下來(lái)我們?cè)俣x一個(gè)FileProtocol結(jié)構(gòu),用來(lái)為整個(gè)協(xié)議字符串提供強(qiáng)類型的訪問(wèn),注意這里覆蓋了基類的ToString()方法,這樣在客戶端我們就不需要再手工去編寫(xiě)XML,只要在結(jié)構(gòu)值上調(diào)用ToString()就OK了,會(huì)方便很多。
public struct FileProtocol {? private readonly FileRequestMode mode;? private readonly int port;? private readonly string fileName;? public FileProtocol? ??????? (FileRequestMode mode, int port, string fileName) {? this.mode = mode;? this.port = port;? this.fileName = fileName;? ??? }? public FileRequestMode Mode {? get { return mode; }? ??? }? public int Port {? get { return port; }? ??? }? public string FileName {? get { return fileName; }? ??? }? public override string ToString() {? return String.Format("<protocol><file name=\"{0}\" mode=\"{1}\" port=\"{2}\" /></protocol>", fileName, mode, port);? ??? }? }? 2.3 ProtocolHelper輔助類
這個(gè)類專用于將XML格式的協(xié)議映射為我們上面定義的強(qiáng)類型對(duì)象,這里我沒(méi)有加入try/catch異常處理,因?yàn)閰f(xié)議對(duì)用戶來(lái)說(shuō)是不可見(jiàn)的,而且客戶端應(yīng)該總是發(fā)送正確的協(xié)議,我覺(jué)得這樣可以讓代碼更加清晰:
public class ProtocolHelper {? private XmlNode fileNode;? private XmlNode root;? public ProtocolHelper(string protocol) {? ??????? XmlDocument doc = new XmlDocument();? ??????? doc.LoadXml(protocol);? ??????? root = doc.DocumentElement;? ??????? fileNode = root.SelectSingleNode("file");? ??? }? // 此時(shí)的protocal一定為單條完整protocal private FileRequestMode GetFileMode() {? string mode = fileNode.Attributes["mode"].Value;? ??????? mode = mode.ToLower();? if (mode == "send")? return FileRequestMode.Send;? else return FileRequestMode.Receive;? ??? }? // 獲取單條協(xié)議包含的信息 public FileProtocol GetProtocol() {? ??????? FileRequestMode mode = GetFileMode();? string fileName = "";? int port = 0;? ??????? fileName = fileNode.Attributes["name"].Value;? ??????? port = Convert.ToInt32(fileNode.Attributes["port"].Value);? return new FileProtocol(mode, port, fileName);? ??? }? }? OK,我們又耽誤了點(diǎn)時(shí)間,下面就讓我們進(jìn)入正題吧。
3.客戶端發(fā)送數(shù)據(jù)
3.1 服務(wù)端的實(shí)現(xiàn)
我們還是將一個(gè)問(wèn)題分成兩部分來(lái)處理,先是發(fā)送數(shù)據(jù),然后是接收數(shù)據(jù)。我們先看發(fā)送數(shù)據(jù)部分的服務(wù)端。如果你從第一篇文章看到了現(xiàn)在,那么我覺(jué)得更多的不是技術(shù)上的問(wèn)題而是思路,所以我們不再將重點(diǎn)放到代碼上,這些應(yīng)該很容易就看懂了。
class Server {? static void Main(string[] args) {? ??????? Console.WriteLine("Server is running ... ");? ??????? IPAddress ip = IPAddress.Parse("127.0.0.1");? ??????? TcpListener listener = new TcpListener(ip, 8500);? ??????? listener.Start();?????????? // 開(kāi)啟對(duì)控制端口 8500 的偵聽(tīng) ??????? Console.WriteLine("Start Listening ...");? while (true) {? // 獲取一個(gè)連接,同步方法,在此處中斷 ??????????? TcpClient client = listener.AcceptTcpClient();????????????? ??????????? RemoteClient wapper = new RemoteClient(client);? ??????????? wapper.BeginRead();? ??????? }? ??? }? }? public class RemoteClient {? private TcpClient client;? private NetworkStream streamToClient;? private const int BufferSize = 8192;? private byte[] buffer;? private ProtocolHandler handler;? public RemoteClient(TcpClient client) {? this.client = client;? // 打印連接到的客戶端信息 ??????? Console.WriteLine("\nClient Connected!{0} <-- {1}",? ??????????? client.Client.LocalEndPoint, client.Client.RemoteEndPoint);? // 獲得流 ??????? streamToClient = client.GetStream();? ??????? buffer = new byte[BufferSize];? ??????? handler = new ProtocolHandler();? ??? }? // 開(kāi)始進(jìn)行讀取 public void BeginRead() {?????? ??????? AsyncCallback callBack = new AsyncCallback(OnReadComplete);? ??????? streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);? ??? }? // 再讀取完成時(shí)進(jìn)行回調(diào) private void OnReadComplete(IAsyncResult ar) {? int bytesRead = 0;? try {? lock (streamToClient) {? ??????????????? bytesRead = streamToClient.EndRead(ar);? ??????????????? Console.WriteLine("Reading data, {0} bytes ...", bytesRead);? ??????????? }? if (bytesRead == 0) throw new Exception("讀取到0字節(jié)");? string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);? ??????????? Array.Clear(buffer,0,buffer.Length);??????? // 清空緩存,避免臟讀 // 獲取protocol數(shù)組 string[] protocolArray = handler.GetProtocol(msg);? foreach (string pro in protocolArray) {? // 這里異步調(diào)用,不然這里可能會(huì)比較耗時(shí) ??????????????? ParameterizedThreadStart start =? new ParameterizedThreadStart(handleProtocol);? ??????????????? start.BeginInvoke(pro, null, null);? ??????????? }? // 再次調(diào)用BeginRead(),完成時(shí)調(diào)用自身,形成無(wú)限循環(huán) lock (streamToClient) {? ??????????????? AsyncCallback callBack = new AsyncCallback(OnReadComplete);? ??????????????? streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);? ??????????? }? ??????? } catch(Exception ex) {? if(streamToClient!=null)? ??????????????? streamToClient.Dispose();? ??????????? client.Close();? ??????????? Console.WriteLine(ex.Message);????? // 捕獲異常時(shí)退出程序 ??????? }? ??? }? // 處理protocol private void handleProtocol(object obj) {? string pro = obj as string;? ??????? ProtocolHelper helper = new ProtocolHelper(pro);? ??????? FileProtocol protocol = helper.GetProtocol();? if (protocol.Mode == FileRequestMode.Send) {? // 客戶端發(fā)送文件,對(duì)服務(wù)端來(lái)說(shuō)則是接收文件 ??????????? receiveFile(protocol);? ??????? } else if (protocol.Mode == FileRequestMode.Receive) {? // 客戶端接收文件,對(duì)服務(wù)端來(lái)說(shuō)則是發(fā)送文件 // sendFile(protocol); ??????? }? ??? }? private void receiveFile(FileProtocol protocol) {? // 獲取遠(yuǎn)程客戶端的位置 ??????? IPEndPoint endpoint = client.Client.RemoteEndPoint as IPEndPoint;? ??????? IPAddress ip = endpoint.Address;? // 使用新端口號(hào),獲得遠(yuǎn)程用于接收文件的端口 ??????? endpoint = new IPEndPoint(ip, protocol.Port);? // 連接到遠(yuǎn)程客戶端 ??????? TcpClient localClient;? try {? ??????????? localClient = new TcpClient();? ??????????? localClient.Connect(endpoint);? ??????? } catch {? ??????????? Console.WriteLine("無(wú)法連接到客戶端 --> {0}", endpoint);? return;? ??????? }? // 獲取發(fā)送文件的流 ??????? NetworkStream streamToClient = localClient.GetStream();? // 隨機(jī)生成一個(gè)在當(dāng)前目錄下的文件名稱 string path =? ??????????? Environment.CurrentDirectory + "/" + generateFileName(protocol.FileName);? byte[] fileBuffer = new byte[1024]; // 每次收1KB ??????? FileStream fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write);? // 從緩存buffer中讀入到文件流中 int bytesRead;? int totalBytes = 0;? do {? ??????????? bytesRead = streamToClient.Read(buffer, 0, BufferSize);???????????? ??????????? fs.Write(buffer, 0, bytesRead);? ??????????? totalBytes += bytesRead;? ??????????? Console.WriteLine("Receiving {0} bytes ...", totalBytes);? ??????? } while (bytesRead > 0);? ??????? Console.WriteLine("Total {0} bytes received, Done!", totalBytes);? ??????? streamToClient.Dispose();? ??????? fs.Dispose();? ??????? localClient.Close();? ??? }? // 隨機(jī)獲取一個(gè)圖片名稱 private string generateFileName(string fileName) {? ??????? DateTime now = DateTime.Now;? return String.Format(? "{0}_{1}_{2}_{3}", now.Minute, now.Second, now.Millisecond, fileName? ??????? );? ??? }? }? 這里應(yīng)該沒(méi)有什么新知識(shí),需要注意的地方有這么幾個(gè):
在OnReadComplete()回調(diào)方法中的foreach循環(huán),我們使用委托異步調(diào)用了handleProtocol()方法,這是因?yàn)閔andleProtocol即將執(zhí)行的是一個(gè)讀取或接收文件的操作,也就是一個(gè)相對(duì)耗時(shí)的操作。
在handleProtocol()方法中,我們深切體會(huì)了定義ProtocolHelper類和FileProtocol結(jié)構(gòu)的好處。如果沒(méi)有定義它們,這里將是不堪入目的處理XML以及類型轉(zhuǎn)換的代碼。
handleProtocol()方法中進(jìn)行了一個(gè)條件判斷,注意sendFile()方法我屏蔽掉了,這個(gè)還沒(méi)有實(shí)現(xiàn),但是我想你已經(jīng)猜到它將是后面要實(shí)現(xiàn)的內(nèi)容。
receiveFile()方法是實(shí)際接收客戶端發(fā)來(lái)文件的方法,這里沒(méi)有什么特別之處。需要注意的是文件存儲(chǔ)的路徑,它保存在了當(dāng)前程序執(zhí)行的目錄下,文件的名稱我使用generateFileName()生成了一個(gè)與時(shí)間有關(guān)的隨機(jī)名稱。
3.2客戶端的實(shí)現(xiàn)
我們現(xiàn)在先不著急實(shí)現(xiàn)客戶端S1、R1等用戶菜單,首先完成發(fā)送文件這一功能,實(shí)際上,就是為上一節(jié)SendMessage()加一個(gè)姐妹方法SendFile()。
class Client {? static void Main(string[] args) {? ??????? ConsoleKey key;? ??????? ServerClient client = new ServerClient();? string filePath = Environment.CurrentDirectory + "/" + "Client01.jpg";? if(File.Exists(filePath))? ??????????? client.BeginSendFile(filePath);? ??????? Console.WriteLine("\n\n輸入\"Q\"鍵退出。");? do {? ??????????? key = Console.ReadKey(true).Key;? ??????? } while (key != ConsoleKey.Q);? ??? }? }? public class ServerClient {? private const int BufferSize = 8192;? private byte[] buffer;? private TcpClient client;? private NetworkStream streamToServer;? public ServerClient() {? try {? ??????????? client = new TcpClient();? ??????????? client.Connect("localhost", 8500);????? // 與服務(wù)器連接 ??????? } catch (Exception ex) {? ??????????? Console.WriteLine(ex.Message);? return;? ??????? }? ??????? buffer = new byte[BufferSize];? // 打印連接到的服務(wù)端信息 ??????? Console.WriteLine("Server Connected!{0} --> {1}",? ??????????? client.Client.LocalEndPoint, client.Client.RemoteEndPoint);? ??????? streamToServer = client.GetStream();? ??? }? // 發(fā)送消息到服務(wù)端 public void SendMessage(string msg) {? byte[] temp = Encoding.Unicode.GetBytes(msg);?? // 獲得緩存 try {? lock (streamToServer) {? ??????????????? streamToServer.Write(temp, 0, temp.Length); // 發(fā)往服務(wù)器 ??????????? }? ??????????? Console.WriteLine("Sent: {0}", msg);? ??????? } catch (Exception ex) {? ??????????? Console.WriteLine(ex.Message);? return;? ??????? }? ??? }? // 發(fā)送文件 - 異步方法 public void BeginSendFile(string filePath) {? ??????? ParameterizedThreadStart start =? new ParameterizedThreadStart(BeginSendFile);? ??????? start.BeginInvoke(filePath, null, null);? ??? }? private void BeginSendFile(object obj) {? string filePath = obj as string;? ??????? SendFile(filePath);? ??? }? // 發(fā)送文件 -- 同步方法 public void SendFile(string filePath) {? ??????? IPAddress ip = IPAddress.Parse("127.0.0.1");? ??????? TcpListener listener = new TcpListener(ip, 0);? ??????? listener.Start();? // 獲取本地偵聽(tīng)的端口號(hào) ??????? IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint;? int listeningPort = endPoint.Port;? // 獲取發(fā)送的協(xié)議字符串 string fileName = Path.GetFileName(filePath);? ??????? FileProtocol protocol =? new FileProtocol(FileRequestMode.Send, listeningPort, fileName);? string pro = protocol.ToString();? ??????? SendMessage(pro);?????? // 發(fā)送協(xié)議到服務(wù)端 // 中斷,等待遠(yuǎn)程連接 ??????? TcpClient localClient = listener.AcceptTcpClient();? ??????? Console.WriteLine("Start sending file...");? ??????? NetworkStream stream = localClient.GetStream();? // 創(chuàng)建文件流 ??????? FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);?????????? byte[] fileBuffer = new byte[1024];???? // 每次傳1KB int bytesRead;? int totalBytes = 0;? // 創(chuàng)建獲取文件發(fā)送狀態(tài)的類 ??????? SendStatus status = new SendStatus(filePath);? // 將文件流轉(zhuǎn)寫(xiě)入網(wǎng)絡(luò)流 try {? do {? ??????????????? Thread.Sleep(10);?????????? // 為了更好的視覺(jué)效果,暫停10毫秒:-) ??????????????? bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);????????????????? ??????????????? stream.Write(fileBuffer, 0, bytesRead);? ??????????????? totalBytes += bytesRead;??????????? // 發(fā)送了的字節(jié)數(shù) ??????????????? status.PrintStatus(totalBytes); // 打印發(fā)送狀態(tài) ??????????? } while (bytesRead > 0);? ??????????? Console.WriteLine("Total {0} bytes sent, Done!", totalBytes);? ??????? } catch {? ??????????? Console.WriteLine("Server has lost...");? ??????? }? ??????? stream.Dispose();? ??????? fs.Dispose();? ??????? localClient.Close();? ??????? listener.Stop();? ??? }? }? 接下來(lái)我們來(lái)看下這段代碼,有這么兩點(diǎn)需要注意一下:
???? 在Main()方法中可以看到,圖片的位置為應(yīng)用程序所在的目錄,如果你跟我一樣處于調(diào)試模式,那么就在解決方案的Bin目錄下的Debug目錄中放置三張圖片Client01.jpg、Client02.jpg、Client03.jpg,用來(lái)發(fā)往服務(wù)端。
???? 我在客戶端提供了兩個(gè)SendFile()方法,和一個(gè)BeginSendFile()方法,分別用于同步和異步傳輸,其中私有的SendFile()方法只是一個(gè)輔助方法。實(shí)際上對(duì)于發(fā)送文件這樣的操作我們幾乎總是需要使用異步操作。
???? SendMessage()方法中給streamToServer加鎖很重要,因?yàn)镾endFile()方法是多線程訪問(wèn)的,而在SendFile()方法中又調(diào)用了SendMessage()方法。
???? 我另外編寫(xiě)了一個(gè)SendStatus類,它用來(lái)記錄和打印發(fā)送完成的狀態(tài),已經(jīng)發(fā)送了多少字節(jié),完成度是百分之多少,等等。本來(lái)這個(gè)類的內(nèi)容我是直接寫(xiě)入在Client類中的,后來(lái)我覺(jué)得它執(zhí)行的工作已經(jīng)不屬于Client本身所應(yīng)該執(zhí)行的領(lǐng)域之內(nèi)了,我記得這樣一句話:當(dāng)你覺(jué)得類中的方法與類的名稱不符的時(shí)候,那么就應(yīng)該考慮重新創(chuàng)建一個(gè)類。我覺(jué)得用在這里非常恰當(dāng)。
下面是SendStatus的內(nèi)容:
// 即時(shí)計(jì)算發(fā)送文件的狀態(tài) public class SendStatus {? private FileInfo info;? private long fileBytes;? public SendStatus(string filePath) {? ??????? info = new FileInfo(filePath);? ??????? fileBytes = info.Length;? ??? }? public void PrintStatus(int sent) {? string percent = GetPercent(sent);? ??????? Console.WriteLine("Sending {0} bytes, {1}% ...", sent, percent);? ??? }? // 獲得文件發(fā)送的百分比 public string GetPercent(int sent){???? decimal allBytes = Convert.ToDecimal(fileBytes);? decimal currentSent = Convert.ToDecimal(sent);? decimal percent = (currentSent / allBytes) * 100;? ??????? percent = Math.Round(percent, 1);?? //保留一位小數(shù) if (percent.ToString() == "100.0")? return "100";? else return percent.ToString();? ??? }? }? 3.3程序測(cè)試
接下里我們運(yùn)行一下程序,來(lái)檢查一下輸出,首先看下服務(wù)端:
接著是客戶端,我們能夠看到發(fā)送的字節(jié)數(shù)和進(jìn)度,可以想到如果是圖形界面,那么我們可以通過(guò)擴(kuò)展SendStatus類來(lái)創(chuàng)建一個(gè)進(jìn)度條:
最后我們看下服務(wù)端的Bin\Debug目錄,應(yīng)該可以看到接收到的圖片:
本來(lái)我想這篇文章就可以完成發(fā)送和接收,不過(guò)現(xiàn)在看來(lái)沒(méi)法實(shí)現(xiàn)了,因?yàn)槿绻^續(xù)下去這篇文章就太長(zhǎng)了,我正嘗試著盡量將文章控制在15頁(yè)以內(nèi)。那么我們將在下篇文章中再完成接收文件這一部分。
轉(zhuǎn)載于:https://www.cnblogs.com/bennylam/archive/2010/07/24/1784279.html
總結(jié)
以上是生活随笔為你收集整理的C#网络编程:4订立协议和发送文件的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。