C# TCP应用编程三 异步TCP应用编程
利用TcpListener和TcpClient类在同步方式下接收、发送数据以及监听客户端连接时,在操作没有完成之前一直处于阻塞状态,这对于接受、发送数据量不大的情况或者操作勇士较短的情况下是比较方便的。但是,对于执行完成时间可能较长的任务,如传送大文件等,使用同步操作可能就不太合适了,这种情况下,最好的办法是使用异步操作。
所谓异步操作方式,就是我们希望让某个工作开始以后,能在这个工作尚未完成的时候继续处理其他工作。就行我们(主线程)安排A(子线程A)负责处理客人来访时办理一系列登记手续。在同步工作方式下,如果没有人来访,A就只能一直在某个房间等待,而不能同时做其他工作,显然这种方式不利于并行处理。我们希望的是,没有人来访时,A不一定一直在这个房间等待,亦可以到别处继续做其他事,二八这个工作交给总控室人员完成,这里的总控室就是Windows操作系统本身。总控室如何及时通知A呢?可以让A先告诉总控室一个手机号F(callback需要的方法名F),以便有人来访时总控室可以立即电话通知A(callback)。这样一来,一旦有客人来访,总控室人员(委托)就会立即给A打电话(通过委托自动运行方法F),A接到通知后,再处理客人来访时需要的登记手续(在方法F中完成需要的工作)。
异步操作最大的优点是可以在一个操作没有完成之前同时进行其他的操作。.NET框架供了一种称为AsyncCallback(异步回调)的委托,该委托允许启动异步的功能,并在条件具备时调用提供的回调方法(是一种在操作或活动完成时由委托自动调用的方法),然后在这个方法中完成并结束未完成的工作。
使用异步TCP 应用编程时,除了套接字有对应的异步操作方式外,TcpListener和TcpClient类也提供了异步操作方法。
异步操作方式下,每个Begin方法都有一个匹配的End方法。在程序中例用Begin方法开始执行异步操作,然后又委托在条件具备时调用End方法完成并接受异步操作。
下表列出了TcpListener、TcpClient以及套接字提供的部分异步操作方法。
表 TcpListener和TcpClient及Socket提供的部分异步操作方法
1 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类提供的方法有:
Reset方法:将信号的状态设置为非终止状态,即不让操作系统发出信号,从而导致等待收到信号才能继续执行的线程阻塞。
Set方法:将信号的状态设置为非终止状态,即不让操作系统发出信号,从而导致等待收到信号才能继续执行的线程阻塞。
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方法。
2 AsyncCallback委托
AsyncCallback委托用于引用异步操作完成时调用的回调方法。在异步操作方式下,由于程序可以在启动该异步操作后继续执行其他代码,因此必须有一种机制,以保证该异步操作完成时能及时通知调用者。这种机制可以通过AsyncCallback委托实现。
异步操作的每一个方法都有一个Begin...方法和End...方法,例如BeginAcceptTcpClient和EndAcceptTcpClient。程序调用Begin…方法时,系统会自动在线程池中创建对应的线程进行异步操作,从而保证调用方和被调用方同时执行,当线程池中的Begin…方法执行完毕时,会自动通过AsyncCallback 委托调用在Begin…方法的参数中指定的回调方法。
回调方法是在程序中事先定义的,在回调方法中,通过End…方法获取Begin…方法的返回值和所有输入/输出参数,从而达到异步操作方式下完成参数传递的目的。
3 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方法),因此系统会自动将该状态信息从关联的BeginAcceptTcpClicent方法传递到AcceptTcpClientCallback方法。注意在回调代码中,必须调用EndAcceptTcpClient方法完成客户端连接。关键代码为:
void AcceptTcpClientCallBack(IAsyncResult ar) { ...... TcpListener myListener =new (TcpListener)ar.AsyncState; TcpClient client = myListener.EndAcdceptTcpClient(ar); ...... }
程序执行EndAcceptRcpClient方法后,会自动完成客户端连接请求,并返回包含底层套接字的TcpClient对象,接下来我们就可以例用这个对象与客户端进行通信了。
默认情况下,程序执行BeginAcceptTcpClient方法后,在该方法返回状态信息之前,不会像同步TCP方式那样阻塞等待客户端连接,而是继续往下执行。如果我们希望在其返回状态信息之前阻塞当前线程的执行,可以调用ManualResetEvent对象的WaitOne方法。
4 BeginConnect方法和EndConnect方法
BeginConnect方法和EndConnect方法包含在命名空间System.Net.Sockets下的TcpClient类和Socket类中,这里我们只导论TcpClient类中的方法。
在异步TCP应用编程中,BeginConnect方法通过异步方式向远程住居发出连接请求。该方法有三种重载形式,方法原型为:
public IAsyncResult BeginConnect(IPAddress address, int port, AsyncCallback requestCallback, Object state); public IAsyncResult BeginConnect(IPAddress[] addresses, int port, AsyncCallback requestCallback, Object state); public IAsyncResult BeginConnect(string host, int port, AsyncCallback requestCallback, Object state);
其中 address为远程主机的IPAddress对象;port为远程主机的端口号;requestCallback为AsyncCallback类型的委托。
BeginConnect方法在操作完成前不会阻塞,程序中调用BeginConnet方法时,系统会自动动用独立的线程来执行该方法,直到与远程主机连接成功或抛出异常。如果在调用BeginConnect方法之后向阻塞当前线程,可以调用ManualResetEvent对象的WaitOne方法。
异步BeginConnect只有在调用了EndConnect方法之后才算执行完毕。因此程序中需要在提供给requestCallback委托调用的方法中调用TcpClient对象的EndConnect方法。关键代码为:
...... AsyncCallback requestCCallback = new AsyncCallback(RequestCallback); ...... void RequestCallback(IAsyncResult ar) { ...... tcpClient = (TcpClient)ar.AsyncState; client.EndConnect(ar) ...... }
在自定义的RequestCallback中,通过获取的状态细腻得到新的TcpClient类型的对象,并调用EndConnect结束连接请求。
5 发送数据
在异步TCP应用编程中,如果本机已经和远程主机建立连接,就可以用System.NetSockets命名空间下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 对象发送请求的字节数或引发异常。关键代码为:
…… NetworkStream stream = tcpClient.GetStream() …… byte[] bytesData = System.Text.Encoding.UTF8.GetBytes(str + "\r\n"); stream.BeginWrite(bytesData, 0, bytesData.Length, new AsyncCallback(SendCallback), stream); stream.Flush(); …… private void SendCallback(IAsyncResult ar) { …… stream.EndWrite(ar); }
如果希望在BeginWrite 方法得到传递的状态信息之前使当前线程(即调用BeginWrite 方法的线程)阻塞,可以使用ManualResetEvent 对象WaitOne 方法。并在回调方法中调用Set使当前线程继续执行。
6 接收数据
与发送数据相似,如果本机已经和远程主机建立了连接,就可以用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是在BegionRead完成时执行的AsyncCallback委托;state包含用户定义的任何附加数据的对象。
BeginRead方法启动从传入网络缓冲区中异步读取数据的操作。调用BeginRead完成时执行的AsyncCallback委托;state包含用户定义的任何附加数据的对象。
在程序中,必须创建上实现AsyncCallback委托的回调方法,并将名称传递给BeginRead方法。state参数必须至少包含NetWorkStream对象。一般情况下,我们希望在回调方法内获得所接收的数据,因此常见小型的类或结构来保存读取缓冲区以及其他任何有用的信息,并通过state参数将结构或类的实例传递给BeginRead方法。
在回调方法中,必须调用EndRead 方法完成读取操作。系统执行BeginRead 时,将一直等待直到数据接收完毕或者遇到错误,从而得到可用的字节数,然后自动使用一个单独的线程来执行指定的回调方法,并阻塞EndRead 方法,直到所提供的NetworkStream 对象将可用数据读取完毕,或者达到size 参数指定的字节数。关键代码为:
...... NetworkStream stream = tcpClient.GetStream(); ...... ReadObject readObject = new ReadObject(stream, client.ReceiveBufferSize); stream.BeginRead(readObject.bytes, 0, readObject.bytes.Length, ReadCallback, readObject); ...... void ReadCallback(IAsyncResult ar) { ...... ReadObject readObject = (ReadObject)ar.AsyncState; int count = readObject.netStream.EndRead(ar); //处理读取的保存在ReadObject类中的字节数组 ...... }
如果希望在调用BeginRead方法之后使用当前线程(即调用BeginRead的线程)阻塞,可以使用ManualResetEvent对象的WaitOne方法,并在回调方法中调用该对象的set方法使当前线程继续执行。