《TCP/IP网络编程》学习笔记 | Chapter 23:IOCP
《TCP/IP网络编程》学习笔记 | Chapter 23:IOCP
- 《TCP/IP网络编程》学习笔记 | Chapter 23:IOCP
- 通过重叠 I/O 理解 IOCP
- epoll 和 IOCP 的性能比较
- 实现非阻塞模式的套接字
- 以纯重叠 I/O 方式实现回声服务器端
- 重新实现客户端
- 测试
- 从重叠 I/O 模型到 IOCP 模型
- 分阶段实现 IOCP 程序
- 创建“完成端口"
- 连接完成端口对象和套接字
- 确认完成端口已完成的 I/O 和线程的 I/O 处理
- 实现基于 IOCP 的回声服务器端
- IOCP 性能更优的原因
- 习题
- (1)完成端口对象将分配多个线程用于处理 I/O。如何创建这些线程?如何分配?请从源代码级别进行说明。
- (2)CreateIoCompletionPort 函数与其他函数不同,提供 2 种功能。请问是哪 2 种?
- (3)完成端口对象和套接字之间的连接意味着什么?如何连接?
- (4)下列关于 IOCP 的说法错误的是?
- (5)判断下列关于 IOCP 中选择合理线程数的方法是否合适。
- (6)利用本章的 IOCP 模型实现聊天服务器端,该聊天服务器端应当结合第 20 章的聊天客户端 chat_clnt_win.c 正常运行。编写程序时不必刻意套用本章 IOCP 示例中的框架,那样反而会加大实现难度。
《TCP/IP网络编程》学习笔记 | Chapter 23:IOCP
通过重叠 I/O 理解 IOCP
epoll 和 IOCP 的性能比较
两种模型都很优秀,它们的差异主要在于操作系统内部的工作机制。
服务器端的响应时间和并发服务数是衡量服务器端好坏的重要因素。硬件性能和分配带宽充足情况下,若响应时间和并发服务数出了问题,查看以下两点:
- 低效的 I/O 结构或低效的 CPU 使用
- 数据库设计和查询语句(Query)的结构
实现非阻塞模式的套接字
前一章中只介绍了执行重叠 I/O 的 Sender 和 Receiver,但还未利用该模型实现过服务器端。因此,我们先利用重叠 I/O 模型实现回声服务器端。
首先介绍创建非阻塞模式套接字的方法。我们曾在第 17 章创建过非阻塞模式的套接字,与之类似,在 Windows 中通过如下函数调用将套接字属性改为非阻塞模式。
SOCKET hLisnSock;
int mode = 1;
.....
hListSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); //创建重叠 IO
ioctlsocket(hLisnSock, FIONBIO, &mode); // 设定套接字为非阻塞属性
.....
ioctlsocket 函数负责控制套接字 I/O 方式,其调用具有如下含义:将 hLisnSock 句柄引用的套接字 I/O 模式(FIONBIO)改为变量 mode 中指定的形式。也就是说,FIONBIO 是用于更改套接字 I/O 模式的选项,该函数的第三个参数中传入的变量中若存有 0,则说明套接字是阻塞模式的;如果存有非 0 值,则说明已将套接字模式改为非阻塞模式。改为非阻塞模式后,除了以非阻塞模式进行 I/O 外,还具有如下特点:
- 如果在没有客户端连接请求的状态下调用 accept 函数,将直接返回 INVALID_SOCKET。调用 WSAGetLastError 函数时返回 WSAEWOULDBLOCK。
- 调用 accept 函数时创建的套接字同样具有非阻塞属性。
因此,针对非阻塞套接字调用 accept 函数并返回 INVALID_SOCKET 时,应该通过 WSAGetLastError 函数确认返回 INVALID_SOCKET 的理由,再进行适当处理。
以纯重叠 I/O 方式实现回声服务器端
由于代码量较大,我们分3个部分学习。
片段 1:main 函数之前
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>#define BUF_SIZE 1024void CALLBACK ReadCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK WriteCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *message);typedef struct
{SOCKET hClntSock; // 套接字句柄char buf[BUF_SIZE]; // 读写数据放置地址WSABUF wsaBuf; // 指向放置读写的数据的地址和大小
} PER_IO_DATA, *LPPER_IO_DATA;
PER_IO_DATA 结构体中的信息足够进行数据交换。
片段 2:main 函数
int main(int argc, char *argv[])
{WSADATA wsaData;SOCKET hLisnSock, hRecvSock;SOCKADDR_IN lisnAdr, recvAdr;LPWSAOVERLAPPED lpOvLp;DWORD recvBytes, flagInfo = 0;LPPER_IO_DATA hbInfo;u_long mode = 1;int recvAdrsz;if (argc != 2){printf("Usage: %s <port>\n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error!");hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);ioctlsocket(hLisnSock, FIONBIO, &mode);memset(&lisnAdr, 0, sizeof(lisnAdr));lisnAdr.sin_family = AF_INET;lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);lisnAdr.sin_port = htons(atoi(argv[1]));if (bind(hLisnSock, (SOCKADDR *)&lisnAdr, sizeof(lisnAdr)) == SOCKET_ERROR)ErrorHandling("bind() error");if (listen(hLisnSock, 5) == SOCKET_ERROR)ErrorHandling("listen() error");recvAdrsz = sizeof(recvAdr);while (1){SleepEx(100, TRUE);hRecvSock = accept(hLisnSock, (SOCKADDR *)&recvAdr, &recvAdrsz);if (hRecvSock == INVALID_SOCKET){if (WSAGetLastError() == WSAEWOULDBLOCK)continue;elseErrorHandling("accept() error");}puts("Client connected.....");lpOvLp = (LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED));memset(lpOvLp, 0, sizeof(WSAOVERLAPPED));hbInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));hbInfo->hClntSock = (DWORD)hRecvSock;(hbInfo->wsaBuf).buf = hbInfo->buf;(hbInfo->wsaBuf).len = BUF_SIZE;lpOvLp->hEvent = (HANDLE)hbInfo;WSARecv(hRecvSock, &(hbInfo->wsaBuf), 1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoutine);}closesocket(hRecvSock);closesocket(hLisnSock);WSACleanup();return 0;
}
注意几点:
- 之所以在循环内部申请 WSAOVERLAPPED 结构体空间,是因为每个客户端都需要独立的 WSAOVERLAPPED 结构体变量。
- 基于 Completion Routine 函数的重叠 I/O 中不需要事件对象,因此,hEvent 中可以写入自定义结构体信息。
- 调用 WSARecv 函数时将 ReadCompRoutine 函数指定为 Completion Routine。其
中第六个参数 WSAOVERLAPPED 结构体变量地址值将传递到 Completion Routine 的第三个参数,因此,Completion Routine 函数内可以访问完成 I/O 的套接字句柄和缓冲。 - 为了运行 Completion Routine 函数,循环调用 SleepEx 函数。
片段 3:2 个 Completion Routine 函数
void CALLBACK ReadCompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);SOCKET hSock = hbInfo->hClntSock;LPWSABUF bufInfo = &(hbInfo->wsaBuf);DWORD sentBytes;if (szRecvBytes == 0){ // 如果接收到了 EOF 那么则关闭套接字以及释放对应的空间closesocket(hSock);free(lpOverlapped->hEvent);free(lpOverlapped);puts("Client disconnected.....");}else{ // 如果不为零那么一定有需要回声的内容bufInfo->len = szRecvBytes;WSASend(hSock, bufInfo, 1, &sentBytes, 0, lpOverlapped, WriteCompRoutine);}
}void CALLBACK WriteCompRoutine(DWORD dwError, DWORD szSendBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);SOCKET hSock = hbInfo->hClntSock;LPWSABUF bufInfo = &(hbInfo->wsaBuf);DWORD recvBytes, flagInfo = 0;// 发送后默认等待再次回声WSARecv(hSock, bufInfo, 1, &recvBytes, &flagInfo, lpOverlapped, ReadCompRoutine);
}void ErrorHandling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}
实际的回声服务是通过这两个函数完成的。
上述示例的工作原理整理如下:
- 有新的客户端连接时调用 WSARecv 函数,并以非阻塞模式接收数据,接收完成后调用 ReadCompRoutine 函数。
- 调用 ReadCompRoutine 函数后调用 WSASend 函数,并以非阻塞模式发送数据,发送完成后
调用 WriteCompRoutine 函数。 - 此时调用的 WriteCompRoutine 函数将再次调用 WSARecv 函数,并以非阻塞模式等待接收数据。
通过交替调用 ReadCompRoutine 函数和 WriteCompRoutine 函数,反复执行数据的接收和发送操作。另外,每次增加 1 个客户端都会定义 PER_IO_DATA 结构体,以便将新创建的套接字句柄和缓冲信息传递给 ReadCompRoutine 函数和 WriteCompRoutine 函数。同时将该结构体地址值写入 WSAOVERLAPPED 结构体成员 hEvent,并传递给 Completion Routine 函数。这非常重要,可概括如下:“使用 WSAOVERLAPPED 结构体成员 hEvent 向完成 I/O 时自动调用的 Completion Routine 函数内部传递客户端信息(套接字和缓冲)。”
接下来需要验证运行结果,先要编写回声客户端,因为使用第 4 章的回声客户端会无法得到预想的结果。
重新实现客户端
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>#define BUF_SIZE 1024void ErrorHanding(const char *message);int main(int argc, char *argv[])
{WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHanding("WSAStartup() error!");SOCKET hSocket = socket(PF_INET, SOCK_STREAM, 0);if (hSocket == INVALID_SOCKET)ErrorHanding("socket() error!");int szAddr = sizeof(SOCKADDR_IN);SOCKADDR_IN servAddr;memset(&servAddr, 0, szAddr);servAddr.sin_family = AF_INET;servAddr.sin_addr.s_addr = inet_addr(argv[1]);servAddr.sin_port = htons(atoi(argv[2]));if (connect(hSocket, (SOCKADDR *)&servAddr, szAddr) == SOCKET_ERROR)ErrorHanding("connect() error!");elseputs("Connected.......");while (1){fputs("Input message(Q to quit): ", stdout);char message[BUF_SIZE] = {0};fgets(message, BUF_SIZE - 1, stdin);if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))break;int strLen = strlen(message);send(hSocket, message, strLen, 0);int readLen = 0;while (1){readLen += recv(hSocket, &message[readLen], BUF_SIZE - 1, 0);if (readLen >= strLen)break;}message[strLen] = '\0';printf("Message from server: %s\n", message);}closesocket(hSocket);WSACleanup();return 0;
}void ErrorHanding(const char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}
考虑到 TCP 的传输特性而重复调用了 recv 函数,直至接收完所有数据。
测试
编译:
gcc CmplRouEchoServ_win.c -lwsock32 -o CmplRouEchoServ
gcc StableEchoClnt_win.c -lws2_32 -o StableEchoClnt
运行结果:

从重叠 I/O 模型到 IOCP 模型
重叠 I/O 模型回声服务器端的缺点:重复调用非阻塞模式的 accept 函数和以进入 alertable wait 状态为目的的 SleepEx 函数将影响性能。
如果正确理解了之前的示例,应该不难发现这一点。既不能为了处理连接请求而只调用 accept 函数,也不能为了 Completion Routine 而只调用 SleepEx 函数,因此轮流调用了非阻塞模式的 accept 函数和 SleepEx 函数(设置较短的超时时间)。这个恰恰是影响性能的代码结构。
这属于重叠I/O结构固有的缺陷。
可以考虑如下方法:让 main 线程(在 main 函数内部)调用 accept 函数,再单独创建 1 个线程负责客户端 I/O。
其实这就是 IOCP 中采用的服务器端模型。换言之,IOCP 将创建专用的 I/O 线程,该线程负责与所有客户端进行I/O。
分阶段实现 IOCP 程序
本节我们编写最后一种服务器模型 IOCP,比阅读代码更重要的是理解 IOCP 本身。
IOCP 关注焦点:
- I/O 是否以非阻塞模式工作?
- 如何确定非阻塞模式的 I/O 是否完成?
创建“完成端口"
IOCP 中已完成的 I/O 信息将注册到完成端口对象(Completion Port,简称 CP 对象),但这个过
程并非单纯的注册,首先需要经过如下请求过程:“该套接字的 I/O 完成时,请把状态信息注册到指定 CP 对象。"
该过程称为“套接字和CP对象之间的连接请求”。因此,为了实现基于 IOCP 模型的服务器端,需要做如下 2 项工作。
- 创建完成端口对象。
- 建立完成端口对象和套接字之间的联系。
此时的套接字必须被赋予重叠属性。上述 2 项工作可以通过 1 个函数完成,但为了创建 CP 对象,先介绍如下函数。
#include <windows.h>HANDLE CreateIoCompletionport(HANDLE FileHandle,HANDLE ExistingCompletionPort,ULONG_PTR Completionkey,DWORD NumberofConcurrentThreads
);
参数:
- FileHandle:创建 CP 对象时传递 INVALID_HANDLE_VALUE。
- ExistingCompletionPort:创建 CP 对象时传递 NULL。
- CompletionKey:创建 CP 对象时传递 0。
- NumberOfConcurrentThreads:分配给 CP 对象的用于处理 I/O 的线程数。例如,该参数为 2 时,说明分配给 CP 对象的可以同时运行的线程数最多为 2 个;如果该参数为 0,系统中 CPU 个数(内核数)就是可同时运行的最大线程数。
成功时返回 CP 对象句柄,失败时返回 NULL。
以创建 CP 对象为目的调用上述函数时,只有最后一个参数才真正具有含义。可以用如下代码段将分配给 CP 对象的用于处理 I/O 的线程数指定为 2。
HANDLE hCpObject;
.....
hCpObject = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 2);
连接完成端口对象和套接字
既然有了 CP 对象,接下来就要将该对象连接到套接字,只有这样才能使已完成的套接字 I/O 信息注册到 CP 对象。下面以建立连接为目的再次介绍 CreateIoCompletionPort 函数。
#include <windows.h>HANDLE CreateIoCompletionport(HANDLE FileHandle,HANDLE ExistingCompletionPort,ULONG_PTR Completionkey,DWORD NumberofConcurrentThreads
);
参数:
- FileHandle:要连接到 CP 对象的套接字句柄。
- ExistingCompletionPort:要连接套接字的 CP 对象句柄。
- CompletionKey:传递已完成 I/O 相关信息,关于该参数将在稍后介绍的 GetQueuedCompletionStatus 函数中共同讨论。
- NumberOfConcurrentThreads:无论传递何值,只要该函数的第二个参数非 NULL 就会忽略。
成功时返回 CP 对象句柄,失败时返回 NULL。
上述函数的第二种功能就是将 FileHandle 句柄指向的套接字和 ExistingCompletionPort 指向的 CP 对象相连。该函数的调用方式如下:
HANDLE hCpObject;
SOCKET hSock;
......
CreateIoCompletionPort((HANDLE)hSock, hCpObject, (DWORD)ioInfo, 0);
调用 CreateIoCompletionPort 函数后,只要针对 hSock 的 I/O 完成,相关信息就将注册到 hCpObject 指向的 CP 对象。
确认完成端口已完成的 I/O 和线程的 I/O 处理
我们已经掌握了 CP 对象的创建及其与套接字建立连接的方法,接下来就要学习如何确认 CP 中注册的已完成的 I/O。完成该功能的函数如下。
#include <windows.h>BOOL GetQueuedCompletionStatus(HANDLE CompletionPort,LPDWORD lpNumberOfBytes,PULONG_PTR lpCompletionKey,LPOVERLAPPED * lpoverlapped,DWORD dwMilliseconds
);
参数:
- CompletionPort:注册有已完成 I/O 信息的CP对象句柄。
- IpNumberOfBytes:用于保存 I/O 过程中传输的数据大小的变量地址值。
- IpCompletionKey:用于保存 CreateIoCompletionPort 函数的第三个参数值的变量地址值。
- IpOverlapped:用于保存调用 WSASend、WSARecv 函数时传递的 OVERLAPPED 结构体地址的变量地址值。
- dwMilliseconds:超时信息超过该指定时间后将返回FALSE并跳出函数。传递 INFINITE 时,程序将阻塞,直到已完成 I/O 信息写入 CP 对象。
成功时返回 TRUE,失败时返回 FALSE。
虽然只介绍了 2 个 IOCP 相关函数,但依然有些复杂,特别是上述函数的第三个和第四个参数更是如此。其实这 2 个参数主要是为了获取需要的信息而设置的,下面介绍这 2 种信息的含义。
- 通过 GetQueuedCompletionStatus 函数的第三个参数得到的是以连接套接字和 CP 对象为目的而调用的 CreateloCompletionPort 函数的第三个参数值。
- 通过 GetQueueCompletionStatus 函数的第四个参数得到的是调用 WSASend、WSARecv 函数时传入的 WSAOVERLAPPED 结构体变量地址值。
IOCP 中将创建全职 I/O 线程,由该线程针对所有客户端进行 I/O。程序员自行创建调用 WSASend、WSARecv 等 I/O 函数的线程,该线程为了确认 I/O 的完成会调用 GetQueuedCompletionStatus 函数。
虽然任何线程都能调用 GetQueuedCompletionStatus 函数,但实际得到 I/O 完成信息的线程数不会超过调用 CreateIoCompletionPort 函数时指定的最大线程数。
实现基于 IOCP 的回声服务器端
同样分段进行讲解。
片段 1:main 函数之前
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <winsock2.h>
#include <windows.h>#define BUF_SIZE 100
#define READ 3
#define WRITE 5typedef struct // socket info
{SOCKET hClntSock;SOCKADDR_IN clntAdr;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;typedef struct // buffer info
{OVERLAPPED overlapped;WSABUF wsaBuf;char buffer[BUF_SIZE];int rwMode; // READ or WRITE
} PER_IO_DATA, *LPPER_IO_DATA;DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIo);
void ErrorHandling(char *message);
其中,PER_HANDLE_DATA 是保存与客户端相连套接字的结构体。将 I/O 中使用的缓冲和重叠 I/O 中需要的 OVERLAPPED 结构体变量封装到同一结构体中进行定义。
片段 2:main 函数
int main(int argc, char *argv[])
{WSADATA wsaData;HANDLE hComPort;SYSTEM_INFO sysInfo;LPPER_IO_DATA ioInfo;LPPER_HANDLE_DATA handleInfo;SOCKET hServSock;SOCKADDR_IN servAdr;DWORD recvBytes, flags = 0;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error!");hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); // 创建 CP 对象GetSystemInfo(&sysInfo); // 获得当前系统的信息// 创建线程数等于 CPU 数量for (int i = 0; i < sysInfo.dwNumberOfProcessors; i++)_beginthreadex(NULL, 0, (LPVOID)EchoThreadMain, (LPVOID)hComPort, 0, NULL);hServSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);memset(&servAdr, 0, sizeof(servAdr));servAdr.sin_family = AF_INET;servAdr.sin_addr.s_addr = htonl(INADDR_ANY);servAdr.sin_port = htons(atoi(argv[1]));if (bind(hServSock, (SOCKADDR *)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)ErrorHandling("bind() error");if (listen(hServSock, 5) == SOCKET_ERROR)ErrorHandling("listen() error");while (1){SOCKET hClntSock;SOCKADDR_IN clntAdr;int addrLen = sizeof(clntAdr);hClntSock = accept(hServSock, (SOCKADDR *)&clntAdr, &addrLen);if (hClntSock == INVALID_SOCKET){if (WSAGetLastError() == WSAEWOULDBLOCK)continue;elseErrorHandling("accept() error");}handleInfo = (LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));handleInfo->hClntSock = hClntSock;memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen);CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (ULONG_PTR)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;ioInfo->rwMode = READ;WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);}closesocket(hServSock);WSACleanup();return 0;
}
套接字的重叠 I/O 完成时,已完成信息将写入连接的 CP 对象,这会引起 GetQueuedCompletionStatus 函数的返回。请注意观察 handleInfo,它同样是在 GetQueuedCompletionStatus 函数返回时得到的。
IOCP 本身不会帮我们区分输入完成和输出完成的状态。无论输入还是输出,只通知完成 I/O 的状态,因此需要通过额外的变量区分这 2 种 I/O。PER_IO_DATA 结构体中的 rwMode 就用于完成该功能。
WSARecv 函数的第七个参数为 OVERLAPPED 结构体变量地址值,该值也可以在 GetQueuedCompletionStatus 函数返回时得到。
片段 3:线程的 main 函数
DWORD WINAPI EchoThreadMain(LPVOID pComPort)
{HANDLE hComPort = (HANDLE)pComPort;SOCKET sock;DWORD bytesTrans;LPPER_HANDLE_DATA handleInfo;LPPER_IO_DATA ioInfo;DWORD flags = 0;while (1){GetQueuedCompletionStatus(hComPort, &bytesTrans, (PULONG_PTR)&handleInfo, (LPOVERLAPPED *)&ioInfo, INFINITE);sock = handleInfo->hClntSock;if (ioInfo->rwMode == READ){puts("message received!");if (bytesTrans == 0){ // 传输 EOF 时closesocket(sock);free(handleInfo);free(ioInfo);continue;}memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));ioInfo->wsaBuf.len = bytesTrans;ioInfo->rwMode = WRITE;WSASend(sock, &(ioInfo->wsaBuf), 1, NULL, 0, &(ioInfo->overlapped), NULL);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;ioInfo->rwMode = READ;WSARecv(sock, &(ioInfo->wsaBuf), 1, NULL, &flags, &(ioInfo->overlapped), NULL);}else{puts("message sent!");free(ioInfo);}}return 0;
}void ErrorHandling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}
指针 iolnfo 中保存的既是 OVERLAPPED 结构体变量地址值,也是 PER_IO_DATA 结构体变量地址值。因此,可以通过检查 rwMode 成员中的值判断是输入完成还是输出完成。
完整程序:
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <winsock2.h>
#include <windows.h>#define BUF_SIZE 100
#define READ 3
#define WRITE 5typedef struct // socket info
{SOCKET hClntSock;SOCKADDR_IN clntAdr;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;typedef struct // buffer info
{OVERLAPPED overlapped;WSABUF wsaBuf;char buffer[BUF_SIZE];int rwMode; // READ or WRITE
} PER_IO_DATA, *LPPER_IO_DATA;DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIo);
void ErrorHandling(char *message);int main(int argc, char *argv[])
{WSADATA wsaData;HANDLE hComPort;SYSTEM_INFO sysInfo;LPPER_IO_DATA ioInfo;LPPER_HANDLE_DATA handleInfo;SOCKET hServSock;SOCKADDR_IN servAdr;DWORD recvBytes, flags = 0;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error!");hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); // 创建 CP 对象GetSystemInfo(&sysInfo); // 获得当前系统的信息// 创建线程数等于 CPU 数量for (int i = 0; i < sysInfo.dwNumberOfProcessors; i++)_beginthreadex(NULL, 0, (LPVOID)EchoThreadMain, (LPVOID)hComPort, 0, NULL);hServSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);memset(&servAdr, 0, sizeof(servAdr));servAdr.sin_family = AF_INET;servAdr.sin_addr.s_addr = htonl(INADDR_ANY);servAdr.sin_port = htons(atoi(argv[1]));if (bind(hServSock, (SOCKADDR *)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)ErrorHandling("bind() error");if (listen(hServSock, 5) == SOCKET_ERROR)ErrorHandling("listen() error");while (1){SOCKET hClntSock;SOCKADDR_IN clntAdr;int addrLen = sizeof(clntAdr);hClntSock = accept(hServSock, (SOCKADDR *)&clntAdr, &addrLen);if (hClntSock == INVALID_SOCKET){if (WSAGetLastError() == WSAEWOULDBLOCK)continue;elseErrorHandling("accept() error");}handleInfo = (LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));handleInfo->hClntSock = hClntSock;memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen);CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (ULONG_PTR)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;ioInfo->rwMode = READ;WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);}closesocket(hServSock);WSACleanup();return 0;
}DWORD WINAPI EchoThreadMain(LPVOID pComPort)
{HANDLE hComPort = (HANDLE)pComPort;SOCKET sock;DWORD bytesTrans;LPPER_HANDLE_DATA handleInfo;LPPER_IO_DATA ioInfo;DWORD flags = 0;while (1){GetQueuedCompletionStatus(hComPort, &bytesTrans, (PULONG_PTR)&handleInfo, (LPOVERLAPPED *)&ioInfo, INFINITE);sock = handleInfo->hClntSock;if (ioInfo->rwMode == READ){puts("message received!");if (bytesTrans == 0){ // 传输 EOF 时closesocket(sock);free(handleInfo);free(ioInfo);continue;}memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));ioInfo->wsaBuf.len = bytesTrans;ioInfo->rwMode = WRITE;WSASend(sock, &(ioInfo->wsaBuf), 1, NULL, 0, &(ioInfo->overlapped), NULL);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;ioInfo->rwMode = READ;WSARecv(sock, &(ioInfo->wsaBuf), 1, NULL, &flags, &(ioInfo->overlapped), NULL);}else{puts("message sent!");free(ioInfo);}}return 0;
}void ErrorHandling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}
编译:
gcc IOCPEchoServ_win.c -lws2_32 -o IOCPEchoServ
运行结果:

IOCP 性能更优的原因
- 因为是非阻塞模式的 I/O,所以不会由 I/O 引发延迟。
- 查找已完成I/O时无需添加循环。
- 无需将作为 I/O 对象的套接字句柄保存到数组进行管理。
- 可以调整处理 I/O 的线程数,所以可在实验数据的基础上选用合适的线程数。
IOCP 是 Windows 特有的功能,所以很大程度上要归功于操作系统。
习题
(1)完成端口对象将分配多个线程用于处理 I/O。如何创建这些线程?如何分配?请从源代码级别进行说明。
首先使用 CreateIoCompletionPort 函数创建 IOCP 对象:
HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, // 首次创建时使用此参数NULL, 0, // 初始线程数(实际忽略)0 // 并发线程数(通常为 CPU 核心数)
);
然后将 FileHandle 句柄指向的套接字和 CP 对象相连:
HANDLE hDevice = CreateFile(...); // 或 socket()
CreateIoCompletionPort(hDevice, // 要关联的句柄hCompletionPort, // 目标 IOCP(ULONG_PTR)key, // 自定义标识(如会话上下文)0 // 并发线程数(继承 IOCP 的设置)
);
调用 CreateIoCompletionPort 函数后,只要针对 hSock 的 I/O 完成,相关信息就将注册到 hCpObject 指向的 CP 对象。
每个线程调用 GetQueuedCompletionStatus 等待 I/O 事件:
DWORD WINAPI WorkerThreadFunction(LPVOID lpParam) {HANDLE hCompletionPort = (HANDLE)lpParam;DWORD dwBytesTransferred;ULONG_PTR completionKey;LPOVERLAPPED pOverlapped;while (true) {BOOL status = GetQueuedCompletionStatus(hCompletionPort,&dwBytesTransferred,&completionKey,&pOverlapped,INFINITE // 无限等待事件);if (!status) {// 处理错误或连接关闭if (pOverlapped == NULL) break; // 收到退出信号DWORD err = GetLastError();HandleError(err);continue;}// 处理完成事件HandleIOCompletion(completionKey, dwBytesTransferred, pOverlapped);}return 0;
}
(2)CreateIoCompletionPort 函数与其他函数不同,提供 2 种功能。请问是哪 2 种?
- 创建“完成端口”对象。
- 建立“完成端口”对象和套接字之间的联系。
(3)完成端口对象和套接字之间的连接意味着什么?如何连接?
意味着将已完成的套接字 I/O 信息注册到 CP 对象,使得该套接字上发生的异步操作(如读写操作)的完成事件能够通过完成端口机制进行通知和调度。通过CreateIoCompletionPort 函数实现。
下面以建立连接为目的再次介绍 CreateIoCompletionPort 函数。
#include <windows.h>HANDLE CreateIoCompletionport(HANDLE FileHandle,HANDLE ExistingCompletionPort,ULONG_PTR Completionkey,DWORD NumberofConcurrentThreads
);
参数:
- FileHandle:要连接到 CP 对象的套接字句柄。
- ExistingCompletionPort:要连接套接字的 CP 对象句柄。
- CompletionKey:传递已完成 I/O 相关信息,关于该参数将在稍后介绍的 GetQueuedCompletionStatus 函数中共同讨论。
- NumberOfConcurrentThreads:无论传递何值,只要该函数的第二个参数非 NULL 就会忽略。
成功时返回 CP 对象句柄,失败时返回 NULL。
该函数的调用方式如下:
HANDLE hCpObject;
SOCKET hSock;
......
CreateIoCompletionPort((HANDLE)hSock, hCpObject, (DWORD)ioInfo, 0);
调用 CreateIoCompletionPort 函数后,只要针对 hSock 的 I/O 完成,相关信息就将注册到 hCpObject 指向的 CP 对象。
(4)下列关于 IOCP 的说法错误的是?
a. 以最少的线程处理多数 I/O 的结构,因此可以减少上下文切换引起的性能低下。
b. 执行 I/O 的过程中,服务器端无需等待I/O完成,可以执行其他任务,故能提高 CPU 效率。
c. I/O 完成时会自动调用相关 Completion Routine 函数,因此没必要调用特定函数以等待 I/O 完成。
d. 除 Windows 外,其他操作系统同样支持 IOCP,所以这种模型具有良好的移植性。
答:
c、d。
(5)判断下列关于 IOCP 中选择合理线程数的方法是否合适。
- 通常会选择与 CPU 数同样数量的线程。(√)
- 最好在条件允许的范围内通过实验决定线程数。(√)
- 分配的线程数越多越好。例如,1 个线程就足够多的情况下应该多分配几个,比如创建 3 个线程分配给 IOCP。(×)
(6)利用本章的 IOCP 模型实现聊天服务器端,该聊天服务器端应当结合第 20 章的聊天客户端 chat_clnt_win.c 正常运行。编写程序时不必刻意套用本章 IOCP 示例中的框架,那样反而会加大实现难度。
服务器端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <process.h>
#include <winsock2.h>
#include <windows.h>#define BUF_SIZE 120
#define MAX_CLNT 256
#define READ 3
#define WRITE 5typedef struct // socket info
{SOCKET hClntSock;SOCKADDR_IN clntAdr;char name[20];
} PER_CLIENT_INFO, *LPPER_CLIENT_INFO;typedef struct // buffer info
{OVERLAPPED overlapped;WSABUF wsaBuf;char buffer[BUF_SIZE];int rwMode; // READ or WRITE
} PER_IO_DATA, *LPPER_IO_DATA;DWORD WINAPI WorkerThread(LPVOID pComPort);
void SendMsg(char *msg, int len);
void ErrorHandling(char *msg);int clntCnt = 0;
PER_CLIENT_INFO clntSocks[MAX_CLNT];
HANDLE hMutex;int main(int argc, char *argv[])
{WSADATA wsaData;HANDLE hComPort;SYSTEM_INFO sysInfo;LPPER_IO_DATA ioInfo;LPPER_CLIENT_INFO clntInfo;SOCKET hServSock;SOCKADDR_IN servAdr;DWORD recvBytes, flags = 0;if (argc != 2){printf("Usage : %s <port>\n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error!");hMutex = CreateMutex(NULL, FALSE, NULL);// 创建 CP 对象hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);// 获得当前系统的信息GetSystemInfo(&sysInfo);// 创建线程数等于 CPU 数量for (int i = 0; i < sysInfo.dwNumberOfProcessors; i++)_beginthreadex(NULL, 0, (LPVOID)WorkerThread, (LPVOID)hComPort, 0, NULL);hServSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);memset(&servAdr, 0, sizeof(servAdr));servAdr.sin_family = AF_INET;servAdr.sin_addr.s_addr = htonl(INADDR_ANY);servAdr.sin_port = htons(atoi(argv[1]));if (bind(hServSock, (SOCKADDR *)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)ErrorHandling("bind() error");if (listen(hServSock, 5) == SOCKET_ERROR)ErrorHandling("listen() error");// 接受客户端连接while (1){SOCKET hClntSock;SOCKADDR_IN clntAdr;int addrLen = sizeof(clntAdr);hClntSock = accept(hServSock, (SOCKADDR *)&clntAdr, &addrLen);if (hClntSock == INVALID_SOCKET){if (WSAGetLastError() == WSAEWOULDBLOCK)continue;elseErrorHandling("accept() error");}WaitForSingleObject(hMutex, INFINITE);clntSocks[clntCnt++].hClntSock = hClntSock;ReleaseMutex(hMutex);clntInfo = (LPPER_CLIENT_INFO)malloc(sizeof(PER_CLIENT_INFO));clntInfo->hClntSock = hClntSock;memcpy(&(clntInfo->clntAdr), &clntAdr, addrLen);CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (ULONG_PTR)clntInfo, 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;ioInfo->rwMode = READ;WSARecv(clntInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);}closesocket(hServSock);WSACleanup();return 0;
}DWORD WINAPI WorkerThread(LPVOID pComPort)
{HANDLE hComPort = (HANDLE)pComPort;SOCKET sock;DWORD bytesTrans;LPPER_CLIENT_INFO clntInfo;LPPER_IO_DATA ioInfo;DWORD flags = 0;while (1){GetQueuedCompletionStatus(hComPort, &bytesTrans, (PULONG_PTR)&clntInfo, (LPOVERLAPPED *)&ioInfo, INFINITE);sock = clntInfo->hClntSock;if (ioInfo->rwMode == READ){if (bytesTrans == 0){ // 传输 EOF 时closesocket(sock);free(clntInfo);free(ioInfo);continue;}memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));ioInfo->wsaBuf.len = bytesTrans;ioInfo->rwMode = WRITE;SendMsg(ioInfo->buffer, bytesTrans); // broadcastioInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));ioInfo->wsaBuf.len = BUF_SIZE;ioInfo->wsaBuf.buf = ioInfo->buffer;ioInfo->rwMode = READ;WSARecv(sock, &(ioInfo->wsaBuf), 1, NULL, &flags, &(ioInfo->overlapped), NULL);}else{free(ioInfo);}}return 0;
}void SendMsg(char *msg, int len)
{ // 发送给全部人int i;WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clntCnt; i++)send(clntSocks[i].hClntSock, msg, len, 0);ReleaseMutex(hMutex);
}void ErrorHandling(char *msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}
编译:
gcc IOCPChatServ.c -lws2_32 -o IOCPChatServ
运行结果:

相关文章:
《TCP/IP网络编程》学习笔记 | Chapter 23:IOCP
《TCP/IP网络编程》学习笔记 | Chapter 23:IOCP 《TCP/IP网络编程》学习笔记 | Chapter 23:IOCP通过重叠 I/O 理解 IOCPepoll 和 IOCP 的性能比较实现非阻塞模式的套接字以纯重叠 I/O 方式实现回声服务器端重新实现客户端测试从重叠 I/O 模型到 IOCP 模型…...
Java学习打卡-Day25-注解和反射、Class类
注解(JDK5引入) 什么是注解? Java注解(Annotation),也叫元数据。一种代码级别的说明,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面…...
电感、互感器、变压器和磁珠综合对比——《器件手册--电感/线圈/变压器/磁珠篇》
三、电感/线圈/变压器/磁珠 名称 定义 特点...
CAD导入arcgis中保持面积不变的方法
1、加载CAD数据,选择面数据,如下: 2、加载进来后,右键导出数据,导出成面shp数据,如下: 3、选择存储路径,导出面后计算面积,如下: 4、与CAD中的闭合线面积核对…...
rustdesk自建服务器怎么填写客户端配置信息
目录 # id、api、中继都怎么填?rustdesk程序启动后服务不自动启动 # id、api、中继都怎么填? rustdesk程序启动后服务不自动启动 完全退出RudtDesk程序(右下角托盘区有的话,需要右键点退出) 创建windows服务ÿ…...
c++进阶之----智能指针
1.概念 在 C 中,智能指针是一种特殊的指针类型,它封装了裸指针(raw pointer)的行为,并通过 RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制自动管理动态分配的…...
六、测试分类
设计测试用例 万能公式:功能测试性能测试界面测试兼容性测试安全性测试易用性测试 弱网测试:fiddler上行速率和下行速率 安装卸载测试 在工作中: 1.基于需求文档来设计测试用例(粗粒度) 输入字段长度为6~15位 功…...
Apollo源码总结
官方课程 项目源码 源码库简介: 1.apollo:算法库 2.apollo-kernel:改进LINUX内核。添加实时性补丁。 3. apollo-platform:改进ROS系统。去中心化,增加共享内存通信方式,兼容protobuf。 4.apollo-contrib&am…...
电感详解:定义、作用、分类与使用要点
一、电感的基本定义 电感(Inductor) 是由导线绕制而成的储能元件,其核心特性是阻碍电流变化,将电能转化为磁能存储。 基本公式: 自感电动势: E -L * (di/dt) (L:电感值,…...
AI编程案例拆解|基于机器学习XX评分系统-前端篇
文章目录 1. 定价使用DeepSeek估价小红书调研 2. 确定工作事项利用DeepSeek生成具体工作事项 3. 和客户沟通约会议沟通确定内容样式 4. 前端部分设计使用DeepSeek生成UI设计在Cursor中生成并提问前置条件开始编程 关注不迷路,励志拆解100个AI编程、AI智能体的落地应…...
java数组06:Arrays类
Arrays类 数组的工具类java.util. Arrays 由于数组对象本身并没有什么方法可以供我们调用,但API中是了一个工具类Arrays供我们使用,从而可以对数据对象进行一些基本的操作。 查看JDK帮助文档 Arrays类中的方法都是static修饰的静态方法,在使用的时候可以直接使用类名进行调用…...
TQTT_KU5P开发板教程---实现流水灯
文档实现功能介绍 本文档是学习本开发板的基础,通过设置计数器使led0到led7依次闪烁,让用户初步认识vivado基本的开发流程以及熟悉项目的创建。本开发板的所有教程所使用的软件都是vivado2024.1版本的。可以根据网上的教程下载与安装。 硬件资源 此次教程…...
Model Context Protocol(MCP)模型上下文协议
Model Context Protocol(MCP)模型上下文协议 前言一、什么是MCP二、MCP的作用三、MCP与Function call对比四、构建一个简单的MCP DEMO环境准备实现MCP Server运行 ServerMCP Client端配置验证 总结 前言 在Agent时代,将Agent确立为大模型未来…...
第十二章:FreeRTOS多任务创建与删除
FreeRTOS多任务创建与删除教程 概述 本教程介绍FreeRTOS多任务的创建与删除方法,主要涉及两个核心函数: 任务创建:xTaskCreate()任务删除:vTaskDelete() 实践步骤 1. 准备工程文件 复制005工程并重命名为006 2. 创建多个任务…...
Seed-Thinking-v1.5:字节豆包新推理模型发布,200B参数战胜Deepseek
摘要 本文引入了Seed-Thinking-v1.5,能够在响应之前通过思考进行推理,从而提高了各种基准测试的性能。Seed-Thinking-v1.5在AIME 2024上获得86.7分,在Codeforces上获得55.0分,在GPQA上获得77.3分,展示了优秀的STEM和编…...
C#.NET模拟用户点击按钮button1.PerformClick自动化测试
PerformClick英文词是什么意思,几个词组成 PerformClick 是一个由两个英文单词组成的合成词,下面为你分别解释每个部分的含义以及整个词在编程语境中的意义: 单个单词含义 Perform:它是一个动词,读音为 /pəˈfɔːm/ÿ…...
微服务之间调用外键“翻译”的方法概述
写在前面的话:减少strean流操作,减少多层嵌套for循环。使用普通for循环和map的方式进行转换, 第一步查询数据 List<Student> findList studentDao.findList(findMap); 第二步准备遍历和赋值 if(CollectionUtil.isNotEmpty(findLis…...
AIDD-人工智能药物设计-提升分子预测反事实解释可靠性
UQ 过滤:提升分子预测反事实解释可靠性 目录 I-INF 指标结合 F1 评分,为评估大分子复合物(包括 RNA-蛋白质)的界面相互作用网络提供了可靠且全面的新方法。通过使用生成的人工 CAR 序列微调蛋白质语言模型(PLM),显著提高了 CAR-T 细胞活性的预测准确性,有效克服了合成蛋…...
软件界面设计:打造用户喜爱的交互体验
在数字化飞速发展的当下,软件已渗透进生活的各个角落,从日常使用的社交、购物软件,到专业领域的办公、设计软件,其重要性不言而喻。而软件界面作为用户与软件交互的桥梁,直接决定了用户对软件的第一印象与使用体验。出…...
【前端】webpack一本通
今日更新完毕,不定期补充,建议关注收藏点赞。 目录 简介使用webpack默认只能处理js文件 ->引入加载器对JS语法降级,兼容低版本语法合并文件再次打包进阶 工作原理html-webpack-plugin插件webpack开发服务器引入使用webpack-dev-server模块…...
代码学习总结(一)
代码学习总结(一) 这个系列的博客是记录下自己学习代码的历程,有来自平台上的,有来自笔试题回忆的,主要基于 C 语言,包括题目内容,代码实现,思路,并会注明题目难度&…...
DeepSeek在应急救援领域的应用解决方案
DeepSeek在应急救援领域的应用解决方案 一、引言 1.1 应急救援领域现状 近年来,我国应急管理工作全面加强,取得了显著成效。然而,一系列重特大灾害事故暴露出我国应急管理体系存在诸多问题短板。例如,在责任落实、应急处突、法…...
第十五届蓝桥杯C/C++B组省赛真题讲解(分享去年比赛的一些真实感受)
试题A——握手问题 一、解题思路 直接用高中学的排列组合思路 二、代码示例 #include<bits/stdc.h> using namespace std; int fun(int n) {int sum0;for(int i0;i<n;i){for(int ji1;j<n;j)sum; } return sum; } int main() {cout<<fun(50)-fun(7); }三、…...
【Qt】qDebug() << “中文测试“; 乱码问题
环境 Qt Creator版本:4.7.1 编译器:MSVC2015_32bit 解法一 在.pro文件中添加 msvc:QMAKE_CXXFLAGS -execution-charset:utf-8注意: 1、需要清理项目,并重新qmake,然后构建。 测试项目下载:https://do…...
Koordinator-NodeSLO
Reconcile() 获取node和nodeSLO,设置nodeExist和nodeSLOExistnode和nodeSLO都不存在,直接返回若!nodeExist 删除对应nodeSLO 若!nodeSLOExist 初始化nodeSLO创建nodeSLO 若nodeExist和nodeSLOExist都存在 获取nodeSLOSpec,若nodeSLOSpec改变…...
Vue接口平台学习六——接口列表及部分调试页面
一、实现效果图及界面布局简单梳理 整体布局分左右,左边调试,右边显示接口列表 左侧: 一个输入框按钮;下面展示信息,大部分使用代码编辑器就好了,除了请求体传文件类型需要额外处理。然后再下方显示响应信…...
2025年消防设施操作员考试题库及答案
一、多选题 31.区域报警系统主要由()等组成。 A.火灾探测器 B.手动火灾报警按钮 C.火灾声光警报器 D消防联动控制器 E.区域型火灾报警控制器 答案:ABCE 解析:根据《基础知识》第215页,“消防联动控制器”一项可…...
【C语言】预处理(下)(C语言完结篇)
一、#和## 1、#运算符 这里的#是一个运算符,整个运算符会将宏的参数转换为字符串字面量,它仅可以出现在带参数的宏的替换列表中,我们可以将其理解为字符串化。 我们先看下面的一段代码: 第二个printf中是由两个字符串组成的&am…...
深入理解全排列算法:DFS与回溯的完美结合
全排列问题是算法中的经典问题,其目标是将一组数字的所有可能排列组合列举出来。本文将详细解析如何通过深度优先搜索(DFS)和回溯法高效生成全排列,并通过模拟递归过程帮助读者彻底掌握其核心思想。 问题描述 给定一个正整数 n&a…...
低频rfid手持机,助力动物耳标智能化管理
低频RFID手持机,助力动物耳标智能化管理,正逐步成为现代畜牧业不可或缺的工具。它不仅能够高效读取动物耳标中的信息,如唯一识别码、疫苗接种记录、健康状态等,还极大地提升了数据录入的准确性和时效性。 1.精准识别与追踪 通过…...
