• C#网络编程:4订立协议和发送文件


    前面两篇文章所使用的范例都是传输字符串,有的时候我们可能会想在服务端和客户端之间传递文件。比如,考虑这样一种情况,假如客户端显示了一个菜单,当我们输入S1、S2或S3(S为Send缩写)时,分别向服务端发送文件Client01.jpg、Client02.jpg、Client03.jpg;当我们输入R1、R2或R3时(R为Receive缩写),则分别从服务端接收文件Server01.jpg、Server02.jpg、Server03.jpg。那么,我们该如何完成这件事呢?此时可能有这样两种做法:

    类似于FTP协议,服务端开辟两个端口,并持续对这两个端口侦听:一个用于接收字符串,类似于FTP的控制端口,它接收各种命令(接收或发送文件);一个用于传输数据,也就是发送和接收文件。

    服务端只开辟一个端口,用于接收字符串,我们称之为控制端口。当接到请求之后,根据请求内容在客户端开辟一个端口专用于文件传输,并在传输结束后关闭端口。

    现在我们只关注于上面的数据端口,回忆一下在第二篇中我们所总结的,可以得出:当我们使用上面的方法一时,服务端的数据端口可以为多个客户端的多次请求服务;当我们使用方法二时,服务端只为一个客户端的一次请求服务,但是因为每次请求都会重新开辟端口,所以实际上还是相当于可以为多个客户端的多次请求服务。同时,因为它只为一次请求服务,所以我们在数据端口上传输文件时无需采用异步传输方式。但在控制端口我们仍然需要使用异步方式。

    从上面看出,第一种方式要好得多,但是我们将采用第二种方式。至于原因,你可以回顾一下Part.1(基本概念和操作)中关于聊天程序模式的讲述,因为接下来一篇文章我们将创建一个聊天程序,而这个聊天程序采用第三种模式,所以本文的练习实际是对下一篇的一个铺垫。

    1.订立协议

    1.1发送文件

    我们先看一下发送文件的情况,如果我们想将文件client01.jpg由客户端发往客户端,那么流程是什么:

    客户端开辟数据端口用于侦听,并获取端口号,假设为8005。

    假设客户端输入了S1,则发送下面的控制字符串到服务端:[file=Client01.jpg, mode=send, port=8005]。

    服务端收到以后,根据客户端ip和端口号与该客户端建立连接。

    客户端侦听到服务端的连接,开始发送文件。

    传送完毕后客户端、服务端分别关闭连接。

    此时,我们订立的发送文件协议为:[file=Client01.jpg, mode=send, port=8005]。但是,由于它是一个普通的字符串,在上一篇中,我们采用了正则表达式来获取其中的有效值,但这显然不是一种好办法。因此,在本文及下一篇文章中,我们采用一种新的方式来编写协议:XML。对于上面的语句,我们可以写成这样的XML:

    1. <protocol><file name="client01.jpg" mode="send" port="8005" /></protocol>

    这样我们在服务端就会好处理得多,接下来我们来看一下接收文件的流程及其协议。

    NOTE:这里说发送、接收文件是站在客户端的立场说的,当客户端发送文件时,对于服务器来收,则是接收文件。

    1.2接收文件

    接收文件与发送文件实际上完全类似,区别只是由客户端向网络流写入数据,还是由服务端向网络流写入数据。

    客户端开辟数据端口用于侦听,假设为8006。

    假设客户端输入了R1,则发送控制字符串:<protocol><file name="Server01.jpg" mode="receive" port="8006" /></protocol>到服务端。

    服务端收到以后,根据客户端ip和端口号与该客户端建立连接。

    客户端建立起与服务端的连接,服务端开始网络流中写入数据。

    传送完毕后服务端、客户端分别关闭连接。

    2.协议处理类的实现

    和上面一章一样,在开始编写实际的服务端客户端代码之前,我们首先要编写处理协议的类,它需要提供这样两个功能:1、方便地帮我们获取完整的协议信息,因为前面我们说过,服务端可能将客户端的多次独立请求拆分或合并。比如,客户端连续发送了两条控制信息到服务端,而服务端将它们合并了,那么则需要先拆开再分别处理。2、方便地获取我们所想要的属性信息,因为协议是XML格式,所以还需要一个类专门对XML进行处理,获得字符串的属性值。

    2.1 ProtocalHandler辅助类

    我们先看下ProtocalHandler,它与上一篇中的RequestHandler作用相同。需要注意的是必须将它声明为实例的,而非静态的,这是因为每个TcpClient都需要对应一个ProtocalHandler,因为它内部维护的patialProtocal不能共享,在协议发送不完整的情况下,这个变量用于临时保存被截断的字符串。

    1. public class ProtocolHandler { 
    2. private string partialProtocal; // 保存不完整的协议
    3. public ProtocolHandler() { 
    4.         partialProtocal = "";      
    5.     } 
    6. public string[] GetProtocol(string input) { 
    7. return GetProtocol(input, null); 
    8.     } 
    9. // 获得协议
    10. private string[] GetProtocol(string input, List<string> outputList) { 
    11. if (outputList == null) 
    12.             outputList = new List<string>(); 
    13. if (String.IsNullOrEmpty(input)) 
    14. return outputList.ToArray(); 
    15. if (!String.IsNullOrEmpty(partialProtocal)) 
    16.             input = partialProtocal + input; 
    17. string pattern = "(^<protocol>.*?</protocol>)"; 
    18. // 如果有匹配,说明已经找到了,是完整的协议
    19. if (Regex.IsMatch(input, pattern)) { 
    20. // 获取匹配的值
    21. string match = Regex.Match(input, pattern).Groups[0].Value; 
    22.             outputList.Add(match); 
    23.             partialProtocal = ""; 
    24. // 缩短input的长度
    25.             input = input.Substring(match.Length); 
    26. // 递归调用
    27.             GetProtocol(input, outputList); 
    28.         } else { 
    29. // 如果不匹配,说明协议的长度不够,
    30. // 那么先缓存,然后等待下一次请求
    31.             partialProtocal = input; 
    32.         } 
    33. return outputList.ToArray(); 
    34.     } 

    因为现在它已经不是本文的重点了,所以我就不演示对于它的测试了,本文所附带的代码中含有它的测试代码(我在ProtocolHandler中添加了一个静态类Test())。

    2.2 FileRequestType枚举和FileProtocol结构

    因为XML是以字符串的形式在进行传输,为了方便使用,我们最好构建一个强类型来对它们进行操作,这样会方便很多。我们首先可以定义FileRequestMode枚举,它代表是发送还是接收文件:

    1. public enum FileRequestMode { 
    2.     Send = 0, 
    3.     Receive 

    接下来我们再定义一个FileProtocol结构,用来为整个协议字符串提供强类型的访问,注意这里覆盖了基类的ToString()方法,这样在客户端我们就不需要再手工去编写XML,只要在结构值上调用ToString()就OK了,会方便很多。

    1. public struct FileProtocol { 
    2. private readonly FileRequestMode mode; 
    3. private readonly int port; 
    4. private readonly string fileName; 
    5. public FileProtocol 
    6.         (FileRequestMode mode, int port, string fileName) { 
    7. this.mode = mode; 
    8. this.port = port; 
    9. this.fileName = fileName; 
    10.     } 
    11. public FileRequestMode Mode { 
    12. get { return mode; } 
    13.     } 
    14. public int Port { 
    15. get { return port; } 
    16.     } 
    17. public string FileName { 
    18. get { return fileName; } 
    19.     } 
    20. public override string ToString() { 
    21. return String.Format("<protocol><file name=\"{0}\" mode=\"{1}\" port=\"{2}\" /></protocol>", fileName, mode, port); 
    22.     } 

    2.3 ProtocolHelper辅助类

    这个类专用于将XML格式的协议映射为我们上面定义的强类型对象,这里我没有加入try/catch异常处理,因为协议对用户来说是不可见的,而且客户端应该总是发送正确的协议,我觉得这样可以让代码更加清晰:

    1. public class ProtocolHelper { 
    2. private XmlNode fileNode; 
    3. private XmlNode root; 
    4. public ProtocolHelper(string protocol) { 
    5.         XmlDocument doc = new XmlDocument(); 
    6.         doc.LoadXml(protocol); 
    7.         root = doc.DocumentElement; 
    8.         fileNode = root.SelectSingleNode("file"); 
    9.     } 
    10. // 此时的protocal一定为单条完整protocal
    11. private FileRequestMode GetFileMode() { 
    12. string mode = fileNode.Attributes["mode"].Value; 
    13.         mode = mode.ToLower(); 
    14. if (mode == "send") 
    15. return FileRequestMode.Send; 
    16. else
    17. return FileRequestMode.Receive; 
    18.     } 
    19. // 获取单条协议包含的信息
    20. public FileProtocol GetProtocol() { 
    21.         FileRequestMode mode = GetFileMode(); 
    22. string fileName = ""; 
    23. int port = 0; 
    24.         fileName = fileNode.Attributes["name"].Value; 
    25.         port = Convert.ToInt32(fileNode.Attributes["port"].Value); 
    26. return new FileProtocol(mode, port, fileName); 
    27.     } 

    OK,我们又耽误了点时间,下面就让我们进入正题吧。

    3.客户端发送数据

    3.1 服务端的实现

    我们还是将一个问题分成两部分来处理,先是发送数据,然后是接收数据。我们先看发送数据部分的服务端。如果你从第一篇文章看到了现在,那么我觉得更多的不是技术上的问题而是思路,所以我们不再将重点放到代码上,这些应该很容易就看懂了。

    1. class Server { 
    2. static void Main(string[] args) { 
    3.         Console.WriteLine("Server is running ... "); 
    4.         IPAddress ip = IPAddress.Parse("127.0.0.1"); 
    5.         TcpListener listener = new TcpListener(ip, 8500); 
    6.         listener.Start();           // 开启对控制端口 8500 的侦听
    7.         Console.WriteLine("Start Listening ..."); 
    8. while (true) { 
    9. // 获取一个连接,同步方法,在此处中断
    10.             TcpClient client = listener.AcceptTcpClient();             
    11.             RemoteClient wapper = new RemoteClient(client); 
    12.             wapper.BeginRead(); 
    13.         } 
    14.     } 
    15. public class RemoteClient { 
    16. private TcpClient client; 
    17. private NetworkStream streamToClient; 
    18. private const int BufferSize = 8192; 
    19. private byte[] buffer; 
    20. private ProtocolHandler handler; 
    21. public RemoteClient(TcpClient client) { 
    22. this.client = client; 
    23. // 打印连接到的客户端信息
    24.         Console.WriteLine("\nClient Connected!{0} <-- {1}", 
    25.             client.Client.LocalEndPoint, client.Client.RemoteEndPoint); 
    26. // 获得流
    27.         streamToClient = client.GetStream(); 
    28.         buffer = new byte[BufferSize]; 
    29.         handler = new ProtocolHandler(); 
    30.     } 
    31. // 开始进行读取
    32. public void BeginRead() {      
    33.         AsyncCallback callBack = new AsyncCallback(OnReadComplete); 
    34.         streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null); 
    35.     } 
    36. // 再读取完成时进行回调
    37. private void OnReadComplete(IAsyncResult ar) { 
    38. int bytesRead = 0; 
    39. try { 
    40. lock (streamToClient) { 
    41.                 bytesRead = streamToClient.EndRead(ar); 
    42.                 Console.WriteLine("Reading data, {0} bytes ...", bytesRead); 
    43.             } 
    44. if (bytesRead == 0) throw new Exception("读取到0字节"); 
    45. string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead); 
    46.             Array.Clear(buffer,0,buffer.Length);        // 清空缓存,避免脏读
    47. // 获取protocol数组
    48. string[] protocolArray = handler.GetProtocol(msg); 
    49. foreach (string pro in protocolArray) { 
    50. // 这里异步调用,不然这里可能会比较耗时
    51.                 ParameterizedThreadStart start = 
    52. new ParameterizedThreadStart(handleProtocol); 
    53.                 start.BeginInvoke(pro, null, null); 
    54.             } 
    55. // 再次调用BeginRead(),完成时调用自身,形成无限循环
    56. lock (streamToClient) { 
    57.                 AsyncCallback callBack = new AsyncCallback(OnReadComplete); 
    58.                 streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null); 
    59.             } 
    60.         } catch(Exception ex) { 
    61. if(streamToClient!=null) 
    62.                 streamToClient.Dispose(); 
    63.             client.Close(); 
    64.             Console.WriteLine(ex.Message);      // 捕获异常时退出程序
    65.         } 
    66.     } 
    67. // 处理protocol
    68. private void handleProtocol(object obj) { 
    69. string pro = obj as string; 
    70.         ProtocolHelper helper = new ProtocolHelper(pro); 
    71.         FileProtocol protocol = helper.GetProtocol(); 
    72. if (protocol.Mode == FileRequestMode.Send) { 
    73. // 客户端发送文件,对服务端来说则是接收文件
    74.             receiveFile(protocol); 
    75.         } else if (protocol.Mode == FileRequestMode.Receive) { 
    76. // 客户端接收文件,对服务端来说则是发送文件
    77. // sendFile(protocol);
    78.         } 
    79.     } 
    80. private void receiveFile(FileProtocol protocol) { 
    81. // 获取远程客户端的位置
    82.         IPEndPoint endpoint = client.Client.RemoteEndPoint as IPEndPoint; 
    83.         IPAddress ip = endpoint.Address; 
    84. // 使用新端口号,获得远程用于接收文件的端口
    85.         endpoint = new IPEndPoint(ip, protocol.Port); 
    86. // 连接到远程客户端
    87.         TcpClient localClient; 
    88. try { 
    89.             localClient = new TcpClient(); 
    90.             localClient.Connect(endpoint); 
    91.         } catch { 
    92.             Console.WriteLine("无法连接到客户端 --> {0}", endpoint); 
    93. return; 
    94.         } 
    95. // 获取发送文件的流
    96.         NetworkStream streamToClient = localClient.GetStream(); 
    97. // 随机生成一个在当前目录下的文件名称
    98. string path = 
    99.             Environment.CurrentDirectory + "/" + generateFileName(protocol.FileName); 
    100. byte[] fileBuffer = new byte[1024]; // 每次收1KB
    101.         FileStream fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write); 
    102. // 从缓存buffer中读入到文件流中
    103. int bytesRead; 
    104. int totalBytes = 0; 
    105. do { 
    106.             bytesRead = streamToClient.Read(buffer, 0, BufferSize);            
    107.             fs.Write(buffer, 0, bytesRead); 
    108.             totalBytes += bytesRead; 
    109.             Console.WriteLine("Receiving {0} bytes ...", totalBytes); 
    110.         } while (bytesRead > 0); 
    111.         Console.WriteLine("Total {0} bytes received, Done!", totalBytes); 
    112.         streamToClient.Dispose(); 
    113.         fs.Dispose(); 
    114.         localClient.Close(); 
    115.     } 
    116. // 随机获取一个图片名称
    117. private string generateFileName(string fileName) { 
    118.         DateTime now = DateTime.Now; 
    119. return String.Format( 
    120. "{0}_{1}_{2}_{3}", now.Minute, now.Second, now.Millisecond, fileName 
    121.         ); 
    122.     } 

    这里应该没有什么新知识,需要注意的地方有这么几个:

    在OnReadComplete()回调方法中的foreach循环,我们使用委托异步调用了handleProtocol()方法,这是因为handleProtocol即将执行的是一个读取或接收文件的操作,也就是一个相对耗时的操作。

    在handleProtocol()方法中,我们深切体会了定义ProtocolHelper类和FileProtocol结构的好处。如果没有定义它们,这里将是不堪入目的处理XML以及类型转换的代码。

    handleProtocol()方法中进行了一个条件判断,注意sendFile()方法我屏蔽掉了,这个还没有实现,但是我想你已经猜到它将是后面要实现的内容。

    receiveFile()方法是实际接收客户端发来文件的方法,这里没有什么特别之处。需要注意的是文件存储的路径,它保存在了当前程序执行的目录下,文件的名称我使用generateFileName()生成了一个与时间有关的随机名称。

    3.2客户端的实现

    我们现在先不着急实现客户端S1、R1等用户菜单,首先完成发送文件这一功能,实际上,就是为上一节SendMessage()加一个姐妹方法SendFile()。

    1. class Client { 
    2. static void Main(string[] args) { 
    3.         ConsoleKey key; 
    4.         ServerClient client = new ServerClient(); 
    5. string filePath = Environment.CurrentDirectory + "/" + "Client01.jpg"; 
    6. if(File.Exists(filePath)) 
    7.             client.BeginSendFile(filePath); 
    8.         Console.WriteLine("\n\n输入\"Q\"键退出。"); 
    9. do { 
    10.             key = Console.ReadKey(true).Key; 
    11.         } while (key != ConsoleKey.Q); 
    12.     } 
    13. public class ServerClient { 
    14. private const int BufferSize = 8192; 
    15. private byte[] buffer; 
    16. private TcpClient client; 
    17. private NetworkStream streamToServer; 
    18. public ServerClient() { 
    19. try { 
    20.             client = new TcpClient(); 
    21.             client.Connect("localhost", 8500);      // 与服务器连接
    22.         } catch (Exception ex) { 
    23.             Console.WriteLine(ex.Message); 
    24. return; 
    25.         } 
    26.         buffer = new byte[BufferSize]; 
    27. // 打印连接到的服务端信息
    28.         Console.WriteLine("Server Connected!{0} --> {1}", 
    29.             client.Client.LocalEndPoint, client.Client.RemoteEndPoint); 
    30.         streamToServer = client.GetStream(); 
    31.     } 
    32. // 发送消息到服务端
    33. public void SendMessage(string msg) { 
    34. byte[] temp = Encoding.Unicode.GetBytes(msg);   // 获得缓存
    35. try { 
    36. lock (streamToServer) { 
    37.                 streamToServer.Write(temp, 0, temp.Length); // 发往服务器
    38.             } 
    39.             Console.WriteLine("Sent: {0}", msg); 
    40.         } catch (Exception ex) { 
    41.             Console.WriteLine(ex.Message); 
    42. return; 
    43.         } 
    44.     } 
    45. // 发送文件 - 异步方法
    46. public void BeginSendFile(string filePath) { 
    47.         ParameterizedThreadStart start = 
    48. new ParameterizedThreadStart(BeginSendFile); 
    49.         start.BeginInvoke(filePath, null, null); 
    50.     } 
    51. private void BeginSendFile(object obj) { 
    52. string filePath = obj as string; 
    53.         SendFile(filePath); 
    54.     } 
    55. // 发送文件 -- 同步方法
    56. public void SendFile(string filePath) { 
    57.         IPAddress ip = IPAddress.Parse("127.0.0.1"); 
    58.         TcpListener listener = new TcpListener(ip, 0); 
    59.         listener.Start(); 
    60. // 获取本地侦听的端口号
    61.         IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint; 
    62. int listeningPort = endPoint.Port; 
    63. // 获取发送的协议字符串
    64. string fileName = Path.GetFileName(filePath); 
    65.         FileProtocol protocol = 
    66. new FileProtocol(FileRequestMode.Send, listeningPort, fileName); 
    67. string pro = protocol.ToString(); 
    68.         SendMessage(pro);       // 发送协议到服务端
    69. // 中断,等待远程连接
    70.         TcpClient localClient = listener.AcceptTcpClient(); 
    71.         Console.WriteLine("Start sending file..."); 
    72.         NetworkStream stream = localClient.GetStream(); 
    73. // 创建文件流
    74.         FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);          
    75. byte[] fileBuffer = new byte[1024];     // 每次传1KB
    76. int bytesRead; 
    77. int totalBytes = 0; 
    78. // 创建获取文件发送状态的类
    79.         SendStatus status = new SendStatus(filePath); 
    80. // 将文件流转写入网络流
    81. try { 
    82. do { 
    83.                 Thread.Sleep(10);           // 为了更好的视觉效果,暂停10毫秒:-)
    84.                 bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);                 
    85.                 stream.Write(fileBuffer, 0, bytesRead); 
    86.                 totalBytes += bytesRead;            // 发送了的字节数
    87.                 status.PrintStatus(totalBytes); // 打印发送状态
    88.             } while (bytesRead > 0); 
    89.             Console.WriteLine("Total {0} bytes sent, Done!", totalBytes); 
    90.         } catch { 
    91.             Console.WriteLine("Server has lost..."); 
    92.         } 
    93.         stream.Dispose(); 
    94.         fs.Dispose(); 
    95.         localClient.Close(); 
    96.         listener.Stop(); 
    97.     } 

    接下来我们来看下这段代码,有这么两点需要注意一下:

    •    在Main()方法中可以看到,图片的位置为应用程序所在的目录,如果你跟我一样处于调试模式,那么就在解决方案的Bin目录下的Debug目录中放置三张图片Client01.jpg、Client02.jpg、Client03.jpg,用来发往服务端。

    •    我在客户端提供了两个SendFile()方法,和一个BeginSendFile()方法,分别用于同步和异步传输,其中私有的SendFile()方法只是一个辅助方法。实际上对于发送文件这样的操作我们几乎总是需要使用异步操作。

    •    SendMessage()方法中给streamToServer加锁很重要,因为SendFile()方法是多线程访问的,而在SendFile()方法中又调用了SendMessage()方法。

    •    我另外编写了一个SendStatus类,它用来记录和打印发送完成的状态,已经发送了多少字节,完成度是百分之多少,等等。本来这个类的内容我是直接写入在Client类中的,后来我觉得它执行的工作已经不属于Client本身所应该执行的领域之内了,我记得这样一句话:当你觉得类中的方法与类的名称不符的时候,那么就应该考虑重新创建一个类。我觉得用在这里非常恰当。

    下面是SendStatus的内容:

    1. // 即时计算发送文件的状态
    2. public class SendStatus { 
    3. private FileInfo info; 
    4. private long fileBytes; 
    5. public SendStatus(string filePath) { 
    6.         info = new FileInfo(filePath); 
    7.         fileBytes = info.Length; 
    8.     } 
    9. public void PrintStatus(int sent) { 
    10. string percent = GetPercent(sent); 
    11.         Console.WriteLine("Sending {0} bytes, {1}% ...", sent, percent); 
    12.     } 
    13. // 获得文件发送的百分比
    14. public string GetPercent(int sent){    
    15. decimal allBytes = Convert.ToDecimal(fileBytes); 
    16. decimal currentSent = Convert.ToDecimal(sent); 
    17. decimal percent = (currentSent / allBytes) * 100; 
    18.         percent = Math.Round(percent, 1);   //保留一位小数
    19. if (percent.ToString() == "100.0") 
    20. return "100"; 
    21. else
    22. return percent.ToString(); 
    23.     } 

    3.3程序测试

    接下里我们运行一下程序,来检查一下输出,首先看下服务端:

    接着是客户端,我们能够看到发送的字节数和进度,可以想到如果是图形界面,那么我们可以通过扩展SendStatus类来创建一个进度条:

    最后我们看下服务端的Bin\Debug目录,应该可以看到接收到的图片:

    本来我想这篇文章就可以完成发送和接收,不过现在看来没法实现了,因为如果继续下去这篇文章就太长了,我正尝试着尽量将文章控制在15页以内。那么我们将在下篇文章中再完成接收文件这一部分。

  • 相关阅读:
    mybatis中的动态语句中多条件or如何书写
    安装kibana的docker版
    安装elasticsearch的docker版
    git回滚push过的代码
    java中支付宝支付
    05 docker镜像删除
    远程仓库的搭建
    本地git工作流
    创建本地仓库
    git安装
  • 原文地址:https://www.cnblogs.com/bennylam/p/1784279.html
Copyright © 2020-2023  润新知