完成端口技术,IOCP(complete port)
就是系统帮你完成网络IO操作,在客户端极多的情况下,这种模型效率很高。
多线程模型下:每个客户端都分配一个线程的话,那么CPU会把大部分时间片都浪费在线程之间的调度上,而不是每个线程中对网络数据的处理上。
而I/O重叠模型让CPU的工作更多集中在网络数据的处理而非线程调度。
一个很有趣的比喻:IOCP技术相当于——老板派秘书去前台接待一个客人(WSARecv),然后继续做老板的事情,秘书会一直等待资料夹有新文件然后拿给你然后继续等待有新文件(loop循环),前台接待客人之后把客户资料放到资料夹之后就不会继续去前台接待客人了(接收到了I/O完成确认),这个时候如果你不再派前台继续去等待接待客人(WSARecv),那么你的资料夹不会有新的资料了,这时就需要再次指派前台去等待接待新的客人(再次WSARecv)
先来看与socket()的对比:
Socket()为了实现非阻塞,可以用多线程实现;socket() 函数创建一个通讯端点并返回一个套接口,在应用于阻塞套接口时会阻塞。
WSASocket()是socket的windows平台的实现,是微软专门为windows操作系统开发的socket网络编程接口,而socket是通用的网络编程接口,其发送操作和接收操作都可以被重叠使用:接收函数可以被多次调用,发出接收缓冲区,准备接收到来的数据;发送函数也可以被多次调用,组成一个发送缓冲区队列。而socket()却只能发过之后等待回消息才可做下一步操作。
send()一次之可以发送一个缓冲区,对于发送大量的数据或数据包时会造成性能低下(多次调用send()造成从用户态到核心套的转换),解决方法是减少send()调用次数(申请一个大点的BUF缓冲区一次性发送数据),但这增加了编码的工作。
WSASend()可以支持一次发送多个BUFFER的请求,每个被发送的数据被填充到WSABUF结构中,然后传递给WSASend函数同时提供BUF的数量,这样WSASend就能上面的工作而减少send的调用次数,从而提高了性能。
recv()和WSARecv()大同小异,只是功能上有实质的区分。
为了实现重叠I/O,WSASend和WSARecv函数执行后就立即返回了(异步),其他事情(什么时候执行完毕,什么时候开始执行)交给系统来管,进行重叠I/O的I/O完成确认很关键。
函数 WSAGetOverlappedResult()就是为了实现I/O的完成确认存在的:
BOOL WSAGetOverlappedResult(
SOCKET S, //进行重叠I/O的套接字句柄
LPWSAOVERLAPPED lpOverlapped, //进行重叠I/O传递的WSAOVERLAPPED结构体变量的地址
LPDWORD lpcbTransfer, //用于保存实际传输自己数的变量地址值
BOOL fwait, //该函数正在进行I/O,fwait等待I/O完成,该值为false时跳出函数
LPDWORD lpdwFlags //调用WSARecv时,用于获取附加信息,若不需要传NULL
)
IOCP中已完成的I/O信息将注册到完成端口对象(CP对象),但这个过程并非单纯的注册:当该套接字的I/O完成时,要把状态信息注册到指定CP对象(一般情况下,我们需要且仅需要一个CP对象)。
为了完成上面的注册请求,需要程序员:
这两个工作都用CreateIoCompletionPort()函数完成:
HANDLE CreateIoCompletionPort(
HANDLE fileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
fileHandle传INVALID_HANDLE_VALUE;
ExistingCompletionPort传NULL;
CompletionKey传0;
NumberOfConcurrentThreads传分配給该CP对象同时可运行的线程数,若传0则线程数等于CPU个数。
以上调用方法是为了建立完成端口
按这种传参方法,调用这个函数,Windows系统会为我们把完成端口的所有一切的东西后台给我们弄好,然后返回给我们一个HANDLE,只要这个HANDLE不是NULL,就证明CP对象成功创建。
获取CPU数量:
SYSTEM_INFO si;
GetSystemInfo(&si);
int m_nProcessors = si.dwNumberOfProcessors;
获取CPU数量后,创建CPU数量X2倍的线程数(两倍是为了充分利用CPU资源):
m_nThreads = 2 * m_nProcessors;
HANDLE* m_phWorkerThreads = new HANDLE[m_nThreads];
for (int i = 0; i < m_nThreads; i++)
{
m_phWorkerThreads[i] = ::CreateThread(0, 0, _WorkerThread, …);
}
CP端口创建完毕,我们要用这个CP端口来完成网络通信。
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData);
struct sockaddr_in ServerAddress;
SOCKET m_sockListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
//I/O重叠 使用WSASocket()
ZeroMemory((char *)&ServerAddress, sizeof(ServerAddress));
ServerAddress.sin_family = AF_INET;
ServerAddress.sin_addr.s_addr = htonl(INADDR_ANY);
ServerAddress.sin_port = htons(8888);
if (SOCKET_ERROR == bind(m_sockListen, (struct sockaddr *) &ServerAddress, sizeof(ServerAddress)))
listen(m_sockListen,SOMAXCONN))
//SOMAXCONN是最大监听队列长度(Maximum queue length specifiable by listen)
//你也可以自己自定义指定长度
HANDLE CreateIoCompletionPort(
HANDLE fileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
fileHandle 传 要连接的套接字句柄;
ExistingCompletionPort 传 连接的CP对象句柄;
CompletionKey 传 已完成的I/O相关信息(GetQueuedCompletionStatus函数有关);
NumberOfConcurrentThreads 传 无乱传递什么,只要该函数第二个参数非NULL则会被忽略。
以上调用方法不是为了建立完成端口,而是为了将新连入的SOCKET与完成端口绑定在一起。
至此SOCKET初始化完毕,可以使用这个SOCKET投递AcceptEX请求。