文件传输
前面的案例都是传输字符串.还有一种常见的情况,就是在服务端和客户端之间传输文件.
计入,客户端显示了一个菜单:当输入S1,S2或S3时,分别向服务端发送文件Client01.jpg,Client02.jpg,Client02.jpg;当输入R1,R2或R3时,分别从服务端接受文件Server01.jpg,Server02.jpg,Server03.jpg.那么,如何完成这项功能呢?
方法1:类似FTP协议,服务端监听两个端口:一个称为控制端口,用于接受各种命令字符串(只是接受或发送文件);一个称为数据端口,用于传输实际数据,就是发送和接收文件
方法2:服务端只监听一个端口,用于接受来自客户端的命令字符串,称为控制端口.客户端在发出命令字符串之前,先开辟一个本地端口专用于收发文件,然后向服务端发送命令字符串,其中包含了客户端专用于收发数据的端口号.服务端接收到请求之后,连接至客户端专用于收发文件的端口,然后传输数据,结束后关闭连接.
现在只关注两种方式中的数据端口,当使用方法1的时候,服务端的数据端口需要同时为多个客户端的多次请求服务,因此需要采用异步方式;使用方法2时,服务端与客户端在每次传输数据时都会重新建立新的连接,在传输完成后立即关闭,因此在数据端口上传输文件时不需要采用异步方式.但是在控制端口仍然需要使用异步方式.
咱们使用方法2来实现文件的接受.
订立协议
1.发送文件
先看一下发送文件的情况:如果要将Client01.jpg由客户端发往服务端,可以归纳为以下几个步骤:
(1).客户端对端口进行监听,假设端口号为8005.
(2).假设客户输入了S1,则发送下面的控制字符串到服务端:[file=Client01.jpg,mode=send.port=8005].
(3)服务端收到以后,根据客户端IP和端口号与该客户端建立连接
(4)客户端向网络流羞辱数据,开始发送文件.
(5)传送完毕之后客户端和服务端关闭数据端口的连接.
类似之前的规则,订立的发布文件协议可以为:[file=Client01.jpg,mode=send,port=8005].但是,由于他是一个普通的字符串,在前面我们采用了正则表达式的方法来获得其中的有效值,但还有更好地办法----使用XML.对于上面的语句,我们可以写成这样的XML:
<protocol><file name=”Client01.jpg” mode=senf,port=”8005” /></protocol>
这样处理起来就会方便多了,因为处理XML格式有更丰富的类型支持.接下来看一下接收文件的流程及其协议.
2.接收文件
接收文件与发送文件实际上完全类似,区别只是:当发送文件时,由客户端向网络流写入数据;当接收文件时,由服务端想网络流介入数据.接受文件的步骤如下:
(1).客户端对端口进行监听,假设端口号为8006.
(2).假设客户端输入了R1,则发送控制字符串:<protocol><file name=”Server01.jpg” mode=”receive” port=”8006”/></protocol>到服务端.
(3).服务端接收到以后,根据客户端IP和端口号与该客户端建立连接.
(4).服务端向网络流写入数据,开始发送文件.
(5).传送完毕后客户端和服务端关闭数据端口的连接.
协议处理类的实现
在开始编写实际的服务端客户端代码之前,首先要编写处理协议的类,这个类需要两个功能:一是方便获取完整的协议信息,因为前面说过,服务端可能将客户端的多次独立请求拆分或合并.比如,客户端连续发送了两条控制信息到服务端,而服务端将它们合并了,则需要先拆开再分别处理.二是方便获取节点信息,因为协议是XML格式,所以还需要一个类专门对XML进行处理,获得节点的值.
1.ProtocolHandler辅助类
先看下ProtocolHandler,它与前面说过的ResquestHandler作用相同.需要注意的是必须将它声明为实例的,而非静态的,这是因为每个TcpClient都需要对应一个ProtocolHandler,它内部维护的partialProtocol不能共享,在协议发送不完整的情况下,这个变量用于临时保存被截断的字符串.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Server { public class ProtocolHandler { private string partialProtocol;//保存不完整的协议 public ProtocolHandler() { partialProtocol = ""; } public string[] GetProtocol(string input) { return GetProtocol(input,null); } //获得协议 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(partialProtocol)) { input = partialProtocol + input; } string pattern = "(^<protocol>.*?</protocol>)"; //如果有匹配,说明已经找到了,是完整的协议 if (Regex.IsMatch(input,pattern)) { //获取匹配的值 string match = Regex.Match(input,pattern).Groups[0].Value; outputList.Add(match); partialProtocol = ""; //缩短input的长度 input = input.Substring(match.Length); //递归调用 GetProtocol(input,outputList); } else { //如果不匹配,说明协议长度不够 //那么先缓存,然后等待下一次请求 partialProtocol = input; } return outputList.ToArray(); } } }
因为这个类不是重点,这里贴下代码就够了.
2.FileRequestType枚举类型和FileProtocol结构
因为XML是以字符串的形式进行传输,为了方便使用,最好构建一个强类型对它们进行操作,这样会方便很多.首先可以定义FileRequestMode枚举类型,用来表示是发送文件还是接受文件:
public enum FileRequestMode { Send=0, Receive }
接下来再定义一个FileProtocol结构,用来为整个协议字符串提供强类型的访问,注意这里覆盖了基类的ToString()方法,这样在客户端就不需要再手工去编写XML,只要在FileProtocol结构值上调用ToString()就行了:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Server { 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); } } }
3.ProtocolHelper辅助类
这个类专用于将XML格式的协议映射为上面定义的强类型对象,这里没有加入try/catch异常处理,因为协议对用户来说是不可见的,而客户端应该总是发送正确的协议:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; namespace Server { 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"); } //此时的protocol已定位单条完整的protocol private FileRequestMode GetFileMode() { string mode = fileNode.Attributes["mode"].Value; mode = mode.ToLower(); if (mode=="send") { return FileRequestMode.Send; } else { return FileRequestMode.Receive; } } //获取单条协议包含的信息 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); } } }
关于XML部分的内容,我们会详细讲解.
客户端发送文件
我们还是讲一个问题分为两部分来处理:先是客户端发送文件,然后是服务端接收文件.
1.客户端的实现
不着急实现客户端S1,R1等用户菜单,首先完成发送文件这一功能为前面的Client类再添加一个SendFile()方法.
using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using System.Net; using System.Threading; using System.IO; using Server; namespace Client { public class Client { private const int BufferSize = 8192; private byte[] buffer; private TcpClient client; private NetworkStream streamToServer; public Client() { try { client = new TcpClient(); client.Connect("192.168.3.19",8500); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } buffer = new byte[BufferSize]; //打印连接到的服务器信息 Console.WriteLine("Server Connected! Local: {0}-->Server:{1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint); streamToServer = client.GetStream(); } //发送消息到服务器 public void SendMessage(string msg) { byte[] temp = Encoding.Unicode.GetBytes(msg);//获得缓存 try { streamToServer.Write(temo,0,temp.Length); Console.WriteLine("Sent: {0}", msg); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } //发送文件--异步方法 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); } //发送文件 public void SendFile(string filePath) { IPAddress ip = IPAddress.Parse("192.168.3.19"); TcpListener listener = new TcpListener(ip,0); listener.Start(); //获取本地监听的端口号 IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint; int listeningPort = endPoint.Port; //获取发送的协议字符串 string fileName = Path.GetFileName(filePath); FileProtocol protocol = new FileProtocol(FileRequestMode.Send,listeningPort,fileName); string pro = protocol.ToString(); SendMessage(pro);//发送协议到服务端 //中断,等待远程连接 TcpClient localClient = listener.AcceptTcpClient(); Console.WriteLine("Start sending file..."); NetworkStream stream = localClient.GetStream(); //创建文件流 FileStream fs = new FileStream(filePath,FileMode.Open,FileAccess.Read); byte[] fileBuffer = new byte[1024]; //每次传输1KB int bytesRead; int totalBytes = 0; //创建获取文件发送状态的类 SendStatus status = new SendStatus(); //将文件流转写入网络流 try { do { Thread.Sleep(10);//模拟远程传输视觉效果,暂停10秒 bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length); stream.Write(fileBuffer, 0, bytesRead); totalBytes += bytesRead; status.PrintStatus(totalBytes); } while (bytesRead > 0); Console.WriteLine("Total {0} bytes sent ,Done!", totalBytes); } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { stream.Dispose(); fs.Dispose(); localClient.Close(); listener.Stop(); } } } }
SendStatus类:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Client { 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); } //获得文件发送的百分比 private string GetPercent(int sent) { decimal allBytes = Convert.ToDecimal(fileBytes); decimal currentSent = Convert.ToDecimal(sent); decimal percent = (currentSent / allBytes) * 100; percent = Math.Round(percent,1);//保留一位小数 if (percent.ToString()=="100.0") { return "100"; } else { return percent.ToString(); } } } }
主程序:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Client { class Program { static void Main(string[] args) { ConsoleKey key; Client c = new Client(); string filePath = Environment.CurrentDirectory + "/" + "Client01.jpg"; if (File.Exists(filePath)) { c.BeginSendFile(filePath); } Console.WriteLine("\n\n按Q退出"); do { key = Console.ReadKey(true).Key; } while (key!=ConsoleKey.Q); } } }
这段代码游侠买几个地方需要注意:
1.)在Main()方法中可以看到,图片的位置为应用程序所在的目录.如果处于调试模式,那么在项目的Bin目录下的Debug目录中防止三种图片Client01.jpg,Client02.jpg,Client03.jpg,作为测试目的.
2.)客户端提供了一个SendFile()和两个BeginSendFile()方法,分别用于同步和异步传输,其中私有的BeginSendFile()方法只是个辅助方法,实际上对于发送文件这样的操作,集合总是需要使用异步方式.
3.)另外编写了一个SendStatus类,用来记录和打印完成的状态,已经发送了多少字节.
2.服务端的实现
服务端要完成的就是解析协议,连接至客户端,根据协议内容,判断时发送文件还是接收文件,下面是代码部分:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Server { public class Server { private TcpClient client; private NetworkStream streamToClient; private const int BufferSize = 8192; private byte[] buffer; private ProtocolHandler handler; public Server(TcpClient client) { this.client = client; //打印连接到的客户端信息 Console.WriteLine("ClientConnected! Local :{0}<--Client: {1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint); //获得流 streamToClient = client.GetStream(); buffer = new byte[BufferSize]; handler = new ProtocolHandler(); } //开始进行读取 public void BeginRead() { AsyncCallback callBack = new AsyncCallback(OnReadComplete); streamToClient.BeginRead(buffer,0,BufferSize,callBack,null); } //读取完成时进行回调 private void OnReadComplete(IAsyncResult ar) { int bytesRead = 0; try { bytesRead = streamToClient.EndRead(ar); Console.WriteLine("Reading data,{0} bytes...",bytesRead); if (bytesRead==0) { Console.WriteLine("Client offline."); return; } string msg = Encoding.Unicode.GetString(buffer,0,bytesRead); Array.Clear(buffer,0,buffer.Length);//清空缓存,避免脏读 //获取protocol数组 string[] protocolArray = handler.GetProtocol(msg); foreach (string pro in protocolArray) { //这里异步调用,不然这里会比较耗时 ParameterizedThreadStart start = new ParameterizedThreadStart(handleProtocol); start.BeginInvoke(pro,null,null); } //再次调用BeginRead(),完成时调用自身,形成无限循环 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); } } //处理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) { //客户端发送文件,对服务端来说则是接收文件 receiveFile(protocol); } else if (protocol.Mode==FileRequestMode.Receive) { //客户端接收文件,对服务端来说是发送文件 //sendFile(protocol); } } //接收文件 private void receiveFile(FileProtocol protocol) { //获取远程客户端的位置 IPEndPoint endPoint = client.Client.RemoteEndPoint as IPEndPoint; IPAddress ip = endPoint.Address; //使用新端口,获得远程用于接收文件的端口 endPoint = new IPEndPoint(ip,protocol.Port); //连接到远程客户端 TcpClient localClient; try { localClient = new TcpClient(); localClient.Connect(endPoint); } catch (Exception ex) { Console.WriteLine("无法连接到客户端 --> {0}",endPoint); return; } //获取发送文件的流 NetworkStream streamToCLient = localClient.GetStream(); //随机生成一个在当前目录下的文件名称 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(); } //随机获取一个图片名称 private string generateFileName(string fileName) { DateTime now = DateTime.Now; return string.Format("{0}_{1}_{2}_{3}",now.Minute,now.Second,now.Millisecond,fileName); } } }
主程序如下:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace Server { class Program { static void Main(string[] args) { Console.WriteLine("Server is running..."); IPAddress ip = IPAddress.Parse("192.168.1.113"); TcpListener listener = new TcpListener(ip,8500); listener.Start(); Console.WriteLine("Start Listening..."); while (true) { //获取一个连接,同步方法,在此处中断 TcpClient client = listener.AcceptTcpClient(); Server wapper = new Server(client); wapper.BeginRead(); } } } }
这段代码需要注意这么几点:
1.在OnReadComplete()回调方法的foreach循环中,使用委托异步调用了handlerProtocol()方法,这是因为handlerProtocol即即将执行的是一个读取或接收文件的操作,也就是一个相对耗时的操作.
2.在handlerProtocol()方法中,可以体会到定义ProtocolHelper类和FileProtocol结构的好处,如果不定义它们,这里将是杂乱的处理XML以及类型转换的代码.
3.handlerProtocol()方法中进行了一个条件判断,注意sendFile()被屏蔽了,这里还没有为服务单发送文件至客户端.
4.receiveFile()方法是实际接受客户端发来文件的方法,这里没有什么特别之处.需要注意的是文件存储的路径,它保存在了当前程序执行的目录下,文件的名称使用GetFileName()方法生成一个与时间有关的随机名称.
最后我想说一点,因为楼主中途换个一个教室,程序怎么都出错,结果搞了半天发现是IP地址出了问题,每个教室的IP地址可能不同,还有一点需要注意,别忘了添加对解决方案的引用,要不然会报错.
不管怎么说,程序算是完成了,楼主这里用了一个40M大小的文件进行测试(原本是一个avi,楼主改了一下后缀名)....程序是如此的慢,为啥呢?因为咱们一次只传输1KB...
到现在为止,咱们关于网络编程的内容算是搞一个段落了,开头第一部分讲解了TCP协议,套接字,即时通信程序三种开发模式,以及两个基本操作----监听端口和连接远程服务器.
第二部分是一个简单的实例:从客户端传输字符串到服务器,服务器接受并处理字符串,在将处理过的字符串发回给客户端.
第三部分是异步传输和文本边界,这是对第二部分的强化.
第四部分是发送文件.