《编程-C#网络编程》由会员上传分享,免费在线阅读,更多相关内容在行业资料-天天文库。
C#网络编程(基本概念和操作)-Part.l引言C#网络编程系列文章计划简单地讲述网络编程方面的基础知识,由于本人在这方面功カ有限,所以只能提供ー些初步的入门知识,希望能对刚开始学习的朋友提供一些帮助。如果想要更加深入的内容,可以参考相关书籍。本文是该系列第一篇,主要讲述了基于套接字(Socket)进行网络编程的基本概念,其中包括TCP协议、套接字、聊天程序的三种开发模式,以及两个基本操作:侦听端口、连接远程服务端;第二篇讲述了一个简单的范例:从客户端传输字符串到服务端,服务端接收并打印字符串,将字符串改为大写,然后再将字符串回发到客户端,客户端最后打印传回的字符串;第三篇是第二篇的一个强化,讲述了第二篇中没有解决的ー个问题,并使用了异步传输的方式来完成和第二篇同样的功能;第四篇则演示了如何在客户端与服务端之间收发文件;第五篇实现了一个能够在线聊天并进行文件传输的聊天程序,实际上是对前面知识的一个综合应用。与本文相关的还有-一篇文章是:C#编写简单的聊天程序,但这个聊天程序不及本系列中的聊天程序功能强大,实现方式也不相同。网络编程基本概念1.面向连接的传输协议:TCP对于TCP协议我不想说太多东西,这属于大学课程,又涉及计算机科学,而我不是“学院派”,对于这部分内容,我觉得作为开发人员,只需要掌握与程序相关的概念就可以了,不需要做太艰深的研究。我们苜先知道TCP是面向连接的,它的意思是说两个远程主机(或者叫进程,因为实际上远程通信是进程之间的通信,而进程则是运行中的程序),必须首先进行ー个握手过程,确认连接成功,之后才能传输实际的数据。比如说进程A想将字符串“It'safinedaytoday"发给进程B,
1它首先要建立连接。在这ー过程中,它首先需要知道进程B的位置(主机地址和端口号)。随后发送・个不包含实际数据的请求报文,我们可以将这个报文称之为“hello”。如果进程B接收到了这个“hello”,就向进程A回复ー个“hello”,进程A随后オ发送实际的数据“It'safinedaytoday”。关于TCP第二个需要了解的,就是它是全双工的。意思是说如果两个主机上的进程(比如进程A、进程B),一旦建立好连接,那么数据就既可以由A流向B,也可以由B流向A。除此以外,它还是点对点的,意思是说ー个TCP连接总是两者之间的,在发送中,通过ー个连接将数据发给多个接收方是不可能的。TCP还有一个特性,就是称为可靠的数据传输,意思是连接建立后,数据的发送一定能够到达,并且是有序的,就是说发的时候你发了ABC,那么收的一方收到的也一定是ABC,而不会是BCA或者别的什么。编程中与TCP相关的最重要的ー个概念就是套接字.我们应该知道网络七层协议,如果我们将上面的应用程、表示层、会话层笼统地算作一层(有的教材便是如此划分的),那么我们编写的网络应用程序就位于应用层,而大家知道TCP是属于传输层的协议,那么我们在应用层如何使用传输层的服务呢(消息发送或者文件上传下载)?大家知道在应用程序中我们用接口来分离实现,在应用层和传输层之间,则是使用套接字来进行分离。它就像是传输层为应用层开的ー个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据;而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,我们是不知道也不需要知道的,我们也不会关心它如何传输,这属于网络其它层次的工作。举个例子,如果你想写封邮件发给远方的朋友,那么你如何写信、将信打包,属于应用层,信怎么写,怎么打包完全由我们做主;而当我们将信投入邮筒时,邮筒的那个口就是套接字,在进入套接字之后,就是传输层、网络层等(邮局、公路交管或者航线等)其它层次的工作了。我们从来不会去关心信是如何从西安发往北京的,我们只知道写好了投入邮筒就0K了。可以用下面这两幅图来表示它;
2www.tracefact.net注意在上面图中,两个主机是对等的,但是按照约定,我们将发起请求的一方称为客户端,将另一端称为服务端。可以看出两个程序之间的对话是通过套接字这个出入口来完成的,实际上套接字包含的最重要的也就是两个信息:连接至远程的本地的端口信息(本机地址和端口号),连接到的远程的端口信息(远程地址和端口号)。注意上面词语的微妙变化,ー个是本地地址,一个是远程地址。这里又出现了了一个名词端口。一般来说我们的计算机上运行着非常多的应用程序,它们可能都需要向远程主机打交道,所以远程主机就需要有一个ID来标识它想与本地机器上的哪个应用程序打交道,这里的ID就是端口。将端口分配给ー个应用程序,那么来自这个端口的数据则总是针对这个应用程序的。有这样一个很好的例子:可以将主机地址想象为电话号码,而将端口号想象为分机号。
3在.NET中,尽管我们可以直接对套接字编程,但是.NET提供了两个类将対套接字的编程进行了一个封装,使我们的使用能够更加方便,这两个类是TcpClient和TcpListener,它与套接字的关系如下:从上面图中可以看出TcpClient和TcpListener对套接字进行了封装。从中也可以看出,TcpListener用于接受连接请求,而TcpClient则用于接收和发送流数据这幅图的意思是TcpListener持续地保持对端口的侦听,一旦收到ー个连接请求后,就可以获得一个TcpClient对象,而对于数据的发送和接收都有TcpClient去完成。此时,TcpListener并没有停止工作,它始终持续地保持对端口的侦听状态。我们考虑这样ー种情况:两台主机,主机A和主机B,起初它们谁也不知道谁在哪儿,当它们想要进行对话时,总是需要有一方发起连接,而另一方则需要对本机的某一端口进行侦听。而在侦听方收到连接请求、并建立起连接以后,它们之间进行收发数据时,发起连接的一方并不需要再进行侦听。因为连接是全双工的,它可以使用现有的连接进行收发数据。而我们前面已经做了定义:将发起连接的一方称为客户端,另一段称为服务端,则现在可以得出:总是服务端在使用TcpListener类,因为它需要建立起一个初始的连接。1.网络聊天程序的三种模式实现ー个网络聊天程序本应是最后ー篇文章的内容,也是本系列最后的ー个程序,来作为ー个终结。但是我想后面更多的是编码,讲述的内容应该不会太多,所以还是把讲述的东西都放到这里吧。
4网络聊天实现模式1www.tracefact.net当采用这种模式时,即是所谓的完全点対点模式,此时每台计算机本身也是服务器,因为它需要进行端口的侦听。实现这个模式的难点是:各个主机(或终端)之间如何知道其它主机的存在?此时通常的做法是当某ー主机上线时,使用UDP协议进行ー个广播(Broadcast),通过这种方式来“告知”其它主机自己已经在线并说明位置,收到广播的主机发回ー个应答,此时主机便知道其他主机的存在。这种方式我个人并不喜欢,但在C#编り简单的聊天程序这篇文章中,我使用了这种模式,可惜的是我没有实现广播,所以还很不完善。
5网络聊天实现模式2第二种方式较好的解决了上面的问题,它引入了服务器,由这个服务器来专门进行广播。服务器持续保持对端口的侦听状态,每当有主机上线时,首先连接至服务器,服务器收到连接后,将该主机的位置(地址和端口号)发往其他在线主机(绿色箭头标识)。这样其他主机便知道该主机已上线,并知道其所在位置,从而可以进行连接和对话。在服务器进行了广播之后,因为各个主机已经知道了其他主机的位置,因此主机之间的对话就不再通过服务器(黑色箭头表示),而是直接进行连接。因此,使用这种模式时,各个主机依然需要保持对端口的侦听。在某台主机离线时,与登录时的模式类似,服务器会收到通知,然后转告给其他的主机。第三种模式是我觉得最简单也最实用的ー种,主机的登录与离线与第二种模式相同。注意到每台主机在上线时首先就与服务器建立了连接,那么从主机A发往主机B发送消息,就可以通过这样一条路径,主机A->服务器主机B,通过这种方式,各个主机不
6需要在对端口进行侦听,而只需要服务器进行侦听就可以了,大大地简化了开发。而对于ー些较大的文件,比如说图片或者文件,如果想由主机A发往主机B,如果通过服务器进行传输效率会比较低,此时可以临时搭建一个主机A至主机B之间的连接,用于传输大文件。当文件传输结束之后再关闭连接(桔红色箭头标识)。除此以外,由于消息都经过服务器,所以服务器还可以缓存主机间的对话,即是说当主机A发往主机B时,如果主机B己经离线,则服务器可以对消息进行缓存,当主机B下次连接到服务器时,服务器自动将缓存的消息发给主机B。本系列文章最后采用的即是此种模式,不过没有实现过多复杂的功能。接ド来我们的理论知识告一段落,开始下ー阶段ーー漫长的编码。基本操作1.服务端对端口进行侦听
7接下来我们开始编写ー些实际的代码,第一步就是开启对本地机器上某一端U的侦听。首先创建一个控制台应用程序,将项目名称命名为ServerConsole,它代表我们的服务端。如果想要与外界进行通信,第一件要做的事情就是开启对端口的侦听,这就像为计算机打开了一个“门”,所有向这个“门”发送的请求(“敲门”)都会被系统接收到。在C#中可以通过下面几个步骤完成,首先使用本机Ip地址和端口号创建一个System.Net.Sockets.TcpListener类型的实例,然后在该实例上调用Start。方法,从而开启对指定端口的侦听。usingSystem.Net;//引入这两个命名空间,以下同usingSystem.Net.Sockets;using...〃略classServer{staticvoidMain(string[]args){Console.WriteLine(*Serverisrunning...");IPAddressip=newIPAddress(newbyte[](127,0,0,1));TcpListenerlistener=newTcpListener(ip,8500);listener.Start();/Z开始侦听Console.WriteLine("StartListening...");Console.WriteLine("
8
9输入、"Q\"键退出。“);
10dokey=Console.ReadKey(true).Key;}while(key!=ConsoleKey.Q);/Z获得IPAddress对象的另外几种常用方法:IPAddressip=IPAddress.Parse(*127.0.0.1*);IPAddressip=Dns.GetHostEntry("localhost").AddressList[0]上面的代码中,我们开启了对8500端口的侦听。在运行了上面的程序之后,然后打开“命令提示符”,输入“netstat-a”,可以看到计算机器中所有打开的端口的状态。可以从中找到8500端口,看到它的状态是LISTENING,这说明它已经开始了侦听:TCPjimmy:10300.0.0.0:0LISTENINGTCPjimmy:36030.0.0.0:0LISTENINGTCPjimmy:85000.0.0.0:0LISTENINGTCPjimmy:netbios-ssn0.0.0.0:0LISTENING在打开了对端口的侦听以后,服务端必须通过某种方式进行阻塞(比如Console.ReadKey()),使得程序不能够因为运行结束而退出。否则就无法使用“netstat-a”看到端口的连接状态,因为程序已经退出,连接会自然中断,再运行“netstat-a”当然就不会显示端口了。所以程序最后按“Q”退出那段代码是必要的,下面的每段程序都会含有这个代码段,但为了节省空间,我都省略掉了。
111.客户端与服务端连接1.1单ー客户端与服务端连接当服务器开始对端U侦听之后,便可以创建客户端与它建立连接。这ー步是通过在客户端创建一个TcpClient的类型实例完成。每创建一个新的TcpClient便相当于创建了一个新的套接字Socket去与服务端通信,.Net会自动为这个套接字分配•个端口号,上面说过,TcpClient类不过是对Socket进行了一个包装。创建TcpClient类型实例时,可以在构造函数中指定远程服务器的地址和端口号。这样在创建的同时,就会向远程服务端发送一个连接请求(“握手”),一旦成功,则两者间的连接就建立起来了。也可以使用重载的无参数构造函数创建对象,然后再调用Connect。方法,在Connect()方法中传入远程服务器地址和端口号,来与服务器建立连接。这里需要注意的是,不管是使用有参数的构造函数与服务器连接,或者是通过Connect。方法与服务器建立连接,都是同步方法(或者说是阻塞的,英文叫block)。它的意思是说,客户端在与服务端连接成功、从而方法返回,或者是服务端不存、从而抛出异常之前,是无法继续进行后继操作的。这里还有一个名为BeginConnect。的方法,用于实施异步的连接,这样程序不会被阻塞,可以立即执行后面的操作,这是因为可能由于网络拥塞等问题,连接需要较长时间才能完成。网络编程中有非常多的异步操作,凡事都是山简入难,关于异步操作,我们后面再讨论,现在只看同步操作。创建一个新的控制台应用程序项目,命名为Clientconsole,它是我们的客户端,然后添加下面的代码,创建与服务器的连接:classClient{staticvoidMain(string[]args){Console.WriteLine〈'ClientRunning...);TcpClientclient=newTcpClient();
128500);/Zワ服务器连接}catch(Exceptionex){Console.WriteLine(ex.Message);return;)/Z打印连接到的服务端信息Console.WriteLine('ServerConnected!{0}->{1}\client.Client.LocalEndPoint,client.Client.RemoteEndPoint);〃按Q退出上面带代码中,我们通过调用Connect。方法来与服务端连接。随后,我们打印了这个连接消息:本机的Ip地址和端口号,以及连接到的远程Ip地址和端口号。TcpClient的Client属性返回了一个Socket对象,它的LocalEndPoint和RemoteEndPoint属性分别包含了本地和远程的地址信息。先运行服务端,再运行这段代码。可以看到两边的输出情况如下:/Z服务端:Serverisrunning...StartListening.../Z客户端:ClientRunning...ServerConnected!127.0.0.1:4761—>127.0.0.1:8500
13我们看到客户端使用的端口号为4761,上面已经说过,这个端口号是由.NET随机选取的,并不需要我们来设置,并且每次运行时,这个端口号都不同。再次打开“命令提示符”,输入“netstat-a”,可以看到下面的输出:TCPjimmy:85000.0.0.0:0LISTENINGTCPjimmy:8500localhost:4761ESTABLISHEDTCPjimmy:4761localhost:8500ESTABLISHED从这里我们可以得出几个重要信息:1、端口8500和端口4761建立了连接,这个4761端口便是客户端用来与服务端进行通信的端口;2、8500端口在与客户端建立起一个连接后,仍然继续保持在监听状态。这也就是说ー个端口可以与多个远程端口建立通信,这是显然的,大家众所周之的HTTP使用的默认端口为80,但是ー个Web服务器要通过这个端口与多少个浏览器通信啊。2.2多个客户端与服务端连接那么既然ー个服务器端口可以应对多个客户端连接,那么接下来我们就看一下,如何让多个客户端与服务端连接。如同我们上面所说的,一个TcpClient就是ー个Socket,所以我们只要创建多个TcpClient,然后再调用Connect()方法就可以了:classClient{staticvoidMain(string[]args){Console.WriteLineC'ClientRunning・・・);TcpClientclient;for(inti=0;i<=2;i++){
14client=newTcpClient();client.Connect(z,localhost”,8500);/Z与服务器连接}catch(Exceptionex){Console.WriteLine(ex.Message);return;}/Z打印连接到的服务端信息Console.WriteLine("ServerConnected!{0}—>{1}*,client.Client.LocalEndPoint,client.Client.RemoteEndPoint);)//按Q退出)}上面代码最重要的就是client=newTcpClient()这句,如果你将这个声明放到循环外面,再循环的第二趟就会发生异常,原因很显然:ー个TcpClient对象对应ーt*Socket,ー个Socket对应着ー个端口,如果不使用new操作符重新创建对象,那么就相当于使用ー个已经与服务端建立了连接的端口再次与远程建立连接此时,如果在“命令提示符"运行“netstat-a”,则会看到类似下面的的输出:TCPjimmy:85000.0.0.0:0LISTENING
15TCPjimmy:8500localhost:10282ESTABLISHED
16TCPjimmy:8500localhost:10283ESTABLISHEDTCPjimmy:8500localhost:10284ESTABLISHEDTCPjimmy:10282localhost:8500ESTABLISHEDTCPjimmy:10283localhost:8500ESTABLISHEDTCPjimmy:10284localhost:8500ESTABLISHED可以看到创建了三个连接对,并且8500端口持续保持侦听状态,从这里以及上面我们可以推断出TcpListener的Start()方法是ー个异步方法。2.服务端获取客户端连接2.1获取单一客户端连接上面服务端、客户端的代码已经建立起了连接,这通过使用“netstat-a”命令,从端口的状态可以看出来,但这是操作系统看诉我们的。那么我们现在需要知道的就是:服务端的程序如何知道已经与一个客户端建立起了连接?服务器端开始侦听以后,可以在TcpListener实例上调用AcceptTcpClientO来获取与ー个客户端的连接,它返回ー个TcpClient类型实例。此时它所包装的是由服务端去往客户端的Socket,而我们在客户端创建的TcpClient则是由客户端去往服务端的。这个方法是ー个同步方法(或者叫阻断方法,blockmethod),意思就是说,当程序调用它以后,它会一直等待某个客户端连接,然后才会返回,否则就会一直等下去。这样的话,在调用它以后,除非得到ー个客户端连接,不然不会执行接下来的代码。ー个很好的类比就是Console.ReadLineO方法,它读取输入在控制台中的•行字符串,如果有输入,就继续执行下面代码:如果没有输入,就会一直等待下去。
17classServer{staticvoidMain(string[]args){Console.WriteLine("Serverisrunning...");1PAddressip=new[PAddress(newbyte[](127,0,0,1));TcpListenerlistener=newTcpListener(ip,8500);listener.Start();/Z开始侦听Console.WriteLine("StartListening...");/Z获取ー个连接,中断方法TcpClientremoteClient=listener.AcceptTcpClient();/Z打印连接到的客户端信息Console.WriteLine〈ClientConnected!{0}<-{1}\remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);〃按Q退出))运行这段代码,会发现服务端运行到listener.AcceptTcpClient。时便停止了,并不会执行下面的Console.WriteLine()方法。为了让它继续执行下去,必须有一个客户端连接
18到它,所以我们现在运行客户端,与它进行连接。简单起见,我们只在客户端开启ー个端U与之连接:classClient{staticvoidMain(string[]args){Console.WriteLine(AClientRunning・・・");FcpCiientclient=newTcpClient();try{client.Connect("localhost”,8500);/Z与服务器连接}catch(Exceptionex){Console.WriteLine(ex.Message);return;)/Z打印连接到的服务端信息Console.WriteLine(*ServerConnected!{0}一>⑴”,client.Client.LocalEndPoint,client.Client.RemoteEndPoint);//按Q退出))此时,服务端、客户端的输出分别为:/Z服务端Serverisrunning...
19StartListening...
20ClientConnected!127.0.0.1:8500<一127.0.0.1:5188/Z客户端ClientRunning...ServerConnected!127.0.0.1:5188—>127.0.0.1:85003.2获取多个客户端连接现在我们再接着考虑,如果有多个客户端发动对服务器端的连接会怎么样,为了避免你将浏览器向上滚动,来查看上面的代码,我将它拷贝了下来,我们先看下客户端的关键代码:TcpClientclient;for(inti=0;i<=2;i++){try(client=newTcpClient();client.ConnectC'localhost*,8500);/Z与服务器连接}catch(Exceptionex){Console.WriteLine(ex.Message);return;)/Z打印连接到的服务端信息Console.WriteLine("ServerConnected!{0}->⑴",client.Client.LocalEndPoint,
21client.Client.RemoteEndPoint);如果服务端代码不变,我们先运行服务端,再运行客户端,那么接下来会看到这样的输出:/Z服务端Serverisrunning...StartListening...ClientConnected!127.0.0.1:8500<-127.0.0.1:5226/Z客户端ClientRunning...ServerConnected!127.0.0.1:5226-->127.0.0.1:8500ServerConnected!127.0.0.1:5227—>127.0.0.1:8500ServerConnected!127.0.0.1:5228-->127.0.0.1:8500就又回到了本章第2.2小节“多个客户端与服务端连接”中的处境:尽管有三个客户端连接到了服务端,但是服务端程序只接收到了一个。这是因为服务端只调用了一次listener.AcceptTcpClient(),而它只对应ー个连往客户端的Socket。但是操作系统是知道连接已经建立了的,只是我们程序中没有处理到,所以我们当我们输入“netstat-a”时,仍然会看到3对连接都已经建立成功。为了能够接收到三个客户端的连接,我们只要对服务端稍稍进行一下修改,将AcceptTcpClient方法放入ー个do/while循环中就可以了:Console.WriteLine("StartListening
22while(true){/Z获取ー个连接,同步方法TcpClientremoteClient=listener.AcceptTcpClient();/Z打印连接到的客户端信息Console.WriteLine('ClientConnected!{0}<⑴〃,remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);)这样看上去是ー个死循环,但是并不会让你的机器系统资源迅速耗尽。因为前面已经说过了,AcceptTcpClient()再没有收到客户端的连接之前,是不会继续执行的,它的大部分时间都在等待。另外,服务端几乎总是要保持在运行状态,所以这样做并无不可,还可以省去“按Q退出”那段代码。此时再运行代码,会看到服务端可以收到3个客户端的连接了。Serverisrunning...StartListening...ClientConnected!127.0.0.1:8500<一127.0.0.1:5305ClientConnected!127.0.0.1:8500<ーー127.0.0.1:5306ClientConnected!127.0.0.1:8500<-127.0.0.1:5307本篇文章到此就结束了,接ド来ー篇我们来看看如何在服务端与客户端之间收发数据。
23C#网络编程(同步传输字符串)-Part.2服务端客户端通信在与服务端的连接建立以后,我们就可以通过此连接来发送和接收数据。端口与端口之间以流(Stream)的形式传输数据,因为几乎任何对象都可以保存到流中,所以实际上可以在客户端与服务端之间传输任何类型的数据。对客户端来说,往流中写入数据,即为向服务器传送数据;从流中读取数据,即为从服务端接收数据。对服务端来说,往流中写入数据,即为向客户端发送数据;从流中读取数据,即为从客户端接收数据。同步传输字符串我们现在考虑这样ー个任务;客户端打印ー串字符串,然后发往服务端,服务端先输出它,然后将它改为大写,再回发到客户端,客户端接收到以后,最后再次打印一•遍它。我们将它分为两部分;1、客户端发送,服务端接收并输出;2、服务端回发,客户端接收并输出。1.客户端发送,服务端接收并输出1.1服务端程序我们可以在TcpClient上调用GetStreamO方法来获得连接到远程计算机的流。注意这里我用了远程这个词,当在客户端调用时,它得到连接服务端的流;当在服务端调用时,它获得连接客户端的流。接下来我们来看一下代码,我们先看服务端(注意这里没有使用do/while循环):classServer{staticvoidMain(string[]args){constintBufferSize=8192;/Z缓存大小,8192字节
24Console.WriteLine(*Serverirunning...");IPAddressip=newIPAddress(newbyte[]{127,0,0,1));TcpListenerlistener=newTcpListener(ip,8500);listener.Start();/Z开始侦听Console.WriteLine("StartListening...*);/Z获取ー个连接,中断方法TcpClientremoteClient=listener.AcceptTcpClient();/Z打印连接到的客户端信息Console.WriteLine("ClientConnected!{0}<-⑴",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);/Z获得流,并写入buffer中\etworkStreamstreamToClient=remoteClient.GetStreamO;byte[]buffer=newbyte[BufferSize];intbytesRead=streamToClient.Read(buffer,0,BufferSize);Console.WriteLine("Readingdata,(0)bytes...",bytesRead);
25/Z获得请求的字符串stringmsg=Encoding.Unicode.GetString(buffer,0,bytesRead);Console.WriteLine(*Received:{0}",//按Q退出这段程序的上半部分一经很熟悉了,我就不再解释。remoteClient.GetStreamO方法获取到了连接至客户端的流,然后从流中读出数据并保存在了buffer缓存中,随后使用Encoding.Unicode.GetStringO方法,从缓存中获取到了实际的字符串。最后将字符串打印在了控制台上。这段代码有个地方需要注意:在能够读取的字符串的总字节数大于BufferSize的时候会出现字符串截断现象,因为缓存中的数目总是有限的,而对于大对象,比如说闇片或者其它文件来说,则必须采用“分次读取然后转存”这种方式,比如这样:/Z获取字符串byte[]buffer=newbyte[BufferSize];intbytesRead;/Z读取的字节数MemoryStreammsStream=newMemoryStream();do{bytesRead=streamToClient.Read(buffer,0,BufferSize);msStream.Write(buffer,0,bytesRead);}while(bytesRead>0);buffer=msStream.GetBuffer();
26stringmsg=Encoding.Unicode.GetString(buffer);这里我没有使用这种方法,ー个是因为不想关注在太多的细节上面,一个是因为对于字符串来说,8192字节已经很多了,我们通常不会传递这么多的文本。当使用Unicode编码时,8192字节可以保存4096个汉字和英文字符。使用不同的编码方式,占用的字节数有很大的差异,在本文最后面,有一段小程序,可以用来测试Unicode、UTF8、ASCII三种常用编码方式对字符串编码时,占用的字节数大小。现在对客户端不做任何修改,然后运行先运行服务端,再运行客户端。结果我们会发现这样ー件事:服务端再打印完"ClientConnected!127.0.0.1:8500<―127.0.0.1:xxxxx”之后,再次被阻塞了,面没有输出"Readingdata,{0}bytes..."。可见,与AcceptTcpClient()方法类似,这个Read()方法也是同步的,只有当客户端发送数据的时候,服务端オ会读取数据、运行此方法,否则它便会一直等待。1.1客户端程序接下来我们编写客户端向服务器发送字符串的代码,与服务端类似,它先获取连接服务器端的流,将字符串保存到buffer缓存中,再将缓存写入流,写入流这ー过程,相当于将消息发往服务端。classClient{staticvoidMain(string[]args){Console.WriteLine(*ClientRunningTcpClientclient;try(client=newTcpClient();client.Connect(z/localhost*,8500);/Z与服务器连接}catch(Exceptionex){
27Console.WriteLine(ex.Message);return;)/Z打印连接到的服务端信息Console.WriteLine("ServerConnected!{0}->⑴”,client.Client.LocalEndPoint,client.Client.RemoteEndPoint);stringmsg=\"WeicomeToTraceFact.Net\*";NetworkstreamstreamToServer=client.GetStreamO;byte[]buffer=Encoding.Unicode.GetBytes(msg);/Z获得缓存streamToServer.Write(buffer,0,buffer.Length);/Z发往服务器Console.WriteLine("Sent:{0}",msg);〃按Q退出}}现在再次运行程序,得到的输出为:/Z服务端Serverisrunning...StartListening...
28ClientConnected!127.0.0.1:8500<--
29127.0.0.1:7847Readingdata,52bytes...Received:"WelcomeToTraceFact.Net”输入"Q"键退出。/Z客户端ClientRunning...ServerConnected!127.0.0.1:7847—>127.0.0.1:8500Sent:zWelcomeToTraceFact.Net"输入〃Q〃键退出。再继续进行之前,我们假设客户端可以发送多条消息,而服务端要不断的接收来自客户端发送的消息,但是上面的代码只能接收客户端发来的一条消息,因为它ー经输出了“输入Q键退出",说明程序已经执行完毕,无法再进行任何动作。此时如果我们再开启ー个客户端,那么出现的情况是:客户端可以与服务器建立连接,也就是netstat-a显示为ESTABLISHED,这是操作系统所知道的;但是由于服务端的程序已经执行到了最后ー步,只能输入Q键退出,无法再采取任何的动作。回想ー个上面我们需要一个服务器对应多个客户端时,对AcceptTcpClientO方法的处理办法,将它放在了do/while循环中;类似地,当我们需要一个服务端对同一个客户端的多次请求服务时,可以将Read。方法放入到do/while循环中。现在,我们大致可以得出这样几个结论:»如果不使用do/while循环,服务端只有一个listener.AcceptTcpClient()方法和一个TcpClient.GetStream().Read()方法,则服务端只能处理到同一客户端的一条请求。
30»如果使用ー个do/while循环,并将listener.AcceptTcpClient()方法和TcpClient.GetStreamO.ReadO方法都放在这个循环以内,那么服务端将可以处理多个客户端的ー条请求。»如果使用ー个do/while循环,并将listener.AcceptTcpClient()方法放在循环之外,将TcpClient.GetStreamO.Read()方法放在循环以内,那么服务端可以处理一个客户端的多条请求。»如果使用两个do/while循环,对它们进行分别嵌套,那么结果是什么呢?结果并不是可以处理多个客户端的多条请求。因为里层的do/while循环总是在为ー个客户端服务,因为它会中断在TcpClient.GetStream().Read()方法的位置,而无法执行完毕。即使可以通过某种方式让里层循环退出,比如客户端往服务端发去“exit”字符串时,服务端也只能挨个对客户端提供服务。如果服务端想执行多个客户端的多个请求,那么服务端就需要采用多线程。主线程,也就是执行外层do/while循环的线程,在收到ー个TcpClient之后,必须将里层的do/while循环交给新线程去执行,然后主线程快速地重新冋到
31listener.AcceptTcpClient()的位置,以响应其它的客户端。对于第四种情况,实际上是构建・个服务端更为通常的情况,所以需要专门开辟ー个章节讨论,这里暂且放过。而我们上面所做的,即是列出的第一种情况,接下来我们再分别看一下第二种和第三种情况。对于第二种情况,我们按照上面的叙述先对服务端进行ー下改动:do(/Z获取ー个连接,中断方法TcpClientremoteClient=listener.AcceptTcpClient();/Z打印连接到的客户端信息Console.WriteLine(/ZC1ientConnectedI{0}<"⑴”,remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);/Z获得流,并写入buffer中NetworkstreamstreamToClient=remoteClient.GetStreamO;byte[]buffer=newbyte[BufferSize];intbytesRead=streamToClient.Read(buffer,0,BufferSize);Console.WriteLine("'Readingdata,{0}bytes...,bytesRead);/Z获得请求的字符串stringmsg=Encoding.Unicode.GetString(buffer,0,
32bytesRead);Console.WriteLine('Received:{0}",msg);}while(true);然后启动多个客户端,在服务端应该可以看到下面的输出(客户端没有变化):Serverisrunning...StartListening...ClientConnected!127.0.0.1:8500<一127.0.0.1:8196Readingdata,52bytes...Received:"WelcomeToTraceFact.Net*ClientConnected!127.0.0.1:8500<-127.0.0.1:8199Readingdata,52bytes...Received:"WelcomeToTraceFact.Net"由第2种情况改为第3种情况,只需要将do向ド挪动几行就可以了:/Z获取ー个连接,中断方法TcpClientremoteClient=listener.AcceptTcpClient();/Z打印连接到的客户端信息Console.WriteLine("ClientConnected!{0}<--{1}〃,remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);/Z获得流,并写入buffer中NetworkstreamstreamToClient=remoteClient.GetStreamO;
33do(byte[]buffer=newbyte[BufferSize];intbytesRead=streamToClient.Read(buffer,0,BufferSize);Console.WriteLine(^Readingdata,{0}bytes.・.bytesRead);/Z获得请求的字符串stringmsg=Encoding.Unicode.GetString(buffer,0,bytesRead);Console.WriteLine("Received:{0}",msg);}while(true);然后我们再改动一下客户端,让它发送多个请求。当我们按下S的时候,可以输入ー行字符串,然后将这行字符串发送到服务端;当我们输入X的时候则退出循环:NetworkStreamstreamToServer=client.GetStreamO;ConsoleKeykey;Console.WriteLine("Menu:S-Send,X-Exit");do(key=Console.ReadKey(true).Key;if(key=ConsoleKey.S){/Z获取输入的字符串Console.Write("Inputthemessage:");stringmsg=Console.ReadLineO;byte[]buffer=
34缓存streamToServer.Write(buffer,0,buffer.Length);/Z发往服务器Console.WriteLineC^Sent:{0}*,msg);)}while(key!=ConsoleKey.X);接下来我们先运行服务端,然后再运行客户端,输入一些字符串,来进行测试,应该能够看到下面的输出结果:/Z服务端Serverisrunning...StartListening...ClientConnected!127.0.0.1:8500<—127.0.0.1:11004Readingdata,44bytes...Received:欢迎访问我的博客:TraceFact.NetReadingdata,14bytes...Received:我们ー起进步!〃客户端ClientRunning...ServerConnected!127.0.0.1:11004—>127.0.0.1:8500Menu:S-Send,X-ExitInputthemessage:欢迎访问我的博客:TraceFact.NetSent:欢迎访问我的博客:TraceFact.NetInputthemessage:我们ー起进步!Sent:我们ー起进步!
35这里还需要注意・点,当客户端在TcpClient实例上调用Close。方法,或者在流上调用Dispose。方法,服务端的streamToClient.Read。方法会持续地返回0,但是不抛出异常,所以会产生一个无限循环;而如果直接关闭掉客户端,或者客户端执行完毕但没有调用stream.Dispose。或者TcpClient.Close(),如果服务器端此时仍阻塞在Read。方法处,则会在服务器端抛出异常:“远程主机强制关闭了一•个现有连接”。因此,我们将服务端的streamToClient.Read。方法需要写在ー个try/catch中。同理,如果在服务端已经连接到客户端之后,服务端调用remoteClient.Close。,则客户端会得到异常’’无法将数据写入传输连接:您的主机中的软件放弃了一个己建立的连接。”;而如果服务端直接关闭程序的话,则客户端会得到异常“无法将数据写入传输连接:远程主机强迫关闭了一个现有的连接。”。因此,它们的读写操作必须都放入到try/catch块中。2.服务端回发,客户端接收并输出2.2服务端程序我们接着再进行进ー步处理,服务端将收到的字符串改为大写,然后回发,客户端接收后打印。此时它们的角色和上面完全进行了一下对调:对于服务端来说,就好像刚オ的客户端ー样,将字符串写入到流中;而客户端则同服务端ー样,接收并打印。除此以外,我们最好对流的读写操作加上lock,现在我们直接看代码,首先看服务端:classServer{staticvoidMain(string[]args){constintBufferSize=8192;/Z缓存大小,8192BytesConsoleKeykey;Console.WriteLine(*Serverisrunning...");IPAddressip=newIPAddress(newbyte[](127,0,0,1));TcpListenerlistener=new
36TcpListener(ip,8500);listener.Start();/Z开始侦听Console.WriteLine(*StartListening...);/Z获取ー个连接,同步方法,在此处中断TcpClientremoteClient=listener.AcceptTcpClient();/Z打印连接到的客户端信息ConsoleeWriteLineC*ClientConnected!{0}<-⑴",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);/Z获得流XetworkStreamstreamToClient=remoteClient.GetStreamO;do{//写入buffer中byte[]buffer=newbyte[BufferSize];intbytesRead;try(lock(streamToClient){bytesRead=streamToClient.Read(buffer,0,BufferSize);
37if(bytesRead==0)thrownewException,读取到〇字节つ;Console.WriteLine(^Readingdata,{0}bytes..,bytesRead);/Z获得请求的字符串stringmsg=Encoding。Unicode.GetString(buffer,0,bytesRead);Console.WriteLine("Received:{0}",msg);/Z转换成大写并发送msg=msg.ToUpper();buffer=Encoding.Unicode.GetBytes(msg);lock(streamToClient){streamToClient.Write(buffer,0,buffer.Length);)Console.WriteLine("Sent:{0}",msg);}catch(Exceptionex){Console.WriteLine(ex.Message);break;}while(true);
38streamToClient.Dispose();remoteClient.Close();Console.WriteLine(*
39
40输入、"Q\"键退出。”);do{key=Console.ReadKey(true).Key;}while(key!=ConsoleKey.Q);))接下来是客户端:classClient(staticvoidMain(string[]args){Console.WriteLine("ClientRunning...");TcpClientclient;ConsoleKeykey;constintBufferSize=8192;try{client=newTcpClient();client.Connect("localhost",8500);/Z与服务器连接}catch(Exceptionex){Console.WriteLine(ex.Message);return;
41/Z打印连接到的服务端信息Console.WriteLine('ServerConnected!{0}->⑴”,client.Client.LocalEndPoint,client.Client.RemoteEndPoint);XetworkStreamstreamToServer=client.GetStreamO;Console.WriteLine(*Menu:S-Send,X-Exit*);do{key=Console.ReadKey(true).Key;if(key==ConsoleKey.S){/Z获取输入的字符串Console.Write('Inputthemessage:");stringmsg=Console.ReadLineO;byte[]buffer=Encoding.Unicode.GetBytes(msg);/Z获得缓存try(lock(streamToServer){streamToServer.Write(buffer,0,buffer.Length);/Z发往服务器Console.WriteLine(*Sent:
42{0}",msg);intbytesRead;buffer=newbyte[BufferSize];lock(streamToServer){bytesRead=streamToServer.Read(buffer,0,BufferSize);)msg=Encoding.Unicode.GetString(buffer,0,bytesRead);Console.WriteLine("Received:{0}",msg);}catch(Exceptionex){Console.WriteLine(ex.Message);break;)})while(key!=ConsoleKey.X);streamToServer.Dispose0;client.Close();Console.WriteLine("
43
44输入、"Q\"键退出。”);do(
45}while(key!=ConsoleKey.Q);最后我们运行程序,然后输入ー串英文字符串,然后看一下输出:/Z客户端Clientisrunning...ServerConnected!127.0.0.1:12662—>127.0.0.1:8500Menu:S-Send,X-ExitInputthemessage:Hello,I'mjimmyzhang.Sent:Hello,I'mjimmyzhang.Received:HELLO,I'MJIMMYZHANG./Z服务端Serverisrunning...StartListening...ClientConnected!127.0.0.1:8500<一127.0.0.1:12662Readingdata,46bytes...Received:Hello,I'mjimmyzhang.Sent:HELLO,I'MJIMMYZHANG.看到这里,我想你应该对使用TcpClient和TcpListener进行C#网络编程有了一个初步的认识,可以说是刚刚入门了,后面的路还很长。本章的所有操作都是同步操作,像上面的代码也只是作为ー个入门的范例,实际当中,ー个服务端只能为ー个客户端提供服务的情况是不存在的,下面就让我们来看看上面所说的第四种情况,如何进行异步的服务端编程。附录:ASCH、UTF8、Uncicode编码下的中英文字符大小
46privatestaticvoidShowCode(){string[]strArray={"b","abed","乙",甲乙丙丁”};byte[]buffer;stringmode,back;foreach(stringstrinstrArray){for(inti=0;i<=2;i++){if(i==0){buffer=l:ncoding.ASCII.GetBytes(str);back=Encoding.ASCII.GetString(buffer,0,buffer.Length);mode="ASCII";}elseif(i==1){buffer=I'ncoding.UTF8.GetBytes(str);back=Encoding.UTF8.GetString(buffer,0,buffer.Length);mode="UTF8";}else{buffer=Encoding.Unicode.GetBytes(str);back=Encoding.Unicode.GetString(buffer,0,buffer.Length);mode="Unicode”;
47Console.WriteLineC/Mode:{0},String:{1},Buffer.Length:{2}”,mode,str,buffer.Length);Console.WriteLine("Buffer:");for(intj=0;j<=buffer.Length-1;j++){Console.Write(buffer[j]+"つ;)Console.WriteLine("
48Retrived:{0}
49",back);)))输出为:Mode:ASCII,String:b,Buffer.Length:1Buffer:98Retrived:bMode:UTF8,String:b,Buffer.Length:1Buffer:98Retrived:b
50Mode:Unicode,String:b,Buffer.Length:2
51Retrived:bMode:ASCII,String:abed,Buffer.Length:4Buffer:979899100Retrived:abedMode:UTF8,String:abed,Buffer.Length:4Buffer:979899100Retrived:abedMode:Unicode,String:abed,Buffer.Length:8Buffer:9709809901000Retrived:abedMode:ASCII,String:乙,Buffer.Length:1Buffer:63Retrived:?Mode:UTF8,String:乙,Buffer.Length:3Buffer:228185153Retrived:乙Mode:Unicode,String:乙,Buffer.Length:2Buffer:8978Retrived:乙Mode:ASCII,String:甲乙丙丁,Buffer.Length:4Buffer:63636363
52Retrived:????
53Mode:UTF8,String:甲乙丙丁,Buffer.Length:12Buffer:231148178228185153228184153228184129Retrived:甲乙丙丁Mode:Unicode,String:甲乙丙丁,Buffer.Length:8Buffer:5011789782578178Retrived:甲乙丙丁大体上可以得出这么几个结论:・ASCII不能保存中文(貌似谁都知道。・UTF8是变长编码。在对ASCII字符编码时,UTF更省空间,只占1个字节,与ASCII编码方式和长度相同;Unicode在对ASCII字符编码时,占用2个字节,且第2个字节补零。・UTF8在对中文编码时需要占用3个字节;Unicode对中文编码则只需要2个字节。
54C#网络编程(异步传输字符串)-Part.3这篇文章我们将前进一大步,使用异步的方式来对服务端编程,以使它成为ー个真正意义上的服务器:可以为多个客户端的多次请求服务。但是开始之前,我们需要解决上一节中遗留的一个问题。消息发送时的问题这个问题就是:客户端分两次向流中写入数据(比如字符串)时,我们主观上将这两次写入视为两次请求:然而服务端有可能将这两次合起来视为一条请求,这在两个请求间隔时间比较短的情况下尤其如此。同样,也有可能客户端发出一条请求,但是服务端将其视为两条请求处理。下面列出了可能的情况,假设我们在客户端连续发送两条“WelcometoTracefact.net!",则数据到达服务端时可能有这样三种情况:Welc〇met〇TraceFact.comNOTE:在这里我们假设采用ASCII编码方式,因为此时上面的ー个方框正好代表ー个字节,而字符串到达末尾后为持续的〇(因为byte是值类型,且最小为〇)。上面的第一种情况是最理想的情况,此时两条消息被视为两个独立请求由服务端完整地接收。第二种情况的示意图如下,此时一条消息被当作两条消息接收了:we1c0met0TaceFactI-c0m00www.tracefact.net而对于第三种情况,则是两条消息被合并成了一条接收:WeIcome•••WeIcometo
55如果你下载了上篇文章所附带的源码,那么将Client2.cs进行ー下修改,不通过用户输入,而是使用ー个for循环连续的发送三个请求过去,这样会使请求的间隔时间更短,下面是关键代码:stringmsg="WelcometoTraceFact.Net!";for(inti=0;i<=2;i++){byte[]buffer=Encoding.Unicode.GetBytes(msg);/Z获得缓存try{streamToServer.Write(buffer,0,buffer.Length);/Z发往服务器Console.WriteLine("Sent:{0}",msg);}catch(Exceptionex){Console.WriteLine(ex.Message);break;))运行服务端,然后再运行这个客户端,你可能会看到这样的结果:
56可以看到,尽管上面将消息分成了三条单独发送,但是服务端却将后两条合并成了一条。对于这些情况,我们可以这样处理:就好像HTTP协议ー样,在实际的请求和应答内容之前包含了HTTP头,其中是一些与请求相关的信息。我们也可以订立自己的协议,来解决这个问题,比如说,对于上面的情况,我们就可以定义这样ー个协议:[length=XXX]:其中xxx是实际发送的字符串长度(注意不是字节数组buffer的长度),那么对于上面的请求,则我们发送的数据为:“[1ength=25]We1cometoTraceFact.Net!M〇而服务端接收字符串之后,首先读取这个“元数据”的内容,然后再根据“元数据”内容来读取实际的数据,它可能有下面这样两种情况:NOTE:我觉得这里借用“元数据”这个术语还算比较恰当,因为“元数据”就是用来描述数据的数据。・中括号是完整的,可以读取到!ength的字节数。然后根据这个数值与后面的字符串长度相比,如果相等,则说明发来了一条完整信息;如果多了,那么说明接收的字节数多了,取出合适的长度,并将剩余的进行缓存:如果少了,说明接收的不够,那么将收到的进行ー个缓存,等待下次请求,然后将两条合并。“ド“]”中括号本身就不完整,此时读不到length的值,因为中
57括号里的内容被截断了,那么将读到的数据进行缓存,等待读取下次发送来的数据,然后将两次合并之后再按上面的方式进行处理。接下来我们来看下如何来进行实际的操作,实际上,这个问题己经不属于C#网络编程的内容了,而完全是对字符串的处理。所以我们不再编写服务端/客户端代码,直接编写处理这几种情况的方法:publicclassRequestHandler{privatestringtemp=string.Empty;publicstring[]GetActualstring(stringinput){returnGetActualString(input,null);)privatestring[]GetActualString(stringinput,List
58Matchm=Regex.Match(input,pattern);/Z获取消息字符串实际应有的长度length=Convert.Tolnt32(m.Groups[0].Value);/Z获取需要进行截取的位置intstartindex=input.IndexOf(*]*)+1;/Z获取从此位置开始后所有字符的长度output=input.Substring(startlndex);if(output.Length==length){//如果。utput的长度与消息字符串的应有长度相等/Z说明刚好是完整的一条信息outputList.Add(output);temp=;}elseif(output.Length 59行处理temp:input;/Z此时程序应该退出,因为需要等待下一条数据到来才能继续处理}elseif(output.Length>length){//如果之后的长度大于应有的长度,/Z说明消息发完整了,但是有多余的数据//多余的数据可能是截断消息,也可能是多条完整消息//截取字符串output=output.Substring(0,length);outputList.Add(output);temp=/Z缩短input的长度input=input.Substring(startindex+length);/Z递归调用GetActualString(input,outputList);}else{〃说明”ド,”「‘就不完 60temp=input;returnoutputList.ToArray();}}这个方法接收一个满足协议格式要求的输入字符串,然后返回•个数组,这是因为如果出现多次请求合并成一个发送过来的情况,那么就将它们全部返回。随后简单起见,我在这个类中添加了一个静态的Test()方法和Printoutput()帮助方法,进行了一个简单的测试,注意我直接输入了!ength=13,这个是我提前计算好的。publicstaticvoidTest(){RequestHandlerhandler=newRequestHandler();stringinput;/Z第一种情况测试ー一条消息完整发送input="[length=13]明天中秋,祝大家节日快乐!"handler.PrintOutput(input);〃第二种情况测试ー两条完整消息一次发送input="明天中秋,祝大家节日快乐!”;input=String.Format("[length=13]{〇}[length=13]{0}",input);handler.PrintOutput(input);//第三种情况测试Aー两条消息不完整发送input="[length=13]明天中秋,祝大家节日 61快乐![length=13]明天中秋”;handler.PrintOutput(input);input=”,祝大家节日快乐!”;handler.PrintOutput(input);//第三种情况测试B-两条消息不完整发送input="[length=13]明天中秋,祝大家”;handler.PrintOutput(input);input="节日快乐![length=13]明天中秋,祝大家节日快乐!"handler.PrintOutput(input);/Z第四种情况测试一元数据不完整input="[leng";handler.PrintOutput(input);/Z不会有输出input="th=13]明天中秋,祝大家节ロ快乐!handler.PrintOutput(input);}/Z用于测试输出privatevoidPrintOutput(stringinput){Console.WriteLine(input);string[]outputArray 62GetActualString(input);foreach(stringoutputinoutputArray){Console.WriteLine(output);Console.WriteLine();运行上面的程序,可以得到如下的输出:[lengt£13】明天中秋謂㈱祝大家节日快乐I请按任意键维续...thT3呷天中秋,逃大家节日快乐!明天中祝祝大家书日快乐I(lena【lengthY3】明天中秋,祝大家,替大家节日快乐I明美中秋,祝大家节日快乐IllengthT3]明天贈,迅さ家节日快乐!【lengthT3】明天中秋家节日快乐Illength73J明天中秋,祝大家节日快乐IB糠滕歎穗(lengt"13】明天理大,祝大家节日快乐I明天中秋,祝大家节日快求!CVC:\IIlD0YS\systeB32\ea4.exe乐朝快A-B-日天天节,1□!zJ0K,从上面的输出可以看到,这个方法能够满足我们的要求。对于这篇文章最开始提出的问题,可以很轻松地通过加入这个方法来解决,这里就不再演示了,但在本文所附带的源代码含有修改过的程序。在这里花费了很长的时间,接下来让我们回到正题,看下如何使用异步方式完成上一篇中的程序吧。异步传输字符串 63在上一篇中,我们由简到繁,提到了服务端的四种方式:服务ー个客户端的ー个请求、服务ー个客户端的多个请求、服务多个客户端的ー个请求、服务多个客户端的多个请求。我们说到可以将里层的while循环交给一个新建的线程去让它来完成。除了这种方式以外,我们还可以使用一种更好的方式ーー使用线程池中的线程来完成。我们可以使用BeginReadO、BeginWriteO等异步方法,同时让这BeginRead()方法和它的回调方法形成一个类似于while的无限循环:首先在第一层循环中,接收到ー个客户端后,调用BeginRead(),然后为该方法提供・个读取完成后的回调方法,然后在回调方法中对收到的字符进行处理,随后在冋调方法中接着调用BeginRead。方法,并传入回调方法本身。由于程序实现功能和上一篇完全相同,我就不再细述了。而关于异步调用方法更多详细内容,可以参见C#中的委托和事件(续)。1.服务端的实现当程序越来越复杂的时候,就需要越来越高的抽象,所以从现在起我们不再把所有的代码全部都扔进Main。里,这次我创建了一个RemoteClient类,它对于服务端获取到的TcpClient进行了一个包装:publicclassRemoteClient{privateTcpClientclient;privateNetworkstreamstreamToClient;privateconstintBufferSize=8192;privatebyte[]buffer;privateRequestHandlerhandler;publicRemoteClient(TcpClientclient){this,client=client;/Z打印连接到的客户端信息Console.WriteLine(" 64ClientConnected!{0}<一{1} 65client.Client.LocalEndPoint,client.Client.RemoteEndPoint);/Z获得流streamToClient=client.GetStreamO;buffer=newbyte[BufferSize];/Z设置RequestHandlerhandler=newRequestHandler();//在构造函数中就开始准备读取AsyncCalIbackcallBack=newAsyncCalIback(ReadComplete);streamToClient.BeginRead(buffer,0,BufferSize,callBack,null);}/Z再读取完成时进行回调privatevoidReadComplete(lAsyncResultar)(intbytesRead=0;try(lock(streamToClient){bytesRead=streamToClient.EndRead(ar);Console.WriteLine("Readingdata,{0}bytes...”,bytesRead);)if(bytesRead==0)thrownewException("读取到〇字节つ; 66stringmsg=Encoding.Unicode.GetString(buffer,0,bytesRead);Array.Clear(buffer,0,buffer.Length);//清空缓存,避免脏读string[]msgArray=handler.GetActualString(msg);/Z获取实际的字符串/Z遍历获得到的字符串foreach(stringminmsgArray){Console.WriteLine("Received:{0}",m);stringback=m.ToUpperO;/Z将得到的字符串改为大写并重新发送byte[]temp=I'ncoding.Unicode.GetBytes(back);streamToClient.Write(temp,0,temp.Length);streamToClient.Flush();Console.WriteLine("Sent:{0}",back);)//再次调用BeginReadO,完成时调用自身,形成无限循环 67lock(streamToClient){AsyncCal1backcalIBack=newAsyncCalIback(ReadComplete);streamToClient.BeginRead(buffer,0,BufferSize,callBack,null);}}catch(Exceptionex){if(streamToClient!=null)streamToClient.DisposeO;client.Close();Console.WriteLine(ex.Message);/Z捕获异常时退出程序)随后,我们在主程序中仅仅创建TcpListener类型实例,由于RemoteClient类在构造函数中已经完成了初始化的工作,所以我们在下面的while循环中我们甚至不需要调用任何方法:classServer(staticvoidMain(string[]args){Console.WriteLine(*Serverisrunning...");IPAddressip=newIPAddress(newbyte[]{127,0,0,1));TcpListenerlistener=newTcpListener(ip,8500);listener.Start();/Z开始侦听 68Console.WriteLine("StartListeningwhile(true){/Z获取ー个连接,同步方法,在此处中断TcpClientclient=listener.AcceptTcpClient();RemoteClientwapper=newRemoteClient(client);)}}好了,服务端的实现现在就完成了,接下来我们再看一下客户端的实现:1.客户端的实现与服务端类似,我们首先对TcpClient进行ー个简单的包装,使它的使用更加方便一些,因为它是服务端的客户,所以我们将类的名称命名为Serverclient:publicclassServerClient{privateconstintBufferSize=8192;privatebyte[]buffer;privateTcpClientclient;privateNetwf)rkStreamstreamToServer;privatestringmsg二WelcometoTraceFact.Net!"; 69publicServerClient(){ 70client=newTcpClient();client.Connect(*localhost\8500);/Z与服务器连接}catch(Exceptionex){Console.WriteLine(ex.Message);return;)buffer=newbyte[BufferSize];/Z打印连接到的服务端信息Console.WriteLineC*ServerConnected!{0}->{1}\client.Client.LocalEndPoint,client.Client.RemoteEndPoint);streamToServer=client.GetStreamO;)/Z连续发送三条消息到服务端publicvoidSendMessage(stringmsg){msg=String.Format("[length=(0)]{1}msg.Length,msg);for(inti=0;i<=2;i++){byte[]temp=l-ncoding.Unicode.GetBytes(msg);/Z获得缓存try(streamToServer.Write(lemp,0, 71temp.Length);/Z发往服务器Console.WriteLine("Sent:{0}",msg);}catch(Exceptionex){Console.WriteLine(ex.Message);break;lock(streamToServer){AsyncCalIbackcallBack=newAsyncCalIback(ReadComplete);streamToServer.BeginRead(buffer,0,BufferSize,callBack,null);)publicvoidSendMessage(){SendMessage(this,msg);)/Z读取完成时的回调方法privatevoidReadComplete(lAsyncResultar)intbytesRead;try(lock(streamToServer){ 72streamToServer.EndRead(ar);if(bytesRead==0)thrownewException("读取到0字节“);stringmsg=Encoding.Unicode.GetString(buffer,0,bytesRead);Console.WriteLineiReceived:{0}",msg);Array.Clear(buffer,0,buffer.Length);//清空缓存,避免脏读lock(streamToServer){AsyncCallbackcallBack=newAsyncC.iIIbuck(ReadComplete);streamToServer.BeginRead(buffer,0,BufferSize,calIBack,null);)}catch(Exceptionex){if(streamToServer!=nul1)streamToServer.Dispose();client.Close();Console.WriteLine(ex.Message); 73在上面的SendMessage。方法中,我们让它连续发送了三条同样的消息,这么仅仅是为了测试,因为异步操作同样会出现上面说过的:服务器将客户端的请求拆开了的情况。最后我们在Main。方法中创建这个类型的实例,然后调用SendMessageO方法进行测试:classClient{staticvoidMain(string[]args){ConsoleKeykey;ServerClientclient=newServerClient();client.SendMessage();Console.WriteLine(" 74 75输入、"Q\"键退出。”);do{key=Console.ReadKey(true).Key;}while(key!=ConsoleKey.Q);)}是不是感觉很清爽?因为良好的代码重构,使得程序在复杂程度提高的情况ト依然可以在一定程度上保持良好的阅读性。1.程序测试最后ー步,我们先运行服务端,接着连续运行两个客户端,看看它们的输出分别是什么: 76大家可以看到,在服务端,我们可以连接多个客户端,同时为它们服务;除此以外, 77山接收的字节数发现,两个客户端均有两个请求被服务端合并成了一条请求,因为我们在其中加入了特殊的协议,所以在服务端可以对这种情况进行良好的处理。在客户端,我们没有采取类似的处理,所以当客户端收到应答时,仍然会发生请求合并的情况。对于这种情况,我想大家一经知道该如何处理了,就不再多费口舌了。使用这种定义协议的方式有它的优点,但缺点也很明显,如果客户知道了这个协议,有意地输入口ength=xxx],但是后面的长度却不匹配,此时程序就会出错。可选的解决办法是对“ビ和“ド进行编码,当客户端有意输入这两个字符时,我们将它替换成“\[”和“ヽ]”或者别的字符,在读取后再将它还原。关于这个范例就到此结束了,剩下的两个范例都将采用异步传输的方式,并且会加入更多的协议内容。下・篇我们将介绍如何向服务端发送或接收文件。 78C#网络编程(订立协议和发送文件)Part.4文件传输前面两篇文章所使用的范例都是传输字符串,有的时候我们可能会想在服务端和客户端之间传递文件。比如,考虑这样ー种情况,假如客户端显示了一个菜单,当我们输入S1、S2或S3(S为Send缩写)时,分别向服务端发送文件ClientOl.jpg、Client02.jpg、ClientO3.jpg!当我们输入RI、R2或R3时(R为Receive缩写),则分别从服务端接收文件ServerOl.jpg、Server02.jpg、ServerO3.jpg。那么,我们该如何完成这件事呢?此时可能有这样两种做法:«类似于FTP协议,服务端开辟两个端口,并持续对这两个端口侦听:ー个用于接收字符串,类似于FTP的控制端口,它接收各种命令(接收或发送文件):•个用于传输数据,也就是发送和接收文件。«服务端只开辟ー个端口,用于接收字符串,我们称之为控制端口。当接到请求之后,根据请求内容在客户端开辟ー个端口专用于文件传输,并在传输结束后关闭端口。现在我们只关注于上面的数据端口,回忆一下在第二篇中我们所总结的,可以得出:当我们使用上面的方法一时,服务端的数据端口可以为多个客户端的多次请求服务;当我们使用方法二时,服务端只为ー个客户端的一次请求服务,但是因为每次请求都会重新开辟端ロ,所以实际上还是相当于可以为多个客户端的多次请求服务。同时,因为它只为一次请求服务,所以我们在数据端口上传输文件时无需采用异步传输方式。但在控制端口我们仍然需要使用异步方式。从上面看出,第一种方式要好得多,但是我们将采用第二种方式。至于原因,你可以回顾一下Part.1(基本概念和操作)中关于聊天程序模式的讲述,因为接下来ー篇文章我们将创建一个聊天程序,而这个聊天程序采用第三种模式,所以本文的练习实际是对下•篇的ー个铺垫。 791,订立协议1.1发送文件我们先看一下发送文件的情况,如果我们想将文件clientOl.jpg由客户端发往客户端,那么流程是什么:1.客户端开辟数据端U用于侦听,并获取端口号,假设为8005。2.假设客户端输入了S1,则发送下面的控制字符串到服务端:[file=ClientOl.jpg,mode=send,port=8005]〇3.服务端收到以后,根据客户端ip和端口号与该客户端建立连接。4.客户端侦听到服务端的连接,开始发送文件。5.传送完毕后客户端、服务端分别关闭连接。此时,我们订立的发送文件协议为:[file=Client01.jpg,mode=send,port=8005I〇但是,由于它是ー个普通的字符串,在上一篇中,我们采用了正则表达式来获取其中的有效值,但这显然不是一种好办法。因此,在本文及下ー篇文章中,我们采用ー种新的方式来编写协议:XMLo対于上面的语句,我们可以写成这样的XML: 80务端向网络流写入数据。1.客户端开辟数据端口用于侦听,假设为800602.假设客户端输入了R1,则发送控制字符串:くprotocolXfilename=ServerOl.jpg”mode="receive"port="8006"/>〈/protocol>到服务端。3.服务端收到以后,根据客户端ip和端口号与该客户端建立连接。4.客户端建立起与服务端的连接,服务端开始网络流中写入数据。5.传送完毕后服务端、客户端分别关闭连接。2.协议处理类的实现和上面一章一样,在开始编写实际的服务端客户端代码之前,我们首先要编写处理协议的类,它需要提供这样两个功能:1、方便地帮我们获取完整的协议信息,因为前面我们说过,服务端可能将客户端的多次独立请求拆分或合并。比如,客户端连续发送了两条控制信息到服务端,而服务端将它们合并了,那么则需要先拆开再分别处理。2、方便地获取我们所想要的属性信息,因为协议是XML格式,所以还需要一个类专门对XML进行处理,获得字符串的属性值。 812.1ProtocalHandler辅助类我们先看下ProtocalHandler,它与上・篇中的RequestHandler作用相同。需要注意的是必须将它声明为实例的,而非静态的,这是因为每个TcpClient都需要对应ー个ProtocalHandler»因为它内部维护的patialProtocal不能共享,在协议发送不完整的情况下,这个变量用于临时保存被截断的字符串。publicclassProtocolHandler{privatestringpartialProtocal;/Z保存不完整的协议publicProtocolHandler(){partialProtocal=;)publicstring[]GetProtocol(stringinput)(returnGetProtocol(input,null);)/Z获得协议privatestring[]GetProtocol(stringinput,List 82(!String.IsNullOrEmpty(partialProtocal))input=partialProtocal+input;stringpattern=”(くprotocol>.*?く/protocol>)”;II如果有匹配,说明已经找到了,是完整的协议if(Regex.IsMatch(input,pattern)){/Z获取匹配的值stringmatch=Regex.Match(input,pattern).Groups[0].Value;outputList.Add(match);partialProtocal二:〃缩短input的长度input=input.Substring(match.Length);/Z递归调用GetProtocol(input,outputList);}else{//如果不匹配,说明协议的长度不够,/Z那么先缓存,然后等待下一次请求partialProtocal=input;returnoutputList.ToArray(); 83因为现在它已经不是本文的重点了,所以我就不演示对于它的测试了,本文所附带的代码中含有它的测试代码(我在ProtocolHandler中添加了一个静态类Test。)。2.2FileRequestType枚举和FileProtocol结构因为XML是以字符串的形式在进行传输,为了方便使用,我们最好构建一个强类型来对它们进行操作,这样会方便很多。我们首先可以定义FileRequestMode枚举,它代表是发送还是接收文件:publicenumFileRequestMode{Send=0,Receive}接下来我们再定义ー个FileProtocol结构,用来为整个协议字符串提供强类型的访问,注意这里覆盖了基类的ToStringO方法,这样在客户端我们就不需要再手工去编写XML,只要在结构值上调用ToStringO就0K了,会方便很多。publicstructFileProtocol{privatereadonlyIileRequestModemode;privatereadonlyintport;privatereadonlystringfileName;publicFileProtocol(FileRequestModemode,intport,stringfileName){this,mode=mode;this,port=port;this,fi1eName=fi1eName; 84publicFileRequestModeMode{get{returnmode;})publicintPort{get{returnport;}}publicstringFileName{get{returnfileName;}}publicoverridestringToString(){returnString.Format("くprotocol〉くfilename=、"{0}\"mode=\"{1}\"port=\"{2}\"/>",fi1eName,mode,port);})2.3ProtocolHelper辅助类这个类专用于将XML格式的协议映射为我们上面定义的强类型对象,这里我没有加入Ey/catch异常处理,因为协议对用户来说是不可见的,而且客户端应该总是发送正确的协议,我觉得这样可以让代码更加清晰:publicclassProtocolHelper{privateXmlNodefileNode;privateXmlNoderoot; 85publicProtocolHeiper(stringprotocol){XmlDocumentdoc=newXmlDocument();doc.LoadXml(protocol);root=doc.DocumentElement;fileNode=root.SelectSingleNode("file");}//此时的protocal一定为单条完整protocolprivateFileRequestModeGetFileModeO{stringmode=fileNode.Attributes["mode"].Value;mode=mode.ToLower();if(mode=="send")returnFileRequestMode.Send;elsereturnFileRequestMode.Receive;/Z获取单条协议包含的信息publicFileProtocolGetProtocol(){FileRequestModemode=GetFileMode();stringfileName="";intport=0;fileName=fileNode.Attributes["name"].Value;port=Convert.Tolnt32(fileNode.Attributes["port"]. 86Value);returnnewFileProtocol(mode,port,fileName);})OK,我们又耽误了点时间,下面就让我们进入正题吧。2.客户端发送数据2.1服务端的实现我们还是将一个问题分成两部分来处理,先是发送数据,然后是接收数据。我们先看发送数据部分的服务端。如果你从第一篇文章看到了现在,那么我觉得更多的不是技术上的问题而是思路,所以我们不再将重点放到代码上,这些应该很容易就看懂了。classServer{staticvoidMain(string[]args){Console.WriteLineC/Serverisrunning...");IPAddressip=IPAddress.Parse(*127.0.0.1*);TcpListenerlistener=newTcpListener(ip,8500);listener.Start();/Z开启对控制端口8500的侦听Console.WriteLine(*StartListening..."); 87while(true){/Z获取ー个连接,同步方法,在此处中断TcpClientclient=listener.AcceptTcpClient();RemoteClientwapper=newRcmoteClient(client);wapper.BeginReadO;publicclassRemoteClient{privateTcpClientclient;privateNetworkstreamstreamToClient;privateconstintBufferSize=8192;privatebyte[]buffer;privateProtocolHandlerhandler;publicRemoteClient(TcpClientclient){this,client=client;/Z打印连接到的客户端信息Console.WriteLine(" 88ClientConnected!{0}<一{1}”,client.Client.LocalEndPoint,client.Client.RemoteEndPoint);/Z获得流streamToClient=client.GetStreamO; 89buffer=newbyte[BufferSize];handler=newProt<)c()Hlund1:;//开始进行读取publicvoidBeginReadO{AsyncCallbackcallBack=new.AsyncCa11back(OnReadComplete);streamToClient.BeginRead(buffer,0,BufferSize,calIBack,null);}/Z再读取完成时进行回调privatevoidOnReadComplete(lAsyncResultar){intbytesRead=0;try(lock(streamToClient){bytesRead=streamToClient.EndRead(ar);Console.WriteLine("Readingdata,{0}bytes...”,bytesRead);)if(bytesRead==0)thrownewException("读取到〇字节つ;stringmsgEncoding.Unicode.GetString(buffer,0,bytesRead); 90Array.Clear(buffer,0,buffer.Length);//清空缓存,避免脏读/Z获取protoco!数组string[]protocolArray=handler.GetProtocol(msg);foreach(stringproinprotocolArray){/Z这里异步调用,不然这里可能会比较耗时ParameterizedThreadStartstart=newParameterizedThreadStart(hand1eProtocol);start.Beginlnvoke(pro,null,null);)/Z再次调用BeginReadO,完成时调用自身,形成无限循环lock(streamToClient){AsyncCalIbackcallBack=newAsyncCd1Iback(OnReadComplete);streamToClient.BeginRead(buffer,0,BufferSize,callBack,null);}}catch(Exceptionex){if(streamToClient!=null)streamToClient.Dispose();client.Close(); 91Console.WriteLine(ex.Message);/Z捕获异常时退出程序/Z处理protocolprivatevoidhandleProtocol(objectobj){stringpro=objasstring;ProtocolHelperhelper=newProtocolHeiper(pro);FileProtocolprotocol=helper.GetProtocol();if(protocol.Mode==FileRequestMode.Send){/Z客户端发送文件,对服务端来说则是接收文件receiveFile(protocol);}elseif(protocol.Mode=FileRequestMode.Receive){/Z客户端接收文件,对服务端来说则是发送文件//sendFile(protocol);privatevoidreceiveFile(FileProtocolprotocol){/Z获取远程客户端的位置IPEndPointendpoint 92client.Client.RemoteEndPointasIPEndPoint;IPAddressip=endpoint.Address;/Z使用新端口号,获得远程用于接收文件的端口endpoint=newIPEndPoint(ip,protocol.Port);/Z连接到远程客户端TcpClientlocalClient;try(localclient=newTcpClient();localClient.Connect(endpoint);}catch{(ie.WritcLine("无法连接到客户端一>{0}",endpoint);return;)/Z获取发送文件的流XetworkStreamstreamToClient=localClient.GetStreamO;/Z随机生成一个在当前冃录下的文件名称stringpath=Environment.CurrentDirectory+*/*+generateFileName(protocol.FileName);byte[]fileBuffer=newbyte[1024];// 93每次收1KBFi1eStreamfs=newFi1eStream(path,FileMode.CreateNew,FileAccess.Write);//从缓存buffer中读入到文件流中intbytesRead;inttotalBytes=0;do{bytesRead=streamToClient.Read(buffer,0,BufferSize);fs.Write(buffer,0,bytesRead);totalBytes+=bytesRead;Console.WriteLineC'Receiving{0}bytes...”,totalBytes);}while(bytesRead>0);Console.WriteLineC*Total{0}bytesreceived,Done!",totalBytes);streamToClient.Dispose();fs.Dispose();localClient.CloseO;/Z随机获取ー个图片名称privatestringgenerateFi1eName(stringfileName){DateTimenow=DateTime.Now;returnString.Format( 94{〇}—{1}_{2}_{3}”,now.Minute,now.Second,now.Mi11isecond,fi1eName);这里应该没有什么新知识,需要注意的地方有这么儿个:»在OnReadCompleteO回调方法中的foreach循环,我们使用委托异步调用了hand1eProtoco1()方法,这是因为hand1eProtocol即将执行的是一个读取或接收文件的操作,也就是ー个相对耗时的操作。»在handleProtocol()方法中,我们深切体会了定义ProtocolHelper类和FileProtocol结构的好处。如果没有定义它们,这里将是不堪入目的处理XML以及类型转换的代码。•handleProtocol()方法中进行了ー个条件判断,注意sendFileO方法我屏蔽掉了,这个还没有实现,但是我想你已经猜到它将是后面要实现的内容。•receiveFileO方法是实际接收客户端发来文件的方法,这里没有什么特别之处。需要注意的是文件存储的路径,它保存在了当前程序执行的目录下,文件的名称我使用generateFi1eName()生成了一个与时间有关的随机名称。 953.2客户端的实现我们现在先不着急实现客户端SI、R1等用户菜单,首先完成发送文件这一功能,实际上,就是为上ー节SendMessageO加一个姐妹方法SendFileO。classClient{staticvoidMain(string[]args){ConsoleKeykey;ServerClientclient=newServerClient();stringfilePath=Environment.CurrentDirectory+*/"+"ClientOl.jpg";if(File.Exists(filePath))client.BeginSendFile(filePath);Console.WriteLine(" 96 97输入、"Q\"键退出。つ;do(key=Console.ReadKey(true).Key;}while(key!=ConsoleKey.Q);))publicclassServerClient(privateconstintBufferSize=8192;privatebyte[]buffer;privateTcpClientclient;privateNctworkStreamstreamToServer; 98publicServerClient(){try(client=newTcpClient();client.Connect("localhost”,8500);/Z与服务器连接}catch(Exceptionex){Console.WriteLine(ex.Message);return;}buffer=newbyte[BufferSize];/Z打印连接到的服务端信息Console.WriteLine(AServerConnected!{0}->(1}\client.Client.LocalEndPoint,client.Client.RemoteEndPoint);streamToServer=client.GetStreamO;}/Z发送消息到服务端publicvoidSendMessage(stringmsg){byte[]temp=Encoding.Unicode.GetBytes(msg);/Z获得缓存try(lock(streamToServer){streamToServer.Write(temp,0, 99temp.Length);/Z发往服务器Console.WriteLine("Sent:{0}",msg);}catch(Exceptionex){Console.WriteLine(ex.Message);return;))/Z发送文件ー异步方法publicvoidBeginSendFile(stringfilePath)(ParameterizedThreadStartstart=newParameterizedThreadStart(BeginSendFile);start.BeginInvoke(filePath,nul1,null);}privatevoidBeginSendFile(objectobj){stringfilePath=objasstring;SendFile(filePath);)/Z发送文件一同步方法publicvoidSendFile(stringfilePath){IPAddressip=IPAddress.Parse("127.0.0.1"); 100TcpListenerlistener=newTcpListener(ip,0);listener.Start();/Z获取本地侦听的端口号IPEndPointendPoint=listener.LocalEndpointasIPEndPoint;intlisteningPort=endPoint.Port;/Z获取发送的协议字符串stringfileName=Path.GetFileName(filePath);Fi1eProtocolprotocol=newFileProtocol(FileRequestMode.Send,listeningPort,fileName);stringpro=protocol.ToStringO;SendMessage(pro);/Z发送协议到服务端〃中断,等待远程连接TcpClientlocalclient=listener.AcceptTcpClient();Console.WriteLine("Startsendingfile...*);\etworkStreamstream二localClient.GetStreamO;/Z创建文件流 101FileStreamfs=newFi1eStream(filePath,FileMode.Open,FileAccess.Read);byte[]fileBuffer=newbyte[1024];/Z每次传1KBintbytesRead;inttotalBytes=0;/Z创建获取文件发送状态的类SendStatusstatus=newSendStatus(filePath);//将文件流转写入网络流try(do{Thread.Sleep(lO);/Z为了更好的视觉效果,暂停10毫秒:一)bytesRead=fs.Read(fileBuffer,0,fileBuffer.Length);stream.Write(fileBuffer,0,bytesRead);totalBytes+=bytesRead;/Z发送了的字节数status.PrintStatus(totalBytes);/Z打印发送状态}while(bytesRead>0);Console.WriteLine(zzTotal{0}bytessent,Done!”,totalBytes);}catch{ 102Console.WriteLine("Serverhasstream.DisposeO;fs.Dispose();localClient.Close();listener.Stop();))接下来我们来看下这段代码,有这么两点需要注意一下:・在Main。方法中可以看到,图片的位置为应用程序所在的目录,如果你跟我一样处于调试模式,那么就在解决方案的Bin目录下的Debug目录中放置三张图片ClientOl.jpg,Client02.jpg,Client03.jpg,用来发往服务端。・我在客户端提供了两个SendFileO方法,和一个BeginSendFileO方法,分别用于同步和异步传输,其中私有的SendFileO方法只是ー个辅助方法。实际上对于发送文件这样的操作我们几乎总是需要使用异步操作。•SendMessageO方法中给streamToServer加锁很重要,因为SendFileO方法是多线程访问的,而在SendFileO方法中又调用了SendMessageO方法。 103»我另外编写了一个SendStatus类,它用来记录和打印发送完成的状态,已经发送了多少字节,完成度是百分之多少,等等。本来这个类的内容我是直接写入在Client类中的,后来我觉得它执行的工作已经不属于Client本身所应该执行的领域之内了,我记得这样一句话:当你觉得类中的方法与类的名称不符的时候,那么就应该考虑重新创建一个类,我觉得用在这里非常恰当。下面是SendStatus的内容:/Z即时计算发送文件的状态publicclassSendStatus{privateFilelnfoinfo;privatelongfileBytes;publicSendStatus(stringfilePath){info=newFilelnfo(filePath);fileBytes=info.Length;}publicvoidPrintStatus(intsent){stringpercent=GetPercent(sent);Console.WriteLine("Sending{0}bytes,{1}%...",sent,percent);/Z获得文件发送的百分比 104publicstringGetPercent(intsent){decimalallBytes=Convert.ToDecimal(fileBytes);decimalcurrentSent=Convert.ToDecimal(sent);decimalpercent=(currentSent/allBytes)*100;percent=Mcith.Round(percent,1);〃保留一位小数if(percent.ToStringO=="100.0")return"100*;elsereturnpercent.ToStringO;3.3程序测试接下里我们运行ー下程序,来检查一下输出,首先看ド服务端: 105接着是客户端,我们能够看到发送的字节数和进度,可以想到如果是图形界面,那么我们可以通过扩展SendStatus类来创建一个进度条:caC:\IIVB0YS\systeB32\cad.exeSendingSendingSendingSendingSendingSendingSendingSending240640241664242688243712244736245760246478246478bytes.bytes.bytes.bytes,bytes,bytes,bytes,bytes.97.6%98.0798.5z98.9z99.3z99.7z100z.100z.Total246478bytessent.Done?最后我们看下服务端的Bin'Debug目录,应该可以看到接收到的图片:ゝServerConsole\ServerConsole\bin\Debug名标―I大小L类型rjf2_26_468_Client01..jpg:241KBJPEG图像时6_5_92l_Client01.jpg241KBJPEG图像回6_10_468_C1ientO1.jpg241KBJPEG图像回6_12_468_C1ient01.jpg241KBJPEG图像,ti|6_30_906_ClientOl.jpg241KBJPEG图像i,^1ServerConsole.exe11KB应用程序ServerConsole.pdb24KBProgramDebugコServerConsole.vshost.exe14KB应用程序本来我想这篇文章就可以完成发送和接收,不过现在看来没法实现了,因为如果继续下去这篇文章就太长了,我正尝试着尽量将文章控制在15页以内。那么我们将在下篇文章中再完成接收文件这一部分。 106C#网络编程(接收文件)-Part.5这篇文章将完成Pari.4中剩余的部分,它们本来是ー篇完整的文章,但是因为上・篇比较长,合并起来页数太多,浏览起来可能会比较不方便,我就将它拆为两篇了,本文便是它的后半部分。我们继续进行上一篇没有完成的步骤:客户端接收来自服务端的文件。4.客户端接收文件4.1服务端的实现对于服务端,我们只需要实现ヒ•章遗留的sendFileO方法就可以了,它起初在handleProtocol中是注释掉的。另外,由于创建连接、获取流等操作与receiveFileO是没有区别的,所以我们将它提出来作为一个公共方法getStreamToClientO。下面是服务端的代码,只包含新增改过的代码,对于原有方法我只给出了签名:classServer{staticvoidMain(string[]args){Console.WriteLine(*Serverisrunning...");IPAddressip=IPAddress.ParseC127.0.0.T);TcpListenerlistener=newTcpListener(ip,8500);listener.Start()I〃开启对控制端口8500的侦听Console.WriteLineiStartListening...");while(true){ 107/Z获取ー个连接,同步方法,在此处中断TcpClientclient=listener.AcceptTcpClient();RemoteClientwapper=newRemoteClient(client);wapper.BeginReadO;)}}publicclassRemoteClient{/Z字段略publicRemoteClient(TcpClientclient){}//开始进行读取publicvoidBeginReadO{}/Z再读取完成时进行回调privatevoidOnReadComplete(IAsyncResultar){}/Z处理protocolprivatevoidhandleProtocol(objectobj){stringpro=objasstring;ProtocolHelperhelper=newProtocolHe1per(pro);Fi1eProtocolprotocol=helper.GetProtocol(); 108if(protocol.Mode==FileRequestMode.Send){/Z客户端发送文件,对服务端来说则是接收文件receiveFile(protocol);}elseif(protocol.Mode=FileRequestMode.Receive){/Z客户端接收文件,对服务端来说则是发送文件sendFile(protocol);))/Z发送文件privatevoidsendFile(Fi1eProtocolprotocol){TcpClientlocalclient;NetworkStreamstreamToClient=getStreamToClient(protocol,outlocalClient);/Z获得文件的路径stringfilePath=Environment.CurrentDirectory++protoco1.Fi1eName;/Z创建文件流FileStreamfs=newFi1eStream(filePath,FileMode.Open,FileAccess.Read); 109byte[]fileBuffer=newbyte[1024];/Z每次传1KBintbytesRead;inttotalBytes=0;/Z创建获取文件发送状态的类SendStatusstatus=newSendStatus(filePath);//将文件流转写入网络流try(do(Thread.Sleep(lO);/Z为了更好的视觉效果,暂停10毫秒:一)bytesRead=fs.Read(fileBuffer,0,fileBuffer.Length);streamToClient.Write(fileBuffer,0,bytesRead);totalBytes+=bytesRead;/Z发送了的字节数status.PrintStatus(totalBytes);/Z打印发送状态}while(bytesRead>0);Console.WriteLine("Total{0}bytessent,Done!z,totalBytes);}catch{Console.WriteLine(*Serverhaslost..."); 110streamToClient.DisposeO;fs.Dispose();localClient.Close();)/Z接收文件privatevoidreceiveFile(FileProtocolprotocol){}/Z获取连接到远程的流一公共方法privateNetworkstreamgetStreamToClient(FileProtocolprotocol,outTcpClientlocalClient){/Z获取远程客户端的位置IPEndPointendpoint=client.Client.RemoteEndPointasIPEndPoint;IPAddressip=endpoint.Address;/Z使用新端口号,获得远程用于接收文件的端口endpoint=newIPEndPoint(ip,protocol.Port);/Z连接到远程客户端try(localClient=newTcpClient();localClient.Connect(endpoint);}catch{Console.WriteLine("无法连接到客户端ー->{0}",endpoint); 111localClient=null;returnnul1;)/Z获取发送文件的流XetworkStreamstreamToClient=localClient.GetStreamO;returnstreamToClient;)/Z随机获取ー个图片名称privatestringgenerateFileName(stringfileName){}}服务端的sendFile方法和客户端的SendFileO方法完全类似,上面的代码几乎是ー次编写成功的。另外注意我将客户端使用的SendStatus类也拷贝到了服务端。接下来我们看下客户端。4.2客户端的实现首先要注意的是客户端的SendFileO接收的参数是文件全路径,但是在写入到协议时只获取了路径中的文件名称。这是因为服务端不需要知道文件在客户端的路径,所以协议中只写文件名;而为了使客户端的SendFileO方法更通用,所以它接收本地文件的全路径。客户端的ReceiveFileO的实现也和服务端的receiveFile。方法类似,同样,由于要保存到本地,为了避免文件名重复,我将服务端的generateFileName。方法复制了过来。publicclassServerClient:IDisposable{/Z字段略publicServerClient(){} 112/Z发送消息到服务端publicvoidSendMessage(stringmsg){}/Z发送文件ー异步方法publicvoidBeginSendFile(stringfilePath)privatevoidSendFile(objectobj){}/Z发送文件一同步方法publicvoidSendFile(stringfilePath){}/Z接收文件ー异步方法publicvoidBeginReceiveFile(stringfileName){ParameterizedThreadStartstart=newParameterizedThreadStart(ReceiveFile);start.Beginlnvoke(fileName,null,null);publicvoidReceiveFile(objectobj){stringfileName=objasstring;ReceiveFile(fileName);//接收文件一同步方法publicvoidReceiveFile(stringfileName){ 113IPAddressip=IPAddress.ParseC127.0.0.1");TcpListenerlistener=newTcpListener(ip,0);listener.Start();/Z获取本地侦听的端口号IPEndPointendPoint=listener.LocalEndpointasIPEndPoint;intlisteningPort=endPoint.Port;/Z获取发送的协议字符串Fi1eProtocolprotocol=newFileProtocol(FileRequestMode.Receive,listeningPort,fileName);stringpro=protocol.ToStringO;SendMessage(pro);/Z发送协议到服务端〃中断,等待远程连接TcpClientlocalClient=listener.AcceptTcpClient();Console.WriteLine("Startsendingfile...;\etworkStreamstream=localClient.GetStreamO; 114/Z获取文件保存的路劲stringfilePath=Environment.CurrentDirectory++generateFileName(fileName);/Z创建文件流FileStreamfs=newFi1eStream(filePath,FileMode.CreateNew,FileAccess.Write);byte[]fileBuffer=newbyte[1024];/Z每次传1KBintbytesRead;inttotalBytes=0;//从缓存buffer中读入到文件流中do(bytesRead=stream.Read(buffer,0,BufferSize);fs.Write(buffer,0,bytesRead);totalBytes+=bytesRead;Console.WriteLine("Receiving{0}bytes...”,totalBytes);}while(bytesRead>0);Console.WriteLine("Total{0}bytesreceived,Done!",totalBytes);fs.DisposeO;stream.DisposeO; 115listener.StopO;/Z随机获取ー个图片名称privatestringgenerateFi1eName(stringfileName){}publicvoidDisposeO{if(streamToServer!=null)streamToServer.DisposeO;if(client!=null)client.Close();))上面关键的一句就是创建协议那句,注意到将mode由Send改为了Receive,同时传去了想要接收的服务端的文件名称。3.3程序测试现在我们已经完成了所有收发文件的步骤,可以看到服务端的所有操作都是被动的,接下来我们修改客户端的Main。程序,创建一个菜单,然后根据用户输入发送或者接收文件。classProgram{staticvoidMain(string[]args){ServerClientclient=newServerClient();stringinput;stringpath 116Environment.CurrentDirectory+do{Console.WriteLine(*SendFile:SI-ClientOl.jpg,S2-Client02.jpg,S3-Client03.jpg");Console.WriteLine(*ReceiveFile:RI-ServerOl.jpg,RI-Server02.jpg,R3-Server03.jpg");Console.WriteLine("Press'Q'toexit. 117");Console.Write("Enteryourchoice:つ;input=Console.ReadLineO;switch(input.ToUpper()){case"SI":client.BeginSendFile(path+“ClientOl.jpg");break;case"S2":client.BeginSendFile(path+"Client02.jpg");break;caseS3:client.BeginSendFi1e(path+"Client02.jpg");break;case"RI*:client.BeginReceiveFile( 118break;case"R2":client.BeginReceiveFile(*Server01.jpg");break;case"R3":client.BeginReceiveFile("ServerOl.jpg");break;}}while(input.ToUpper()!="Q");client.DisposeO;))由于这是ー个控制台应用程序,并且采用了异步操作,所以这个菜单的出现顺序有点混乱。我这里描述起来比较困难,你将代码下载下来后运行ー下就知道了:-)程序的运行结果和上一节类似,这里我就不再贴图了。接下来是本系列的最后ー篇,将发送字符串与传输文件的功能结合起来,创建一个可以发送消息并能收发文件的聊天程序,至于语音聊天嘛...等我学习了再告诉你たく、
此文档下载收益归作者所有