《TCP/IP网络编程》学习笔记 | Chapter 22:重叠 I/O 模型

《TCP/IP网络编程》学习笔记 | Chapter 22:重叠 I/O 模型

理解重叠 I/O 模型

第 21 章异步处理的并非 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的套接字,可以通过如下函数完成:

#include <winsock2.h>

SOCKET WSASocket(
	int af, 
	int type, 
	int protocol, 
	LPWSAPROTOCOL_INFO loProtocolInfo,
	GROUP g,
	DWORD dwFlags
);

参数:

  • af:协议族信息
  • type:套接字数据传输方式
  • protocol:2 个套接字之间使用的协议信息
  • lpProtocolInfo:包含创建的套接字信息的WSAPROTOCOL_INFO结构体变量地址值,不需要时传递 NULL。
  • g:为扩展函数而预约的参数,可以使用 0
  • dwFlags:套接字属性信息

成功时返回套接字句柄,失败时返回 INVALID_SOCKET。

各位对前 3 个参数比较熟悉,第四个和第五个参数与目前的工作无关,可以简单设置为 NULL 和 0。可以向最后一个参数传递 WSA_FLAG_OVERLAPPED,赋予创建出的套接字重叠 I/O 特性。

可以通过如下函数调用创建出可以进行重叠 I/O 的非阻塞模式的套接字。

WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

执行重叠 I/O 的 WSASend 函数

创建出具有重叠 I/O 属性的套接字后,接下来 2 个套接字(服务器端/客户端之间的)连接过程与一般的套接字连接过程相同,但 I/O 数据时使用的函数不同。

先介绍重叠 I/O 中使用的数据输出函数:

#include <winsock2.h>

int WSASend(
	SOCKET s,
	LPWSABUF lpBuffers,
	DWORD dwBufferCount,
	LPDWORD lpNumberOfBytesSent,
	DWORD dwFlags,
	LPWSAOVERLAPPED lpOverlapped,
	LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

参数:

  • s:套接字句柄,传递具有重叠 I/O 属性的套接字句柄时,以重叠 I/O 模型输出。
  • IpBuffers:WSABUF 结构体变量数组的地址值,WSABUF 中存有待传输数据。
  • dwBufferCount:第二个参数中数组的长度。
  • IpNumberOfBytesSent:用于保存实际发送字节数的变量地址值
  • dwFlags:用于便改数据传输特性,如传递 MSG_OOB 时发送 OOB 模式的数据。
  • IpOverlapped:WSAOVERLAPPED 结构体变量的地址值,使用事件对象,用于确认完成数据传输。
  • IpCompletionRoutine:传入 Completion Routine 函数的入口地址值,可以通过该函数确认是否完成数据传输。

成功时返回 0,失败时返回 SOCKET_ERROR。

接下来介绍上述函数的第二个结构体参数类型,该结构体中存有待传输数据的地址和大小等信息。

typedef struct __WSABUF
{
    u_long len; // 待传输数据的大小
    char FAR * buf; // 缓冲地址值
} WSABUF, *LPWSABUF;

利用上述函数和结构体,传输数据时可以按如下方式编写代码:

WSAEVENT event;
WSAOVERLAPPED overlapped;
WSABUF dataBuf;
char buf[BUF_SIZE] = {"待传输的数据"};
int revcBytes = 0;
......
event = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = event;
dataBuf.len = sizeof(buf);
dataBuf.buf = buf;
WSASend(hSocket, &dataBuf, 1, &recvBytes, 0, &overlapped, NULL);
......

调用 WSASend 函数时将第三个参数设置为 1,因为策二个参数中待传输数据的缓冲个数为 1。另外,多余参数均设置为 NULL 或 0,其中需要注意第六个和第七个参数。

第六个参数中的 WSAOVERLAPPED 结构体定义如下:

typedef struct _WSAOVERLAPPED
{ 
    DWORD Internal;
    DWORD InternalHigh;
    DWORD Offset;
    DWORD OffsetHigh;
    WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

Internal、InternalHigh 成员是进行重叠 I/O 时操作系统内部使用的成员,而 Offset、OffsetHigh 同样属于具有特殊用途的成员。所以各位实际只需要关注 hEvent 成员。

关于 WSAOVERLAPPED 结构体有 3 点需要注意:

  1. 为了进行重叠 I/O,WSASend 函数的 lpOverlapped 参数中应该传递有效的结构体变量地址值,而不是 NULL。
  2. 若向 lpOverlapped 传递 NULL,WSASend 函数的第一个参数中的句柄所指的套接字将以阻
    塞模式工作。
  3. 利用 WSASend 函教同时向多个目标传输数据时,需要分别构建传入第六个参数的 WSAOVERLAPPED 结构体变量。这是因为,进行重叠 I/O 的过程中,操作系统将使用 WSAOVERLAPPED 结构体变量。

WSASend 函数调用过程中,函数返回时间点和数据传输完成时间点并非总不一致。分为以下两种情况:

  • 如果输出缓冲是空的,且传输的数据并不大,那么函数调用后可以立即完成数据传输。此时,WSASend 函数将返回 0,lpNumberOfBytesSent 中将保存实际传输的数据大小的信息。
  • 反之,WSASend 函数返回后仍需要传输数据时,将返回 SOCKET_ERROR,并将 WSA_IO_PENDING 注册为错误代码,该代码可以通过 WSAGetLastError 函数(稍后再介绍)得到。这时应该通过如下函效获取实际传输的数据大小。
#include <winsock2.h>

BOOL WSAGetOverlappedResult(
	SOCKET s,
	LPWSAOVERLAPPED lpOverlapped,
	LPDWORD lpcbTransfer,
	BOOL fWait,
	LPDWORD lpdwFlags
);

参数:

  • s:进行重叠 I/O 的套接字句柄。
  • IpOverlapped:进行重叠 I/O 时传递的 WSAOVERLAPPED 结构体变量的地址值。
  • lpcbTransfer:用于保存实际传输的字节数的变量地址值。
  • fWait:如果调用该函数时仍在进行 I/O,fWait 为 TRUE 时等待 I/O 完成,fWait 为 FALSE 时将返回 FALSE 并跳出函数。
  • IpdwFlags:调用 WSARecv 函数时,用于获取附加信息(例如 OOB 消息)。如果不需要,可以传递 NULL。

成功时返回 TRUE,失败时返回 FALSE。

通过此函数不仅可以获取数据传输结果,还可以验证接收数据的状态。

进行重叠 I/O 的 WSARecv 函数

#include <winsock2.h>

int WSARecv(
	SOCKET s,
	LPWSABUF lpBuffers,
	DWORD dwBufferCount,
	LPDWORD lpNumberOfBytesRecvd,
	LPDWORD lpFlags,
	LPWSAOVERLAPPED lpOverlapped,
	LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

参数:

  • s:具有重叠 I/O 属性套接字句柄。
  • IpBuffers:用于保存接收数据的 WSABUF 结构体变量数组的地址值。
  • dwBufferCount:第二个参数中数组的长度。
  • lpNumberOfBytesRecvd:用于保存接收字节数的变量地址值。
  • lpFlags:用于设置或读取传输特性信息。
  • IpOverlapped:WSAOVERLAPPED 结构体变量地址值。
  • IpCompletionRoutine:Completion Routine 函数地址值。

成功时返回 0,失败时返回 SOCKET_ERROR。

Gather 输出指将多个缓冲中的数据累积到一定程度后一次性输出,Scatter 输入指将接收的数据分批保存。
重叠 I/O 的 WSASend 和 WSARecv 函数可以获得 writev & readv 函数的 Gather/Scatter I/O 功能。

重叠 I/O 的 I/O 完成确认

重叠 I/O 中有 2 种方法确认 I/O 的完成并获取结果。

  • 利用 WSASend、WSARecv 函数的第六个参数,基于事件对象。
  • 利用 WSASend、WSARecv 函数的第七个参数,基于 Completion Routine。

只有理解了这 2 种方法,才能算是掌握了重叠 I/O。首先介绍利用第六个参数的方法。

使用事件对象

直接给出示例。希望各位通过该示例验证如下 2 点:

  • 完成 I/O 时,WSAOVERLAPPED 结构体变量引用的事件对象将变为 signaled 状态。
  • 为了验证 I/O 的完成和完成结果,需要调用 WSAGetOvrlappedResult 函数。

发送端代码:

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

void ErrorHandling(char *msg);

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET hSocket;
    SOCKADDR_IN sendAdr;

    WSABUF dataBuf;
    char msg[] = "Network is Computer!";
    DWORD sendBytes = 0;

    WSAEVENT evObj;
    WSAOVERLAPPED overlapped;

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port> \n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error");

    hSocket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    memset(&sendAdr, 0, sizeof(sendAdr));
    sendAdr.sin_family = AF_INET;
    sendAdr.sin_addr.s_addr = inet_addr(argv[1]);
    sendAdr.sin_port = htons(atoi(argv[2]));

    if (connect(hSocket, (SOCKADDR *)&sendAdr, sizeof(sendAdr)) == SOCKET_ERROR)
        ErrorHandling("connect() error");

    evObj = WSACreateEvent();
    memset(&overlapped, 0, sizeof(overlapped));
    overlapped.hEvent = evObj;
    dataBuf.len = strlen(msg) + 1;
    dataBuf.buf = msg;

    if (WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL) == SOCKET_ERROR)
    {
        if (WSAGetLastError() == WSA_IO_PENDING)
        {
            puts("Background data send");
            WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
            WSAGetOverlappedResult(hSocket, &overlapped, &sendBytes, FALSE, NULL);
        }
        else
        {
            ErrorHandling("WSASend() error");
        }
    }

    printf("Send data size: %d \n", sendBytes);

    WSACloseEvent(evObj);
    closesocket(hSocket);
    WSACleanup();
    return 0;
}

void ErrorHandling(char *msg)
{
    fputs(msg, stderr);
    fputc('\n', stderr);
    exit(1);
}

上述示例调用的 WSAGetLastError 函数定义如下。调用套接字相关函数后,可以通过该函数获取错误信息。

#include<winsock2.h>

int WSAGetLastError(void); // 返回错误代码(表示错误原因)

上述示例中该函数的返回值为 WSA_IO_PENDING,由此可以判断 WSASend 函数的调用结果并非发生了错误,而是尚未完成的状态。

下面介绍与上述示例配套使用的接收端代码:

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 1024

void ErrorHandling(char *msg);

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET hLisnSock, hRecvSock;
    SOCKADDR_IN lisnAdr, recvAdr;
    int recvAdrSz;

    WSABUF dataBuf;
    WSAEVENT evObj;
    WSAOVERLAPPED overlapped;

    char buf[BUF_SIZE];
    DWORD recvBytes = 0, flags = 0;

    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);
    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);
    hRecvSock = accept(hLisnSock, (SOCKADDR *)&recvAdr, &recvAdrSz);

    evObj = WSACreateEvent();
    memset(&overlapped, 0, sizeof(overlapped));
    overlapped.hEvent = evObj;
    dataBuf.len = BUF_SIZE;
    dataBuf.buf = buf;

    if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
    {
        if (WSAGetLastError() == WSA_IO_PENDING)
        {
            puts("Background data receive");
            WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
            WSAGetOverlappedResult(hRecvSock, &overlapped, &recvBytes, FALSE, NULL);
        }
        else
        {
            ErrorHandling("WSARecv() error");
        }
    }

    printf("Received message: %s \n", buf);
    WSACloseEvent(evObj);
    closesocket(hRecvSock);
    closesocket(hLisnSock);
    WSACleanup();
    return 0;
}

void ErrorHandling(char *msg)
{
    fputs(msg, stderr);
    fputc('\n', stderr);
    exit(1);
}

编译:

gcc OverlappedSend_win.c -lws2_32 -o overlappedSend
gcc OverlappedRecv_win.c -lws2_32 -o overlappedRecv

运行结果:

在这里插入图片描述

使用 Completion Routine 函数

前面的示例通过事件对象验证了 I/O 完成与否,下面介绍如何通过 WSASend、WSARecv 函数的最后一个参数中指定的 Completion Routine 函数验证 I/O 完成情况。

注册 Completion Routine 函数的含义:Pending 的 I/O 完成时调用此函数。I/O 完成时调用注册过的函数进行事后处理,这就是 Completion Routine 的运作方式。

然而,如果执行重要任务时突然调用 Completion Routine 函数,有可能破坏程序的正常执行流。因此,操作系统通常会预先定义规则:只有请求 I/O 的线程处于 alertable wait 状态时才能调用 Completion Routine 函数。alertable wait 状态是等待接收操作系统消息的线程状态,调用下列函数时进入该状态:

  • WaitForSingleObjectEx
  • WaitForMultipleObjectsEx
  • WSAWaitForMultipleEvents
  • SleepEx

第一、第二、第四个函数提供的功能与 WaitForSingleObject、WaitForMultipleObjects、Sleep 函数相同。上述函数只增加了 1 个参数,如果该参数为 TRUE,则相应线程将进入 alertable wait 状态。另外,第 21 章介绍过以 WSA 为前缀的函数,该函数的最后一个参数设置为 TRUE 时,线程同样 alertable wait 状态。因此,启动 I/O 任务后,执行完紧急任务时可以调用上述任一函数验证 I/O 完成与否。此时操作系统知道线程进入 alertable wait 状态,如果有已完成的 I/O,则调用相应 Completion Routine 函数。 调用后,上述函数将全部返回 WAIT_IO_COMPLETION 并开始执行接下来的程序。

下面将之前的接收端代码改为 Completion Routine 方式。

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 1024

void CALLBACK CompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *msg);

WSABUF dataBuf;
char buf[BUF_SIZE];
DWORD recvBytes = 0, flags = 0;

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET hLisnSock, hRecvSock;
    SOCKADDR_IN lisnAdr, recvAdr;

    WSAOVERLAPPED overlapped;
    WSAEVENT evObj;

    int idx, 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);
    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);
    hRecvSock = accept(hLisnSock, (SOCKADDR *)&recvAdr, &recvAdrSz);
    if (hRecvSock == INVALID_SOCKET)
        ErrorHandling("accept() error");

    memset(&overlapped, 0, sizeof(overlapped));
    dataBuf.len = BUF_SIZE;
    dataBuf.buf = buf;
    evObj = WSACreateEvent(); // 没什么用的事件对象

    if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, CompRoutine) == SOCKET_ERROR)
    {
        if (WSAGetLastError() == WSA_IO_PENDING)
        {
            puts("Background data receive");
        }
    }

    idx = WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE);
    if (idx == WAIT_IO_COMPLETION)
        puts("Overlapped I/O Completed");
    else
        ErrorHandling("WSARecv() error");
    WSACloseEvent(evObj);
    closesocket(hRecvSock);
    closesocket(hLisnSock);
    WSACleanup();
    return 0;
}

void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
    if (dwError != 0)
        ErrorHandling("CompRoutine error");
    else
    {
        recvBytes = szRecvBytes;
        printf("Received message: %s \n", buf);
    }
}

void ErrorHandling(char *msg)
{
    fputs(msg, stderr);
    fputc('\n', stderr);
    exit(1);
}

编译:

gcc CmplRoutinesRecv_win.c -lws2_32 -o cmplRoutinesRecv

运行结果:

在这里插入图片描述

下面给出传入 WSARecv 函数的最后一个参数的 Completion Routine 函数原型。

void CALLBACK CompletionRoutine(
  DWORD        dwError,
  DWORD        cbTransferred,
  LPWSAOVERLAPPED lpOverlapped,
  DWORD        dwFlags
);

其中第一个参数中写入错误信息(正常结束时写入 0),第二个参数中写入实际收发的字节数。第三个参数中写入 WSASend、WSARecv 函数的参数 IpOverlapped,dwFlags 中写入调用 I/O 函数时传入的特性信息或 0。

另外,返回值类型 void 后插入的 CALLBACK 关键字与 main 函数中声明的关键字 WINAPI 相同,都是声明函数的调用规范,所以定义 Completion Routine 函数时必须添加。

习题

(1)异步通知 I/O 模型与重叠 I/O 模型在异步处理方面有哪些区别?

特性异步通知 I/O 模型重叠 I/O 模型
概念应用程序发起 I/O 请求后,不必阻塞等待操作完成,而是由操作系统在完成时发出通知。通知可以通过信号、回调函数、事件、状态标志等方式传递给应用程序程序调用 I/O 函数时会传入一个专门的数据结构(即 OVERLAPPED 结构),系统则在后台开始执行 I/O 操作,并立即返回。应用程序可以稍后查询操作的状态,也可以借助事件句柄来获得完成通知
平台Unix/LinuxWindows
核心机制事件轮询、信号、回调OVERLAPPED 结构体
典型应用Nginx、RedisIIS、Windows 高性能服务器

总之,异步通知 I/O 模型异步处理的并非 I/O,而是“通知”。而重叠 I/O 模型才是真正异步方式处理 I/O 的方法,通过非阻塞异步 I/O 函数和确认 I/O 完成状态的方法完成。

(2)请分析非阻塞 I/O、通知 I/O、重叠 I/O 之间的关系。

三者可以理解为从低级到高级的演进关系,但实现机制不同。

非阻塞 I/O 是一种简单的模式,它使得 I/O 调用在数据暂未就绪时立即返回,并由应用层主动轮询检测。

通知 I/O 则是在非阻塞基础上,通过信号、事件或回调的方式让内核主动告知应用程序数据就绪,从而避免了频繁的轮询。

重叠 I/O 是 Windows 特有的异步 I/O 实现方式,它通过 OVERLAPPED 结构以及多种内核通知机制(包括 IOCP、完成例程或事件对象)使得应用能够真正实现 I/O 与处理并行,从而大幅提高应用的并发性能和扩展性。

总的来说,这三种模型都是为了解决传统阻塞 I/O 带来的线程挂起与低 CPU 利用率的问题,从实现方式上可以看出:

  • 非阻塞 I/O 仅改变调用返回方式;

  • 通知 I/O 则把状态变化通过信号或事件反馈给应用;

  • 重叠 I/O 则提供了完整的异步 I/O 框架,充分释放应用线程,使得 I/O 操作与计算能并行进行。

(3)阅读如下代码,请指出问题并给出解决方案。

while (1)
{
    hRecvSock = accept(hLisnSock, (SOCKADDR *)&recvAdr, &recvAdrSz);
    evObj = WSACreateEvent();
    memset(&overlapped, 0, sizeof(overlapped));
    overlapped.hEvent = evObj;
    dataBuf.len = BUF_SIZE;
    dataBuf.buf = buf;
    WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL);
}

虽然不完整,但足以通过这部分代码片段发现结构上存在的问题。

答:

代码存在的问题:

  1. 代码中通过传入 OVERLAPPED 结构调用了 WSARecv(),这表明是在发起一个异步接收操作。然而调用后没有调用 WSAWaitForMultipleEvents()、GetOverlappedResult() 或使用 I/O 完成端口(IOCP)等机制来等待或处理异步操作的完成,导致异步操作的结果“无人问津”。如果不等待或处理完成通知,可能会出现数据还未拷贝完毕就关闭连接或继续下一轮循环的问题。
  2. 每次循环中都调用 WSACreateEvent() 创建一个新事件句柄,但代码中没有相应的关闭(如调用 WSACloseEvent())操作。这会导致事件句柄不断累积,最终耗尽系统资源。
  3. 代码未对 accept、WSACreateEvent、以及 WSARecv 的返回值进行检查。如果出现错误,程序无法及时捕捉并做出处理。此外,接受到一个新连接后,也没有对 hRecvSock 做进一步的管理,如关闭连接或为每个连接启动独立的处理线程/机制;这种设计会导致后续连接的管理混乱。
  4. 同一个 buf 缓冲区、OVERLAPPED 结构以及事件句柄可能被重复使用而没有独立跟踪每个连接的状态。如果主循环中直接重复调用这一系列异步操作,而不区分每个连接的操作完成状态,则会造成数据覆盖和状态混乱问题。

改进方案:

  1. 为每个新 Socket (hRecvSock) 和事件对象 (evObj) 分配独立内存(如结构体),并在操作完成后释放。
  2. 使用 closesocket 关闭 Socket,WSACloseEvent 关闭事件对象。

完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 1024

void ErrorHandling(char *msg);

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    int iResult;

    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
    // 初始化 Winsock
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");

    // 创建监听 socket
    SOCKET hLisnSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (hLisnSock == INVALID_SOCKET)
    {
        printf("socket failed: %d\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    // 绑定地址
    struct sockaddr_in servAddr;
    ZeroMemory(&servAddr, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = INADDR_ANY;
    servAddr.sin_port = htons(atoi(argv[1]));

    if (bind(hLisnSock, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
        ErrorHandling("bind() error");
    if (listen(hLisnSock, 5) == SOCKET_ERROR)
        ErrorHandling("listen() error");

    while (1)
    {
        SOCKET hRecvSock;
        struct sockaddr_in recvAdr;
        int recvAdrSz = sizeof(recvAdr);

        // 接受新的连接
        hRecvSock = accept(hLisnSock, (struct sockaddr *)&recvAdr, &recvAdrSz);
        if (hRecvSock == INVALID_SOCKET)
        {
            printf("accept failed: %d\n", WSAGetLastError());
            continue; // 出错时跳过本次循环
        }
        printf("Accepted connection from %s:%d\n", inet_ntoa(recvAdr.sin_addr), ntohs(recvAdr.sin_port));

        // 为当前连接创建一个事件对象用于异步操作
        WSAEVENT evObj = WSACreateEvent();
        if (evObj == WSA_INVALID_EVENT)
        {
            printf("WSACreateEvent failed: %d\n", WSAGetLastError());
            closesocket(hRecvSock);
            continue;
        }

        // 初始化 OVERLAPPED 结构
        OVERLAPPED overlapped;
        ZeroMemory(&overlapped, sizeof(overlapped));
        overlapped.hEvent = evObj;

        // 设置接收缓冲区并发起异步接收
        char buf[BUF_SIZE];
        WSABUF dataBuf;
        dataBuf.len = BUF_SIZE;
        dataBuf.buf = buf;

        DWORD recvBytes = 0, flags = 0;

        if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
        {
            int err = WSAGetLastError();
            if (err != WSA_IO_PENDING)
            {
                printf("WSARecv failed immediately: %d\n", err);
                WSACloseEvent(evObj);
                closesocket(hRecvSock);
                continue;
            }
            // 异步操作已挂起,等待其完成
            DWORD dwWait = WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, TRUE);
            if (dwWait == WSA_WAIT_FAILED)
            {
                printf("WSAWaitForMultipleEvents failed: %d\n", WSAGetLastError());
                WSACloseEvent(evObj);
                closesocket(hRecvSock);
                continue;
            }
            // 操作已完成,获取结果
            BOOL bRet = WSAGetOverlappedResult(hRecvSock, &overlapped, &recvBytes, FALSE, &flags);
            if (!bRet)
            {
                printf("WSAGetOverlappedResult failed: %d\n", WSAGetLastError());
                WSACloseEvent(evObj);
                closesocket(hRecvSock);
                continue;
            }
        }
        // 如果 WSARecv 返回非错误,则操作可能已经同步完成
        printf("Received %d bytes: %.*s\n", recvBytes, recvBytes, buf);

        // (此处可对接收到的数据进行处理,例如回显数据)

        // 清理当前连接相关资源
        WSACloseEvent(evObj);
        closesocket(hRecvSock);
    }

    // 通常不会运行到这里
    closesocket(hLisnSock);
    WSACleanup();
    return 0;
}

void ErrorHandling(char *msg)
{
    fputs(msg, stderr);
    fputc('\n', stderr);
    exit(1);
}

(4)请从源代码角度说明调用 WSASend 函数后如何验证 I/O 是否进入 Pending 状态。

WSASend 函数允许调用者在发起重叠 I/O 操作后立即返回,而不必等待操作完成。其典型调用过程通常如下:

  1. 准备一个 OVERLAPPED 结构(通常将其中的 hEvent 字段设置成一个事件句柄,以便后续等待)。
  2. 调用 WSASend 进行数据发送。此时有两种情况:
  3. 如果 WSAGetLastError 函数的返回值为 WSA_IO_PENDING,这表示操作已成功发起,但尚未完成,处于 Pending 状态;此时 I/O 操作将在后台执行,稍后会通过事件、回调或完成端口等机制通知调用者操作完成。

WSAGetLastError 函数原型:

#include<winsock2.h>

int WSAGetLastError(void); // 返回错误代码(表示错误原因)

示例代码:

DWORD sendBytes = 0;
DWORD flags = 0;
OVERLAPPED ol;
ZeroMemory(&ol, sizeof(ol));
// 可选:设置 ol.hEvent 为一个事件句柄,如果希望等待操作完成
ol.hEvent = hEvent;  // hEvent 已通过 WSACreateEvent 创建

// 调用 WSASend 发送数据
int ret = WSASend(hSocket, &dataBuf, 1, &sendBytes, flags, &ol, NULL);
if (ret == SOCKET_ERROR) {
    int err = WSAGetLastError();
    if (err == WSA_IO_PENDING) {
        // 说明 I/O 操作已进入 Pending 状态,WSASend 发起的异步发送操作正在后台执行
        // 此时应用程序可以继续做其他事,稍后使用WSAWaitForMultipleEvents或者
        // WSAGetOverlappedResult来等待并获得操作完成结果
    } else {
        // 其它错误,处理错误情况
    }
} else {
    // ret==0, 表示操作同步完成,此时 sendBytes 表示实际发送的字节数
}

(5)线程的“alertable wait 状态”的含义是什么?说出能使线程进入这种状态的 2 个函数。

alertable wait 状态是线程在等待一个或多个内核对象(比如事件、互斥体、信号量等)的同时处于可被异步过程调用中断和执行的状态。只有请求 I/O 的线程处于 alertable wait 状态时才能调用 Completion Routine 函数。

调用下列函数时进入该状态:

  • WaitForSingleObjectEx
  • WaitForMultipleObjectsEx
  • WSAWaitForMultipleEvents
  • SleepEx
内容概要:本文系统研究了双环模型预测控制(MPC)在表贴式永磁同步电机(SPMSM)中的应用,聚焦于转速-电流双环控制结构的建模与Simulink仿真实现。通过建立电机的离散化数学模型,结合模型预测控制理论,详细阐述了预测模型构建、目标函数设计、约束条件处理及优化求解等核心环节,实现了对电机转速与电流的高性能动态调控。研究在Simulink环境中搭建了完整的仿真系统,验证了所提控制策略在动态响应速度、抗干扰能力及稳态精度方面的显著优势,充分展现了MPC在高精度电机驱动领域的应用潜力,为先进电机控制技术的工程化提供了有效的理论依据与实践参考。; 适合人群:具备自动控制理论、电机控制基础知识及Simulink仿真操作经验的电气工程、自动化、电力电子等相关专业的研究生、科研人员和工程技术人员。; 使用场景及目标:①用于高校及科研机构开展先进电机控制算法的教学演示与科研攻关;②为工业界中对高动态性能、高精度要求的电机驱动系统(如数控机床、机器人、新能源汽车电驱动系统)的设计与优化提供技术验证平台;③支撑永磁同步电机在高端制造、绿色能源等战略新兴产业中的先进控制技术研发。; 阅读建议:读者应结合提供的Simulink仿真模型进行深入探究,重点关注预测时域、控制时域、权重系数等关键参数的整定方法及其对系统整体性能的影响机制,建议通过设置不同工况、引入外部扰动等方式进行对比仿真实验,以深化对模型预测控制内在机理的理解与掌握。
内容概要:本文围绕“基于多VSG独立微网的多目标二次控制MATLAB模型研究”展开,详细阐述了利用Simulink对多虚拟同步发电机(VSG)构成的独立微网系统进行建模与仿真,实现频率调节、电压支撑与有功无功功率均分等多目标协同优化的二次控制策略。研究引入先进的最优控制算法,解决微网在孤岛运行模式下的功率动态分配、频率电压恢复及系统稳定性问题,并通过MATLAB/Simulink平台构建完整仿真模型,验证所提控制策略在不同负载扰动下的有效性、鲁棒性与动态响应性能。; 适合人群:具备电力系统分析、现代控制理论基础以及MATLAB/Simulink仿真能力的电气工程、自动化等相关专业的硕士研究生、科研人员及从事微网控制系统开发的工程技术人才。; 使用场景及目标:① 深入理解多VSG在独立微网中的并联运行机理与协同控制架构;② 掌握基于Simulink的微网二次控制系统的建模方法与仿真流程;③ 实现频率、电压与功率分配的多目标优化控制仿真验证;④ 为微网控制系统的设计、算法优化及科研课题提供可靠的仿真依据和技术参考。; 阅读建议:建议读者结合文中控制策略,动手搭建Simulink模型,重点关注控制器参数整定对系统动态性能的影响,可通过对比不同工况下的仿真结果,进一步优化控制算法以提升系统鲁棒性与响应精度。
【重要提示】本资源设置为0积分下载,若非0积分请勿轻易下载 亲爱的CSDN用户: 首先感谢你点进这个资源页面。我需要提前说明一个重要情况: 本资源原本已设置为“0积分下载”,即作者希望完全免费共享。但CSDN平台有时会根据文件的下载热度、文件大小、用户权限等因素,自动将部分资源的积分调整为非0数值(如1积分、2积分、5积分等)。这是平台系统的自动行为,而非作者本人的设定。 因此,如果你当前看到该资源的下载所需积分不是0(例如显示为1、2、3&hellip;&hellip;),请谨慎决定是否下载。 如果你按照非0积分支付并下载后发现资源内容不符合预期、链接失效,或者实际上该资源本应是免费的,作者无法为此承担积分损失或退还操作。强烈建议:仅在页面显示为0积分时进行下载。 另外,本资源描述中并未直接提供具体的下载地址或外部链接,因为它本身是一个通过CSDN官方上传通道提交的文件/内容包。如果你看到描述中没有外部网盘地址,这是正常的——资源文件应通过CSDN内置的“下载”按钮获取。若因平台积分显示异常导致你支付了积分,请优先联系CSDN客服咨询积分退还政策,作者没有权限修改平台自动设定的积分值。 感谢你的理解与支持。技术分享本应开放,但受限于平台规则,特此提醒如上。祝学习进步!
【重要提示】本资源设置为0积分下载,若非0积分请勿轻易下载 亲爱的CSDN用户: 首先感谢你点进这个资源页面。我需要提前说明一个重要情况: 本资源原本已设置为“0积分下载”,即作者希望完全免费共享。但CSDN平台有时会根据文件的下载热度、文件大小、用户权限等因素,自动将部分资源的积分调整为非0数值(如1积分、2积分、5积分等)。这是平台系统的自动行为,而非作者本人的设定。 因此,如果你当前看到该资源的下载所需积分不是0(例如显示为1、2、3&hellip;&hellip;),请谨慎决定是否下载。 如果你按照非0积分支付并下载后发现资源内容不符合预期、链接失效,或者实际上该资源本应是免费的,作者无法为此承担积分损失或退还操作。强烈建议:仅在页面显示为0积分时进行下载。 另外,本资源描述中并未直接提供具体的下载地址或外部链接,因为它本身是一个通过CSDN官方上传通道提交的文件/内容包。如果你看到描述中没有外部网盘地址,这是正常的——资源文件应通过CSDN内置的“下载”按钮获取。若因平台积分显示异常导致你支付了积分,请优先联系CSDN客服咨询积分退还政策,作者没有权限修改平台自动设定的积分值。 感谢你的理解与支持。技术分享本应开放,但受限于平台规则,特此提醒如上。祝学习进步!
代码下载地址: https://pan.quark.cn/s/a4b39357ea24 Git在全球范围内被公认为最为流行的分布式版本控制系统,其在软件开发行业中占据着不可或缺的地位。Git-2.21.0-64-bit 以及 TortoiseGit-2.8.0.0-64bit 是两款专门为Windows操作系统设计的Git相关软件。Git-2.21.0-64-bit 代表了Git的命令行版本,而TortoiseGit则是一个图形化界面工具,它为用户呈现了一种更为直观的操作体验。 Git的主要优势体现在其分布式架构上。每一个通过Git克隆得到的仓库都是一个自给自足的、完整的文件库,其中包含了所有的历史版本记录以及修订追踪详情。因此,即便在缺乏网络连接的环境下,开发者依然能够在本地执行版本控制任务,例如进行提交、切换分支以及合并代码等操作。这种架构设计显著提升了开发效率,特别是在处理大型项目或进行团队协作时更为明显。 Git的分支管理功能是其另一项突出的能力。开发者借助简单的指令即可迅速完成分支的创建、切换和合并,这一特性对于并行开发、试验新功能或解决bug等问题提供了极大的便利。例如,开发者可以开辟一个新分支来实施新功能,在开发完成后将其整合回主分支,而不会对其他团队成员的工作造成干扰。 TortoiseGit是Git的一个补充工具,它将Git的操作指令无缝嵌入到Windows资源管理器中,使得Git的使用体验类似于常规的文件管理操作。TortoiseGit-2.8.0.0-64bit.msi 文件正是这个图形化界面的安装包,它提供了右键菜单的快捷方式,让用户能够更加便捷地进行版本控制活动。与此同时,TortoiseGit-LanguagePack-2.8.0.0...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值