使用异步多线程TCP Socket 实现进程间通信

最近要看winsock的资料,孙鑫的15和16节有介绍,这短时间也找了一些资料,不过理解的并没有深入,自己在一点点尝试着做符合我们project方向的实验,希望这两周能有一些初步的结果出来。我那个上看了一个 帖子,带我理解了一些知识,贴过来吧,作者是 尹学渊  ,blog地址是http://www.cnblogs.com/erwin/archive/2007/04/20/721074.html,按照作者的意思注明转帖的。
进程间通信有很多种方式,比如说 Pipe,共享内存,DDE,Socket等,关于进程通信方面的知识我在这里就不讨论了,大家可以看我博客里的另一些文章有讲...今天我们主要讨论怎么样使用Socket实现进程间通信。

          本程序将使用Socket 的WSAEventSelect异步方式,使用多线程,建立一个服务器类,使用此类的时候只需要将处理数据的函数指针传给该类,进行数据处理,本例中,服 务器将启动一个客户端进程,让客户端像服务器发送数据,然后服务端收到数据后将数据打印出来进!从而达到进程通信的目的,当然,如果想进行数据的特殊处理 的话,只需要改写处理函就行了,因为,服务器类只是接收函数指针。

         本类在C++ builder 6.0 和 VC 6.0调试通过....不过大家注意看程序注释,不同的编译器使用需要修改一点地方,程序注释说的很清楚,我就不在这里介绍了(注意BCB中包含的头文件 位置,本类的头文件一定要放在最上面,因为winsock2.h的冲突问题,有关这个问题,有兴趣的朋友自己上网搜,因为时间有限,一篇文章不可能写太多 的内容,请大家谅解)

         下面我们先来了解一下 Socket 的异步方式 ! (程序源码示例请翻到文章尾下载)

         首先,Wi ndows套接字在两种模式下执行I / O操作:锁定和非锁定。

          在锁定模式下,在I / O操作完成前,执行操作的Wi nsock函数(比如send和recv)会一直等候下去,不会立即返回程序(将控制权交还给程序)。而在非锁定模式下, Wi nsock函数无论如何都会立即返回。在Windows CE和Windows 95(安装Winsock 1)平台上运行的应用程序仅支持极少的I / O模型,所以我们必须采取一些适当的步骤,让锁定和非锁定套接字能够满足各种场合的要求。


1 锁定模式

          对于处在锁定模式的套接字,我们必须多加留意,因为在一个锁定套接字上调用任何一个Winsock API函数,都会产生相同的后果—耗费或长或短的时间“等待”。大多数Wi nsock应用都是遵照一种“生产者-消费者”模型来编制的。在这种模型中,应用程序需要读取(或写入)指定数量的字节,然后以它为基础执行一些计算。

          这种方式的问题在于,假如没有数据处于“待决”状态,那么recv函数可能永远都无法返回。这是由于从语句可以看出:只有从系统的输入缓冲区中读回点什么 东西,才允许返回!有些程序员可能会在recv中使用MSG_PEEK标志,或者调用ioctlsocke( t 设置FIONREAD选项),在系统的缓冲区中,事先“偷看”是否存在足够的字节数量。然而,在不实际读入数据的前提下,仅仅“偷看”数据(如实际读入数 据,便会将其从系统缓冲区中将其删除),可不是一件光彩的事情。我们认为,这是一种非常不好的编程习惯,应尽全力避免。在“偷看”的时候,对系统造成的开 销是极大的,因为仅仅为了检查有多少个字节可用,便发出一个或者更多的系统调用。以后,理所当然地,还需要牵涉到进行实际recv调用,将数据从系统缓冲 区内删除的开销。那么,如何避免这一情况呢?在此,我们的目标是防止由于数据的缺乏(这可能是网络出了故障,也可能是客户机出了问题),造成应用程序完全 陷于“凝固”状态,同时不必连续性地检视系统网络缓冲!为达此目的,一个办法是将应用程序划分为一个读线程,以及一个计算线程。两个线程都共享同一个数据 缓冲区。对这个缓冲区的访问需要受到一定的限制,这是用一个同步对象来实现的,比如一个事件或者Mutex(互斥体)。“读线程”的职责是从网络连续地读 入数据,并将其置入共享缓冲区内。读线程将计算线程开始工作至少需要的数据量拿到手后,便会触发一个事件,通知计算线程:你老兄可以开始干活了!随后,计 算线程从缓冲区取走(删除)一个数据块,然后进行要求的计算。

         对锁定套接字来说,它的一个缺点在于:应用程序很难同时 通过多个建好连接的套接字通信。使用前述的办法,我们可对应用程序进行修改,令其为连好的每个套接字都分配一个读线程,以及一个数据处理线程。尽管这仍然 会增大一些开销,但的确是一种可行的方案。唯一的缺点便是扩展性极差,以后想同时处理大量套接字时,恐怕难以下手。


2 非锁定模式

         除了锁定模式,我们还可考虑采用非锁定模式的套接字。尽管这种套接字在使用上存在着些许难度,但只要排除了这项困难,它在功能上还是非常强大的。除具备锁定套接字已有的各项优点之外,还进行了少许扩充,功能更强。

             将一个套接字置为非锁定模式之后, Winsock API调用会立即返回。大多数情况下,这些调用都会“失败”,并返回一个WSAEWOULDBLOCK错误。什么意思呢?它意味着请求的操作在调用期间没 有时间完成。举个例子来说,假如在系统的输入缓冲区中,尚不存在“待决”的数据,那么recv(接收数据)调用就会返回WSAEWOULDBLOCK错 误。通常,我们需要重复调用同一个函数,直至获得一个成功返回代码。

            由于非锁定调用会频繁返回 WSAEWOULDBLOCK错误,所以在任何时候,都应仔细检查所有返回代码,并作好“失败”的准备。许多程序员易犯的一个错误便是连续不停地调用一个 函数,直到它返回成功的消息为止。例如,假定在一个紧凑的循环中不断地调用recv,以读入2 0 0个字节的数据,那么与使用前述的MSG_PEEK标志来“轮询”一个锁定套接字相比,前一种做法根本没有任何优势可言。为此, Wi nsock的套接字I / O模型可帮助应用程序判断一个套接字何时可供读写。


         锁定和非锁定套接字模式都存在 着优点和缺点。其中,从概念的角度说,锁定套接字更易使用。但在应付建立连接的多个套接字时,或在数据的收发量不均,时间不定时,却显得极难管理。而另一 方面,假如需要编写更多的代码,以便在每个Wi nsock调用中,对收到一个WSAEWOULDBLOCK错误的可能性加以应付,那么非锁定套接字便显得有些难于操作。在这些情况下,可考虑使用“套接 字I / O模型”,它有助于应用程序通过一种异步方式,同时对一个或多个套接字上进行的通信加以管理。


3 套接字I/O模型

             共有五种类型的套接字I / O模型,可让Wi nsock应用程序对I / O进行管理,它们包括: select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)以及 completion port(完成端口)。在这一节里,我们打算向大家解释每种I / O模型的特点,同
时讲述如何利用这些模型,来开发自己的应用程序,以便同时管理一个或多个套接字请求。

本文主要针对事件选择进行讲解和程序实现,对于其他方式,有兴趣的朋友可以上MSDN上研究,呵呵。

4 WSAEventSelect

            事件通知模型要求我们的应用程序针对打算使用的每一个套接字,首先创建一个事件对象。创建方法是调用WSACreateEvent函数,它的定义如下:
                WSAEVENT  WSACreateEvent(void);
      WSACreateEvent函数的返回值很简单,就是一个创建好的事件对象句柄。事件对象句柄到手后,接下来必须将其与某个套接字关联在一起,同时注册自己感兴趣的网络事件类型,要做到这一点,方法是调用WSAEventSelect函数,对它的定义如下:
            int WSAEventSelect (
              SOCKET s,               
              WSAEVENT hEventObject,  
              long lNetworkEvents     
               );

其 中, s参数代表自己感兴趣的套接字。hEventObject参数指定要与套接字关联在一起的事件对象—用WSAC reateEvent取得的那一个。而最后一个参数lNetworkEvents,则对应一个“位掩码”,用于指定应用程序感兴趣的各种网络事件类型的一 个组合。要想获知对这些事件类型的详细说明,请参考早先讨论过的WSAAsyncSelect I/O模型。为WSAEventSelect创建的事件拥有两种工作状态,以及两种工作模式。其中,两种工作状态分别是“已传信”(signaled)和 “未传信”(nonsignaled)。工作模式则包括“人工重设”(manual reset)和“自动重设”(auto reset)。WSACreateEvent最开始在一种未传信的工作状态中,并用一种人工重设模式,来创建事件句柄。随着网络事件触发了与一个套接字关 联在
一起的事件对象,工作状态便会从“未传信”转变成“已传信”。由于事件对象是在一种人工重设模式中创建的,所以在完成了一个I / O请求的处理之后,我们的应用程序需要负责将工作状态从已传信更改为未传信。要做到这一点,可调用WSAResetEvent函数,对它的定义如下:
BOOL WSAResetEvent(
  WSAEVENT hEvent 
);


该函数唯一的参数便是一个事件句柄;基于调用是成功还是失败,会分别返回T R U E或FA L S E。应用程序完成了对一个事件对象的处理后,便应调用WSACloseEvent函数,释放由事件句柄使用的系统资源。对WSACloseEvent函数的定义如下:

BOOL WSACloseEvent(
  WSAEVENT hEvent  
);


该函数也要拿一个事件句柄作为自己唯一的参数,并会在成功后返回TRUE,失败后返回FALSE。
一 个套接字同一个事件对象句柄关联在一起后,应用程序便可开始I / O处理;方法是等待网络事件触发事件对象句柄的工作状态。WSAWaitForMultipleEvents函数的设计宗旨便是用来等待一个或多个事件对 象句柄,并在事先指定的一个或所有句柄进入“已传信”状态后,或在超过了一个规定的时间周期后,立即返回。下面是 WSAWaitForMultipleEvents函数的定义: DWORD WSAWaitForMultipleEvents(
  DWORD
cEvents,                 
  const WSAEVENT FAR *lphEvents
  BOOL fWaitAll,                 
  DWORD dwTimeOUT,               
  BOOL fAlertable                
);

其中, cEvents和lphEvents参数定义了由WSAEVENT对象构成的一个数组。在这个数组中,cEvents指定的是事件对象的数量,而lphEvents对应的是一个指针,用于直接引用该数组。
要 注意的是, WSAWaitForMultipleEvents只能支持由WSA_MAXIMUM_WAIT_EVENTS对象规定的一个最大值,在此定义成6 4个。因此,针对发出WSAWaitForMultipleEvents调用的每个线程,该I / O模型一次最多都只能支持6 4个套接字。假如想让这个模型同时管理不止6 4个套接字,必须创建额外的工作者线程,以便等待更多的事件对象。fWaitAl l 参数指定了WSAWaitForMultiple Events如何等待在事件数组中的对象。若设为TRUE,那么只有等lphEvents数组内包含的所有事件对象都已进入“已传信”状态,函数才会返 回;但若设为FALSE,任何一个事件对象进入“已传信”状态,函数就会返回。就后一种情况来说,返回值指出了到底是哪个事件对象造成了函数的返回。通 常,应用程序应将该参数设为FALSE,一次只为一个套接字事件提供服务。dwTimeout参数规定了 WSAWaitForMultipleEvents最多可等待一个网络事件发生有多长时间,以毫秒为单位,这是一项“超时”设定。超过规定的时间,函数就 会立即返回,即使由fWaitAl l参数规定的条件尚未满足也如此。如超时值为0,函数会检测指定的事件对象的状态,并立即返回。这样一来,应用程序实际便可实现对事件对象的“轮询”。但 考虑到它对性能造成的影响,还是应尽量避免将超时值设为0。假如没有等待处理的事件, WSAWaitForMultipleEvents便会返回WSA_WAIT_TIMEOUT。如dwsTimeout设为WSA _ INFIN ITE(永远等待),那么只有在一个网络事件传信了一个事件对象后,函数才会返回。最后一个参数是fAlertable,在我们使用 WSAEventSelect模型的时候,它是可以忽略的,且应设为FALSE。该参数主要用于在重叠式I / O模型中,在完成例程的处理过程中使用。本章后面还会对此详述。
若WSAWaitForMultipleEvents收到一个事件对象的网络事件 通知,便会返回一个值,指出造成函数返回的事件对象。这样一来,我们的应用程序便可引用事件数组中已传信的事件,并检索与那个事件对应的套接字,判断到底 是在哪个套接字上,发生了什么网络事件类型。对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去预定 义值WSA_WAIT_EVENT_0,得到具体的引用值(即索引位置)知道了造成网络事件的套接字后,接下来可调用 WSAEnumNetworkEvents函数,调查发生了什么类型的网络事件。该函数定义如下:
int WSAEnumNetworkEvents (
  SOCKET
s,                          
  WSAEVENT hEventObject,             
  LPWSANETWORKEVENTS lpNetworkEvents 
);


s 参数对应于造成了网络事件的套接字。hEventObject参数则是可选的;它指定了一个事件句柄,对应于打算重设的那个事件对象。由于我们的事件对象 处在一个“已传信”状态,所以可将它传入,令其自动成为“未传信”状态。如果不想用hEventObject参数来重设事件,那么可使用 WSAResetEvent 函数, 该函数早先已经讨论过了。最后一个参数是lpNetworkEvents,代表一个指针,指向WSANETWORKEVENTS结构,用于接收套接字上发 生的网络事件类型以及可能出现的任何错误代码。下面是WSANETWORKEVENTS结构的定:
typedef struct _WSANETWORKEVENTS {
   long lNetworkEvents;
   int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;


lNetworkEvents参数指定了一个值,对应于套接字上发生的所有网络事件类型。
注 意一个事件进入传信状态时,可能会同时发生多个网络事件类型。例如,一个繁忙的服务器应用可能同时收到FD_READ和FD_WRITE通知。 iErrorCode参数指定的是一个错误代码数组,同lNetworkEvents中的事件关联在一起。针对每个网络事件类型,都存在着一个特殊的事件 索引,名字与事件类型的名字类似,只是要在事件名字后面添加一个“ _BIT”后缀字串即可。例如,对FD_READ事件类型来说,iErrorCode数组的索引标识符便是FD_READ_BIT。

完成 了对WSANETWORKEVENTS结构中的事件的处理之后,我们的应用程序应在所有可用的套接字上,继续等待更多的网络事件。在程序中,我们阐释了如 何使用WSAEventSelect这种I / O模型,来开发一个服务器应用,同时对事件对象进行管理。这个程序主要着眼于开发一个基本的服务器应用要涉及到的步骤,令其同时负责一个或多个套接字的管 理。

说明:程序中关于进程,线程的函数请大家自己查看MSDN,或者看我博客里的其他文章,由于时间有限,我就不在这里进行介绍了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值