这些参数也没什么好讲的吧,用处一目了然了。而对于其中的那个CompletionKey,我们后面会详细提到。
到此才算是Socket全部初始化完毕了。
初始化Socket完毕之后,就可以在这个Socket上投递AcceptEx请求了。 【第四步】在这个监听Socket上投递AcceptEx请求 这里的处理比较复杂。
这个AcceptEx比较特别,而且这个是微软专门在Windows操作系统里面提供的扩展函数,也就是说这个不是Winsock2标准里面提供的,是微软为了方便咱们使用重叠I/O机制,额外提供的一些函数,所以在使用之前也还是需要进行些准备工作。
微软的实现是通过mswsock.dll中提供的,所以我们可以通过静态链接mswsock.lib来使用AcceptEx。但是这是一个不推荐的方式,我们应该用WSAIoctl 配合
SIO_GET_EXTENSION_FUNCTION_POINTER参数来获取函数的指针,然后再调用AcceptEx。
这是为什么呢?因为我们在未取得函数指针的情况下就调用AcceptEx的开销是很大的,因为AcceptEx 实际上是存在于Winsock2结构体系之外的(因为是微软另外提供的),所以如果我们直接调用AcceptEx的话,首先我们的代码就只能在微软的平台上用了,没有办法在其他平台上调用到该平台提供的AcceptEx的版本(如果有的话), 而且更糟糕的是,我们每次调用AcceptEx时,Service Provider都得要通过WSAIoctl()获取一次该函数指针,效率太低了,所以还不如我们自己直接在代码中直接去这么获取一下指针好了。 获取AcceptEx函数指针的代码大致如下:
[cpp] view plaincopyprint?
1.
2. LPFN_ACCEPTEX m_lpfnAcceptEx; // AcceptEx函数指针 3. GUID GuidAcceptEx = WSAID_ACCEPTEX; // GUID,这个是识别
AcceptEx函数必须的 4. DWORD dwBytes = 0;
5.
6. WSAIoctl(
7. m_pListenContext->m_Socket,
8. SIO_GET_EXTENSION_FUNCTION_POINTER, 9. &GuidAcceptEx, 10. sizeof(GuidAcceptEx), 11. &m_lpfnAcceptEx, 12. sizeof(m_lpfnAcceptEx), 13. &dwBytes, 14. NULL, 15. NULL);
LPFN_ACCEPTEX m_lpf GUID GuidAcceptEx = WSDWORD dwBytes = 0; WSAIoctl( 具体实现就没什么可说的了,因为都是固定的套路,那个GUID是微软给定义好的,直接拿过来用就行了,WSAIoctl()就是通过这个找到AcceptEx的地址的,另外需要注意的是,通过WSAIoctl获取AcceptEx函数指针时,只需要随便传递给WSAIoctl()一个有效的SOCKET即可,该Socket的类型不会影响获取的AcceptEx函数指针。 然后,我们就可以通过其中的指针m_lpfnAcceptEx调用AcceptEx函数了。 AcceptEx函数的定义如下: [cpp] view plaincopyprint? 1. BOOL AcceptEx (
2. SOCKET sListenSocket, 3. SOCKET sAcceptSocket, 4. PVOID lpOutputBuffer,
5. DWORD dwReceiveDataLength, 6. DWORD dwLocalAddressLength,
7. DWORD dwRemoteAddressLength, 8. LPDWORD lpdwBytesReceived, 9. LPOVERLAPPED lpOverlapped 10. );
BOOL AcceptEx ( SOCKET sListe SOCKET sAccep PVOID lpOutpu DWORD dwRecei DWORD dwLocal 乍一看起来参数很多,但是实际用起来也很简单: ? 参数1--sListenSocket, 这个就是那个唯一的用来监听的Socket了,没什么说的; ? 参数2--sAcceptSocket, 用于接受连接的socket,这个就是那个需要我们事先建好
的,等有客户端连接进来直接把这个Socket拿给它用的那个,是AcceptEx高性能的关键所在。
? 参数3--lpOutputBuffer,接收缓冲区,这也是AcceptEx比较有特色的地方,既然
AcceptEx不是普通的accpet函数,那么这个缓冲区也不是普通的缓冲区,这个缓冲区包含了三个信息:一是客户端发来的第一组数据,二是server的地址,三是client地址,都是精华啊…但是读取起来就会很麻烦,不过后面有一个更好的解决方案。
? 参数4--dwReceiveDataLength,前面那个参数lpOutputBuffer中用于存放数据的
空间大小。如果此参数=0,则Accept时将不会待数据到来,而直接返回,如果此参数不为0,那么一定得等接收到数据了才会返回…… 所以通常当需要Accept接收数据时,就需要将该参数设成为:sizeof(lpOutputBuffer) - 2*(sizeof sockaddr_in +16),也就是说总长度减去两个地址空间的长度就是了,看起来复杂,其实想明白了也没啥……
? 参数5--dwLocalAddressLength,存放本地址地址信息的空间大小; ? 参数6--dwRemoteAddressLength,存放本远端地址信息的空间大小; ? 参数7--lpdwBytesReceived,out参数,对我们来说没用,不用管; ? 参数8--lpOverlapped,本次重叠I/O所要用到的重叠结构。
这里面的参数倒是没什么,看起来复杂,但是咱们依旧可以一个一个传进去,然后在对应的IO操作完成之后,这些参数Windows内核自然就会帮咱们填满了。
但是非常悲催的是,我们这个是异步操作,我们是在线程启动的地方投递的这个操作, 等我们再次见到这些个变量的时候,就已经是在Worker线程内部了,因为Windows会直
接把操作完成的结果传递到Worker线程里,这样咱们在启动的时候投递了那么多的IO请求,这从Worker线程传回来的这些结果,到底是对应着哪个IO请求的呢?。。。。 聪明的你肯定想到了,是的,Windows内核也帮我们想到了:用一个标志来绑定每一个IO操作,这样到了Worker线程内部的时候,收到网络操作完成的通知之后,再通过这个标志来找出这组返回的数据到底对应的是哪个Io操作的。 这里的标志就是如下这样的结构体: [cpp] view plaincopyprint?
1.
2. typedef struct _PER_IO_CONTEXT{
3. OVERLAPPED m_Overlapped; // 每一个重叠I/O网络操作都要有一
个
4. SOCKET m_sockAccept; // 这个I/O操作所使用的Socket,每个连接
的都是一样的
5. WSABUF m_wsaBuf; // 存储数据的缓冲区,用来给重叠操作传递参
数的,关于WSABUF后面还会讲
6. char m_szBuffer[MAX_BUFFER_LEN]; // 对应WSABUF里的缓冲区 7. OPERATION_TYPE m_OpType; // 标志这个重叠I/O操作是做什么
的,例如Accept/Recv等 8.
9. } PER_IO_CONTEXT, *PPER_IO_CONTEXT;
typedef struct _PER_IO_CONTE OVERLAPPED m_Overlapped; SOCKET m_sockAccept WSABUF m_wsaBuf; char m_szBuffer[M 这个结构体的成员当然是我们随便定义的,里面的成员你可以随意修改(除了OVERLAPPED那个之外……)。 但是AcceptEx不是普通的accept,buffer不是普通的buffer,那么这个结构体当然也不能是普通的结构体了…… 在完成端口的世界里,这个结构体有个专属的名字“单IO数据”,是什么意思呢?也就是说每一个重叠I/O都要对应的这么一组参数,至于这个结构体怎么定义无所谓,而且这个结构体也不是必须要定义的,但是没它……还真是不行,我们可以把它理解为线程参数,就好比你使用线程的时候,线程参数也不是必须的,但是不传还真是不行……
除此以外,我们也还会想到,既然每一个I/O操作都有对应的PER_IO_CONTEXT结构体,而在每一个Socket上,我们会投递多个I/O请求的,例如我们就可以在监听Socket上投递多个AcceptEx请求,所以同样的,我们也还需要一个“单句柄数据”来管理这个句柄上所有的I/O请求,这里的“句柄”当然就是指的Socket了,我在代码中是这样定义的: [cpp] view plaincopyprint?
1.
2. typedef struct _PER_SOCKET_CONTEXT 3. {
4. SOCKET m_Socket; // 每一个客户端连接的Socket 5. SOCKADDR_IN m_ClientAddr; // 这个客户端的地址
6. CArray<_PER_IO_CONTEXT*> m_arrayIoContext; // 数组,所有客户端IO操
作的参数,
7. // 也就是说对于每一个客户端Socket 8. // 是可以在上面同时投递多个IO请求的 9. } PER_SOCKET_CONTEXT, *PPER_SOCKET_CONTEXT;
typedef struct _PER_SOCKET_CONT{ SOCKET m_So SOCKADDR_IN m_Cl CArray<_PER_IO_CONTEXT*> m_a 这也是比较好理解的,也就是说我们需要在一个Socket句柄上,管理在这个Socket上投递的每一个IO请求的_PER_IO_CONTEXT。 当然,同样的,各位对于这些也可以按照自己的想法来随便定义,只要能起到管理每一个IO请求上需要传递的网络参数的目的就好了,关键就是需要跟踪这些参数的状态,在必要的时候释放这些资源,不要造成内存泄漏,因为作为Server总是需要长时间运行的,所以如果有内存泄露的情况那是非常可怕的,一定要杜绝一丝一毫的内存泄漏。 至于具体这两个结构体参数是如何在Worker线程里大发神威的,我们后面再看。