上篇介绍了重叠IO模型,它已经把我们的等待数据到来和拷贝数据到我们程序的缓冲区这个时间全部交给了操作系统去完成了,它已经很完善了。但是,如果我们想要把服务端的性能做的更好一点的话,它还是有点不足的,比如说,重叠IO它只有一个工作者线程在工作,要想获得更高的效率,我们应该增加一些线程。而且,重叠IO它只能同时进行64个客户端的数据收发,这也是一个很大的问题。为此,就产生了完成端口模型,它就解决了这些问题。可以说,在windows下做大型服务端的开发,完成端口模型是不二之选。
前面几种模型呢,我们都是在main函数里面直接写的,由于这个完成端口比较重要,我们就给它稍微封装一下。首先,新建一个完成端口的类,在构造函数里面我们对它的一些数据成员进行初始化和对网络环境的初始化。其次它有几个公有成员函数,第一个是Initialize初始化函数,来看下它的声明:
bool Initialize(
NOTIFYPROC pNotifyProc, //回调函数地址
int nPort //端口
);
第一个参数的的类型是我们自定义的一个函数指针,它的格式如下:
typedef void (CALLBACK* NOTIFYPROC)(LPVOID pContext,LPBYTE lpBuf, DWORD dwSize);
在Initialize这个函数里面我们做了哪些事情呢,首先,我们调用WSASocket来建立一个监听的udp的socket,然后进行绑定,因为是udp所以不用进行监听了。接下来,我们调用CreateIoCompletionPort创建一个完成端口对象,其声明如下:
HANDLE CreateIoCompletionPort (
HANDLE FileHandle, // 文件的句柄
HANDLE ExistingCompletionPort, // 已存在的完成端口对象,为NULL表示新建一个完成端口
ULONG_PTR CompletionKey, // 传递给处理函数的参数
DWORD NumberOfConcurrentThreads // 最大访问该IO操作的线程数
);
我们给这些参数赋值为无效值或空,这样我们就新建了一个完成端口了。
接下来,我们再次调用CreateIoCompletionPort函数,这是我们给的参数与上面的不一样了,第一个参数我们把监听的那个socket传进去,因为socket也是一个特殊的文件句柄。第二个参数我们把刚创建的完成端口传进去。第三个参数我们也把监听的socket传过去,第四个参数我们一般把它赋值为CPU核心数的2倍左右的个数。调用完该函数之后,我们就已经将一个socket与我们的完成端口进行了一个绑定。
与完成端口绑定好了之后,我们就创建一些工作者线程,创建线程的数量与上面那个函数的第四个参数的个数就行了。创建好线程之后我们会调用另外一个成员函数PostRecv函数,来投递一个接收请求。至此,Initialize成员函数就到此为止了。
接下来看下工作者线程是如何工作的。在工作者线程函数里,有一个循环,我们调用了GetQueuedCompletionStatus这么一个函数,它和重叠IO模型l里面的WSAWaitForMultipleEvents函数的功能是相似的,这个函数呢是用于监控一个完成端口,看这个完成端口上所投递的任务是否完成,如果完成就返回,否则继续监控。其声明如下:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // 指定的完成端口
LPDWORD lpNumberOfBytes, // 传输的字节数
PULONG_PTR lpCompletionKey, // 用于接收IO操作完成后与文件句柄相关联的Completion Key
LPOVERLAPPED *lpOverlapped, // overlapped结构
DWORD dwMilliseconds // 等待时间
);
我们在最后一个参数传了一个INFINITE,表示无限的等待。第三个参数overlapped结构我们也和重叠IO的单IO结构一样,对OVERLAPPED结构进行了扩展。该类声明如下:
class OVERLAPPEDPLUS
{
public:
OVERLAPPED m_ol;<span style="white-space:pre"> </span>//overlapped结构
WSABUF DataBuf; <span style="white-space:pre"> </span>//WSABUF结构,里面保存了大小和指针
CHAR Buffer[BUFSIZE]; <span style="white-space:pre"> </span>//缓冲区
unsigned long recvBytes; <span style="white-space:pre"> </span>//存储接收到的字节数
<span style="white-space:pre"> </span>SOCKADDR_IN remoteAddr; <span style="white-space:pre&