珠海中英文网站建设,最近免费高清观看mv,宿迁哪家做网站好,最火的网络销售平台转载自#xff1a;http://www.tracefact.net/CSharp-Programming/Network-Programming-Part4.aspx 文件传输 前面两篇文章所使用的范例都是传输字符串#xff0c;有的时候我们可能会想在服务端和客户端之间传递文件。比如#xff0c;考虑这样一种情况#xff0c;假如客户端…转载自http://www.tracefact.net/CSharp-Programming/Network-Programming-Part4.aspx 文件传输 前面两篇文章所使用的范例都是传输字符串有的时候我们可能会想在服务端和客户端之间传递文件。比如考虑这样一种情况假如客户端显示了一个菜单当我们输入S1、S2或S3S为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则发送下面的控制字符串到服务端[fileClient01.jpg, modesend, port8005]。服务端收到以后根据客户端ip和端口号与该客户端建立连接。客户端侦听到服务端的连接开始发送文件。传送完毕后客户端、服务端分别关闭连接。此时我们订立的发送文件协议为[fileClient01.jpg, modesend, port8005]。但是由于它是一个普通的字符串在上一篇中我们采用了正则表达式来获取其中的有效值但这显然不是一种好办法。因此在本文及下一篇文章中我们采用一种新的方式来编写协议XML。对于上面的语句我们可以写成这样的XML protocolfile nameclient01.jpg modesend port8005 //protocol 这样我们在服务端就会好处理得多接下来我们来看一下接收文件的流程及其协议。 NOTE这里说发送、接收文件是站在客户端的立场说的当客户端发送文件时对于服务器来收则是接收文件。 1.2接收文件 接收文件与发送文件实际上完全类似区别只是由客户端向网络流写入数据还是由服务端向网络流写入数据。 客户端开辟数据端口用于侦听假设为8006。假设客户端输入了R1则发送控制字符串protocolfile nameServer01.jpg modereceive port8006 //protocol到服务端。服务端收到以后根据客户端ip和端口号与该客户端建立连接。客户端建立起与服务端的连接服务端开始网络流中写入数据。传送完毕后服务端、客户端分别关闭连接。2.协议处理类的实现 和上面一章一样在开始编写实际的服务端客户端代码之前我们首先要编写处理协议的类它需要提供这样两个功能1、方便地帮我们获取完整的协议信息因为前面我们说过服务端可能将客户端的多次独立请求拆分或合并。比如客户端连续发送了两条控制信息到服务端而服务端将它们合并了那么则需要先拆开再分别处理。2、方便地获取我们所想要的属性信息因为协议是XML格式所以还需要一个类专门对XML进行处理获得字符串的属性值。 2.1 ProtocalHandler辅助类 我们先看下ProtocalHandler它与上一篇中的RequestHandler作用相同。需要注意的是必须将它声明为实例的而非静态的这是因为每个TcpClient都需要对应一个ProtocalHandler因为它内部维护的patialProtocal不能共享在协议发送不完整的情况下这个变量用于临时保存被截断的字符串。 public class ProtocolHandler { private string partialProtocal; // 保存不完整的协议 public ProtocolHandler() { partialProtocal ; } public string[] GetProtocol(string input) { return GetProtocol(input, null); } // 获得协议 private string[] GetProtocol(string input, Liststring outputList) { if (outputList null) outputList new Liststring(); if (String.IsNullOrEmpty(input)) return outputList.ToArray(); if (!String.IsNullOrEmpty(partialProtocal)) input partialProtocal input; string pattern (^protocol.*?/protocol); // 如果有匹配说明已经找到了是完整的协议 if (Regex.IsMatch(input, pattern)) { // 获取匹配的值 string match Regex.Match(input, pattern).Groups[0].Value; outputList.Add(match); partialProtocal ; // 缩短input的长度 input input.Substring(match.Length); // 递归调用 GetProtocol(input, outputList); } else { // 如果不匹配说明协议的长度不够 // 那么先缓存然后等待下一次请求 partialProtocal input; } return outputList.ToArray(); }} 因为现在它已经不是本文的重点了所以我就不演示对于它的测试了本文所附带的代码中含有它的测试代码我在ProtocolHandler中添加了一个静态类Test()。 2.2 FileRequestType枚举和FileProtocol结构 因为XML是以字符串的形式在进行传输为了方便使用我们最好构建一个强类型来对它们进行操作这样会方便很多。我们首先可以定义FileRequestMode枚举它代表是发送还是接收文件 public enum FileRequestMode { Send 0, Receive} 接下来我们再定义一个FileProtocol结构用来为整个协议字符串提供强类型的访问注意这里覆盖了基类的ToString()方法这样在客户端我们就不需要再手工去编写XML只要在结构值上调用ToString()就OK了会方便很多。 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(protocolfile name\{0}\ mode\{1}\ port\{2}\ //protocol, fileName, mode, port); }} 2.3 ProtocolHelper辅助类 这个类专用于将XML格式的协议映射为我们上面定义的强类型对象这里我没有加入try/catch异常处理因为协议对用户来说是不可见的而且客户端应该总是发送正确的协议我觉得这样可以让代码更加清晰 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); } // 此时的protocal一定为单条完整protocal 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); }} OK我们又耽误了点时间下面就让我们进入正题吧。 3.客户端发送数据 3.1 服务端的实现 我们还是将一个问题分成两部分来处理先是发送数据然后是接收数据。我们先看发送数据部分的服务端。如果你从第一篇文章看到了现在那么我觉得更多的不是技术上的问题而是思路所以我们不再将重点放到代码上这些应该很容易就看懂了。 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(); // 开启对控制端口 8500 的侦听 Console.WriteLine(Start Listening ...); while (true) { // 获取一个连接同步方法在此处中断 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(); } // 开始进行读取 public void BeginRead() { AsyncCallback callBack new AsyncCallback(OnReadComplete); streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null); } // 再读取完成时进行回调 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字节); 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()完成时调用自身形成无限循环 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); // 捕获异常时退出程序 } } // 处理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 { 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 ); }} 这里应该没有什么新知识需要注意的地方有这么几个 在OnReadComplete()回调方法中的foreach循环我们使用委托异步调用了handleProtocol()方法这是因为handleProtocol即将执行的是一个读取或接收文件的操作也就是一个相对耗时的操作。在handleProtocol()方法中我们深切体会了定义ProtocolHelper类和FileProtocol结构的好处。如果没有定义它们这里将是不堪入目的处理XML以及类型转换的代码。handleProtocol()方法中进行了一个条件判断注意sendFile()方法我屏蔽掉了这个还没有实现但是我想你已经猜到它将是后面要实现的内容。receiveFile()方法是实际接收客户端发来文件的方法这里没有什么特别之处。需要注意的是文件存储的路径它保存在了当前程序执行的目录下文件的名称我使用generateFileName()生成了一个与时间有关的随机名称。3.2客户端的实现 我们现在先不着急实现客户端S1、R1等用户菜单首先完成发送文件这一功能实际上就是为上一节SendMessage()加一个姐妹方法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); // 与服务器连接 } catch (Exception ex) { Console.WriteLine(ex.Message); return; } buffer new byte[BufferSize]; // 打印连接到的服务端信息 Console.WriteLine(Server Connected{0} -- {1}, client.Client.LocalEndPoint, client.Client.RemoteEndPoint); streamToServer client.GetStream(); } // 发送消息到服务端 public void SendMessage(string msg) { byte[] temp Encoding.Unicode.GetBytes(msg); // 获得缓存 try { lock (streamToServer) { streamToServer.Write(temp, 0, temp.Length); // 发往服务器 } Console.WriteLine(Sent: {0}, msg); } catch (Exception ex) { Console.WriteLine(ex.Message); return; } } // 发送文件 - 异步方法 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(127.0.0.1); 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(filePath); // 将文件流转写入网络流 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 { Console.WriteLine(Server has lost...); } stream.Dispose(); fs.Dispose(); localClient.Close(); listener.Stop(); }} 接下来我们来看下这段代码有这么两点需要注意一下 在Main()方法中可以看到图片的位置为应用程序所在的目录如果你跟我一样处于调试模式那么就在解决方案的Bin目录下的Debug目录中放置三张图片Client01.jpg、Client02.jpg、Client03.jpg用来发往服务端。我在客户端提供了两个SendFile()方法和一个BeginSendFile()方法分别用于同步和异步传输其中私有的SendFile()方法只是一个辅助方法。实际上对于发送文件这样的操作我们几乎总是需要使用异步操作。SendMessage()方法中给streamToServer加锁很重要因为SendFile()方法是多线程访问的而在SendFile()方法中又调用了SendMessage()方法。我另外编写了一个SendStatus类它用来记录和打印发送完成的状态已经发送了多少字节完成度是百分之多少等等。本来这个类的内容我是直接写入在Client类中的后来我觉得它执行的工作已经不属于Client本身所应该执行的领域之内了我记得这样一句话当你觉得类中的方法与类的名称不符的时候那么就应该考虑重新创建一个类。我觉得用在这里非常恰当。下面是SendStatus的内容 // 即时计算发送文件的状态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); } // 获得文件发送的百分比 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); //保留一位小数 if (percent.ToString() 100.0) return 100; else return percent.ToString(); }} 3.3程序测试 接下里我们运行一下程序来检查一下输出首先看下服务端 接着是客户端我们能够看到发送的字节数和进度可以想到如果是图形界面那么我们可以通过扩展SendStatus类来创建一个进度条 最后我们看下服务端的Bin\Debug目录应该可以看到接收到的图片 本来我想这篇文章就可以完成发送和接收不过现在看来没法实现了因为如果继续下去这篇文章就太长了我正尝试着尽量将文章控制在15页以内。那么我们将在下篇文章中再完成接收文件这一部分。转载于:https://www.cnblogs.com/Arlar/p/5860341.html