《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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

UestcXiye

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

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

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

打赏作者

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

抵扣说明:

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

余额充值