IOCP模型与网络编程
一。前言:
在老师分配任务(“尝试利用IOCP模型写出服务端和客户端的代码”)给我时,脑子一片空白,并不知道什么是IOCP模型,会不会是像软件设计模式里面的工厂模式,装饰模式之类的那些呢?嘿嘿,不过好像是一个挺好玩的东西,挺好奇是什么东西来的,又是一个新知识啦~于是,开始去寻找一大堆的资料,为这个了解做准备,只是呢,有时还是想去找一本书去系统地学习一下,毕竟网络的资料还是有点零散。话说,本人学习这个模型的基础是,写过一个简单的Socket服务器及客户端程序,外加一个简单的Socket单服务器对多客户端程序,懂一点点的操作系统原理的知识。于是,本着一个学习与应用的态度开始探究这个IOCP是个什么东西。
二。提出相关问题:
1. IOCP模型是什么?
2. IOCP模型是用来解决什么问题的?它为什么存在?
3. 使用IOCP模型需要用到哪些知识?
4. 如何使用IOCP模型与Socket网络编程结合起来?
5. 学会了这个模型以后与我之前写过的简单的socket程序主要有哪些不同点?
三。部分问题探究及解决:(绝大多数是个人理解,再加上个人是菜鸟,如果有什么不对的地方,欢迎指正)
1. 什么是IOCP?什么是IOCP模型?IOCP模型有什么作用?
1) IOCP(I/O Completion Port),常称I/O完成端口。
2) IOCP模型属于一种通讯模型,适用于(能控制并发执行的)高负载服务器的一个技术。
3) 通俗一点说,就是用于高效处理很多很多的客户端进行数据交换的一个模型。
4) 或者可以说,就是能异步I/O操作的模型。
5) 只是了解到这些会让人很糊涂,因为还是不知道它究意具体是个什么东东呢?
下面我想给大家看三个图:
第一个是IOCP的内部工作队列图。(整合于《IOCP本质论》文章,在英文的基础上加上中文对照)
第二个是程序实现IOCP模型的基本步骤。(整合于《深入解释IOCP》,加个人观点、理解、翻译)
第三个是使用了IOCP模型及没使用IOCP模型的程序流程图。(个人理解绘制)
2. IOCP的存在理由(IOCP的优点)及技术相关有哪些?
之前说过,很通俗地理解可以理解成是用于高效处理很多很多的客户端进行数据交换的一个模型,那么,它具体的优点有些什么呢?它到底用到了哪些技术了呢?在Windows环境下又如何去使用这些技术来编程呢?它主要使用上哪些API函数呢?呃~看来我真是一个问题多多的人,跟前面提出的相关问题变种延伸了不少的问题,好吧,下面一个个来解决。
1) 使用IOCP模型编程的优点
① 帮助维持重复使用的内存池。(与重叠I/O技术有关)
② 去除删除线程创建/终结负担。
③ 利于管理,分配线程,控制并发,最小化的线程上下文切换。
④ 优化线程调度,提高CPU和内存缓冲的命中率。
2) 使用IOCP模型编程汲及到的知识点(无先后顺序)
① 同步与异步
② 阻塞与非阻塞
③ 重叠I/O技术
④ 多线程
⑤ 栈、队列这两种基本的数据结构
3) 需要使用上的API函数
① 与SOCKET相关
1、链接套接字动态链接库:int WSAStartup(...);
2、创建套接字库: SOCKET socket(...);
3、绑字套接字: int bind(...);
4、套接字设为监听状态: int listen(...);
5、接收套接字: SOCKET accept(...);
6、向指定套接字发送信息:int send(...);
7、从指定套接字接收信息:int recv(...);
② 与线程相关
1、创建线程:HANDLE CreateThread(...);
③ 重叠I/O技术相关
1、向套接字发送数据: int WSASend(...);
2、向套接字发送数据包: int WSASendFrom(...);
3、从套接字接收数据: int WSARecv(...);
4、从套接字接收数据包: int WSARecvFrom(...);
④ IOCP相关
1、创建完成端口: HANDLE WINAPI CreateIoCompletionPort(...);
2、关联完成端口: HANDLE WINAPI CreateIoCompletionPort(...);
3、获取队列完成状态: BOOL WINAPI GetQueuedCompletionStatus(...);
4、投递一个队列完成状态:BOOL WINAPI PostQueuedCompletionStatus(...);
四。完整的简单的IOCP服务器与客户端代码实例:
- // IOCP_TCPIP_Socket_Server.cpp
- #include <WinSock2.h>
- #include <Windows.h>
- #include <vector>
- #include <iostream>
- using namespace std;
- #pragma comment(lib, "Ws2_32.lib") // Socket编程需用的动态链接库
- #pragma comment(lib, "Kernel32.lib") // IOCP需要用到的动态链接库
- /**
- * 结构体名称:PER_IO_DATA
- * 结构体功能:重叠I/O需要用到的结构体,临时记录IO数据
- **/
- const int DataBuffSize = 2 * 1024;
- typedef struct
- {
- OVERLAPPED overlapped;
- WSABUF databuff;
- char buffer[ DataBuffSize ];
- int BufferLen;
- int operationType;
- }PER_IO_OPERATEION_DATA, *LPPER_IO_OPERATION_DATA, *LPPER_IO_DATA, PER_IO_DATA;
- /**
- * 结构体名称:PER_HANDLE_DATA
- * 结构体存储:记录单个套接字的数据,包括了套接字的变量及套接字的对应的客户端的地址。
- * 结构体作用:当服务器连接上客户端时,信息存储到该结构体中,知道客户端的地址以便于回访。
- **/
- typedef struct
- {
- SOCKET socket;
- SOCKADDR_STORAGE ClientAddr;
- }PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
- // 定义全局变量
- const int DefaultPort = 6000;
- vector < PER_HANDLE_DATA* > clientGroup; // 记录客户端的向量组
- HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
- DWORD WINAPI ServerWorkThread(LPVOID CompletionPortID);
- DWORD WINAPI ServerSendThread(LPVOID IpParam);
- // 开始主函数
- int main()
- {
- // 加载socket动态链接库
- WORD wVersionRequested = MAKEWORD(2, 2); // 请求2.2版本的WinSock库
- WSADATA wsaData; // 接收Windows Socket的结构信息
- DWORD err = WSAStartup(wVersionRequested, &wsaData);
- if (0 != err){ // 检查套接字库是否申请成功
- cerr << "Request Windows Socket Library Error!\n";
- system("pause");
- return -1;
- }
- if(LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){// 检查是否申请了所需版本的套接字库
- WSACleanup();
- cerr << "Request Windows Socket Version 2.2 Error!\n";
- system("pause");
- return -1;
- }
- // 创建IOCP的内核对象
- /**
- * 需要用到的函数的原型:
- * HANDLE WINAPI CreateIoCompletionPort(
- * __in HANDLE FileHandle, // 已经打开的文件句柄或者空句柄,一般是客户端的句柄
- * __in HANDLE ExistingCompletionPort, // 已经存在的IOCP句柄
- * __in ULONG_PTR CompletionKey, // 完成键,包含了指定I/O完成包的指定文件
- * __in DWORD NumberOfConcurrentThreads // 真正并发同时执行最大线程数,一般推介是CPU核心数*2
- * );
- **/
- HANDLE completionPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0);
- if (NULL == completionPort){ // 创建IO内核对象失败
- cerr << "CreateIoCompletionPort failed. Error:" << GetLastError() << endl;
- system("pause");
- return -1;
- }
- // 创建IOCP线程--线程里面创建线程池
- // 确定处理器的核心数量
- SYSTEM_INFO mySysInfo;
- GetSystemInfo(&mySysInfo);
- // 基于处理器的核心数量创建线程
- for(DWORD i = 0; i < (mySysInfo.dwNumberOfProcessors * 2); ++i){
- // 创建服务器工作器线程,并将完成端口传递到该线程
- HANDLE ThreadHandle = CreateThread(NULL, 0, ServerWorkThread, completionPort, 0, NULL);
- if(NULL == ThreadHandle){
- cerr << "Create Thread Handle failed. Error:" << GetLastError() << endl;
- system("pause");
- return -1;
- }
- CloseHandle(ThreadHandle);
- }
- // 建立流式套接字
- SOCKET srvSocket = socket(AF_INET, SOCK_STREAM, 0);
- // 绑定SOCKET到本机
- SOCKADDR_IN srvAddr;
- srvAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
- srvAddr.sin_family = AF_INET;
- srvAddr.sin_port = htons(DefaultPort);
- int bindResult = bind(srvSocket, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR));
- if(SOCKET_ERROR == bindResult){
- cerr << "Bind failed. Error:" << GetLastError() << endl;
- system("pause");
- return -1;
- }
- // 将SOCKET设置为监听模式
- int listenResult = listen(srvSocket, 10);
- if(SOCKET_ERROR == listenResult){
- cerr << "Listen failed. Error: " << GetLastError() << endl;
- system("pause");
- return -1;
- }
- // 开始处理IO数据
- cout << "本服务器已准备就绪,正在等待客户端的接入...\n";
- // 创建用于发送数据的线程
- HANDLE sendThread = CreateThread(NULL, 0, ServerSendThread, 0, 0, NULL);
- while(true){
- PER_HANDLE_DATA * PerHandleData = NULL;
- SOCKADDR_IN saRemote;
- int RemoteLen;
- SOCKET acceptSocket;
- // 接收连接,并分配完成端,这儿可以用AcceptEx()
- RemoteLen = sizeof(saRemote);
- acceptSocket = accept(srvSocket, (SOCKADDR*)&saRemote, &RemoteLen);
- if(SOCKET_ERROR == acceptSocket){ // 接收客户端失败
- cerr << "Accept Socket Error: " << GetLastError() << endl;
- system("pause");
- return -1;
- }
- // 创建用来和套接字关联的单句柄数据信息结构
- PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA)); // 在堆中为这个PerHandleData申请指定大小的内存
- PerHandleData -> socket = acceptSocket;
- memcpy (&PerHandleData -> ClientAddr, &saRemote, RemoteLen);
- clientGroup.push_back(PerHandleData); // 将单个客户端数据指针放到客户端组中
- // 将接受套接字和完成端口关联
- CreateIoCompletionPort((HANDLE)(PerHandleData -> socket), completionPort, (DWORD)PerHandleData, 0);
- // 开始在接受套接字上处理I/O使用重叠I/O机制
- // 在新建的套接字上投递一个或多个异步
- // WSARecv或WSASend请求,这些I/O请求完成后,工作者线程会为I/O请求提供服务
- // 单I/O操作数据(I/O重叠)
- LPPER_IO_OPERATION_DATA PerIoData = NULL;
- PerIoData = (LPPER_IO_OPERATION_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_OPERATEION_DATA));
- ZeroMemory(&(PerIoData -> overlapped), sizeof(OVERLAPPED));
- PerIoData->databuff.len = 1024;
- PerIoData->databuff.buf = PerIoData->buffer;
- PerIoData->operationType = 0; // read
- DWORD RecvBytes;
- DWORD Flags = 0;
- WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);
- }
- system("pause");
- return 0;
- }
- // 开始服务工作线程函数
- DWORD WINAPI ServerWorkThread(LPVOID IpParam)
- {
- HANDLE CompletionPort = (HANDLE)IpParam;
- DWORD BytesTransferred;
- LPOVERLAPPED IpOverlapped;
- LPPER_HANDLE_DATA PerHandleData = NULL;
- LPPER_IO_DATA PerIoData = NULL;
- DWORD RecvBytes;
- DWORD Flags = 0;
- BOOL bRet = false;
- while(true){
- bRet = GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (PULONG_PTR)&PerHandleData, (LPOVERLAPPED*)&IpOverlapped, INFINITE);
- if(bRet == 0){
- cerr << "GetQueuedCompletionStatus Error: " << GetLastError() << endl;
- return -1;
- }
- PerIoData = (LPPER_IO_DATA)CONTAINING_RECORD(IpOverlapped, PER_IO_DATA, overlapped);
- // 检查在套接字上是否有错误发生
- if(0 == BytesTransferred){
- closesocket(PerHandleData->socket);
- GlobalFree(PerHandleData);
- GlobalFree(PerIoData);
- continue;
- }
- // 开始数据处理,接收来自客户端的数据
- WaitForSingleObject(hMutex,INFINITE);
- cout << "A Client says: " << PerIoData->databuff.buf << endl;
- ReleaseMutex(hMutex);
- // 为下一个重叠调用建立单I/O操作数据
- ZeroMemory(&(PerIoData->overlapped), sizeof(OVERLAPPED)); // 清空内存
- PerIoData->databuff.len = 1024;
- PerIoData->databuff.buf = PerIoData->buffer;
- PerIoData->operationType = 0; // read
- WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);
- }
- return 0;
- }
- // 发送信息的线程执行函数
- DWORD WINAPI ServerSendThread(LPVOID IpParam)
- {
- while(1){
- char talk[200];
- gets(talk);
- int len;
- for (len = 0; talk[len] != '\0'; ++len){
- // 找出这个字符组的长度
- }
- talk[len] = '\n';
- talk[++len] = '\0';
- printf("I Say:");
- cout << talk;
- WaitForSingleObject(hMutex,INFINITE);
- for(int i = 0; i < clientGroup.size(); ++i){
- send(clientGroup[i]->socket, talk, 200, 0); // 发送信息
- }
- ReleaseMutex(hMutex);
- }
- return 0;
- }
- // IOCP_TCPIP_Socket_Client.cpp
- #include <iostream>
- #include <cstdio>
- #include <string>
- #include <cstring>
- #include <winsock2.h>
- #include <Windows.h>
- using namespace std;
- #pragma comment(lib, "Ws2_32.lib") // Socket编程需用的动态链接库
- SOCKET sockClient; // 连接成功后的套接字
- HANDLE bufferMutex; // 令其能互斥成功正常通信的信号量句柄
- const int DefaultPort = 6000;
- int main()
- {
- // 加载socket动态链接库(dll)
- WORD wVersionRequested;
- WSADATA wsaData; // 这结构是用于接收Wjndows Socket的结构信息的
- wVersionRequested = MAKEWORD( 2, 2 ); // 请求2.2版本的WinSock库
- int err = WSAStartup( wVersionRequested, &wsaData );
- if ( err != 0 ) { // 返回值为零的时候是表示成功申请WSAStartup
- return -1;
- }
- if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 ) { // 检查版本号是否正确
- WSACleanup( );
- return -1;
- }
- // 创建socket操作,建立流式套接字,返回套接字号sockClient
- sockClient = socket(AF_INET, SOCK_STREAM, 0);
- if(sockClient == INVALID_SOCKET) {
- printf("Error at socket():%ld\n", WSAGetLastError());
- WSACleanup();
- return -1;
- }
- // 将套接字sockClient与远程主机相连
- // int connect( SOCKET s, const struct sockaddr* name, int namelen);
- // 第一个参数:需要进行连接操作的套接字
- // 第二个参数:设定所需要连接的地址信息
- // 第三个参数:地址的长度
- SOCKADDR_IN addrSrv;
- addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 本地回路地址是127.0.0.1;
- addrSrv.sin_family = AF_INET;
- addrSrv.sin_port = htons(DefaultPort);
- while(SOCKET_ERROR == connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR))){
- // 如果还没连接上服务器则要求重连
- cout << "服务器连接失败,是否重新连接?(Y/N):";
- char choice;
- while(cin >> choice && (!((choice != 'Y' && choice == 'N') || (choice == 'Y' && choice != 'N')))){
- cout << "输入错误,请重新输入:";
- cin.sync();
- cin.clear();
- }
- if (choice == 'Y'){
- continue;
- }
- else{
- cout << "退出系统中...";
- system("pause");
- return 0;
- }
- }
- cin.sync();
- cout << "本客户端已准备就绪,用户可直接输入文字向服务器反馈信息。\n";
- send(sockClient, "\nAttention: A Client has enter...\n", 200, 0);
- bufferMutex = CreateSemaphore(NULL, 1, 1, NULL);
- DWORD WINAPI SendMessageThread(LPVOID IpParameter);
- DWORD WINAPI ReceiveMessageThread(LPVOID IpParameter);
- HANDLE sendThread = CreateThread(NULL, 0, SendMessageThread, NULL, 0, NULL);
- HANDLE receiveThread = CreateThread(NULL, 0, ReceiveMessageThread, NULL, 0, NULL);
- WaitForSingleObject(sendThread, INFINITE); // 等待线程结束
- closesocket(sockClient);
- CloseHandle(sendThread);
- CloseHandle(receiveThread);
- CloseHandle(bufferMutex);
- WSACleanup(); // 终止对套接字库的使用
- printf("End linking...\n");
- printf("\n");
- system("pause");
- return 0;
- }
- DWORD WINAPI SendMessageThread(LPVOID IpParameter)
- {
- while(1){
- string talk;
- getline(cin, talk);
- WaitForSingleObject(bufferMutex, INFINITE); // P(资源未被占用)
- if("quit" == talk){
- talk.push_back('\0');
- send(sockClient, talk.c_str(), 200, 0);
- break;
- }
- else{
- talk.append("\n");
- }
- printf("\nI Say:(\"quit\"to exit):");
- cout << talk;
- send(sockClient, talk.c_str(), 200, 0); // 发送信息
- ReleaseSemaphore(bufferMutex, 1, NULL); // V(资源占用完毕)
- }
- return 0;
- }
- DWORD WINAPI ReceiveMessageThread(LPVOID IpParameter)
- {
- while(1){
- char recvBuf[300];
- recv(sockClient, recvBuf, 200, 0);
- WaitForSingleObject(bufferMutex, INFINITE); // P(资源未被占用)
- printf("%s Says: %s", "Server", recvBuf); // 接收信息
- ReleaseSemaphore(bufferMutex, 1, NULL); // V(资源占用完毕)
- }
- return 0;
- }
五。本次学习资料
几翻周折,终于写出一个比较简单的IOCP模型的服务器与客户端啦,并且也大概了解这个模型的思路啦~没有买书的娃,伤不起啊,只能从网上搜罗资料,幸好有这些文章在,最后为下列这些文章的作者说声谢谢~
一 什么是异步I/O
同步I/O和异步I/O的关键不同就是在发出I/O请求后,线程是否会阻塞。当线程发出一个设备I/O请求的时候,线程会被挂起来,直到设备完成I/O请求为止,这称之为同步I/O。而对于异步I/O,当线程提交了一个设备I/O请求后,可以继续运行,当内核完成I/O的请求后会通知线程I/O已完成。由于与计算机执行的大多数其它操作相比,设备I/O是其中最慢的,所以使用异步I/O在大多数情况下可以大幅度提升程序的新能,当然一些个别情况就不一定使用啦。
二 异步I/O的分类
在Windows下一共有四种异步I/O的技术,它们在启动I/O操作的方法以及用于操作何时完成的方法上有所不同:
- 等待的 重叠I/O。线程发出I/O请求之后继续执行,当线程需要I/O请求的结果才能继续时,则要么等待文件句柄,要么等待在重叠结构中指定的一个事件。根据等待对象的不同,分为等待文件句柄的重叠I/O和等待事件的重叠I/O。等待文件句柄的重叠I/O没有多大用,因为它需要等待和此文件句柄关联的所有操作完成,同步I/O基本差不多。一般我们用的是等待事件的重叠I/O,使用人工事件来实现等待。
- 完成例程的重叠I/O。完成例程,这个名词看上去似乎很高级,说白了就是个毁掉函数。这个方法允许我们向一个设备发出多个I/O请求,这些I/O请求中带有一个回调函数,即完成例程。当I/O请求完成时,如果线程处于可提醒状态,则系统会通知该线程调用它的异步队列中的回调函数来处理完成的I/O。
- I/O完成端口。I/O完成端口允许我们向一个设备同时发出多个I/O请求。它允许一个线程发出I/O请求而另外一个线程对结果进行处理,这个是是和完成例程不同之处。这个技术具有很好的伸缩性和灵活性,算是异步I/O中最好的一种方式。I/O完成端口里面有一套线程池的调度策略,所以免去了我们去维护多线程的烦恼。
三 相关数据结构和函数
下面我们来看看重叠I/O中的第一个方法----重叠I/O(等待)。先从了解重叠结构开始:
1 重叠结构 OVERLAPPED
“overlapped”是执行I/O请求的时间与其他线程执行其他任务的时间是重叠的,OVERLAPPED的结构如下:
typedef struct _OVERLAPPED { ULONG_PTR Internal; //系统保留参数,初始化时不用设置,返回值时保存已处理的I/O请求的错误码 ULONG_PTR InternalHigh; //系统保留参数,初始化时不用设置,返回值时保存已传输的字节数 union { struct { DWORD Offset; //文件偏移量的低位 DWORD OffsetHigh; //文件偏移量的高位 }; PVOID Pointer; }; HANDLE hEvent; //事件句柄,必须是手动事件} OVERLAPPED, *LPOVERLAPPED;
参数说明:
Offset 和OffsetHigh
这两个成员构成一个64位的偏移量。如果当前操作的是文件设备,则表示当前文件I/O操作应该从哪里开始。当执行异步I/O时,系统会忽略与文件关联的内核文件指针,而是用OVERLAPPED中指定的起始偏移量,这样可以避免对同一个对象进行异步调用的时候出现混淆。对于非文件设备,则需要将这两个参数设置为0,否则I/O请求会失败,返回错误ERROR_INVALID_PARAMETER。
hEvent
不同的重叠I/O操作方法中,hEvent的设置不同。在可等待的重叠I/0操作中,如果等待的是操作的文件句柄,则不需要hEvent不需要设置;而如果通过OVERLAPPED结构中的hEvent来等待的,或者是I/O完成端口方法,则需要将此参数设置为一个人工事件,注意不能使自动事件。如果是I/O完成例程的方法,则hEvent一般都用来传递I/O操作的信息。
Internal
用来保存已经处理的I/O请求的错误码。当我们发出异步I/O请求后,设备驱动程序会将internal设置为STATUS_PENDING,表示没有错误,操作尚未开始。
InternalHigh
当异步I/O请求完成时,用来保存已经传输的字节数。
异步I/O的注意事项:
1 在异步I/O请求完成之前,一定不恩能够移动或是销毁在发出I/O请求时所使用的数据缓存和OVERLAPPED机构,否则会使内存遭到破坏。因为我们传给I/O设备的是这两个数据的地址,而I/O设备并不知道我们已经销毁了它们。
2 在执行异步I/O的时候,设备不必以先入先出的方式处理队列中的I/O请求。
3 以异步I/O执行的时候,文件的读写等相关的操作返回为FALSE不一定表示失败。必须调用GetLastError,如果GetLastError返回的是ERRO_IO_PENDING,表示I/O请求已经成功的加入到I/O队列中,会在晚些时候完成。
2 获取重叠I/O状态
当我们使用ReadFile或WriteFile向文件发送读写请求后,函数会立刻返回。大多数情况下,返回时I/O操作都未完成,所以返回的FALSE,自然我们也不知道传输的字节数了。所以我们需要另外一种方法来获取文件I/O完成时传输的字节数,这个函数便是GetOverlappedResult:
BOOLWINAPIGetOverlappedResult( __in HANDLE hFile, //I/O文件句柄 __in LPOVERLAPPED lpOverlapped, //重叠结构 __out LPDWORD lpNumberOfBytesTransferred, //I/O完成时传输的字节数 __in BOOL bWait //Ture:一直等待,Flase:立刻返回 );
参数hFile和lpOverlapped组合在一起可以唯一的表示一个I/O操作。当bWait设置为True,则函数会一直等待此I/O操作完成才会返回,此时lpNumberOfBytesTransfferred中即是传输的字节数。如果bWait设置为False,函数会立刻返回,如果返回会False且GetLastError()为ERRO_IO_PENDING,表示I/O尚未完成,需要轮询检查I/O是否完成。
3 取消队列中的重叠I/O请求
如果想要取消一个已经加入到I/O队列但是尚未处理的I/O请求,则可以调用CancelIoEx取消。
BOOLWINAPICancelIoEx( __in HANDLE hFile, __in_opt LPOVERLAPPED lpOverlapped );
CancelIoEx不仅可以取消掉本线程发出的相关文件的I/O请求,还可以将调用线程外的其他线程发出的待处理的I/O请求取消掉。这个函数会将hFile设备待处理的I/O请求中所有与lpOverlapped参数相关联的请求都取消掉。但是由于每个待处理的I/O请求都有一个其特定的OVERLAPPED结构,所以如果lpOverlpaped非空,则CancelIoEx每次只能取消一个请求。而lpOverlpaped为NULL的话,会取消掉hFileI/O请求队列中的所有I/O请求。
四 例子:使用可等待的重叠I/O进行文件复制操作
这个示例程序使用事件来实现重叠I/O的等待。这个程序实现从输入文件中异步读数据,然后异步的写数据到输出文件中。程序采用多缓冲区的方法进行文件的转换,假设输入和输出各采用N个缓冲区,则N个输入缓冲和N个输出缓冲需要N个输入重叠结构和N个输出重叠结构与其对应。初始时,N个输入缓冲分别发出重叠的读操作,然后程序使用WaitForMultipleObjects来等待单一的I/O操作完成事件,表示一个读或写操作完成。当一个读操作完成时,则对缓冲区进行复制,然后发起一个写操作请求。当写完成时,就可以进行下一个读操作请求了。
I/O设备处理必然让主程序停下来干等I/O的完成,
对这个问题有
方法一:使用另一个线程进行I/O。这个方案可行,但是麻烦。 即 CreateThread(…………);创建一个子线程做其他事情。 Readfile(^…………);阻塞方式读数据。
方法二:使用overlapped I/O。
overlapped I/O是WIN32的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。这项技术使你的程序在I/O进行过程中仍然能够继续处理事务。事实上,操作系统内部正是以线程来I/O完成overlapped I/O。你可以获得线程的所有利益,而不需付出什么痛苦的代价
怎样使用overlapped I/O:
进行I/O操作时,指定overlapped方式
使用CreateFile (),将其第6个参数指定为FILE_FLAG_OVERLAPPED,
就是准备使用overlapped的方式构造或打开文件;
如果采用 overlapped,那么ReadFile()、WriteFile()的第5个参数必须提供一个指针,
指向一个OVERLAPPED结构。 OVERLAPPED用于记录了当前正在操作的文件一些相关信息。
//功能:从指定文件的1500位置读入300个字节
int main()
{
BOOL rc;
HANDLE hFile;
DWORD numread;
OVERLAPPED overlap;
char buf[512];
char szPath=”c:\\xxxx\xxxx”;
hFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, // 以overlapped打开文件
NULL
);
// OVERLAPPED结构实始化为0
memset(&overlap, 0, sizeof(overlap));
//指定文件位置是1500;
overlap.Offset = 1500;
rc = ReadFile(hFile,buf,300,&numread,&overlap);
//因为是overlapped操作,ReadFile会将读文件请求放入读队列之后立即返回(false),而不会等到文件读完才返回(true)
if (rc)
{
…………此处即得到数据了。
//文件真是被读完了,rc为true
// 或当数据被放入cache中,或操作系统认为它可以很快速地取得数据,rc为true
}
else
{
if (GetLastError() == ERROR_IO_PENDING)
{//当错误是ERROR_IO_PENDING,那意味着读文件的操作还在进行中
//等候,直到文件读完
WaitForSingleObject(hFile, INFINITE);
rc = GetOverlappedResult(hFile,&overlap,&numread,FALSE);
//上面二条语句完成的功能与下面一条语句的功能等价:
一只阻塞等到得到数据才继续下面。
// GetOverlappedResult(hFile,&overlap,&numread,TRUE);
}
else
{
//出错了
}
}
CloseHandle(hFile);
return EXIT_SUCCESS;
}
在实际工作中,若有几个操作同一个文件时,
怎么办?我们可以利用OVERLAPPED结构中提供的event来解决上面遇到的问题。
注意,你所使用的event对象必须是一个MANUAL型的;否则,可能产生竞争条件。
int main()
{
int i;
BOOL rc;
char szPath=”x:\\xxxx\xxxx”;
// 以overlapped的方式打开文件
ghFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
for (i=0; i<MAX_REQUESTS; i++) requests 同时有N个同时读取文件
{
//将同一文件按几个部分按overlapped方式同时读
//注意看QueueRequest函数是如何运做的,每次读16384个块
QueueRequest(i, i*16384, READ_SIZE);
}
// 等候所有操作结束;
//隐含条件:当一个操作完成时,其对应的event对象会被激活
WaitForMultipleObjects(MAX_REQUESTS, ghEvents, TRUE, INFINITE);
// 收尾操作
for (i=0; i<MAX_REQUESTS; i++)
{
DWORD dwNumread;
rc = GetOverlappedResult(
ghFile,
&gOverlapped[i],
&dwNumread,
FALSE
);
CloseHandle(gOverlapped[i].hEvent);
}
CloseHandle(ghFile);
return EXIT_SUCCESS;
}
//当读操作完成以后,gOverlapped[nIndex].hEvent会系统被激发
int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount)
{
//构造一个MANUAL型的event对象
ghEvents[nIndex] = CreateEvent(NULL, TRUE, FALSE, NULL);
//将此event对象置入OVERLAPPED结构
gOverlapped[nIndex].hEvent = ghEvents[nIndex];
每个重叠对象对应一个事件。
gOverlapped[nIndex].Offset = dwLocation;
for (i=0; i<MAX_TRY_COUNT; i++) //尝试几次。
{
//文件ghFile唯一
rc = ReadFile(ghFile, gBuffers[nIndex],&dwNumread,&gOverlapped[nIndex]);
if (rc) 如果立刻读到数据则返回真
return TRUE;
err = GetLastError();
if (err == ERROR_IO_PENDING)
{
//当错误是ERROR_IO_PENDING,那意味着读文件的操作还在进行中
return TRUE;
}
// 处理一些可恢复的错误
if ( err == ERROR_INVALID_USER_BUFFER ||
err == ERROR_NOT_ENOUGH_QUOTA ||
err == ERROR_NOT_ENOUGH_MEMORY )
{
sleep(50);
continue;//重试
}
// 如果GetLastError()返回的不是以上列出的错误,放弃
break;
}
return -1;
}
程序流程:
1: N个用户同时读取一个文件的各个部分,且每个用户对应一个重叠对象和事件。
2:调用WaitForMultipleObjects(MAX_REQUESTS, ghEvents, TRUE, INFINITE) 当任何一个用户的读操作完成时,函数停止阻塞。并且ghEvents中对应于的读取数据完毕的用户的事件被激活。
3:调用GetOverlappedResult 取得读取数据完毕的用户编号。
完成端口(I/O completion)原理收藏
异步过程调用(apcs)问题:
只有发overlapped请求的线程才可以提供callback函数(需要一个特定的线程为一个特定的I/O请求服务)。
完成端口(I/O completion)的优点:
不会限制handle个数,可处理成千上万个连接。I/O completion port允许一个线程将一个请求暂时保存下来,由另一个线程为它做实际服务。
并发模型与线程池:
在典型的并发模型中,服务器为每一个客户端创建一个线程,如果很多客户同时请求,则这些线程都是运行的,那么CPU就要一个个切换,CPU花费了更多的时间在线程切换,线程确没得到很多CPU时间。到底应该创建多少个线程比较合适呢,微软件帮助文档上讲应该是2*CPU个。但理想条件下最好线程不要切换,而又能象线程池一样,重复利用。I/O完成端口就是使用了线程池。
理解与使用:
第一步:
在我们使用完成端口之前,要调用CreateIoCompletionPort函数先创建完成端口对象。
定义如下:
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
DWORD CompletionKey,
DWORD NumberOfConcurrentThreads
);
FileHandle:
文件或设备的handle, 如果值为INVALID_HANDLE_VALUE则产生一个没有和任何文件handle有关系的port.( 可以用来和完成端口联系的各种句柄,文件,套接字)
ExistingCompletionPort:
NULL时生成一个新port, 否则handle会加到此port上。
CompletionKey:
用户自定义数值,被交给服务的线程。GetQueuedCompletionStatus函数时我们可以完全得到我们在此联系函数中的完成键(申请的内存块)。在GetQueuedCompletionStatus
中可以完封不动的得到这个内存块,并且使用它。
NumberOfConcurrentThreads:
参数NumberOfConcurrentThreads用来指定在一个完成端口上可以并发的线程数量。理想的情况是,一个处理器上只运行一个线程,这样可以避免线程上下文切换的开销。如果这个参数的值为0,那就是告诉系统线程数与处理器数相同。我们可以用下面的代码来创建I/O完成端口。
隐藏在之创建完成端口的秘密:
1. 创建一个完成端口
CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, dwNumberOfConcurrentThreads);
2. 设备列表,完成端口把它同一个或多个设备相关联。
CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0) ;
第二步:
根据处理器个数,创建cpu*2个工作线程:
CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,0, &ThreadID))
与此同时,服务器调用WSASocket,bind, listen, WSAAccept,之后,调用
CreateIoCompletionPort((HANDLE) Accept, CompletionPort... )把一个套接字句柄和一个完成端口绑定到一起。完成端口又同一个或多个设备相关联着,所以以套接字为基础,投递发送和请求,对I/O处理。接着,可以依赖完成端口,接收有关I/O操作完成情况的通知。再看程序里:
WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
&(PerIoData->Overlapped), NULL)开始调用,这里象前面讲过的一样,既然是异步I/O,所以WSASend和WSARecv的调用会立即返回。
系统处理:
当一个设备的异步I/O请求完成之后,系统会检查该设备是否关联了一个完成端口,如果是,系统就向该完成端口的I/O完成队列中加入完成的I/O请求列。
然后我们需要从这个完成队列中,取出调用后的结果(需要通过一个Overlapped结构来接收调用的结果)。怎么知道这个队列中已经有处理后的结果呢,调用GetQueuedCompletionStatus函数。
工作线程与完成端口:
和异步过程调用不同(在一个Overlapped I/O完成之后,系统调用该回调函数。OS在有信号状态下(设备句柄),才会调用回调函数(可能有很多APCS等待处理了))
GetQueuedCompletionStatus
在工作线程内调用GetQueuedCompletionStatus函数。
GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
LPDWORD lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);
CompletionPort:指出了线程要监视哪一个完成端口。很多服务应用程序只是使用一个I/O完成端口,所有的I/O请求完成以后的通知都将发给该端口。
lpNumberOfBytesTransferred:传输的数据字节数
lpCompletionKey:
完成端口的单句柄数据指针,这个指针将可以得到我们在CreateIoCompletionPort中申请那片内存。
lpOverlapped:
重叠I/O请求结构,这个结构同样是指向我们在重叠请求时所申请的内存块,同时和lpCompletionKey,一样我们也可以利用这个内存块来存储我们要保存的任意数据。
dwMilliseconds:
等待的最长时间(毫秒),如果超时,lpOverlapped被设为NULL,函数返回False.
GetQueuedCompletionStatus功能及隐藏的秘密:
GetQueuedCompletionStatus使调用线程挂起,直到指定的端口的I/O完成队列中出现了一项或直到超时。(I/0完成队列中出现了记录)调用GetQueuedCompletionStatus时,调用线程的ID(cpu*2个线程,每个ServerWorkerThread的线程ID)就被放入该等待线程队列中。
等待线程队列很简单,只是保存了这些线程的ID。完成端口会按照后进先出的原则将一个线程队列的ID放入到释放线程列表中。
这样,I/O完成端口内核对象就知道哪些线程正在等待处理完成的I/O请求。当端口的I/O完成队列出现一项时,完成端口就唤醒(睡眠状态中变为可调度状态)等待线程队列中的一个线程。线程将得到完成I/O项中的信息:传输的字节数,完成键(单句柄数据结构)和Overlapped结构地址,线程是通过GetQueuedCompletionStatus返回这些信息,等待CPU的调度。
GetQueuedCompletionStatus返回可能有多种原因,如果传递无效完成端口句柄,函数返回False,GetLastError返回一个错误(ERROR_INVALID_HANDLE),如果超时,返回False, GetLastError返回WAIT_TIMEOUT, i/o完成队列删除一项,该表项是一个成功完成的I/O请求,则返回True。
调用GetQueuedCompletionStatus的线程是后进先出的方式唤醒的,比如有4个线程等待,如果有一个I/O,最后一个调用GetQueuedCompletionStatus的线程被唤醒来处理。处理完之后,再调用 GetQueuedCompletionStatus进入等待线程队列中。
深入分析完成端口线程池调度原理:
假设我们运行在2CPU的机器上。创建完成端口时指定2个并发,创建了4个工作线程加入线程池中等待完成I/O请求,且完成端口队列(先入先出)中有3个完成I/O的请求的情况:
工作线程运行, 创建了4个工作线程,调用GetQueuedCompletionStatus时,该调用线程就进入了睡眠状态,假设这个时候,I/O完成队列出现了三项,调用线程的ID就被放入该等待线程队列中。
I/O完成端口内核对象(第3个参数等级线程队列),因此知道哪些线程正在等待处理完成的I/O请求。当端口的I/O完成队列出现一项时,完成端口就唤醒(睡眠状态中变为可调度状态)等待线程队列中的一个线程(前面讲过等待线程队列是后进先出)。所以线程D将得到完成I/O项中的信息:传输的字节数,完成键(单句柄数据结构)和Overlapped结构地址,线程是通过GetQueuedCompletionStatus返回这些信息。
在前面我们指定了并发线程的数目是2,所以I/O完成端口唤醒2个线程,线程D和线程C,另两个继续休眠(线程B,线程A),直到线程D处理完了,发现表项里还有要处理的,就唤醒同一线程继续处理。
线程并发量:
并发量限制了与该完成端口相关联的可运行线程的数目, 它类似阀门的作用。 当与该完成端口相关联的可运行线程的总数目达到了该并发量,系统就会阻塞任何与该完成端口相关联的后续线程的执行, 直到与该完成端口相关联的可运行线程数目下降到小于该并发量为止。所以解释了线程池中的运行线程可能会比设置的并发线程多的原因。
它的作用:
最有效的假想是发生在有完成包在队列中等待,而没有等待被满足,因为此时完成端口达到了其并发量的极限。此时,一个正在运行中的线程调用 GetQueuedCompletionStatus时,它就会立刻从队列中取走该完成包。这样就不存在着环境的切换,因为该处于运行中的线程就会连续不断地从队列中取走完成包,而其他的线程就不能运行了。
注意:如果池中的所有线程都在忙,客户请求就可能拒绝,所以要适当调整这个参数,获得最佳性能。
线程并发:D线程挂起,加入暂停线程,醒来后又加入释放线程队列。
线程的安全退出:
PostQueudCompletionStatus函数,我们可以用它发送一个自定义的包含了OVERLAPPED成员变量的结构地址,里面包含一个状态变量,当状态变量为退出标志时,线程就执行清除动作然后退出。
完成端口使用需要注意的地方:
1.在执行wsasend和wsarecv操作前,请先将overlapped结构体使用memset进行清零。