客户端发送数据


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()生成了一个与时间有关的随机名称。

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. 程序测试

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

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

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

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

 

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
张国生
下一篇:Part5
评论列表

发表评论

评论内容
昵称:
关联文章

客户发送数据
客户发送,服务接收并输出
客户接收文件
客户的实现
服务获取客户连接
2.客户与服务连接
FTP客户工具 FileZilla
消息发送时的问题
FTP客户工具 WinSCP ( 推荐,免费 )
服务回发,客户接收并输出
FTP客户工具 FlashFXP (推荐)
GZUpdate自动升级程序客户演示
网易闪电邮客户中配置企业邮箱的方法
ABP VNext框架中Winform终端的开发和客户授权信息的处理
FTP服务软件 Xlight
GZUpdate自动升级服务 .NET C/S Winform客户程序自动升级演示
C# 邮件发送,阿里云邮箱参数设置,邮件发送测试工具下载
服务实现
FTP服务软件 Serv-U
有限在线用户的场景中,前后分离是多此一举