第3章TCP网络程序开发 TCP(Transmission Control Protocol,传输控制协议)是一种面向连接(连接导向)的、可靠的、基于字节流的、全双工的传输层(transport layer)通信协议。在简化的计算机OS模型中,TCP位于IP层之上,用于完成传输层的指定功能。TCP的工作过程与人们日常生活中的打电话相似,要经过建立连接、传输数据和连接终止3个步骤。 3.1TCP程序开发主要技术 TCP程序开发的主要技术有使用套接字进行TCP传输、使用TCP类进行网络传输和TCP同步异步等。 3.1.1使用套接字进行TCP传输 套接字分为两种: 一种是面向连接的(connectionoriented)套接字,另一种是无连接的(connectionless)套接字。使用TCP协议编程的套接字是面向连接的,通过它建立两个IP地址端点之间的会话,一旦建立了这种连接就可以在设备之间进行可靠传输。 根据连接的启动方式和本地套接字要连接的目标,套接字的连接过程可分为以下3个阶段: (1) 服务器监听: 是指服务器套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。 图31面向连接的套接字编程 (2) 客户端请求: 是指由客户端的套接字提出连接请求,要连接的目标是服务器的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器套接字的地址和端口号,然后再向服务器套接字提出连接请求。 (3) 连接确认: 是指当服务器套接字监听到客户端套接字的连接请求时,它就响应客户端套接字的请求,把服务器套接字的信息发给客户端,一旦客户端确认了此信息,连接即可建立。而服务器套接字继续监听其他客户端套接字的连接请求。面向连接的套接字编程框架如图31所示。 1. 建立连接 服务器和客户端通信的前提是服务器首先在指定的端口监听是否有客户端的连接请求,当客户端向服务器发起连接请求并被服务器接收后,双方即可建立连接。 (1) 服务器编程。在服务器程序中,首先创建一个本地套接字对象。例如: Socket localSocket=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp); 然后将套接字绑定到用于TCP通信的本地IP地址和窗口上。Bind方法用于完成绑定工作。例如: IPHostEntry local=Dns.GetHostByName(Dns.GetHostName()); IPEndPoint iep=new IPEndPoint(local.AddressList[0], 1180); localSocket.Bind(iep); 将套接字与端口绑定后,就用Listen方法等待客户端发出连接尝试。例如: locatSocket.Listen(10); Listen方法自动将客户端连接请求放到请求队列中,参数指出系统等待用户服务程序排队的连接数,超过连接数的任何客户端都不能与服务器进行通信。 在Listen方法执行之后,服务器已经做好了接收任何连接的准备。这时,可用Accept方法从请求队列中获取连接。例如: localSocket.Accept(); 程序执行到Accept方法时被阻塞,直到接收到客户端的连接请求后才继续执行下一条语句。服务器一旦接收了客户端的连接请求,Accept方法立即返回一个与客户端通信的新的套接字。该套接字中既包含了本机的IP地址和端口号,也包含了客户端的IP地址和端口号。然后就可以利用此套接字与该客户端进行通信了。 (2) 客户端编程。客户端利用Socket的Connect方法向远程主机的端点发起连接请求,并将自身绑定到系统自动分配的端点上。例如: IPAddress remoteHost=IPAddress.Parse("192.168.0.1"); IPEndPoint iep=new IPEndPoint(remoteHost, 1180); Socket localSocket=new Socket( AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp); localSocket.Connect(iep); 程序运行后,客户端与服务器端建立连接之前,系统不会执行Connect语句下面的语句,而是处于阻塞状态,直到连接成功或出现异常为止。 2. 发送和接收消息 一旦客户端与服务器建立连接,客户机和服务器都可以使用Socket对象的Send和Receive方法进行通信。 (1) 服务器编程。当服务器接收客户端的连接请求成功时,Accept方法返回包含该客户端IP地址及端口号信息的套接字。服务器可以用该套接字与客户端通信。 … Socket clientSocket=localSocket.Accept(); //建立连接后,利用Send方法向客户端发送信息 clientSocket.Send(Encoding.ASCII.GetBytes("server send Hello")); //接收客户端信息 byte[] myresult=new Byte[1024]; int receiveNum=clientSocket.Receive(myresult); Console.WriteLine("接收客户端信息:{0}",Encoding.ASCII.GetString(myresult)); … (2) 客户端编程。客户端可直接使用本地套接字的Send方法向服务器发送信息,利用Receive方法接收服务器信息。 … localSocket.Connect(iep); //建立连接成功后,向服务器发送信息 string sendMessage="client send Message Hello"+DateTime.Now; localSocket.Send(Encoding.ASCII.GetBytes(sendMessage)); Console.WriteLine("向服务器发送信息:{0}", sendMessage); //接收服务器信息 byte[] result=new Byte[1024]; localSocket.Receive(result); Console.WriteLine("接收服务器信息:{0}", Encoding.ASCII.GetString(result)); … 3. 关闭连接 通信完成后,首先用Shutdown方法停止会话,然后关闭Socket实例。表31说明了Socket.Shutdown方法可以使用的值。 表31Socket.ShutDown值 名称说明 SocketShutdown.Receive防止套接字上接收数据,如果收到额外的数据,将发送一个RST信号 SocketShutdown.Send防止套接字上发送数据,在所有存留的缓冲器中的数据发送出去之后,发送一个FIN信号 SocketShutdown.Both在套接字上既停止发送也停止接收 关闭连接的一般用法: localSocket.Shutdown(SocketShutdown.Both); localSocket.Close(); 该方法允许Socket对象一直等待,直到将内部缓冲区的数据发送完为止。 3.1.2使用TCP类进行网络传输 为了简化网络编程的复杂度,.NET对套接字进行了封装,封装后的类就是TcpListener类和TcpClienr类,它们都在System.Net.Sockets命名空间下。值得注意的是,TcpListener类和TcpClienr类只支持标准协议编程。如果要编写非标准协议的程序,只能使用套接字来实现。 TcpListener类用于监听客户端的连接请求; TcpClienr类用于提供本地主机和远程主机的连接信息。 1. TcpListener类 TcpListener类用于监听和接收传入的连接请求。该类的构造函数有两种常用的重载形式。 1) TcpListener(IPEndPoint iep) 其中,iep是IPEndPoint类型的对象,iep包含了服务器端的IP地址和端口号。该构造函数通过IPEndPoint类型的对象在指定的IP地址与端口监听客户端连接请求。 2) TcpListener(IPAddress localAddr, int port) 该构造函数建立一个TcpListener对象,在参数中直接指定本机IP地址和端口,并通过指定的本机IP地址和端口监听传入的连接请求。 构造了TcpListener对象后,就可以监听客户端的连接请求了。与TcpClient相似,TcpListener也分别提供了同步方法和异步方法。在同步工作方式下,对应以下几种方法。 (1) AcceptSocket方法。该方法用于在同步阻塞方式下获取并返回一个用来接收和发送数据的套接字对象,同时从传入的连接队列中移除该客户端的连接请求。该套接字包含了本地和远程主机的IP地址和端口号,然后通过调用Socket对象的Send方法和Receive方法和远程主机进行通信。 (2) AcceptTcpClient方法。该方法用于在同步阻塞方式下获取并返回一个可以用来接收和发送数据的封装了Socket的TcpClient对象。 (3) Start方法。该方法用于启动监听,构造函数为: public void Start() public void Start(int backlog) 整型参数backlog为请求队列的最大长度,即最多允许的客户端连接个数。Start方法被调用后,将自己的LocalEndPoint和底层Socket对象绑定起来,并自动调用Socket对象的Listen方法开始监听来自客户端的请求。如果接收了一个客户端请求,Start方法会自动将该请求插入请求队列,然后继续监听下一个请求,直到调用Stop方法停止监听。当TcpListener接收的请求超过请求队列的最大长度或小于0时,等待接收连接请求的远程主机将会抛出SocketException类型的异常。 (4) Stop方法。该方法用于停止监听请求,方法原型为: public void Stop() 程序执行Stop方法后,会立即停止监听客户端连接请求,并关闭底层的Socket对象。等待队列中的请求将会丢失,等待接收连接请求的远程主机会抛出套接字异常。 表32列出了TcpListener类常用的方法。 表32TcpListener类常用的方法 方法说明 AccepetSocket从端口处接收一个连接,并赋予它Socket对象 AcceptTcpClient从端口处接收一个连接,并赋予它TcpClient对象 Equals判断两个TcpClient对象是否相等 Pending确定是否有挂起的连接请求: true有连接挂起,false无连接挂起 Start开始侦听传入的连接请求 Stop关闭侦听器 ToString创建TcpListener对象的字符串表示 2. TcpClient类 TcpClient类归类于System.Net.Socket命名空间下。利用TcpClient类提供的方法,可以通过网络进行连接、发送和接收网络数据流。该类的构造函数有4种重载形式。 1) TcpClient() 该构造函数创建一个默认的TcpClient对象,该对象自动选择客户端尚未使用的IP地址和端口号。创建该对象后,即可用Connect方法与服务器端进行连接。例如: TcpClient tcpClient=new TcpClient(); tcpClient.Connect("www.abcd.com", 51888); 2) TcpClient(AddressFamily family) 该构造函数创建的TcpClient对象也能自动选择客户端尚未使用的IP地址和端口号,但是使用AddressFamily枚举指定了使用哪种网络协议。创建该对象后,即可用Connect方法与服务器端进行连接。例如: TcpClient tcpClient=new TcpClient(AddressFamily.InterNetwork); tcpClient.Connect("www.abcd.com", 51888); 3) TcpClient(IPEndPoint iep) 其中,iep是IPEndPoint类型的对象,iep指定了客户端的IP地址和端口号。当客户端的主机有一个以上的IP地址时,可使用此构造函数选择要使用的客户端主机IP地址。例如: IPAddress[] address=Dns.GetHostAddresses(Dns.GetHostName()); IPEndPoint iep=new IPEndPoint(address[0], 51888); TcpClient tcpClient=new TcpClient(iep); tcpClient.Connect("www.abcd.com", 51888); 4) TcpClient(string hostname,int port) 这是使用最方便的一种构造函数。该构造函数可直接指定服务器端域名和端口号,而且无须使用connect方法。客户端主机的IP地址和端口号自动选择。例如: TcpClient tcpClient=new TcpClient("www.abcd.com", 51888); 表33和表34分别列出了TcpClient类的常用属性和方法。 表33TcpClient类的常用属性 属性含义 Client获取或设置基础套接字 LingerState获取或设置套接字保持连接的时间 NoDelay获取或设置一个值,该值在发送或接收缓冲区未满时禁用延迟 ReceiveBufferSize获取或设置Tcp接收缓冲区的大小 ReceiveTimeout获取或设置套接字接收数据的超时时间 SendBufferSize获取或设置Tcp发送缓冲区的大小 SendTimeout获取或设置套接字发送数据的超时时间 表34TcpClient类的常用方法 方法含义 Close释放TcpClient实例,而不关闭基础连接 Connect用指定的主机名和端口号将客户端连接到TCP主机 BeginConnect开始对远程主机异步连接的请求 EndConnect结束对远程主机异步连接的请求 GetStream获取能够发送和接收数据的NetworkStream对象 3. 编写服务器端TCP应用程序的一般步骤 (1) 创建一个TcpListener对象,然后调用该对象的Start方法在指定的端口进行监听。 示例代码: //声明 private IPAddress localIP; //IP地址 private int port=5656; //端口 private TcpListener tcpListener; //监听套接字 //初始化 IPAddress[] listenIP=Dns.GetHostAddresses(""); localIP=listenIP[0]; //初始化IP为本地地址 //创建TcpListener对象,开始监听 tcpListener=new TcpListener(localIP, port); tcpListener.Start(); (2) 在单独的线程中,首先循环调用AcceptTcpLient方法接收客户端的连接请求,从该方法的返回结果中得到与该客户端对应的TcpClient对象,并利用该对象的GetStream方法得到NetworkStream对象; 然后利用该对象得到其他使用更方便的对象,例如BinaryReader对象、BinaryWriter对象,为进一步与对方通信做准备。 //启动一个线程接收请求 Thread threadAccept=new Thread(AcceptClientConnect); threadAccept.Start(); //线程执行AcceptClientConnect方法接收请求 private void AcceptClientConnect() { while (true) { try { tcpClient=tcpListener.AcceptTcpClient(); if (tcpClient!=null) { networkStream=tcpClient.GetStream(); Br=new BinaryReader(networkStream ); Bw=new BinaryWriter(networkStream ); } } Catch { ... } } } (3) 每得到一个新的TcpClient对象,就创建一个与该客户对应的线程,在线程中与对应的客户进行通信。例如: Thread threadReceive=new Thread(ReceiveMessage); threadReceive.Start(); 其中,ReceiveMessage是接收消息的方法。 (4) 根据传送的情况确定是否关闭与客户机的连接。 if(br!=null) { br.Close(); } if (bw !=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } 在关闭连接之前,要先关闭读写流br和bw。 在停止服务后,服务器可以断开监听: tcpListener.Stop(); 4. 编写客户端TCP应用程序的一般步骤 (1) 利用TcpClient的构造函数创建一个TcpClient对象。 private TcpClient tcpClient; tcpClient=new TcpClient(); (2) 使用Connect方法与服务器连接。 tcpClient.Connect(remoteHost.HostName, 5656); (3) 使用TcpClient对象的GetStream方法得到网络流,然后利用该网络流与服务器进行数据传输。 if (tcpClient!=null) { statusStrip1.Invoke(showStatus, "连接成功!"); networkStream=tcpClient.GetStream(); br=new BinaryReader(networkStream); bw=new BinaryWriter(networkStream); } (4) 创建一个线程监听指定的窗口,循环接收并处理服务器发送过来的信息。 Thread threadReceive=new Thread(ReceiveMessage); threadReceive.Start(); 其中,ReceiveMessage方法用来循环接收消息。 (5) 完成工作后,向服务器发送关闭消息,并关闭与服务器的连接。 3.1.3同步与异步 利用TCP开发应用程序时,.NET框架提供两种工作方式,一种是同步工作方式(syschronization),另一种是异步工作方式(asynchronous)。 同步工作方式是指利用TCP编写的程序执行到监听或接收语句时,在未完成当前工作(侦听到连接请求或收到对方发来的数据)前不再继续往下执行。 使用同步TCP编写服务器端程序的一般步骤为: (1) 创建一个包含采用的网络类型、数据传输类型和协议类型的本地套接字对象,并将其与服务器的IP地址和端口号绑定。这个过程可以通过Socket类或者TcpListener类完成。 (2) 在指定的端口进行监听,以便接收客户端的连接请求。 (3) 一旦接收了客户端的连接请求,就根据客户端发送的连接信息创建与该客户端对应的Socket对象或者TcpClient对象。 (4) 根据创建的Socket对象或者TcpClient对象,分别与每个连接的客户进行数据传输。 (5) 根据传送信息情况确定是否关闭与对方的连接。 异步工作方式是指程序执行到监听或接收语句时,不论当前工作是否完成,都会继续往下执行。 使用同步TCP编写客户端程序的一般步骤为: (1) 创建一个包含传输过程中采用的网络类型、数据传输类型和协议类型的Socket对象或者TcpClient对象。 (2) 使用Connect方法与远程服务器建立连接。 (3) 与服务器进行数据传输。 (4) 完成工作后,向服务器发送关闭信息,并关闭与服务器的连接。 1. 同步TCP编程实例 【例31】编写如图32所示Windows程序。使用同步TCP编程,实现客户端与服务器通信,演示服务器与客户端相互收发信息的过程,以了解同步TCP的运行原理。 1) 界面设计 在VS 2010中,新建两个(客户端和服务器端)Windows应用程序,项目名分别是Tcp_Tb_Client和Tcp_Tb_Server。 图32例31的主界面 从界面上可以看到,这个程序包括客户端和服务器端两个进程,服务器侦听连接,用户可在客户端进程界面上设置要连接的服务器端的IP。客户端和服务器端可以双向发送消息。为了让大家更直观地理解同步和异步的工作机制,在客户端和服务器端添加了进度条,进度条将实时显示程序运行和数据收发的进度。 2) 客户端程序主要代码 public partial class Form1 : Form { private IPAddress localAddress; private const int port=5656; private TcpClient tcpClient; private NetworkStream networkStream; private BinaryReader br; private BinaryWriter bw; /*------------声明委托------------*/ private delegate void ShowMessage(string str); //显示消息 private ShowMessage showMessage; private delegate void ShowStatus(string str); //显示状态 private ShowStatus showStatus; private delegate void ShowProgress(int progress); //显示进度 private ShowProgress showProgress; private delegate void ResetText(); //重置消息文本 private ResetText resetText; /*------------声明委托------------*/ public Form1() { InitializeComponent(); /*----------定义委托----------*/ //显示消息 showMessage=new ShowMessage(ShwMsgforView); //显示状态 showStatus=new ShowStatus(ShwStatusInfo); //显示进度 showProgress=new ShowProgress(ShwProgressProc); //重置消息文本 resetText=new ResetText(ResetMsgTxt); /*----------定义委托----------*/ } /*----------定义回调函数----------*/ //显示消息 private void ShwMsgforView(string str) { str=System.DateTime.Now.ToString()+Environment.NewLine+str+Environment.NewLine; int txtGetMsgLength=this.richTextBox1.Text.Length; this.richTextBox1.AppendText(str); this.richTextBox1.ScrollToCaret(); } //显示状态 private void ShwStatusInfo(string str) { toolStripStatusLabel1.Text=str; } //显示进度 private void ShwProgressProc(int progress) { toolStripProgressBar1.Value=progress; } //重置消息文本 private void ResetMsgTxt() { textBox1.Text=""; textBox1.Focus(); } //发起连接请求 private void ConnectoServer() { try { statusStrip1.Invoke(showStatus, "正在连接..."); IPHostEntry remoteHost=Dns.GetHostEntry(textBox2.Text); tcpClient=new TcpClient(); statusStrip1.Invoke(showProgress, 1); tcpClient.Connect(remoteHost.HostName, 5656); //非同步 //非同步操作 statusStrip1.Invoke(showProgress, 100); //间歇延时 DateTime nowtime=DateTime.Now; while (nowtime.AddSeconds(1)>DateTime.Now) { } if(tcpClient!=null) { statusStrip1.Invoke(showStatus, "连接成功!"); networkStream=tcpClient.GetStream(); br=new BinaryReader(networkStream); bw=new BinaryWriter(networkStream); } } catch { statusStrip1.Invoke(showStatus, "连接失败!"); //间歇延时 DateTime now=DateTime.Now; while (now.AddSeconds(1)>DateTime.Now) { } statusStrip1.Invoke(showProgress, 0); statusStrip1.Invoke(showStatus, "就绪"); } } //接收消息 private void ReceiveMessage() { statusStrip1.Invoke(showStatus, "接收中..."); for (int i=0; i<5; i++) { try { string rcvMsgStr=br.ReadString(); //同步操作1 //附加操作1 statusStrip1.Invoke(showProgress, i+1); if (rcvMsgStr!=null) { richTextBox1.Invoke(showMessage, rcvMsgStr); } } catch { if (br!=null) { br.Close(); } if (bw!=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } statusStrip1.Invoke(showStatus, "连接断开!"); statusStrip1.Invoke(showProgress, 0); break; } } statusStrip1.Invoke(showStatus, "接收了" + 5 + "条消息."); } //发送消息 private void SendMessage(object state) { statusStrip1.Invoke(showStatus, "正在发送..."); for (int i=0; i<5; i++) { try { bw.Write(state.ToString()); //非同步操作 statusStrip1.Invoke(showProgress, i+1); DateTime now=DateTime.Now; //间歇延时 while (now.AddSeconds(5)>DateTime.Now) { } bw.Flush(); } catch { if (br!=null) { br.Close(); } if (bw!=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } statusStrip1.Invoke(showStatus, "连接断开!"); statusStrip1.Invoke(showProgress, 0); break; } } statusStrip1.Invoke(showStatus, "完毕"); //间歇延时 DateTime nowtime=DateTime.Now; while (nowtime.AddSeconds(1)>DateTime.Now) { } statusStrip1.Invoke(showProgress, 0); textBox1.Invoke(resetText, null); } 3) 服务器端程序主要代码 public partial class Form1 : Form { private IPAddress localAddress; private const int port=5656; private TcpListener tcpListener; private TcpClient tcpClient; private NetworkStream networkStream; private BinaryReader br; private BinaryWriter bw; /*------------声明委托------------*/ //显示消息 private delegate void ShowMessage(string str); private ShowMessage showMessage; //显示状态 private delegate void ShowStatus(string str); private ShowStatus showStatus { //显示进度 private delegate void ShowProgress(int progress); private ShowProgress showProgress; private delegate void ResetText(); //重置消息文本 private ResetText resetText; } /*------------声明委托------------*/ public Form1() { InitializeComponent(); /*----------定义委托----------*/ showMessage=new ShowMessage(ShwMsgforView); //显示消息 showStatus=new ShowStatus(ShwStatusInfo); //显示状态 showProgress=new ShowProgress(ShwProgressProc); //显示进度 resetText=new ResetText(ResetMsgTxt); //重置消息文本 } /*----------定义回调函数----------*/ //显示消息 private void ShwMsgforView(string str) { str=System.DateTime.Now.ToString()+Environment.NewLine+str+Environment.NewLine; int txtGetMsgLength=this.richTextBox1.Text.Length; this.richTextBox1.AppendText(str); this.richTextBox1.ScrollToCaret(); } //显示状态 private void ShwStatusInfo(string str) { toolStripStatusLabel1.Text=str; } //显示进度 private void ShwProgressProc(int progress) { toolStripProgressBar1.Value=progress; } private void ResetMsgTxt() //重置消息文本 { textBox1.Text=""; textBox1.Focus(); } /*----------定义回调函数----------*/ //接收请求 private void AcceptClientConnect() { statusStrip1.Invoke(showStatus, "[" + localAddress + ":" + port + "] 侦听..."); DateTime nowtime=DateTime.Now; //间歇延时 while (nowtime.AddSeconds(1) > DateTime.Now) { } try { statusStrip1.Invoke(showStatus, "等待连接..."); statusStrip1.Invoke(showProgress, 1); tcpClient=tcpListener.AcceptTcpClient(); //同步操作1 //附加操作1 statusStrip1.Invoke(showProgress, 100); if (tcpClient!=null) { statusStrip1.Invoke(showStatus, "接收了一个连接请求."); networkStream=tcpClient.GetStream(); br=new BinaryReader(networkStream); bw=new BinaryWriter(networkStream); } } catch { statusStrip1.Invoke(showStatus, "停止侦听."); if (tcpListener!=null) tcpListener.stop(); //间歇延时 DateTime now=DateTime.Now; while (now.AddSeconds(1) > DateTime.Now) { } statusStrip1.Invoke(showProgress, 0); statusStrip1.Invoke(showStatus, "就绪"); } } //接收消息 private void ReceiveMessage() { statusStrip1.Invoke(showStatus, "接收中..."); for (int i=0; i < 5; i++) { try { string rcvMsgStr=br.ReadString(); //同步操作2 //附加操作2 statusStrip1.Invoke(showProgress, i+1); if (rcvMsgStr!=null) { richTextBox1.Invoke(showMessage, rcvMsgStr); } } catch { if (br!=null) { br.Close(); } if (bw!=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } statusStrip1.Invoke(showStatus, "连接断开!"); statusStrip1.Invoke(showProgress, 0); DateTime now=DateTime.Now; //间歇延时 while (now.AddSeconds(2) > DateTime.Now) { } //重启一个线程等待接收新的请求 Thread threadAccept=new Thread(AcceptClientConnect); threadAccept.Start(); break; } } statusStrip1.Invoke(showStatus, "接收了"+5+"条消息."); } //发送消息 private void SendMessage(object state) { statusStrip1.Invoke(showStatus, "正在发送..."); for (int i=0; i < 5; i++) { try { bw.Write(state.ToString()); //非同步 statusStrip1.Invoke(showProgress, i + 1); //非同步操作 DateTime now=DateTime.Now; //间歇延时 while (now.AddSeconds(5) > DateTime.Now) { } bw.Flush(); } catch { if (br!=null) { br.Close(); } if (bw!=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } statusStrip1.Invoke(showStatus, "连接断开!"); statusStrip1.Invoke(showProgress, 0); DateTime now=DateTime.Now; //间歇延时 while (now.AddSeconds(2) > DateTime.Now) { } //重启一个线程等待接收新的请求 Thread threadAccept=new Thread(AcceptClientConnect); threadAccept.Start(); break; } } statusStrip1.Invoke(showStatus, "完毕"); DateTime nowtime=DateTime.Now; //间歇延时 while (nowtime.AddSeconds(1) > DateTime.Now) { } statusStrip1.Invoke(showProgress, 0); richTextBox1.Invoke(resetText, null); } 2. 异步TCP编程实例 【例32】编写如图33所示的Windows程序。使用异步TCP编程,实现客户端与服务器通信,演示服务器与客户端相互收发信息的过程,理解异步TCP的编程方法。 图33例32的主界面 1) 界面设计 异步与同步的设计界面相同,但方法是用异步实现,程序运行后可以通过观察进度条知道二者之间的不同。 2) 客户端程序主要代码 public partial class Form1 : Form { private TcpClient tcpClient; private NetworkStream networkStream; private BinaryReader br; private BinaryWriter bw; /*------------声明委托------------*/ //显示消息 private delegate void ShowMessage(string str); private ShowMessage showMessage; private delegate void ShowStatus(string str); //显示状态 private ShowStatus showStatus; //显示进度 private delegate void ShowProgress(int progress); private ShowProgress showProgress; //重置消息文本 private delegate void ResetText(); private ResetText resetText; //异步调用 private delegate void ReceiveMessageDelegate(out string receiveMessage); private ReceiveMessageDelegate receiveMessageDelegate; private delegate void SendMessageDelegate(string sendMessage); private SendMessageDelegate sendMessageDelegate; /*------------声明委托------------*/ public Form1() { InitializeComponent(); /*----------定义委托----------*/ showMessage=new ShowMessage(ShwMsgforView); //显示消息 showStatus=new ShowStatus(ShwStatusInfo); //显示状态 showProgress=new ShowProgress(ShwProgressProc); //显示进度 resetText=new ResetText(ResetMsgTxt); //重置消息文本 //接收消息 receiveMessageDelegate=new ReceiveMessageDelegate(AsyncRcvMsg); //发送消息 sendMessageDelegate=new SendMessageDelegate(AsyncSndMsg); /*----------定义委托----------*/ } /*----------定义回调函数----------*/ //显示消息 private void ShwMsgforView(string str) { str=System.DateTime.Now.ToString()+Environment.NewLine+str+Environment.NewLine; int txtGetMsgLength=this.richTextBox1.Text.Length; this.richTextBox1.AppendText(str); this.richTextBox1.Select(txtGetMsgLength, str.Length-Environment.NewLine.Length*2-str.Length); this.richTextBox1.SelectionColor=Color.Red; this.richTextBox1.ScrollToCaret(); } //显示状态 private void ShwStatusInfo(string str) { toolStripStatusLabel1.Text=str; } //显示进度 private void ShwProgressProc(int progress) { toolStripProgressBar1.Value=progress; } //重置消息文本 private void ResetMsgTxt() { textBox1.Text=""; textBox1.Focus(); } //异步方法 private void AsyncRcvMsg(out string receiveMessage) { receiveMessage=null; try { receiveMessage=br.ReadString(); } catch { if (br!=null) { br.Close(); } if (bw!=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } statusStrip1.Invoke(showStatus, "连接断开!"); statusStrip1.Invoke(showProgress, 0); } } private void AsyncSndMsg(string sendMessage) { try { bw.Write(sendMessage); DateTime now=DateTime.Now; //间歇延时 while (now.AddSeconds(5) > DateTime.Now) { } bw.Flush(); } catch { if (br!=null) { br.Close(); } if (bw!=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } statusStrip1.Invoke(showMessage, "连接断开!"); statusStrip1.Invoke(showProgress, 0); } } /*----------定义回调函数----------*/ //发起连接请求 private void ConnectoServer() { AsyncCallback requestcallback=new AsyncCallback(RequestCallBack); statusStrip1.Invoke(showStatus, "正在连接..."); statusStrip1.Invoke(showProgress, 1); tcpClient=new TcpClient(AddressFamily.InterNetwork); IAsyncResult result=tcpClient.BeginConnect(IPAddress.Parse(textBox2.Text), 5656, requestcallback, tcpClient);//异步操作1 while (result.IsCompleted==false) { Thread.Sleep(30); } } //回调函数,用于向服务进程发起连接请求 private void RequestCallBack(IAsyncResult iar) { try { tcpClient=(TcpClient)iar.AsyncState; tcpClient.EndConnect(iar); statusStrip1.Invoke(showProgress, 100); DateTime nowtime=DateTime.Now; //间歇延时 while (nowtime.AddSeconds(1) > DateTime.Now) { } if (tcpClient!=null) { statusStrip1.Invoke(showStatus, "连接成功!"); networkStream=tcpClient.GetStream(); br=new BinaryReader(networkStream); bw=new BinaryWriter(networkStream); } } catch { statusStrip1.Invoke(showStatus, "连接失败!"); //间歇延时 DateTime now=DateTime.Now; while (now.AddSeconds(1) > DateTime.Now) { } statusStrip1.Invoke(showProgress, 0); statusStrip1.Invoke(showStatus, "准备就绪"); } } //接收消息 private void ReceiveMessage() { statusStrip1.Invoke(showStatus, "接收中..."); string receiveString=null; for (int i=0; i < 5; i++) { try { IAsyncResult result=receiveMessageDelegate.BeginInvoke(out receiveString,null, null);//异步操作2 int j=1; while (result.IsCompleted==false) { statusStrip1.Invoke(showProgress, j); j++; if (j==5) { j=0; } Thread.Sleep(500); } receiveMessageDelegate.EndInvoke(out receiveString,result); statusStrip1.Invoke(showProgress, 5); if (receiveString!=null) { richTextBox1.Invoke(showMessage, receiveString); } } catch { DateTime now=DateTime.Now; //间歇延时 while (now.AddSeconds(2) > DateTime.Now) { } break; } } statusStrip1.Invoke(showStatus, "接收了"+5+"条消息."); } //发送消息 private void SendMessage(object state) { statusStrip1.Invoke(showStatus, "正在发送..."); for (int i=0; i < 5; i++) { try { IAsyncResult result=sendMessageDelegate.BeginInvoke(state.ToString(), null, null);//异步操作3 while (result.IsCompleted==false) { Thread.Sleep(30); } sendMessageDelegate.EndInvoke(result); statusStrip1.Invoke(showProgress, i+1); } catch { //间歇延时 DateTime now=DateTime.Now; while (now.AddSeconds(2) > DateTime.Now) { } break; } } statusStrip1.Invoke(showStatus, "完毕"); //间歇延时 DateTime nowtime=DateTime.Now; while (nowtime.AddSeconds(1) > DateTime.Now) { } statusStrip1.Invoke(showProgress, 0); richTextBox1.Invoke(resetText, null); } 3) 服务器端程序主要代码 public partial class Form1 : Form { private IPAddress localAddress; private const int port=5656; private TcpListener tcpListener; private TcpClient tcpClient; private NetworkStream networkStream; private BinaryReader br; private BinaryWriter bw; /*------------声明委托------------*/ private delegate void ShowMessage(string str); //显示消息 private ShowMessage showMessage; private delegate void ShowStatus(string str); //显示状态 private ShowStatus showStatus; //显示进度 private delegate void ShowProgress(int progress); private ShowProgress showProgress; //重置消息文本 private delegate void ResetText(); private ResetText resetText; //异步调用(与要调用的方法具有相同签名) private delegate void ReceiveMessageDelegate(out string receiveMessage); private ReceiveMessageDelegate receiveMessageDelegate; private delegate void SendMessageDelegate(string sendMessage); private SendMessageDelegate sendMessageDelegate; /*------------声明委托------------*/ public Form1() { InitializeComponent(); /*----------定义委托----------*/ //显示消息 showMessage=new ShowMessage(ShwMsgforView); //显示状态 showStatus=new ShowStatus(ShwStatusInfo); //显示进度 showProgress=new ShowProgress(ShwProgressProc); //重置消息文本 resetText=new ResetText(ResetMsgTxt); //接收消息 receiveMessageDelegate=new ReceiveMessageDelegate(AsyncRcvMsg); //发送消息 sendMessageDelegate=new SendMessageDelegate(AsyncSndMsg); /*----------定义委托----------*/ } /*----------定义回调函数----------*/ //显示消息 private void ShwMsgforView(string str) { str=System.DateTime.Now.ToString()+Environment.NewLine+str+Environment.NewLine; int txtGetMsgLength=this.richTextBox1.Text.Length; this.richTextBox1.AppendText(str); this.richTextBox1.Select(txtGetMsgLength, str.Length-Environment.NewLine.Length * 2-str.Length); this.richTextBox1.SelectionColor=Color.Red; this.richTextBox1.ScrollToCaret(); } //显示状态 private void ShwStatusInfo(string str) { toolStripStatusLabel1.Text=str; } //显示进度 private void ShwProgressProc(int progress) { toolStripProgressBar1.Value=progress; } //重置消息文本 private void ResetMsgTxt() { textBox1.Text=""; textBox1.Focus(); } //异步方法 private void AsyncRcvMsg(out string receiveMessage) { receiveMessage=null; try { receiveMessage=br.ReadString(); } catch { if (br!=null) { br.Close(); } if (bw!=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } statusStrip1.Invoke(showStatus, "连接断开!"); statusStrip1.Invoke(showProgress, 0); } } private void AsyncSndMsg(string sendMessage) { try { bw.Write(sendMessage); //间歇延时 DateTime now=DateTime.Now; while (now.AddSeconds(5) > DateTime.Now) { } bw.Flush(); } catch { if (br!=null) { br.Close(); } if (bw!=null) { bw.Close(); } if (tcpClient!=null) { tcpClient.Close(); } statusStrip1.Invoke(showMessage, "连接断开!"); statusStrip1.Invoke(showProgress, 0); } } /*----------定义回调函数----------*/ //接收请求 private void AcceptClientConnect() { statusStrip1.Invoke(showStatus, "["+localAddress+":" + port+"]侦听..."); //间歇延时 DateTime nowtime=DateTime.Now; while (nowtime.AddSeconds(1) > DateTime.Now) { } AsyncCallback acceptcallback=new AsyncCallback(AcceptClientCallBack); statusStrip1.Invoke(showStatus, "等待连接..."); statusStrip1.Invoke(showProgress, 1); IAsyncResult result= tcpListener.BeginAcceptTcpClient(acceptcallback, tcpListener); //异步操作1 int i=2; while (result.IsCompleted==false) { statusStrip1.Invoke(showProgress, i); i++; if (i==100) { i=0; } Thread.Sleep(30); } } //回调函数,用于处理客户进程的连接请求 private void AcceptClientCallBack(IAsyncResult iar) { try { tcpListener=(TcpListener)iar.AsyncState; tcpClient=tcpListener.EndAcceptTcpClient(iar); statusStrip1.Invoke(showProgress, 100); if (tcpClient!=null) { statusStrip1.Invoke(showStatus, "接收了一个连接请求."); networkStream=tcpClient.GetStream(); br=new BinaryReader(networkStream); bw=new BinaryWriter(networkStream); } } catch { statusStrip1.Invoke(showStatus, "停止侦听."); //间歇延时 DateTime now=DateTime.Now; while (now.AddSeconds(1) > DateTime.Now) { } statusStrip1.Invoke(showProgress, 0); statusStrip1.Invoke(showStatus, "准备就绪"); } } //接收消息 private void ReceiveMessage() { statusStrip1.Invoke(showStatus, "接收中..."); string receiveString=null; for (int i=0; i < 5; i++) { try { IAsyncResult result=receiveMessageDelegate.BeginInvoke(out receiveString, null, null);//异步操作2 int j=1; while (result.IsCompleted==false) { statusStrip1.Invoke(showProgress, j); j++; if (j==5) { j=0; } Thread.Sleep(500); } receiveMessageDelegate.EndInvoke(out receiveString,result); statusStrip1.Invoke(showProgress, 5); if (receiveString!=null) { richTextBox1.Invoke(showMessage, receiveString); } } catch { DateTime now=DateTime.Now; //间歇延时 while (now.AddSeconds(2) > DateTime.Now) { } //重启一个线程等待接收新的请求 Thread threadAccept=new Thread(AcceptClientConnect); threadAccept.Start(); break; } } statusStrip1.Invoke(showStatus, "接收了"+5+"条消息."); } //发送消息 private void SendMessage(object state) { statusStrip1.Invoke(showStatus, "正在发送..."); for (int i=0; i < 5; i++) { try { IAsyncResult result=sendMessageDelegate.BeginInvoke(state.ToString(), null, null);//异步操作3 while (result.IsCompleted==false) { Thread.Sleep(30); } sendMessageDelegate.EndInvoke(result); statusStrip1.Invoke(showProgress, i+1); } catch { DateTime now=DateTime.Now; //间歇延时 while (now.AddSeconds(2) > DateTime.Now) { } //重启一个线程等待接收新的请求 Thread threadAccept=new Thread(AcceptClientConnect); threadAccept.Start(); break; } } statusStrip1.Invoke(showStatus, "完毕"); DateTime nowtime=DateTime.Now; //间歇延时 while (nowtime.AddSeconds(1) > DateTime.Now) { } statusStrip1.Invoke(showProgress, 0); textBox1.Invoke(resetText, null); } 3.2基于同步TCP的网络聊天程序开发 3.2.1功能介绍及界面设计 【例33】编写如图34所示的Windows程序。使用同步TCP编程,实现客户端与服务器端之间进行聊天。 1. 界面设计 在VS 2010中,新建两个(客户端和服务器端)Windows应用程序,项目名分别是TCP_Client和TCP_Server。 图34例33的主界面 程序上的控件描述如表35和表36所示。 表35客户端控件描述 名称控 件 类 型功 能 描 述 Form1Form程序主窗体 groupBox1GroupBox存放连接操作的各个控件 label1Label显示本机IP地址 label2Label提示服务器输入IP地址 textBox2TextBox输入服务器IP地址 button2Button“连接”按钮 button3Button“断开”按钮 groupBox2GroupBox存放聊天操作的各个控件 richTextBox1RichTextBox显示聊天消息 textBox1TextBox需要发送的消息 button1Button“发送”按钮 statusStripLabel1StatusStripLabel显示当前状态 statusStrip1StatusStrip存放statusStripLabel1来显示当前状态 表36服务器端控件描述 名称控 件 类 型功 能 描 述 Form1Form程序主窗体 toolStrip1ToolStrip存放“启动监听”“停止监听”按钮 toolStrip1Button1ToolStrip1Button“启动监听”按钮 toolStrip1Button2ToolStrip1Button“停止监听”按钮 label1Label显示本机IP地址 richTextBox1RichTextBox显示聊天消息 textBox1TextBox需要发送的消息 button1Button“发送”按钮 groupBox2GroupBox存放聊天操作的各个控件 groupBox1GroupBox存放listBox1 listBox1ListBox显示已连接到服务器端的所有主机 statusStrip1StatusStrip状态栏 statusStripLabel1StatusStripLabel显示文本状态 statusStripLabel2StatusStripLabel显示当前状态 2. 功能介绍 服务器端运行后单击“启动监听”按钮,监听有无试图连接到本机的客户端,当监听到有连接到本机的客户端并且连接成功后,服务器提示有客户端登录,并且服务器向客户端发送“欢迎登录”的欢迎消息。服务器端的listBox1上显示所有连接到它的客户端,并且服务器可以选择任意连接到它的客户端进行聊天。 3.2.2服务器程序编写 编写服务器程序的步骤如下。 (1) 创建一个监听类接收和处理服务器的连接请求。因为当客户端连接成功后,服务器端要向客户端发送欢迎消息,所以服务器的监听类还必须处理向客户端发送欢迎消息这一任务。 监听类(Listener)的程序代码如下: public class AddMessageEventArgs : EventArgs { public string mess; //存放要显示的内容 } class Listener { private Thread th; private TcpListener tcpl; public volatile bool listenerRun=true; //判断是否启动 public event EventHandler OnAddMessage; public event EventHandler OnIpRemod; public Listener() { } //另一个线程开始监听 public void StartListener() { th=new Thread(new ThreadStart(Listen)); th.Start(); } //停止监听 public void Stop() { tcpl.Stop(); th.Abort(); } private void Listen() { try { IPAddress addr=new IPAddress(Dns.GetHostByName(Dns.GetHostName()).AddressList[0].Address); IPEndPoint ipLocalEndPoint=new IPEndPoint(addr, 5656); tcpl=new TcpListener(ipLocalEndPoint); tcpl.Start(); while (listenerRun) { Socket s=tcpl.AcceptSocket(); string remote=s.RemoteEndPoint.ToString(); Byte[] stream=new Byte[1024]; int i=s.Receive(stream); string msg; #region string str=System.Text.Encoding.UTF8.GetString(stream); if (str.Substring(0, 1)=="1") { string str_="欢迎登录!"; TcpClient tcpc=new TcpClient(((IPEndPoint)s.RemoteEndPoint).Address. ToString(), 5657); NetworkStream tcpStream=tcpc.GetStream(); Byte[] data=System.Text.Encoding.UTF8.GetBytes(str_); tcpStream.Write(data, 0, data.Length); tcpStream.Close(); tcpc.Close(); msg="<"+remote +">"+"上线"; AddMessageEventArgs arg=new AddMessageEventArgs(); arg.mess=msg; OnAddMessage(this, arg); } else if (str.Substring(0, 1)=="0") { msg="<"+remote+">"+"断开"; AddMessageEventArgs argRe=new AddMessageEventArgs(); argRe.mess=remote.ToString(); OnIpRemod(this, argRe); AddMessageEventArgs arg=new AddMessageEventArgs(); arg.mess=msg; OnAddMessage(this, arg); } #endregion else { msg="<"+remote+">"+System.Text.UTF8Encoding.UTF8.GetString(stream); AddMessageEventArgs arg=new AddMessageEventArgs(); arg.mess=msg; OnAddMessage(this, arg); } } } catch (System.Security.SecurityException) { MessageBox.Show("防火墙禁止连接"); } catch (Exception) { } } } (2) 服务器还要能够和客户端聊天,创建一个发送类用于向客户端发送聊天消息。 发送类(Sender)的程序代码如下: class Sender { private string obj; //目标主机 public Sender(string str) { obj=str; } public void Send(string str/*需要发送的字符串*/) { try { TcpClient tcpc=new TcpClient(obj, 5657); NetworkStream tcpStream=tcpc.GetStream(); Byte[] data=System.Text.UTF8Encoding.UTF8.GetBytes(str); tcpStream.Write(data, 0, data.Length); tcpStream.Close(); tcpc.Close(); } catch (Exception) { MessageBox.Show("连接被目标主机拒绝"); } } } (3) 在Windows窗体应用程序中,用户要有必要的输入和操作,所以在窗体类中也要能够实现相应的功能。 Windows窗体类(Form)的程序代码如下: public partial class Form1 : Form { public Form1() { InitializeComponent(); } public bool appRun=true; private Listener lis; //监听对象 private Sender sen; //发送对象 string netIp; string chatTo; string myip; IPAddress myscanip; //返回信息 public void AddMessage(object sender, AddMessageEventArgs e) { string message=e.mess; string appendText; string[] sep=message.Split('>'); string[] sepIp=sep[0].Split('<', ':'); bool checkIp=true; for (int i=0; i < listBox1.Items.Count; i++) { if (listBox1.Items[i].ToString()==sepIp[1]) checkIp=false; } if (checkIp && sep[1]!="断开") { this.listBox1.Items.Add(sepIp[1].Trim()); chatTo=sepIp[1]; } appendText=sep[0]+">:"+System.DateTime.Now.ToString()+Environment.NewLine+sep[1]+Environment.NewLine; int txtGetMsgLength=this.richTextBox1.Text.Length; this.richTextBox1.AppendText(appendText); this.richTextBox1.Select(txtGetMsgLength, appendText.Length-Environment.NewLine.Length * 2-sep[1].Length); this.richTextBox1.SelectionColor=Color.Red; this.richTextBox1.ScrollToCaret(); } //下线 public void IpRemo(object sender, AddMessageEventArgs e) { string[] sep=e.mess.Split(':'); try { int index=0; for (int i=0; i < listBox1.Items.Count; i++) { if (listBox1.Items[i].ToString()==sep[0].ToString()) { index=i; this.listBox1.Items.RemoveAt(index); } } } catch { MessageBox.Show("没有这个IP"); } } //启动监听 private void toolStripButton1_Click(object sender, EventArgs e) { this.start_listen(); this.toolStripStatusLabel2.Text="监听已启动"; } //停止监听 private void toolStripButton2_Click(object sender, EventArgs e) { try { lis.listenerRun=false; lis.Stop(); this.toolStripStatusLabel2.Text="监听已停止"; } catch (NullReferenceException) { } } private void Form1_Load(object sender, EventArgs e) { System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls=false; netIp=getNetId();//网关 this.label1.Text="本主机IP是:"+GetMyIpAddress(); } //连接 private void start_listen() { try { if (lis.listenerRun==true) { lis.listenerRun=false; lis.Stop(); } } catch (NullReferenceException){} finally { lis=new Listener(); lis.OnAddMessage+=new EventHandler(this.AddMessage); lis.OnIpRemod +=new EventHandler(this.IpRemo); lis.StartListener(); } } //获取网络号 string getNetId() { string NetId; string ip=GetMyIpAddress(); NetId=ip.Substring(0, ip.LastIndexOf(".") + 1); return NetId; } //获取本机IP private static string GetMyIpAddress() { IPAddress addr=new System.Net.IPAddress(Dns.GetHostByName(Dns.GetHostName()).AddressList[0].Address); return addr.ToString(); } //发送 private void button1_Click(object sender, EventArgs e) { if (listBox1.SelectedIndex < 0 && chatTo=="" && chatTo==null ) { MessageBox.Show("请选择目标主机"); return; } else if (textBox1.Text.Trim()=="") { MessageBox.Show("消息内容不能为空!", "错误"); this.textBox1.Focus(); return; } else { try { sen=new Sender(chatTo); sen.Send(textBox1.Text); string appendText; appendText="Me:"+System.DateTime.Now.ToString()+Environment.NewLine+textBox1.Text+Environment.NewLine; int txtGetMsgLength=this.richTextBox1.Text.Length; this.richTextBox1.AppendText(appendText); this.richTextBox1.Select(txtGetMsgLength,appendText.Length-Environment.NewLi ne.Length * 2-textBox1.Text.Length); this.richTextBox1.SelectionColor=Color.Blue; this.richTextBox1.ScrollToCaret(); } catch { } this.textBox1.Text=""; this.textBox1.Focus(); } } private void listBox1_MouseDoubleClick(object sender, MouseEventArgs e) { if (e.Clicks!=0) { if (listBox1.SelectedItem!=null) { this.start_listen(); chatTo=listBox1.SelectedItem.ToString(); } } } 3.2.3客户端程序编写 客户端与服务器端基本一样,都有相应的监听类来监听和接收服务器端发送过来的信息,也有相应的发送类向服务器发送消息还有相应的窗体类来处理用户的输入和操作。 客户端监听类(Listener)的程序代码如下: public class AddMessageEventArgs : EventArgs { public string mess; //存放要显示的内容 } class Listener { private Thread th; private TcpListener tcpl; public volatile bool listenerRun=true; //是否启动 public event EventHandler OnAddMessage; public Listener() { } //启动另一个线程开始监听 public void StartListener() { th=new Thread(new ThreadStart(Listen)); th.Start(); } public void Stop()//停止监听 { tcpl.Stop(); th.Abort(); } private void Listen() { try { IPAddress addr=new IPAddress(Dns.GetHostByName(Dns.GetHostName()).AddressList[0].Address); IPEndPoint ipLocalEndPoint=new IPEndPoint(addr, 5657); tcpl=new TcpListener(ipLocalEndPoint); tcpl.Start(); while (listenerRun) { Socket s=tcpl.AcceptSocket(); string remote=s.RemoteEndPoint.ToString(); Byte[] stream=new Byte[512]; int i=s.Receive(stream); string msg="<"+remote+">"+ System.Text.UTF8Encoding.UTF8.GetStr ing(stream); AddMessageEventArgs arg=new AddMessageEventArgs(); arg.mess=msg; OnAddMessage(this, arg); } } catch (System.Security.SecurityException) { MessageBox.Show("防火墙禁止连接"); } catch (Exception) { MessageBox.Show("监听已经停止"); } } } 发送类(Sender)的程序代码如下: private string obj; //目标主机 public Sender(string str) { obj=str; } public void Send(string str/*需要发送的字符串*/) { try { TcpClient tcpc=new TcpClient(obj, 5656); NetworkStream tcpStream=tcpc.GetStream(); Byte[]=dataSystem.Text.UTF8Encoding.UTF8.GetBytes(str); tcpStream.Write(data, 0, data.Length); tcpStream.Close(); tcpc.Close(); } catch (Exception) { MessageBox.Show("连接被目标主机拒绝"); } } 窗体类(Form)的程序代码如下: public partial class Form1 : Form { public Form1() { InitializeComponent(); } public bool appRun=true; private Listener lis; //监听对象 private Sender sen; //发送对象 string netIp; string chatTo; public void AddMessage(object sender, AddMessageEventArgs e) { string message=e.mess; string appendText; string[] sep=message.Split('>'); appendText=sep[0]+">:"+System.DateTime.Now.ToString()+Environment.NewLine+sep[1]+Environment.NewLine; int txtGetMsgLength=this.richTextBox1.Text.Length; this.richTextBox1.AppendText(appendText); this.richTextBox1.Select(txtGetMsgLength, appendText.Length-Environment.NewLine.Length * 2-sep[1].Length); this.richTextBox1.SelectionColor=Color.Red; this.richTextBox1.ScrollToCaret(); } private void Form1_Load(object sender, EventArgs e) { System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls=false; netIp=getNetId(); //网关 this.label1.Text="本机IP:"+GetMyIpAddress(); start_listen(); } //连接服务器 private void button2_Click(object sender, EventArgs e) { if (textBox2.Text.Trim()=="") { MessageBox.Show("请输入主机号"); return; } else { try { chatTo=textBox2.Text; TcpClient tcpc=new TcpClient(textBox2.Text, 5656); NetworkStream tcpStream=tcpc.GetStream(); Byte[] data=System.Text.UTF8Encoding.UTF8.GetBytes("1"); tcpStream.Write(data, 0, data.Length); tcpStream.Close(); tcpc.Close(); this.toolStripStatusLabel1.Text="当前状态:已连接到服务器"; } catch (SocketException) { MessageBox.Show("目标主机没有启动监听", "系统提示"); } } } //断开连接 private void button3_Click(object sender, EventArgs e) { try { sen=new Sender(chatTo); sen.Send("0"); //发送一个断开标志 lis.listenerRun=false; lis.Stop(); this.toolStripStatusLabel1.Text="当前状态:与服务器断开连接"; } catch (NullReferenceException) { } } private void button1_Click(object sender, EventArgs e) { if (textBox2.Text.Trim()=="") { MessageBox.Show("请选择目标主机"); return; } else if (textBox1.Text.Trim()=="") { MessageBox.Show("消息内容不能为空!", "错误"); this.textBox1.Focus(); return; } else { try { sen=new Sender(chatTo); sen.Send(textBox1.Text); string appendText; appendText="Me:"+System.DateTime.Now.ToString()+Environment.NewLine+textBox1.Text+Environment.NewLine; int txtGetMsgLength=this.richTextBox1.Text.Length; this.richTextBox1.AppendText(appendText); this.richTextBox1.Select(txtGetMsgLength,appendText.Length-Environment.NewLine.Length * 2-textBox2.Text.Length); this.richTextBox1.SelectionColor=Color.Blue; this.richTextBox1.ScrollToCaret(); } catch { } this.textBox1.Text=""; this.textBox1.Focus(); } } //连接方法 private void start_listen() { try { if (lis.listenerRun==true) { lis.listenerRun=false; lis.Stop(); } } catch (NullReferenceException) { } finally { lis=new Listener(); lis.OnAddMessage+=new EventHandler(this.AddMessage); lis.StartListener(); } } //获取网络号 string getNetId() { string netId; string ip=GetMyIpAddress(); netId=ip.Substring(0, ip.LastIndexOf(".")+1); return netId; } //获取本机IP private static string GetMyIpAddress() { IPAddress addr=new System.Net.IPAddress(Dns.GetHostByName(Dns.GetHostName()).AddressList[0].Address); return addr.ToString(); } 3.3基于异步TCP的网络聊天程序开发 利用TcpListener和TcpClient类在同步方式下接收、发送数据以及监听客户端连接时,在操作没有完成之前一直处于阻塞状态,这在接收、发送数据量不大,或者操作用时较短的情况下是比较方便的。但是,对于执行时间较长的任务,如传送大文件等,使用同步操作就不太合适了,这种情况下,最好的办法是使用异步操作。 异步操作的最大优点是可以在一个操作没有完成之前同时进行其他的操作。.NET框架提供了一种称为AsyncCallback(异步回调)的委托,该委托允许启动异步的功能,并在条件具备时调用提供的回调方法(是一种在操作或活动完成时由委托自动调用的方法),然后在这个方法中完成并结束未完成的工作。 3.3.1异步程序编程方法 使用异步TCP应用编程时,除了套接字有对应的异步操作方式外,TcpListener和TcpClient类也提供了异步操作的方法。 异步操作方式下,每个Begin方法都有一个匹配的End方法。在程序中利用Begin方法开始执行异步操作,然后由委托在条件具备时调用End方法完成并结束异步操作。 表37列出了TcpListener、TcpClient以及Socket提供的部分异步操作方法。 表37TcpListener、TcpClient及Socket提供的部分异步操作方法 类提供的方法说明 TcpListener BeginAcceptTcpClient开始一个异步操作接收一个传入的连接尝试 EndAcceptTcpClient异步接收传入的连接尝试,并创建新的TcpClient处理远程主机通信 TcpClient BeginConnect开始一个对远程主机连接的异步请求 EndConnect异步接收传入的连接尝试 Socket BeginReceive开始从连接的Socket中异步接收数据 EndReceive结束挂起的异步读取 BeginSend将数据异步发送到连接的Socket EndSend结束挂起的异步发送 1. AsyncCallback委托 AsyncCallback委托用于引用异步操作完成时调用的回调方法。在异步操作方式下,由于程序可以在启动异步操作后继续执行其他代码,因此必须有一种机制,以保证该异步操作完成时能及时通知调用者。这种机制可以通过AsyncCallback委托实现。 异步操作的每一个方法都有一个Begin…方法和一个End…方法,例如BeginAcceptTcpClient和EndAcceptTcpClient。程序调用Begin…方法时,系统会自动在线程池中创建对应的线程进行异步操作,从而保证调用方和被调用方同时执行; 当线程池中的Begin…方法执行完毕时,会自动通过AsyncCallback委托调用在Begin…方法的参数中指定的回调方法。 回调方法是在程序中事先定义的,在回调方法中,通过End…方法获取Begin…方法的返回值和所有输入/输出参数,从而达到在异步操作方式下完成参数传递的目的。 2. BeginAcceptTcpClient和EndAcceptTcpClient方法 BeginAcceptTcpClient和EndAcceptTcpClient方法包含在System.Net.Sockets命名空间下的TcpListener类中。在异步TCP应用编程中,服务器端可以使用TcpListener类提供的BeginAcceptTcpClient方法接收新的客户端连接请求。在这个方法中,系统自动利用线程池创建需要的线程,并在操作完成时利用异步回调机制调用提供给它的方法,同时返回相应的状态参数。其方法原型为: public IAsyncResult BeginAcceptTcpClient(AsyncCallback callback, Object state) 其中,参数1为AsyncCallback类型的委托; 参数2为Object类型,用于将状态信息传递给委托提供的方法。例如: AsyncCallback callback=new AsyncCallback(AcceptTcpClientCallback); tcpListener.BeginAcceptTcpClient(callback, tcpListener); 程序执行BeginAcceptTcpClient方法后,立即在线程池中自动创建需要的线程,同时在自动创建的线程中监听客户端连接请求。一旦接收了客户端连接请求,就自动通过委托调用提供给委托的方法,并返回状态信息。这里将委托自动调用的方法命名为AcceptTcpClientCallback,状态信息定义为TcpListener类型的实例tcpListener。在程序中,定义该方法的格式为: void AcceptTcpClientCallback( IAsyncResult ar) { 回调代码 } 方法中传递的参数只有一个,而且必须是IAsyncResult类型的接口,它表示异步操作的状态。由于定义了委托提供的方法(即AcceptTcpClientCallback方法),因此系统会自动将该状态信息从关联的BeginAcceptTcpClient方法传递到自定义的AcceptTcpClientCallback方法。注意,在回调代码中,必须调用EndAcceptTcpClient方法完成客户端连接。关键代码为: void AcceptTcpClientCallback( IAsyncResult ar) { … TcpListener myListener=(TcpListener)ar.AsyncState; TcpClient client=myListener.EndAcceptTcpClient(ar); … } 程序执行EndAcceptTcpClient方法后,会自动完成客户端连接请求,并返回包含底层套接字的TcpClient对象,接下来就可以利用这个对象与客户端进行通信了。 默认情况下,程序执行BeginAcceptTcpClient方法后,在该方法返回状态信息之前,不会像同步TCP方式那样阻塞等待客户端连接,而是继续往下执行。如果希望在其返回状态信息之前阻塞当前线程的执行,可以调用ManualResetEvent对象的WaitOne方法。 3. BeginConnect方法和EndConnect方法 BeginConnect方法和EndConnect方法包含在命名空间System.Net.Sockets下的TcpClient类和Socket类中,这里只讨论TcpClient类中的方法。 在异步TCP应用编程中,BeginConnect方法通过异步方式向远程主机发出连接请求。该方法有3种重载形式,方法原型为: (1) public IAsyncResult BeginConnect(IPAddress address, int port, AsyncCallback requestCallback, Object state) (2) Public IAsyncResult BeginConnect(IPAddress[]addresses,int port,AsyncCallback requestCallback, Object state) (3) public IAsyncResult BeginConnect(string host,int port,AsyncCallback requestCallback, Object state) 其中,address为远程主机的IPAddress对象; port为远程主机的端口号; requestCallback为AsyncCallback类型的委托; state为包含连接操作的相关信息,当操作完成时,此对象会被传递给requestCallback委托。 BeginConnect方法在操作完成前不会阻塞,程序中调用BeginConnect方法时,系统会自动用独立的线程来执行该方法,直到与远程主机连接成功或抛出异常。如果在调用BeginConnect方法之后想阻塞当前线程,可以调用ManualResetEvent对象的WaitOne方法。 异步BeginConnect方法只有在调用了EndConnect方法之后才算执行完毕。因此,程序中需要在提供给requestCallback委托调用的方法中调用TcpClient对象的EndConnect方法。关键代码为: … AsyncCallback requestCallback=new AsyncCallback(RequestCallback); tcpClient.BeginConnect(远程主机IP或域名,远程主机端口号, requestCallback, tcpClient); … void RequestCallback(IAsyncResult ar) { … tcpClient=(TcpClient)ar.AsyncState; client.EndConnect(ar); … } 在自定义的RequestCallback中,通过获取的状态信息得到新的TcpClient类型的对象,并调用EndConnect结束连接请求。 4. 发送数据 在异步TCP应用编程中,如果本机已经和远程主机建立连接,就可以用System.Net.Sockets命名空间下NetworkStream类中的BeginWrite方法发送数据。其方法原型为: public override IAsyncResult BeginWrite(byte[] buffer, int offset, int size,AsyncCallback callback, Object state) 其中,buffer是一组Byte类型的值,用来存放要发送的数据; offset用来存放发送的数据在发送缓冲区中的起始位置; size用来存放发送数据的字节数; callback是异步回调类型的委托; state包含状态信息。 BeginWrite方法用于向一个已经成功连接的套接字异步发送数据。程序中调用BeginWrite方法后,系统会自动在内部产生的单独执行的线程中发送数据。 使用BeginWrite方法异步发送数据,程序必须创建实现AsyncCallback委托的回调方法,并将其名称传递给BeginWrite方法。在BeginWrite方法中,传递的state参数必须至少包含NetworkStream对象。如果回调需要更多信息,则可以创建一个小型的类或结构,用于保存NetworkStream和其他所需的信息,并通过state参数将结构或类的实例传递给BeginWrite方法。 在回调方法中,必须调用EndWrite方法。程序调用BeginWrite后,系统自动使用单独的线程来执行指定的回调方法,并在EndWrite上一直处于阻塞状态,直到NetworkStream对象发送请求的字节数或引发异常。 5. 接收数据 与发送数据相似,如果本机已经和远程主机建立了连接,就可以用System.Net.Sockets命名空间下NetworkStream类中的BeginRead方法接收数据。其方法原型为: public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, Object state); 其中,buffer为字节数组,存储从NetworkStream读取的数据; offset为buffer中开始读取数据的位置; size为从NetworkStream中读取的字节数; callback为在BeginRead完成时执行的AsyncCallback委托; state包含用户定义的任何附加数据的对象。 BeginRead方法启动从传入网络缓冲区中异步读取数据的操作。调用BeginRead方法后,系统自动在单独的执行线程中接收数据。 在程序中,必须创建实现AsyncCallback委托的回调方法,并将其名称传递给BeginRead方法。state参数必须至少包含NetworkStream对象。一般情况下,我们希望在回调方法中获得所接收的数据,因此应创建小型的类或结构来保存读取缓冲区以及其他任何有用的信息,并通过state参数将结构或类的实例传递给BeginRead方法。 在回调方法中,必须调用EndRead方法完成读取操作。系统执行BeginRead时,将一直等待直到数据接收完毕或者遇到错误,从而得到可用的字节数,然后自动使用一个单独的线程来执行指定的回调方法,并阻塞EndRead方法,直到所提供的NetworkStream对象将可用数据读取完毕,或者达到size参数指定的字节数。 6. EventWaitHandle类 虽然我们可以利用异步操作并行完成一系列功能,但是现实中的很多工作是相互关联的,某些工作必须要等另一个工作完成后才能继续。这个问题就是异步操作中的同步问题。 EventWaitHandle类用于在异步操作时控制线程间的同步,即控制一个或多个线程继续执行或者等待其他线程完成。考虑这样一种情况: 假设有两个线程,一个是写线程,另一个是读线程,两个线程是并行运行的。下面是实现代码: using System; using System.Threading; class Program { private int n1, n2, n3; static void Main(string[] args) { Program p=new Program(); Thread t0=new Thread(new ThreadStart(p.WriteThread)); Thread t1=new Thread(new ThreadStart(p.ReadThread)); t0.Start(); t1.Start(); Console.ReadLine(); } private void WriteThread() { Console.WriteLine("t1"); n1=1; n2=2; n3=3; } private void ReadThread() { Console.WriteLine("{0}+{1}+{2}={3}", n1, n2, n3, n1 + n2 + n3); } } 运行这个程序,输出结果为: t1 0+0+0=0 按照一般的思维逻辑,读线程执行的结果应该是1+2+3=6,可实际运行的结果却是0+0+0=0。显然读线程输出的内容是在写线程尚未写入新值之前得到的结果。如果把这个问题一般化,即某些工作是在线程内部完成的,同时启动多个线程后,我们无法准确判断线程内部处理这些工作的具体时间,而又希望保证一个线程完成某些工作后,另一个线程才能在这个基础上继续运行,最好的办法是什么呢? 这个问题实际上就是如何同步线程的问题。在System.Threading命名空间中,有一个EventWaitHandle类,它能够让操作系统通过发出信号完成多个线程之间的同步,需要同步的线程可以先阻塞当前线程,然后根据Windows操作系统发出的信号,决定是继续阻塞等待其他工作完成,还是不再等待而直接继续执行。 这里涉及的EventWaitHandle类提供的方法有以下几种。 (1) Reset方法: 将信号的状态设置为非终止状态,即不让操作系统发出信号,从而导致等待收到信号才能继续执行的线程阻塞。 (2) Set方法: 将事件状态设置为终止状态,这样等待的线程将会收到信号,从而继续执行而不再等待。 (3) WaitOne方法: 阻塞当前线程,等待操作系统为其发出信号,直到收到信号才解除阻塞。 操作系统发出信号的方式有两种: (1) 发一个信号,使某个等待信号的线程解除阻塞,继续执行。 (2) 发一个信号,使所有等待信号的线程全部解除阻塞,继续执行。 这种机制类似于面试,所有等待的线程都是等待面试者,所有等待的面试者均自动在外面排队等待。操作系统让考官负责面试,考官事先告诉大家他发的信号“继续”有两个含义: 一个是对某个等待面试者而言的,考官每次发信号“继续”,意思是只让一个面试者进去面试,其他面试者必须继续等待,至于谁进去,要看排队情况,一般是排在最前面的那个人进去,这种方式称为自动重置(AutoResetEvent); 另一个是对所有面试者而言的,考官每次发信号“继续”,意思是让所有正在门外等待的面试者全部进来面试,当然对不等待的面试者无效,这种方式称为手动重置(ManualResetEvent)。 为什么说“每次”发信号呢?因为不一定所有考生都在外面等待,可能有些考生没有等在门外,所以他这次发出的“继续”只能对等待的面试者起作用,也许他发出这个信号后,又有面试者到了门外,因此可能需要多次发出“继续”的信号。 考官也可以不发任何信号,这样所有正在等待的面试者只能一直等待。 程序员可以认为是控制考官和面试者的“管理员”,程序员既可以告诉考官“不要发信号”(调用EventWaitHandle的Reset方法),也可以告诉考官“发信号”(调用EventWaitHandle的Set方法),同时还可以决定面试者什么时候去参加面试(调用EventWaitHandle的WaitOne方法)。 利用EventWaitHandle类,可以将上面的代码修改为: using System; using System.Threading; class Program { private int n1, n2, n3; //将信号状态设置为非终止,使用手动重置 EventWaitHandle myEventWaitHandle=new EventWaitHandle(false, EventResetMode.ManualReset); static void Main(string[] args) { Program p=new Program(); Thread t0=new Thread(new ThreadStart(p.WriteThread)); Thread t1=new Thread(new ThreadStart(p.ReadThread)); t0.Start(); t1.Start(); Console.ReadLine(); } private void WriteThread() { //允许其他需要等待的线程阻塞 myEventWaitHandle.Reset(); Console.WriteLine("t1"); n1=1; n2=2; n3=3; //允许其他等待的线程继续 myEventWaitHandle.Set(); } private void ReadThread() { //阻塞当前线程,直到收到信号 myEventWaitHandle.WaitOne(); Console.WriteLine("{0}+{1}+{2}={3}", n1, n2, n3, n1 + n2 + n3); } } 程序中增加了一个EventWaitHandle类型的对象myEventWaitHandle,在WriteThread线程开始时,首先让调用WaitOne方法的线程阻塞,然后继续执行该线程,当任务完成时,向所有调用WaitOne方法的线程发出可以继续执行的事件句柄信号。而ReadThread一开始就将自己阻塞了,当WriteThread执行Set方法后才继续往下执行,因此其WriteLine语句输出的结果为1+2+3=6,达到了我们预期的效果。 在异步操作中,为了让具有先后关联关系的线程同步,即让其按照希望的顺序执行,均可以调用EventWaitHandle类提供的Reset、Set和WaitOne方法。 3.3.2界面设计 【例34】编写如图35所示的Windows程序。使用异步TCP编程,实现客户端与服务器之间进行聊天。 图35例34的主界面 这里的异步与同步采用了一样的界面设计,功能也完全相同,唯一的区别就是采用了异步的方法实现。3.3.3服务器端程序编写 编写异步服务器发送与接收信息的步骤如下: (1) 在监听类中声明一个委托ReceiveMessageDelegate用来异步接收客户端的连接请求,并且写出相应的回调方法处理请求和消息。 //声明委托 private delegate void ReceiveMessageDelegate(out string receiveMessage); private ReceiveMessageDelegate receiveMessageDelegate; //创建委托对象 receiveMessageDelegate=new ReceiveMessageDelegate(AsyncRcvMsg); //调用异步方法处理客户端发送过来的连接请求和信息 IAsyncResult result=receiveMessageDelegate.BeginInvoke(out receiveString, null, null); receiveMessageDelegate.EndInvoke(out receiveString, result); 其中,AsyncRcvMsg是回调方法,用来处理客户端的连接请求。 (2) 在发送类中声明一个委托SendMessageDelegate用来异步发送消息给客户端,并且写出相应的回调方法发送消息给客户端。 //声明委托 private delegate void SendMessageDelegate(string sendMessage); private SendMessageDelegate sendMessageDelegate; //创建委托对象 sendMessageDelegate=new SendMessageDelegate(AsyncSndMsg); //调用异步方法向客户端发送数据 IAsyncResult result=sendMessageDelegate.BeginInvoke(str.ToString(), null, null); sendMessageDelegate.EndInvoke(result); 其中,AsyncSndMsg是回调方法,用来向客户端发送消息。 3.3.4客户端程序编写 客户端中使用异步的方法进行信息的发送与接收的步骤与服务器中一样,在此不再赘述。 在窗体类中,通过单击“连接”按钮创建一个新线程连接服务器。该连接也使用异步方式进行。 Thread threadConnect=new Thread(ConnectoServer); threadConnect.Start(); //发起连接请求 private void ConnectoServer() { AsyncCallback requestcallback=new AsyncCallback(RequestCallBack); tcpClient=new TcpClient(AddressFamily.InterNetwork); //异步操作1 IAsyncResultresult=tcpClient.BeginConnect( IPAddress.Parse(textBox2.Text), 5656, requestcallback, tcpClient); } 其中,RequestCallBack是回调方法,用来向服务器端发起连接请求。