使用 异步多线程TCP Socket 实现进程间通信(VC 6.0 , BCB6.0调试通过)

为君一笑 提交于 2020-02-14 23:57:08

   

         进程间通信有很多种方式,比如说 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,或者看我博客里的其他文章,由于时间有限,我就不在这里进行介绍了。

点击这里下载本文示例源码

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!