当 Client 端 socket 与 Server 端 socket 相互通信时,两端均会触发 socket 事件。这里仅简要说明两个 socket 事件:
FD_CONNECT: 连接事件 , 通常 Client 端 socket 调用 socket API 函数 Connect 时所触发,这个事件发生在 Client 端。 ? FD_ACCEPT :正在引入的连接事件,通常 Server 端 socket 正在接收来自 Client 端 socket 连接时触发,这个事件发生在 Server 端。
?
网络传输服务进程 将 socket 事件 保存至 socket 的事件队列中。此外, 网络传输服务进程 还会向 socket window 发送消息 WM_SOCKET_NOTIFY , 通知有 socket 事件 产生,见下文对 socket window 的详细说明。 调用 CSocket::Create 函数后,socket 被创建。 socket 创建过程中调用 CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead) 。该函数的作用是:
将 socket 实例句柄和 socket 指针添加至 当前模块状态 ( 注 1 )的一个映射表变量 m_pmapSocketHandle 中。
? 在 AttachHandle 过程中,会 new 一个 CSocketWnd 实例 ( 基于 CWnd 派生 ) ,这里将这个实例称之为 socket window ,进一步理解为它是存放所有 sockets 的消息池 ( window 消息),请仔细查看,这里 socket 后多加了一个 s ,表示创建的多个 socket 将共享一个 消息池 。 ? 当 Client 端 socket 与 Server 端相互通信时 , 此时 网络传输服务进程 向 socket window 发送消息 WM_SOCKET_NOTIFY ,需要说明的是 CSocketWnd 窗口句柄保存在 当前模块状态 的 m_hSocketWindow 变量中。
?
2、阻塞模式
阻塞模式下 Server 端与 Client 端之间的通信处于同步状态下。在 Server 端直接实例化 CSocket 类,调用 Create 方法创建 socket ,然后调用方法 Listen 开始侦听,最后用一个 while 循环阻塞调用 Accept 函数用于等待来自 Client 端的连接,如果这个 socket 在主线程(主程序)中运行,这将导致主线程的阻塞。因此,需要创建一个新的线程以运行 socket 服务。 调试跟踪至 CSocket::Accept 函数源码:
while(!Accept(...)) {
// The socket is marked as nonblocking and no connections are present to be accepted.
if (GetLastError() == WSAEWOULDBLOCK) PumpMessage(FD_ACCEPT); else
return FALSE; }
它不断调用 CAsyncSocket::Accept ( CSocket 派生自 CAsyncSocket 类)判断 Server 端 socket 的事件队列中是否存在正在引入的连接事件 -
FD_ACCEPT (见 1 ),换句话说,就是判断是否有来自 Client 端 socket 的连接请求。
如果当前 Server 端 socket 的事件队列中存在正在引入的连接事件, Accept 返回一个非 0 值。否则, Accept 返回 0,此时调用 GetLastError 将返回错误代码 WSAEWOULDBLOCK ,表示队列中无任何连接请求。注意到在循环体内有一句代码:
PumpMessage(FD_ACCEPT);
PumpMessage 作为一个消息泵使得 socket window 中的消息能够维持在活动状态。实际跟踪进入 PumpMessage 中,发现这个消息泵与 Accept 函数的调用并不相关,它只是使很少的 socket window 消息(典型的是 WM_PAINT 窗口重绘消息)处于活动状态,而绝大部分的 socket window 消息被阻塞,被阻塞的消息中含有 WM_SOCKET_NOTIFY。
很显然,如果没有来自 Client 端 socket 的连接请求, CSocket 就会不断调用 Accept 产生循环阻塞,直到有来自 Client 端 socket 的连接请求而解除阻塞。
阻塞解除后,表示 Server 端 socket 和 Client 端 socket 已成功连接, Server 端与 Client 端彼此相互调用 Send 和 Receive 方法开始通信。
3、非阻塞模式
在非阻塞模式下 利用 socket 事件 的消息机制, Server 端与 Client 端之间的通信处于异步状态下。
通常需要从 CSocket 类派生一个新类,派生新类的目的是重载 socket 事件 的消息函数,然后在 socket 事件 的消息函数中添入合适的代码以完成 Client 端与 Server 端之间的通信,与阻塞模式相比,非阻塞模式无需创建一
个新线程。
这里将讨论当 Server 端 socket 事件 - FD_ACCEPT 被触发后,该事件的处理函数 OnAccept 是如何进一步被触发的。其它事件的处理函数如 OnConnect, OnReceive 等的触发方式与此类似。
在 1 中已提到 Client/Server 端通信时, Server 端 socket 正在接收来自 Client 端 socket 连接请求,这将会触发 FD_ACCEPT 事件,同时 Server 端的 网络传输服务进程 向 Server 端的 socket window (CSocketWnd )发送事件通知消息 WM_SOCKET_NOTIFY , 通知有 FD_ACCEPT 事件产生 , CsocketWnd 在收到事件通知消息后,调用消息处理函数 OnSocketNotify:
LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam) {
CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY, wParam, lParam); CSocket::ProcessAuxQueue(); return 0L ; }
消息参数 wParam 是 socket 的句柄, lParam 是 socket 事件 。这里稍作解释一下,CSocketWnd 类是作为 CSocket 类的 友元类 ,这意味着它可以访问 CSocket 类中的保护和私有成员函数和变量, AuxQueueAdd 和
ProcessAuxQueue 是 CSocket 类的静态成员函数,如果你对友元不熟悉,请迅速找本有关 C++ 书看一下友元的使用方法吧!
ProcessAuxQueue 是实质处理 socket 事件的函数,在该函数中有这样一句代码:
CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam, TRUE);
其实也就是由 socket 句柄得到发送事件通知消息的 socket 指针 pSocket:从 m_pmapSocketHandle 中查找(见 1 )!
最后, WSAGETSELECTEVENT(lParam) 会取出事件类型,在一个简单的 switch 语句中判断事件类型并调用事件处理函数。在这里,事件类型是 FD_ACCEPT ,当然就调用 pSocket->OnAccept !
结束语
Server 端 socket 处于阻塞调用模式下,它必须在一个新创建的线程中工作,防止主线程被阻塞。
当有多个 Client 端 socket 与 Server 端 socket 连接及通信时,
Server 端采用阻塞模式就显得不适合了,应该采用非阻塞模式 , 利用 socket 事件 的消息机制来接受多个 Client 端 socket 的连接请求并进行通信。 在非阻塞模式下,利用 CSocketWnd 作为所有 sockets 的消息池,是实现 socket 事件 的消息机制的关键技术。文中存在用词不妥和可能存在的技术问题,请大家原谅,也请批评指正,谢谢! 注:
1. 当前模块状态——用于保存当前线程和模块状态的一个结构,可以通过 AfxGetThreadModule() 获得。AFX_MODULE_THREAD_STATE 在 CSocket 重新定义为 _AFX_SOCK_THREAD_STATE 。 2. socket 类型——在 TCP/IP 协议中, Client/Server 网络程序采用 TCP 协议:即 socket 类型为 SOCK_STREAM ,它是可靠的连接方式。在这里不采用 UDP 协议:即 socket 类型为 SOCK_DGRAM ,它是不可靠的连接方式。
3如何设置socket函数的非阻塞调用?
windows的socket在创建后,默认是阻塞调用的,也就是说函数recv,recvfrom,send,sendto等函数都是阻塞的;那么我们如何将他们设置成非阻塞调用呢?我们可以通过windows为我们提供的ioctlsocket 函数实现;先给出一个例子:
BOOL LoadSocketSystem(void) {
WORD wVersionRequested; SOCKADDR_IN addrSrv; BOOL bRet = FALSE; WSADATA wsaData;
SOCKET sockClient; int err; int iMode;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return bRet; }
if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return bRet; }
s_socketStatus.m_hsocket = socket(AF_INET,SOCK_STREAM,0);
if( INVALID_SOCKET == s_socketStatus.m_hsocket ) {
WSACleanup( ); return bRet; }
//-------------------------
// Set the socket I/O mode: In this case FIONBIO // enables or disables the blocking mode for the // socket based on the numerical value of iMode. // If iMode = 0, blocking is enabled;
// If iMode != 0, non-blocking mode is enabled. iMode = 1;
ioctlsocket(s_socketStatus.m_hsocket,FIONBIO,(u_long FAR*) &iMode); s_socketStatus.m_isConnected = TRUE; sockClient = s_socketStatus.m_hsocket;
addrSrv.sin_addr.S_un.S_addr=inet_addr(\addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons(6000);
connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR)); return TRUE; }
上面红色表示的就是将刚才创建的socket设置成非阻塞操作。
当然我们这样做是最简单的,但是如果对于一个复杂的系统,系统的其他部分可能已经对该socket调用了另两个windows函数--WSAAsyncSelect or WSAEventSelect ,这个个函数都是自动的将socket设置成非阻塞形式,所有在这样的情况下你调用ioctlsocket 想将其设置回阻塞形式是不会成功的;要想成功,你必须将由于调用上面两个函数对socket系统产生的影响clear掉,然后才能成功调用ioctlsocket 函数;如果clear呢? 如下调用WSAEventSelect函数:
rc = WSAEventSelect(s, hEventObject, 0);
如果想详细了解上述几个函数,请参阅MSDN。