通知应用程序处理网络数据的几种方法:
事件内核对象《基于事件通知的重叠I/O模型》:缺点:WaitForMultipleObjects()64个Event等待上限的限制
《基于完成例程的重叠I/O模型》:就是发出请求的线程必须得要自己去处理接收请求,负载均衡问题
完成端口(内核对象):网络操作完成的通知,都放在这个队列里面,开好的线程排队从这个队列里面取就行了,取走一个就少一个…。
线程池(多个线程通信accept)+共享内存(将接受区投递出去,内核和用户映射同一块接受区域,解决阻塞问题):
WSAAsyncSelect或者是WSAEventSelect这两个异步模型,没有用到Overlapped机制,虽然实现了异步的接收,但是却不能进行异步的发送。
函数介绍:
一 . 创建完成端口:HANDLE m_hIOCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0 );
HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, // 这里当然是连入的这个套接字句柄了 __in_opt HANDLE ExistingCompletionPort, // 这个就是前面创建的那个完成端口 __in ULONG_PTR CompletionKey, //这个参数就是类似于线程参数一样,在绑定的时候把自己定义的结构体指针传递这样到了Worker线程中
//也可以使用这个 结构体的数据了,相当于参数的传递 __in DWORD NumberOfConcurrentThreads // 这里同样置0 );
但是对于最后一个参数 0,我这里要简单的说两句,这个0可不是一个普通的0,它代表的是NumberOfConcurrentThreads,也就是说,允许应用程序同时执行的线程数量。当然,我们这里为了避免上下文切换,最理想的状态就是每个处理器上只运行一个线程了,所以我们设置为0,就是说有多少个处理器,就允许同时多少个线程运行。
二 . AcceptEx:
AcceptEx比Accept又强大在哪里呢?
1.AcceptEx是在客户端连入之前,就把客户端的Socket建立好了。
2.AcceptEx可以同时在完成端口上投递多个请求。
3.顺便在AcceptEx的同时,收取客户端发来的第一组数据
获取AcceptEx函数指针的代码大致如下:
LPFN_ACCEPTEX m_lpfnAcceptEx; // AcceptEx函数指针
GUID GuidAcceptEx = WSAID_ACCEPTEX; // GUID,这个是识别AcceptEx函数必须的 DWORD dwBytes = 0; WSAIoctl( m_pListenContext->m_Socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx), &m_lpfnAcceptEx, sizeof(m_lpfnAcceptEx), &dwBytes, NULL, NULL);
Accept函数的参数:
BOOL AcceptEx( _In_ SOCKET sListenSocket, // _In_ SOCKET sAcceptSocket, //事先建好的 _In_ PVOID lpOutputBuffer, //接收缓冲区,一是客户端发来的第一组数据,二是server的地址,三是client地址 _In_ DWORD dwReceiveDataLength,//前面那个参数lpOutputBuffer中用于存放数据的空间大小 //如果此参数=0,则Accept时将不会待数据到来,而直接返回,如果此参数不为0,
//那么一定得等接收到数据了才会返回 //需要Accept接收数据时,就需要将该参数设成为:
//sizeof(lpOutputBuffer) - 2*(sizeof sockaddr_in +16) _In_ DWORD dwLocalAddressLength, //存放本地址地址信息的空间大小 _In_ DWORD dwRemoteAddressLength,//存放本远端地址信息的空间大小; _Out_ LPDWORD lpdwBytesReceived, _In_ LPOVERLAPPED lpOverlapped //重叠结构 );
异步操作,我们在线程启动的地方投递这个操作, 等我们再次见到这些变量的时候,就已经是在Worker线程内部了,
因为Windows会直接把操作完成的结果传递到Worker线程里
这样咱们在启动的时候投递了那么多的IO请求,这从Worker线程传回来的这些结果,到底是对应着哪个IO请求的呢?
这里的标志就是如下这样的结构体:
typedef struct _PER_IO_CONTEXT{ OVERLAPPED m_Overlapped; // 每一个重叠I/O网络操作都要有一个 SOCKET m_sockAccept; // 这个I/O操作所使用的Socket,每个连接的都是一样的 WSABUF m_wsaBuf; // 存储数据的缓冲区,用来给重叠操作传递参数的,关于WSABUF后面还会讲 char m_szBuffer[MAX_BUFFER_LEN]; // 对应WSABUF里的缓冲区 OPERATION_TYPE m_OpType; // 标志这个重叠I/O操作是做什么的,例如Accept/Recv等 } PER_IO_CONTEXT, *PPER_IO_CONTEXT;
一个Socket要投递很多次:
ypedef struct _PER_SOCKET_CONTEXT { SOCKET m_Socket; // 每一个客户端连接的Socket SOCKADDR_IN m_ClientAddr; // 这个客户端的地址 CArray<_PER_IO_CONTEXT*> m_arrayIoContext; // 数组,所有客户端IO操作的参数, // 也就是说对于每一个客户端Socket // 是可以在上面同时投递多个IO请求的 } PER_SOCKET_CONTEXT, *PPER_SOCKET_CONTEXT;
三 .监控完成端口
BOOL WINAPI GetQueuedCompletionStatus( __in HANDLE CompletionPort, // 这个就是我们建立的那个唯一的完成端口 __out LPDWORD lpNumberOfBytes, // 操作完成后返回的字节数 __out PULONG_PTR lpCompletionKey, // 这个是我们建立完成端口的时候绑定的那个自定义结构体参数 __out LPOVERLAPPED *lpOverlapped, // 这个是我们在连入Socket的时候一起建立的那个重叠结构 __in DWORD dwMilliseconds // 等待完成端口的超时时间,如果线程不需要做其他的事情,那就INFINITE就行了 );
四.获取客户端的连入地址信息GetAcceptExSockAddrs()
确保我们在结构体PER_IO_CONTEXT定义的时候,把Overlapped变量,定义为结构体中的第一个成员。
PER_IO_CONTEXT* pIoContext = CONTAINING_RECORD(lpOverlapped, PER_IO_CONTEXT, m_Overlapped);
这个宏的含义,就是去传入的lpOverlapped变量里,找到和结构体中PER_IO_CONTEXT中m_Overlapped成员相关的数据。
运行过程:
1.加载库 2.socket 3.bind 4.listen
5.创建几个waiter
6.创建完成端口(listen)
7.将ListenSocket发给完成端口
8.线程池
9.看完成端口状态
10.一旦连接成功,投递接收数据请求