一、概述
IOCP就是单独实现一个线程用来处理所有与客户端的IO的模型
不仅仅是只创建一个线程处理IO,而是至少创建一个线程来负责IO前后的全部处理。
理解IOCP重点不要集中于线程,而是观察:1.IO是否以非阻塞模式工作;2.如果确认非阻塞模式的IO是否完成。
IOCP的完成端口不是指TCP/IP端口号,而是类似一个消息队列,由操作系统把已经完成的重叠I/O请求的通知放入其中。当某项I/O完成之后,其对应的工作线程就会收到一个通知,然后进行其他的操作。IOCP是异步I/O,其依赖于一个工作者线程池。使用工作者线程池限制线程的数量以避免创建太多thread而导致在切换线程时浪费大量的时间。
二、主要函数
1.创建完成端口对象
2.建立完成端口对象与套接字直接的联系
IOCP中已完成的IO信息将被注册到完成端口对象。这个过程并非单纯的注册,首先经过请求过程:“该套接字的IO完成时,把状态信息注册到指定CP对象”。
该过程称为“”套接字和CP对象直接的连接请求“。
#include<windows.h>
// 成功返回CP对象句柄,失败返回NULL
HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads);
// 创建CP对象时,
// 第一个参数为INVALID_HANDLE_VALUE
// 第二个参数为NULL
// 第三个参数为0
// 第四个参数为指定线程并发数,为0表明为系统支持的最优线程数
// 建立CP对象与套接字联系时
// 第一个参数为连接至CP对象的套接字句柄
// 第二个参数为创建的CP对象
// 第三个参数为传递已完成IO相关信息,在GetQueuedCompletionStatus函数中讨论
// 第四个参数无论传递何值,只要第二个参数为非NULL就会忽略。
绑定之后,只有套接字的IO完成,相关信息就会注册到CP对象中。
3.确认完成端口已完成的IO和线程的IO处理
#include<windows.h>
// 成功返回true, 失败返回false
BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey, LPOVERLAPPED *lpOverlapped, DWORD dwMilliseconds);
//CompletionPort 创建并绑定后的完成端口
//lpNumberOfBytes 保存IO过程中传输的数据大小的变量地址值
//lpCompletionKey 用于保存CreateIoCompletionPort绑定的第三个参数的地址值
//lpOverlapped 用于保存WSARecv和WSASend时传递的Overlapped参数地址的变量地址值
//dwMilliseconds 用于保存超时信息,超过返回false并跳出函数,
传入INFINITE,程序阻塞至已完成IO信息完成写入CP对象
注意:
CreateIoCompletionPort的第三个参数是结构体,lpCompletionKey保存的时候该指针的地址
lpOverlapped 也是同样的保存指针的地址
GetQueuedCompletionStatus由处理IOCP中已完成的IO的线程调用
如前所书,IOCP创建全职IO线程,由该线程针对所有客户端进行IO。而且reateIoCompletionPort函数中也有参数用于指定分配给CP对象的最大线程数:是否自动创建线程并处理IO?当然不是!
由程序员自行创建调用WSASend和WSARecv等IO线程,只是该线程为了确认IO的完成会调用GetQueuedCompletionStatus函数。
虽然任何线程都能调用GetQueuedCompletionStatus函数,但是实际得到IO完成信息的线程数不会超过CreateIoCompletionPort指定的最大线程数
三、例子
1.回声Server例子
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <process.h>
#include <windows.h>
#define BUF_SIZE 100
#define READ 3
#define WRITE 5
typedef struct
{
SOCKET hClntSock;
SOCKADDR clntAdr;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
typedef struct
{
OVERLAPPED overlapped;
WSABUF wsaBuf;
char buffer[BUF_SIZE];
int rwMode;
}PER_IO_DATA, *LPER_IO_DATA;
DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIO);
void ErrorHandling(char *message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
HANDLE hComPort;
SYSTEM_INFO sysInfo;
LPER_IO_DATA ioInfo;
LPER_HANDLE_DATA handleInfo;
SOCKET hServSock;
SOCKADDR_IN servAdr;
int recvBytes,i,flags=0,mode = 1;
if(argc!=2)
{
printf("Usage: %s <prot> \n",argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2), &wsaData))
ErrorHandling("WSAStartup() error");
hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0 , 0);
GetSystemInfo(&sysInfo);
for(i=0;i<sysInfo.dwNumberOfProcessors;i++)
{
_beginthreadex(NULL,0,EchoThreadMain,(LPVOID)hComport,0,NULL);
}
hServSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
ioclsocket(hServSock, FINOBIO, &mode);
memset(&servAdr,0,sizeof(servAdr));
lisnAdr.sin_family = AF_INET;
lisnAdr.sin_port = htons(atoi(argv[1]));
lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
if(SOCKET_ERROR == bind(hServSock,(SOCKADDR*)&servAdr,sizeof(servAdr)))
ErrorHandling("bind error");
if(SOCKET_ERROR == listen(hServSock,5))
ErrorHandling("listen error");
while(1)
{
SOCKET hClntSock;
SOCKADDR_IN clntAdr;
hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, sizeof(clntAdr));
handleInfo = (LPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));
handleInfo->hClntSock = hClntSock;
memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen);
CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo,0);
ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
memset(&ioInfo->overlapped,0,sizeof(OVERLAPPED));
ioInfo->wsaBuf.len=BUF_SIZE;
ioInfo->wsaBuf.buf=ioInfo->buffer;
// IOCP本身不会帮我区分输入完成还是输出完成状态,无论输入还是输出,只通知IO完成状态
// 因此需要额外的变量来区分2中IO
ioInfo->rwMode=Read;
// 第7个参数overlapped的地址与PER_IO_DATA结构体ioInfo的地址相同,相当于传入了ioInfo结构体的地址
WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);
}
return 0;
}
包含overlapped的结构体可以包含其他参数,从而通过WSASend和WSARecv可以获得更多的参数信息。
通过其中的标志来决定是发送还是接收,完成端口本身是不会区分的。
而Completion Key是创建完成端口的时候设定的。
通过GetQueuedCompletionStatus的第三个参数和第四个参数,分别是在CreatIoCompletionPort和WSARecv或WSASend的时候绑定的,
2.例子
DWORD WINAPI EchoThreadMain(LPVOID pComPort)
{
HANDLE hComport = (HANDLE)pComport;
LPER_IO_DATA ioInfo;
LPER_HANDLE_DATA handleInfo;
DWORD transBytes
SOCKET sock;
GetQueuedCompletionStatus(hComport, &transBytes, (LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInof, INFINITE);
sock = handleInfo->hClntSock;
if(READ == ioInfo->rwMode)// 表明接收完毕 WSARecv完毕
{
puts("message recieve");
if(transBytes == 0)
{
closesocket(sock);
free(handleInfo);
free(ioInfo);
continue;
}
memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
ioInfo->wsaBuf.len = transBytes;
ioInfo->rwMode = WRITE;//表明接下来的操作是要写,发送
WSASend(hComport, &(ioInfo->wsaBuf), 1, NULL, 0, &(ioInfo->overlapped), NULL);
ioInfo = (LPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
ioInfo->wsaBuf.buf = ioInfo->Buffer;
ioInfo->wsaBuf.len = BUF_SIZE;
ioInof->rwMode = READ;// 表明接下来的状态是要读,接收
WSARecv(hComport, &(ioInfo->wsaBuf), 1, NULL, 0, &(ioInfo->overlapped), NULL);
}
}
四、分析
在硬件性能和带宽充足的情况下,响应时间和并发数量出问题,优先考虑两点:
- 低效的IO结构或低效的CPU使用
- 数据库设计和查询语句结构
IOCP性能更优的原因:
- 非阻塞IO,不会因为IO引发延迟
- 查找已经完成IO的时候不需要循环,select需要遍历套接字数组
- 无需将作为IO对象的套接字句柄保存到数组中管理
- 可以调整处理的线程,所以可以在实验数据的基础上选用合适的线程数