上篇用IOCP实现了一个简单的服务器,在处理消息方面性能已经不错了,但是接爱请求函数却依旧使用的是 accept 函数,所以这部分性能并不够,而Windows在扩展函数中为我们提供了一些选择,本篇就来介绍这些函数。

AcceptEx

这个函数用来异步地投递一个调用来接受客户端连接,其原型如下:

BOOL AcceptEx (
  SOCKET sListenSocket,          // 监听套接字
  SOCKET sAcceptSocket,          // 分配给待连接客户端的套接字
  PVOID lpOutputBuffer,          // 用来接收用户第一份数据的缓冲区
  DWORD dwReceiveDataLength,     // 缓冲区的字节数
  DWORD dwLocalAddressLength,    // 本地套接字地址结构大小
  DWORD dwRemoteAddressLength,   // 远程套接字地址结构大小
  LPDWORD lpdwBytesReceived,     // 新建的客户机连接上所收到的字节数
  LPOVERLAPPED lpOverlapped      // 重叠结构
);

这个函数稍微比 accept 函数麻烦了一点,那么在这里我们列出 accept 的原型来对比着展开介绍:

SOCKET accept (
  SOCKET s,
  struct sockaddr FAR* addr,
  int FAR* addrlen
);

accept 函数只需传入监听套接字和客户套接字的地址与大小,在有连接时返回客户端的套接字。

AcceptEx 函数第一个参数也是监听套接字,而第二个参数是客户端的套接字,这说明 accept 函数是在接受客户端连接后才为其创建套接字的,而 AcceptEx 函数却是先创建好套接字,在客户端连接进来时直接给它用。这就是这个函数效率高之所在,因为创建套接字花费颇高,当用户连接时才为其创建套接字会导致处理变慢。

第三个参数是用来接收客户端的第一份数据的,在客户端连接后 AcceptEx 函数就顺便将其第一份数据接收了,就保存在这里,而第四个参数就是接收这份数据的缓冲区大小。而我们若是想像 accept 函数那样只处理接受请求,后面再接收第一份数据,我们需要将缓冲区的长度设置为0,即第四个参数置0。

dwLocalAddressLengthdwRemoteAddressLength 分别是本地和远程套接字的地址大小,即服务器和客户端的套接字地址大小,都为 sizeof(SOCKADDR_IN) + 16,多加的这16是这个函数内部给传输协议用的,可以看看MSDN对此的描述:

The number of bytes reserved for the remote address information. This must be at least 16 bytes more than the maximum address length for the transport protocol in use.

这两个参数是保存在缓冲区中的,所以实际我们的缓冲区大小应该减去这字节的大小,所以 dwReceiveDataLength 应该传入:len - (sizeof(SOCKADDR_IN) + 16) * 2

lpdwBytesReceived 用来保存所接收第一份数据的实际大小,最后一个参数是重叠参数,我们在重叠IO中就说过,一个函数若有此参数,即表明它是异步的,所以 AcceptEx 函数就是异步的。

大家可能发现这里竟然没有保存客户端地址的参数,其实它在扩展函数中提供了另一个函数 GetAcceptExSockaddrs 专门来获取地址,下面来说这个函数。

GetAcceptExSockaddrs

这个函数用来获取接受套接字的地址信息,原型如下:

VOID GetAcceptExSockaddrs ( 
  PVOID lpOutputBuffer,         // 缓冲区
  DWORD dwReceiveDataLength,    // 缓冲区字节数
  DWORD dwLocalAddressLength,   // 本地地址长度
  DWORD dwRemoteAddressLength,  // 远程地址长度
  LPSOCKADDR *LocalSockaddr,    // 本地socket地址
  LPINT LocalSockaddrLength,    // 本地socket地址长度
  LPSOCKADDR *RemoteSockaddr,   // 远程socket地址
  LPINT RemoteSockaddrLength    // 远程socket地址长度
);

前面的4个参数和 AcceptEx 中的参数相同,便不赘述了。后面4个参数分别表示服务器的地址和客户端的地址以及它们的长度,当有用户连接后,我们就可以在这里分别获得到服务器与客户端的地址信息,非常简单吧。

加载扩展函数

AcceptExGetAcceptExSockaddrs 都包含在 <MSWSock.h> 中,可以直接通过链接 mswsock 库来使用这两个函数,但是因为扩展函数并不存在于 Winsock2 的结构体系中,这种方式消耗较大,这里我将介绍动态获取这些函数的方法。

Winsock2 提供了 WSAIoctl 函数用于对 Windows 套接字提供IO控制,之前我们还用过 Winsock1 中的 ioctlsocket 函数,只是 WSAIoctl 提供了更多的选项(ctl是control的缩写),我们可以通过这个函数来动态获取到扩展函数。其原型如下:

int WSAIoctl (
  SOCKET s,               // IO操作的套接字
  DWORD dwIoControlCode,  // IO控制命令
  LPVOID lpvInBuffer,     // 指向传入数据缓冲区
  DWORD cbInBuffer,       // 数据的长度
  LPVOID lpvOUTBuffer,    // 指向返回的数据缓冲区
  DWORD cbOUTBuffer,      // 返回数据的长度
  LPDWORD lpcbBytesReturned,   // 实际返回的字节数
  LPWSAOVERLAPPED lpOverlapped,  // 重叠结构
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE  // 例行程序
);

要获取扩展函数,dwIoControlCode 应该指定 SIO_GET_EXTENSION_FUNCTION_POINTER 控制命令。

lpvInBuffer 用来指定我们需要获取的函数信息,调用完后该函数会通过 lpvOUTBuffer 返回函数地址。我们可以这样获取到 AcceptExGetAcceptExSockaddrs 函数:

LPFN_ACCEPTEX lpfnAcceptEx;  // AcceptEx函数指针
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs;  // GetAcceptExSockaddrs函数指针
GUID guidAcceptEx = WSAID_ACCEPTEX;  // AcceptEx数据
GUID guidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;  // GetAcceptExSockaddrs数据
DWORD dwBytes;

// 加载AcceptEx函数
WSAIoctl(servSock,
    SIO_GET_EXTENSION_FUNCTION_POINTER,
    &guidAcceptEx, sizeof(guidAcceptEx),
    &lpfnAcceptEx, sizeof(lpfnAcceptEx),
    &dwBytes, NULL, NULL
);

// 加载GetAcceptExSockaddrs函数
WSAIoctl(servSock,
    SIO_GET_EXTENSION_FUNCTION_POINTER,
    &guidGetAcceptExSockaddrs, sizeof(guidGetAcceptExSockaddrs),
    &lpfnGetAcceptExSockaddrs, sizeof(lpfnGetAcceptExSockaddrs),
    &dwBytes, NULL, NULL
);

大家可以看到,获取扩展函数其实并不麻烦。传入要获取的数据,WSAIoctl 返回该数据的函数指针,GUID 类型描述了全局唯一的一个ID,需要用它来指定要获取的数据,具体代码中已经很清晰了,便不多说了,若还有哪里不明白可以留言。

具体使用就放到下篇了,网络编程已经由浅入深地写了好长时间了,不知大家都理解了多少呢?可能这里好多人并不是搞服务器的(我也不是),这些大多其实也都不是C++的东西,而是关于操作系统的,所以我有时间就快点把网络编程这一系列结束吧。我前段时间抽时间学习了下Linux的epoll模型,感觉比iocp实现起来要简单,说起来都是对select模型的改进,再把iocp用一篇写完就顺便说下epoll模型,以全面的介绍Windows和Linux上主要的网络模型,让大家可以对比着参考理解,接着把asio说了就返回写C++的东西了。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.