当前位置: 首页 > article >正文

《TCP/IP网络编程》学习笔记 | Chapter 21:异步通知 I/O 模型

《TCP/IP网络编程》学习笔记 | Chapter 21:异步通知 I/O 模型

  • 《TCP/IP网络编程》学习笔记 | Chapter 21:异步通知 I/O 模型
    • 同步与异步
      • 同步
      • 异步
      • 对比
      • 同步 I/O 的缺点
      • 异步 I/O 的优点
    • 理解异步通知 I/O 模型
    • 实现异步通知 I/O 模型
      • WSAEventSelect 函数和通知
      • manual-reset 模式事件的其他创建方法
      • 验证是否发生事件
      • 区分事件类型
    • 利用异步通知 I/O 模型实现回声服务器端
    • 习题
      • (1)结合 send & recv 函数解释同步和异步方式的 I/O。并请说明同步 I/O 的缺点,以及怎样通过异步 I/O 进行解决。
      • (2)异步 I/O 并不是所有情况下的最佳选择。它具有哪些缺点?何种情况下同步 I/O 更优?可以参考异步 I/O 相关源代码,亦可结合线程进行说明。
      • (3)判断下列关于 select 模型描述的正误。
      • (4)请从源代码的角度说明 select 函数和 WSAEventSelect 函数在使用上的差异。
      • (5)第 17 章的 epoll 可以在条件触发和边缘触发这 2 种方式下工作。哪种方式更适合异步 I/O 模型?为什么?请概括说明。
      • (6)Linux 中的 epoll 同样属于异步 I/O 模型。请说明原因。
      • (7)如何获取 WSAWaitForMultipleEvents 函数可以监视的最大句柄数?请编写代码读取该值。
      • (8)为何异步通知 I/O 模型中的事件对象必须是 manual-reset 模式?
      • (9)请在本章的通知 I/O 模型的基础上编写聊天服务器端。要求该服务器端能够结合第 20 章的聊天客户端 chat_clnt_win.c 运行。

《TCP/IP网络编程》学习笔记 | Chapter 21:异步通知 I/O 模型

同步与异步

同步

同步指的是任务按照顺序执行,一个任务必须等待前一个任务完成后才能继续。换句话说,在进行同步操作时,程序会阻塞在某个操作上,直到该操作完成后才会继续往下执行。

同步方式的数据 I/O:

在这里插入图片描述

可以通过下图解析上述两句话的含义:

在这里插入图片描述

异步

异步是指任务并行执行,程序发起一个操作后不等待其完成,而是继续执行其他任务。程序可以在等待操作完成的同时执行其他操作,最终通过回调、事件、信号等方式获取操作结果。

异步 I/O 是指 I/O 函数的返回时刻与数据接收的完成时刻不一致。

在这里插入图片描述

对比

在这里插入图片描述

同步 I/O 的缺点

进行 I/O 的过程中函数无法返回,所以不能执行其他任务。

异步 I/O 的优点

无论数据是否完成交换都返回函数,这就意味着可以执行其他任务。

异步方式能够比同步方式更有效使用 CPU。

理解异步通知 I/O 模型

通知 I/O 的含义:

在这里插入图片描述

顾名思义,通知 I/O 是指发生了I/O相关的特定情况。典型的通知 I/O 模型是 select 方式,但这种通知是以同步方式进行的,原因在于,需要 I/O 或可以进行 I/O 的时间点(简言之就是 I/O 相关事件发生的时间点)与 select 函数的返回时间点一致。

异步通知 I/O 模型意为通知 I/O 是以异步方式工作的。与“select 函数只在需要或可以进行 I/O 的情况下返回”不同,异步通知 I/O 模型中函数的返回与 I/O 状态无关。

本章的 WSAEventSelect 函数就是 select 函数的差异版本。

可能有人疑问:“既然函数的返回与I/O状态无关,那是否需要监视 I/O 状态变化?”

当然需要!异步通知 I/O 中,指定 I/O 监视对象的函数和实际验证状态变化的函数是相互分离的。因此,指定监视对象后可以离开执行其他任务,最后再回来验证状态变化。

实现异步通知 I/O 模型

异步通知 I/O 模型的实现方法有 2 种:一种是稍后介绍的 WSAEventSelect 函数,第二种是使用 WSAAsyncSelect 函数,第二种方法是 UI 相关内容,不进行介绍,需要了解可自行查阅资料。

WSAEventSelect 函数和通知

如前所述,告知 I/O 状态变化的操作就是“通知”。I/O的状态变化可以分为不同情况:

  • 套接字的状态变化:套接字的I/O状态变化。
  • 发生套接字相关事件:发生套接字I/O相关事件。

这 2 种情况都意味着发生了需要或可以进行 I/O 的事件,我将根据上下文适当混用这些概念。

WSAEventSelect 是 Windows Sockets API(Winsock)中的一个函数,用于将一个 Windows 事件对象与一个套接字(socket)关联,以便在套接字状态发生变化时获得通知。这个函数常用于异步网络编程,特别是在处理多个套接字时。

#include<winsock2.h>int WSAEventSelect(SOCKET s,HANDLE hEvent,long lNetworkEvents
);

参数:

  • s:监视对象的套接字句柄。
  • hEventObject:传递事件对象句柄以验证事件发生与否。
  • INetworkEvents:希望监视的事件类型信息。

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

传入参数 s 的套接字内只要发生 INetworkEvents 中指定的事件之一,WSAEventSelect 函数就将 hEventObject 句柄所指内核对象改为 signaled 状态。因此,该函数又称“连接事件对象和套接字的函数”。该函数以异步通知方式工作。无论事件发生与否,WSAEventSelect 函数调用后都会直接返回。

下面介绍作为该函数第三个参数的事件类型信息,可以通过位或运算同时指定多个信息。

  • FD_READ:是否存在需要接收的数据?
  • FD_WRITE:能否以非阻塞方式传输数据?
  • FD_OOB:是否收到带外数据?
  • FD_ACCEPT:是否有新的连接请求?
  • FD_CLOSE:是否有断开连接的请求?

以上就是 WSAEventSelect 函数的调用方法。

仅从概念上看,WSAEventSelect函数 的功能偏弱。但使用该函数时,没必要针对多个套接字进行调用。从 select 函数返回时,为了验证事件的发生需要再次针对所有句柄调用函数,但通过调用 WSAEventSelect 函数传递的套接字信息已注册到操作系统,所以无需再次调用。这反而是 WSAEventSelect 函数比 select 函数的优势所在。

从函数说明中可以看出,我们还需要知道以下内容:

  1. WSAEventSelect 函数的第二个参数中用到的事件对象的创建方法。
  2. 调用 WSAEventSelect 函数后发生事件的验证方法。
  3. 验证事件发生后事件类型的查看方法。

manual-reset 模式事件的其他创建方法

之前创建事件对象是利用 CreateEvent 函数。 CreateEvent 函数在创建事件对象时,可以在 auto-reset 模式和 manual-reset 模式中任选其一。但是我们只需要 manual-reset 模式 non-signaled 状态的事件对象,所以利用下面的函数创建比较方便。

#include <winsock2.h>WSAEVENT WSACreateEvent(void);

成功时返回事件对象句柄,失败时返回 WSA_INVALID_EVENT。

上述声明中返回类型 WSAEVENT 的定义如下:

#define WSAEVENT HANDLE

实际上就是我们熟悉的内核对象句柄。

另外,可使用如下函数销毁上述函数创建的事件对象:

#include <winsock2.h>BOOL WSACloseEvent(WSAEVENT hEvent);

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

验证是否发生事件

为了验证是否发生事件,需要查看事件对象。完成该任务的函数如下,除了多 1 个参数外,其余部分与 WaitForMultipleObjects 函数完全相同。

#include <winsock2.h>DWORD WSAWaitForMultipleEvents(DWORD cEvents,                // 要等待的事件对象数量const WSAEVENT* lphEvents,    // 指向事件对象句柄数组的指针BOOL fWaitAll,                // 指定是等待所有事件还是任何一个事件DWORD dwTimeout,              // 超时时间(毫秒)BOOL fAlertable               // 指定等待期间是否允许进入警报状态(APC)
);

参数:

  • cEvents:需要验证是否转为 signaled 状态的事件对象的个数。
  • IphEvents:存有事件对象句柄数组地址值。
  • fWaitAll:传递 TRUE 时,所有事件对象在 signaled 状态时返回;传递 FALSE 时,只要其中 1 个变为 signaled 状态就返回。
  • dwTimeout:以 1ms 为单位指定超时,传递 WSA_INFINITE 时,直到变为 signaled 状态时才会返回。
  • fAlertable:传递 TRUE 时进入 alertable_wait(可警告等待)状态(第 22 章)。

返回值减去常量 WSA_WAIT_EVENT_0 时,可以得到转变为 signaled 状态的事件对象句柄对应的索引,可以通过该索引在第二个参数指定的数组中查找句柄。如果有多个事件对象变为 signaled 状态,则会得到其中较小的值。发生超时将返回 WSA_WAIT_TIMEOUT。

可以通过以宏的方式声明的 WSA_MAXIMUM_WAIT_EVENTS 常量得知 WSAWaitForMultipleEvents 函数可以同时监视的最大事件对象数。该常量值为 64。

只通过 1 次函数调用无法得到转为 signaled 状态的所有事件对象句柄的信息。通过该函数可以得到转为 signaled 状态的事件对象中的第一个(按数组中的保存顺序)索引值。但可以利用 “事件对象为 manual-reset 模式” 的特点,通过如下方式获得所有 signaled 状态的事件对象。

int posInfo, startIdx, i;
...
// 等待 hEventArray 中的任意一个事件对象变为 signaled 状态
posInfo = WSAWaitForMultipleEvents(numOfSock, hEventarray, FALSE, WSA_INFINITE, FALSE);
// startIdx 是当前进入 signaled 状态的事件在 hEventArray 数组中的索引
startIdx = posInfo - WSA_WAIT_EVENT_0;
...
// 从第一个 signaled 的事件开始,遍历剩余的事件,检查它们是否已经被触发
for(i = startIdx; i < numOfSock; i++)
{// 仅检查 hEventArray[i] 这个单个事件对象是否被触发。由于超时时间为 0,它不会等待,只会立即返回结果int sigEventIdx = WSAWaitForMultipleEvents(1, &hEventArray[i], TRUE, 0, FALSE);...// 如果这个事件已经 signaled,则 sigEventIdx 返回 WSA_WAIT_EVENT_0// 否则返回 WSA_WAIT_TIMEOUT 或 WSA_WAIT_FAILED
}

之所以能做到这一点,完全是因为事件对象为 manual-reset 模式,这也解释了为何在异步通知 I/O 模型中事件对象必须为 manual-reset 模式。

区分事件类型

既然已经通过WSAWaitForMultipleEvents函数得到了转为signaled状态的事件对象,最后就要确定相应对象进入signaled状态的原因。为完成该任务,我们引入如下函数。调用此函数时,不仅需要signaled状态的事件对象句柄,还需要与之连接的(由WSAEventSelect函数调用引发的)发生的套接字句柄。

#include <winsock2.h>// 
int WSAEnumNetworkEvents(SOCKET s,                           // 套接字句柄WSAEVENT hEventObject,              // 可选的事件对象句柄LPWSANETWORKEVENTS lpNetworkEvents  // 指向 WSANETWORKEVENTS 结构的指针
);

参数:

  • s:发生事件的套接字句柄。该套接字应该与某个事件对象通过 WSAEventSelect 函数关联。
  • hEventObject:一个可选的事件对象句柄。如果这个参数非空,WSAEnumNetworkEvents 函数会自动将这个事件对象的状态重置为 non-signaled 状态。 如果传递的是 NULL,那么该函数仅仅返回网络事件信息,而不重置任何事件。
  • lpNetworkEvents:指向 WSANETWORKEVENTS 结构的指针,用于接收套接字的事件信息。此结构包含了发生的网络事件及其对应的错误码。

WSANETWORKEVENTS 结构体:

typedef struct _WSANETWORKEVENTS
{long lNetworkEvents;  // 事件的掩码(可以是多个事件的组合)int iErrorCode[FD_MAX_EVENTS];  // 每个事件对应的错误码
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

上述结构体的 lNetworkEvents 成员将保存发生的事件信息,与 WSAEventSelect 函数的第三个参数相同,需要接收数据时,该成员为 FD_READ;有连接请求时,该成员为 FD_ACCEPT。因此,可通过如下方式查看发生的事件类型:

WSANETWORKEVENTS netEvents;
...
WSAEnumNetworkEvents(hSock, hEvent, &netEvents);
if(netEvents.lNetworkEvents & FD_ACCEPT)
{// FD_ACCEPT 事件的处理
}if(netEvents.lNetworkEvents & FD_READ)
{// FD_READ 事件的处理
}if(netEvents.lNetworkEvents & FD_CLOSE)
{// FD_CLOSE 事件的处理
}

另外,错误信息将保存到声明为成员的 iErrorCode 数组(发生错误的原因可能很多,因此用数组声明)。验证方法如下:

  • 如果发生 FD_READ 相关错误,则在 iErrorCode[FD_READ_BIT]中保存除 0 以外的其他值。
  • 如果发生 FD_WRITE 相关错误,则在 iErrorCode[FD_WRITE_BIT] 中保存除 0 以外的其他值。

可以通过以下的描述来理解上述内容:“如果发生 FD_XXX 相关错误,则在 iErrorCode[FD_XXX_BIT] 中保存除 0 以外的其他值”。

因此可以用如下方式检查错误:

WSANETWORKEVENTS netEvents;
...
WSAEnumNetworkEvents(hSock,hEvent,&netEvents);
...
if(netEvents.iErrorCode[FD_READ_BIT] != 0
{// 发生 FD_READ 事件相关错误
}

利用异步通知 I/O 模型实现回声服务器端

由于代码较长,所以将分成多个部分进行介绍。

#include <stdio.h>
#include <string.h>
#include <winsock2.h>#define BUF_SIZE 100void CompressSockets(SOCKET hSockArr[], int idx, int total);
void CompressEvents(WSAEVENT hEventArr[], int idx, int total);
void ErrorHandling(char *msg);int main(int argc, char *argv[])
{WSADATA wsaData;SOCKET hServSock, hClntSock;SOCKADDR_IN servAdr, clntAdr;SOCKET hSockArr[WSA_MAXIMUM_WAIT_EVENTS];WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];WSAEVENT newEvent;WSANETWORKEVENTS netEvents;int numOfClntSock = 0;int strLen, i;int posInfo, startIdx;int clntAdrLen;char msg[BUF_SIZE];if (argc != 2){printf("Usage: %s <port>\n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error!");

以上是初始化和声明代码,没有什么要特别说明的。

    hServSock = socket(PF_INET, SOCK_STREAM, 0);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");newEvent = WSACreateEvent();if (WSAEventSelect(hServSock, newEvent, FD_ACCEPT) == SOCKET_ERROR)ErrorHandling("WSAEventSelect() error");hSockArr[numOfClntSock] = hServSock;hEventArr[numOfClntSock] = newEvent;numOfClntSock++;

上述代码创建了用于接收客户端连接请求的服务器端套接字。为了完成监听任务,针对 FD_ACCEPT 事件调用了 WSAEventSelect 函数。此处需要注意如下 2 条语句。

    hSockArr[numOfClntSock] = hServSock;hEventArr[numOfClntSock] = newEvent;

这段代码把通过 WSAEventSelect 函数连接的套接字和事件对象的句柄分别存入 hSockArr 和 hEventArr 数组。也就是说,应该可以通过 hSockArr[idx] 找到连接到套接字的事件对象,反之,也可以通过 hEventArr[idx] 找到连接到事件对象的套接字。因此,该示例将套接字和事件对象句柄保存到数组时统一了保存位置。也就有了下列公式:

  • 与 hSockArr[n] 中的套接字相连的事件对象应保存到 hEventArr[n]。
  • 与 hEventArr[n] 中的事件对象相连的套接字应保存到 hSockArr[n]。

接下来是 while 循环部分,之前学习的大部分内容都在此。

    while (1){posInfo = WSAWaitForMultipleEvents(numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);startIdx = posInfo - WSA_WAIT_EVENT_0;for (i = startIdx; i < numOfClntSock; i++){int sigEventIdx =WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);if ((sigEventIdx == WSA_WAIT_FAILED || sigEventIdx == WSA_WAIT_TIMEOUT)){continue;}else{sigEventIdx = i;WSAEnumNetworkEvents(hSockArr[sigEventIdx], hEventArr[sigEventIdx], &netEvents);if (netEvents.lNetworkEvents & FD_ACCEPT) // 请求连接时{if (netEvents.iErrorCode[FD_ACCEPT_BIT] != 0){puts("Accept Error");break;}clntAdrLen = sizeof(clntAdr);hClntSock = accept(hSockArr[sigEventIdx], (SOCKADDR *)&clntAdr, &clntAdrLen);newEvent = WSACreateEvent();WSAEventSelect(hClntSock, newEvent, FD_READ | FD_CLOSE);hEventArr[numOfClntSock] = newEvent;hSockArr[numOfClntSock] = hClntSock;numOfClntSock++;puts("connected new client...");}if (netEvents.lNetworkEvents & FD_READ) // 接收数据时{if (netEvents.iErrorCode[FD_READ_BIT] != 0){puts("Read Error");break;}strLen = recv(hSockArr[sigEventIdx], msg, sizeof(msg), 0);send(hSockArr[sigEventIdx], msg, strLen, 0);}if (netEvents.lNetworkEvents & FD_CLOSE) // 断开连接时{if (netEvents.iErrorCode[FD_CLOSE_BIT] != 0){puts("Close Error");break;}WSACloseEvent(hEventArr[sigEventIdx]);closesocket(hSockArr[sigEventIdx]);numOfClntSock--;CompressSockets(hSockArr, sigEventIdx, numOfClntSock);CompressEvents(hEventArr, sigEventIdx, numOfClntSock);}}}}WSACleanup();return 0;
}

最后给出上述代码调用的两个函数 CompressSockets 和 CompressEvents 的函数声明。

void CompressSockets(SOCKET hSockArr[], int idx, int total)
{int i;for (i = idx; i < total; i++)hSockArr[i] = hSockArr[i + 1];
}void CompressEvents(WSAEVENT hEventArr[], int idx, int total)
{int i;for (i = idx; i < total; i++)hEventArr[i] = hEventArr[i + 1];
}void ErrorHandling(char *msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}

完整的服务器端代码:

#include <stdio.h>
#include <string.h>
#include <winsock2.h>#define BUF_SIZE 100void CompressSockets(SOCKET hSockArr[], int idx, int total);
void CompressEvents(WSAEVENT hEventArr[], int idx, int total);
void ErrorHandling(char *msg);int main(int argc, char *argv[])
{WSADATA wsaData;SOCKET hServSock, hClntSock;SOCKADDR_IN servAdr, clntAdr;SOCKET hSockArr[WSA_MAXIMUM_WAIT_EVENTS];WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];WSAEVENT newEvent;WSANETWORKEVENTS netEvents;int numOfClntSock = 0;int strLen, i;int posInfo, startIdx;int clntAdrLen;char msg[BUF_SIZE];if (argc != 2){printf("Usage: %s <port>\n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error!");hServSock = socket(PF_INET, SOCK_STREAM, 0);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");newEvent = WSACreateEvent();if (WSAEventSelect(hServSock, newEvent, FD_ACCEPT) == SOCKET_ERROR)ErrorHandling("WSAEventSelect() error");hSockArr[numOfClntSock] = hServSock;hEventArr[numOfClntSock] = newEvent;numOfClntSock++;while (1){posInfo = WSAWaitForMultipleEvents(numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);startIdx = posInfo - WSA_WAIT_EVENT_0;for (i = startIdx; i < numOfClntSock; i++){int sigEventIdx =WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);if ((sigEventIdx == WSA_WAIT_FAILED || sigEventIdx == WSA_WAIT_TIMEOUT)){continue;}else{sigEventIdx = i;WSAEnumNetworkEvents(hSockArr[sigEventIdx], hEventArr[sigEventIdx], &netEvents);if (netEvents.lNetworkEvents & FD_ACCEPT) // 请求连接时{if (netEvents.iErrorCode[FD_ACCEPT_BIT] != 0){puts("Accept Error");break;}clntAdrLen = sizeof(clntAdr);hClntSock = accept(hSockArr[sigEventIdx], (SOCKADDR *)&clntAdr, &clntAdrLen);newEvent = WSACreateEvent();WSAEventSelect(hClntSock, newEvent, FD_READ | FD_CLOSE);hEventArr[numOfClntSock] = newEvent;hSockArr[numOfClntSock] = hClntSock;numOfClntSock++;puts("connected new client...");}if (netEvents.lNetworkEvents & FD_READ) // 接收数据时{if (netEvents.iErrorCode[FD_READ_BIT] != 0){puts("Read Error");break;}strLen = recv(hSockArr[sigEventIdx], msg, sizeof(msg), 0);send(hSockArr[sigEventIdx], msg, strLen, 0);}if (netEvents.lNetworkEvents & FD_CLOSE) // 断开连接时{if (netEvents.iErrorCode[FD_CLOSE_BIT] != 0){puts("Close Error");break;}WSACloseEvent(hEventArr[sigEventIdx]);closesocket(hSockArr[sigEventIdx]);numOfClntSock--;CompressSockets(hSockArr, sigEventIdx, numOfClntSock);CompressEvents(hEventArr, sigEventIdx, numOfClntSock);}}}}WSACleanup();return 0;
}void CompressSockets(SOCKET hSockArr[], int idx, int total)
{int i;for (i = idx; i < total; i++)hSockArr[i] = hSockArr[i + 1];
}void CompressEvents(WSAEVENT hEventArr[], int idx, int total)
{int i;for (i = idx; i < total; i++)hEventArr[i] = hEventArr[i + 1];
}void ErrorHandling(char *msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}

回声客户端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>#define BUF_SIZE 1024void ErrorHanding(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}int main(int argc, char *argv[])
{WSADATA wsaData;SOCKET hSocket;SOCKADDR_IN serverAddr;char message[BUF_SIZE];int strLen;if (argc != 3){printf("Usage: %s <IP> <port>\n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHanding("WSAStartup() error!");hSocket = socket(PF_INET, SOCK_STREAM, 0);if (hSocket == INVALID_SOCKET)ErrorHanding("hSocket() error!");memset(&serverAddr, 0, sizeof(serverAddr));serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = inet_addr(argv[1]);serverAddr.sin_port = htons(atoi(argv[2]));if (connect(hSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)ErrorHanding("connect() error!");elseputs("Connected......");while (1){fputs("Input message(Q to quit): ", stdout);fgets(message, BUF_SIZE, stdin);if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))break;send(hSocket, message, strlen(message), 0);strLen = recv(hSocket, message, BUF_SIZE - 1, 0);message[strLen] = '\0';printf("Message from server: %s\n", message);}closesocket(hSocket);WSACleanup();return 0;
}

编译:

gcc AsynNoticeEchoServer_win.c -lws2_32 -o asynNoticeEchoServ
gcc echo_client_win.c -lwsock32 -o echoClnt

运行结果:

在这里插入图片描述

在这里插入图片描述

习题

(1)结合 send & recv 函数解释同步和异步方式的 I/O。并请说明同步 I/O 的缺点,以及怎样通过异步 I/O 进行解决。

同步 I/O:

在这里插入图片描述

在这里插入图片描述

异步 I/O:

在这里插入图片描述

同步 I/O 的缺点:

  1. 资源浪费:线程在阻塞期间无法执行任何操作,导致 CPU 闲置。高并发场景需要大量线程,增加内存和上下文切换开销。
  2. 可扩展性差:每个连接需要一个线程/进程,难以支持大规模并发
  3. 若多个线程因 I/O 相互等待,可能导致死锁。

异步 I/O 的解决方案:

  • 单线程通过事件驱动处理多个 I/O 操作,减少线程/进程数量。
  • 基于事件循环(如 epoll)或协程实现百万级并发连接。
  • 通过非阻塞调用和回调机制,仅在 I/O 就绪时处理数据,最大化 CPU 利用率。

(2)异步 I/O 并不是所有情况下的最佳选择。它具有哪些缺点?何种情况下同步 I/O 更优?可以参考异步 I/O 相关源代码,亦可结合线程进行说明。

异步 I/O 的缺点:

  1. 异步 I/O 通常依赖回调函数或协程,容易导致代码嵌套过深。
  2. 异步高并发模型依赖操作系统支持大量文件描述符,若系统 ulimit 配置不当,可能直接导致程序崩溃。
  3. 单线程异步模型在处理 CPU 密集型任务时,无法利用多核 CPU。需配合多进程或线程池,反而增加复杂度。

同步 I/O 更优的场景:

  1. 简单业务逻辑与低并发需求
  2. 多线程并行计算
  3. 某些库(如数据库驱动)仅提供同步 API。强行异步化需额外封装,可能得不偿失
  4. 同步代码结构线性,适合团队协作或遗留系统维护

(3)判断下列关于 select 模型描述的正误。

  • select 模型通过函数的返回值通知 I/O 相关事件,故可视为通知 I/O 模型。(√)
  • select 模型中 I/O 相关事件的发生时间点和函数返回的时间点一致,故不属于异步模型。(√)
  • WSAEventSelect 函数可视为 select 方式的异步模型,因为该函数的 I/O 相关事件的通知方式为异步方式。(×)

(4)请从源代码的角度说明 select 函数和 WSAEventSelect 函数在使用上的差异。

select 是一种较为基础的多路复用 I/O 监控机制,适用于较小规模的网络编程,并且具有跨平台兼容性,但随着监控套接字数量的增加,性能会显著下降。

WSAEventSelect 采用事件驱动的异步 I/O 机制,特别适合大量并发连接的网络编程,在 Windows 平台上使用更为高效,但只能在 Windows 上使用。

在这里插入图片描述

(5)第 17 章的 epoll 可以在条件触发和边缘触发这 2 种方式下工作。哪种方式更适合异步 I/O 模型?为什么?请概括说明。

边缘触发(Edge-Triggered, ET)模式更适合异步 I/O 模型,原因如下:

  1. 避免阻塞和等待:ET 模式通过单次通知强制应用程序一次性处理数据,减少后续等待。
  2. 最小化系统调用:ET 仅在状态变化时通知一次,降低内核与应用层交互的频率。
  3. 高吞吐和低延迟:ET 结合非阻塞 I/O 可最大限度利用缓冲区,减少数据滞留。

此外,LT 模式在以下场景中表现不佳:

  • 高频小数据包:频繁触发事件,导致 CPU 空转。
  • 高并发连接:需遍历所有就绪的 socket,时间复杂度为 O(n)。
  • 资源浪费:未及时处理的数据会反复触发事件,增加无效的系统调用。

总之,边缘触发通过单次通知、强制非阻塞处理和最小化系统调用,更契合异步 I/O 模型的高效性和事件驱动特性。而条件触发更适合简单的同步编程场景,但在高并发异步环境中容易引发性能瓶颈。

(6)Linux 中的 epoll 同样属于异步 I/O 模型。请说明原因。

epoll 通过事件通知机制来告知用户线程哪些文件描述符已经就绪,可以进行 I/O 操作。用户线程在等待事件时,不会一直阻塞在 I/O 操作上,而是可以通过 epoll_wait 函数等待多个文件描述符的事件,一旦有事件发生,epoll 会通知用户线程,用户线程再根据通知去处理相应的 I/O 操作。

(7)如何获取 WSAWaitForMultipleEvents 函数可以监视的最大句柄数?请编写代码读取该值。

可以通过以宏的方式声明的 WSA_MAXIMUM_WAIT_EVENTS 常量得知 WSAWaitForMultipleEvents 函数可以同时监视的最大事件对象数。该常量值为 64。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>int main()
{printf("%d\n", WSA_MAXIMUM_WAIT_EVENTS);system("pause");return 0;
}

运行结果:

在这里插入图片描述

(8)为何异步通知 I/O 模型中的事件对象必须是 manual-reset 模式?

  1. 确保所有线程都能接收到事件
    • 在异步通知 I/O 模型中,一个事件对象通常用于通知多个线程或处理多个 I/O 操作。当事件对象处于信号状态时,所有等待这个事件的线程都会被唤醒。如果事件对象是 auto-reset 模式,则一旦一个线程被唤醒,事件对象会被自动重置为非信号状态,这会导致其他线程无法再次接收到相同的事件通知。
    • manual-reset 事件对象允许事件状态保持为信号状态,直到显式重置,这确保了所有等待该事件的线程都能被通知到,避免遗漏事件通知。
  2. 适合处理高并发
    在高并发的异步 I/O 模型中,可能有大量的线程或 I/O 操作需要处理相同的事件。使用 manual-reset 事件对象可以减少对事件对象状态的频繁修改,提高系统效率,避免因事件状态自动重置导致的复杂同步问题。
  3. 减少事件通知的复杂性
    使用 manual-reset 事件对象简化了事件通知机制,因为不需要担心事件对象状态会被自动重置,从而避免了可能的竞争和线程同步问题。程序可以显式地控制何时重置事件对象,并且确保所有需要的线程都能正确地响应事件通知。

(9)请在本章的通知 I/O 模型的基础上编写聊天服务器端。要求该服务器端能够结合第 20 章的聊天客户端 chat_clnt_win.c 运行。

聊天服务器端:

#include <stdio.h>
#include <string.h>
#include <winsock2.h>#define BUF_SIZE 100void CompressSockets(SOCKET hSockArr[], int idx, int total);
void CompressEvents(WSAEVENT hEventArr[], int idx, int total);
void SendMsg(char *msg, int len);
void ErrorHandling(char *msg);int numOfClntSock = 0;
SOCKET hSockArr[WSA_MAXIMUM_WAIT_EVENTS];int main(int argc, char *argv[])
{WSADATA wsaData;SOCKET hServSock, hClntSock;SOCKADDR_IN servAdr, clntAdr;WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];WSAEVENT newEvent;WSANETWORKEVENTS netEvents;int strLen, i;int posInfo, startIdx;int clntAdrLen;char msg[BUF_SIZE];if (argc != 2){printf("Usage: %s <port>\n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error!");hServSock = socket(PF_INET, SOCK_STREAM, 0);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");newEvent = WSACreateEvent();if (WSAEventSelect(hServSock, newEvent, FD_ACCEPT) == SOCKET_ERROR)ErrorHandling("WSAEventSelect() error");hSockArr[numOfClntSock] = hServSock;hEventArr[numOfClntSock] = newEvent;numOfClntSock++;while (1){posInfo = WSAWaitForMultipleEvents(numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);startIdx = posInfo - WSA_WAIT_EVENT_0;for (i = startIdx; i < numOfClntSock; i++){int sigEventIdx = WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);if ((sigEventIdx == WSA_WAIT_FAILED || sigEventIdx == WSA_WAIT_TIMEOUT)){continue;}else{sigEventIdx = i;WSAEnumNetworkEvents(hSockArr[sigEventIdx], hEventArr[sigEventIdx], &netEvents);if (netEvents.lNetworkEvents & FD_ACCEPT) // 请求连接时{if (netEvents.iErrorCode[FD_ACCEPT_BIT] != 0){puts("Accept Error");break;}clntAdrLen = sizeof(clntAdr);hClntSock = accept(hSockArr[sigEventIdx], (SOCKADDR *)&clntAdr, &clntAdrLen);newEvent = WSACreateEvent();WSAEventSelect(hClntSock, newEvent, FD_READ | FD_CLOSE);hEventArr[numOfClntSock] = newEvent;hSockArr[numOfClntSock] = hClntSock;numOfClntSock++;puts("connected new client...");}if (netEvents.lNetworkEvents & FD_READ) // 接收数据时{if (netEvents.iErrorCode[FD_READ_BIT] != 0){puts("Read Error");break;}strLen = recv(hSockArr[sigEventIdx], msg, sizeof(msg), 0);SendMsg(msg, strLen); // broadcast}if (netEvents.lNetworkEvents & FD_CLOSE) // 断开连接时{if (netEvents.iErrorCode[FD_CLOSE_BIT] != 0){puts("Close Error");break;}WSACloseEvent(hEventArr[sigEventIdx]);closesocket(hSockArr[sigEventIdx]);numOfClntSock--;CompressSockets(hSockArr, sigEventIdx, numOfClntSock);CompressEvents(hEventArr, sigEventIdx, numOfClntSock);}}}}closesocket(hServSock);WSACleanup();return 0;
}void CompressSockets(SOCKET hSockArr[], int idx, int total)
{int i;for (i = idx; i < total; i++)hSockArr[i] = hSockArr[i + 1];
}void CompressEvents(WSAEVENT hEventArr[], int idx, int total)
{int i;for (i = idx; i < total; i++)hEventArr[i] = hEventArr[i + 1];
}void SendMsg(char *msg, int len)
{ // 发送给全部人int i;for (i = 0; i < numOfClntSock; i++)send(hSockArr[i], msg, len, 0);
}void ErrorHandling(char *msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <process.h>#define BUF_SIZE 100
#define NAME_SIZE 20unsigned WINAPI SendMsg(void *arg);
unsigned WINAPI RecvMsg(void *arg);
void ErrorHandling(char *msg);char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];int main(int argc, char *argv[])
{WSADATA wsaData;SOCKET hSock;SOCKADDR_IN servAdr;HANDLE hSndThread, hRcvThread;if (argc != 4){printf("Usage: %s <IP> <port> <name>\n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error!");sprintf(name, "[%s]", argv[3]);hSock = socket(PF_INET, SOCK_STREAM, 0);memset(&servAdr, 0, sizeof(servAdr));servAdr.sin_family = AF_INET;servAdr.sin_addr.s_addr = inet_addr(argv[1]);servAdr.sin_port = htons(atoi(argv[2]));if (connect(hSock, (SOCKADDR *)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)ErrorHandling("connect() error");hSndThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg, (void *)&hSock, 0, NULL);hRcvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg, (void *)&hSock, 0, NULL);WaitForSingleObject(hSndThread, INFINITE);WaitForSingleObject(hRcvThread, INFINITE);closesocket(hSock);WSACleanup();return 0;
}unsigned WINAPI SendMsg(void *arg)
{SOCKET hSock = *((SOCKET *)arg);char nameMsg[NAME_SIZE + BUF_SIZE];while (1){fgets(msg, BUF_SIZE, stdin);if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")){closesocket(hSock);exit(0);}sprintf(nameMsg, "%s %s", name, msg);send(hSock, nameMsg, strlen(nameMsg), 0);}return 0;
}unsigned WINAPI RecvMsg(void *arg)
{int hSock = *((SOCKET *)arg);char nameMsg[NAME_SIZE + BUF_SIZE];int strLen;while (1){strLen = recv(hSock, nameMsg, NAME_SIZE + BUF_SIZE - 1, 0);if (strLen == -1)return -1;nameMsg[strLen] = '\0';fputs(nameMsg, stdout);}return 0;
}void ErrorHandling(char *msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}

编译:

gcc AsynNoticeChatServer_win.c -lws2_32 -o asynNoticeChatServer
gcc chat_client_win.c -lwsock32 -o cclnt

运行结果:

在这里插入图片描述

相关文章:

《TCP/IP网络编程》学习笔记 | Chapter 21:异步通知 I/O 模型

《TCP/IP网络编程》学习笔记 | Chapter 21&#xff1a;异步通知 I/O 模型 《TCP/IP网络编程》学习笔记 | Chapter 21&#xff1a;异步通知 I/O 模型同步与异步同步异步对比同步 I/O 的缺点异步 I/O 的优点 理解异步通知 I/O 模型实现异步通知 I/O 模型WSAEventSelect 函数和通知…...

Qt6相对Qt5的主要提升(AI总结)

我&#xff1a; Qt 6 相对于5 有哪些新功能&#xff1f; Qt 6 相对于 Qt 5 有诸多新功能和改进&#xff0c;以下是主要的新增特性&#xff1a; 1. 架构和核心库的重构 模块化设计&#xff1a;Qt 6 采用了更加灵活的模块化设计&#xff0c;开发者可以按需引入必要的功能模块&a…...

消息队列ActiveMQ、RabbitMQ、RocketMQ、Kafka对比分析和选型

ActiveMQ、RabbitMQ、RocketMQ、Kafka对比分析和选型 四大消息队列详细对比 1. ActiveMQ 核心特性&#xff1a; 基于JMS规范&#xff0c;支持多种协议&#xff08;AMQP、STOPP、MQTT等&#xff09;。提供主从架构&#xff08;Master-Slave&#xff09;和共享存储集群。支持持…...

2025:sql注入详细介绍

先说一个阿里云学生无门槛免费领一年2核4g服务器的方法&#xff1a; 阿里云服务器学生无门槛免费领一年2核4g_阿里云学生认证免费服务器-CSDN博客 SQL注入&#xff08;SQL Injection&#xff09;是一种常见的网络安全漏洞&#xff0c;攻击者通过在应用程序的输入参数中注入恶意…...

MyBatis操作数据库进阶——动态SQL

动态 SQL 是根据程序运行时的条件灵活生成不同 SQL 语句‌的技术。它的核心目的是在不修改代码‌ 的前提下&#xff0c;通过条件判断、循环等逻辑&#xff0c;动态拼接 SQL 片段&#xff0c;解决传统 SQL 语句死板、难以应对复杂业务场景的问题。 一、<if> 标签 先来观…...

使用LLama-Factory的简易教程(Llama3微调案例+详细步骤)

引言&#xff1a;一套快速实现 Llama3 中文微调的教程 主要参考&#xff1a;胖虎遛二狗的 B 站教学视频《【大模型微调】使用Llama Factory实现中文llama3微调》 ✅ 笔者简介&#xff1a;Wang Linyong&#xff0c;西工大&#xff0c;2023级&#xff0c;计算机技术 研究方向&am…...

LabVIEW发电平台数据采集系统

本文详细介绍了基于LabVIEW的摇臂式波浪发电平台数据采集系统的设计与实现。通过整合LabVIEW软件与多种传感器技术&#xff0c;本系统能够有效提升数据采集的准确性和效率&#xff0c;为波浪能的利用和发电设备的优化提供科学依据。 ​ 项目背景 随着全球能源需求增长和环境保…...

气象可视化卫星云图的方式:方法与架构详解

气象卫星云图是气象预报和气候研究的重要数据来源。通过可视化技术,我们可以将卫星云图数据转化为直观的图像或动画,帮助用户更好地理解气象变化。本文将详细介绍卫星云图可视化的方法、架构和代码实现。 一、卫星云图可视化方法 1. 数据获取与预处理 卫星云图数据通常来源…...

abaqus 二次开发 No module named ‘abaqusConstants

在 Python 中遇到 “No module named ‘abaqusConstants’” 错误通常意味着 Python 无法找到名为 abaqusConstants 的模块。这可能是由以下几个原因造成的&#xff1a; 拼写错误&#xff1a;首先确认模块名是否正确。通常在 Abaqus 的 Python 环境中&#xff0c;正确的模块名…...

【蓝桥杯】每日练习 Day7

目录 前言 领导者 分析 代码 空调 分析 代码 面包店 分析 代码 前言 今天是第一部分的最后一天&#xff08;主打记忆恢复术和锻炼思维&#xff09;&#xff0c;从明天开始主播会逐步更新从位运算到dp问题的常见题型。 领导者&#xff08;分类讨论&#xff09; 分析 …...

贪心算法(11)(java)加油站

题目&#xff1a;在一条环路上有n个加油站&#xff0c;其中第i个加油站有汽油 gas[i]升.。 你有一辆油箱容量无限的的汽车&#xff0c;从第i个加油站开往第i1个加油站需要消耗汽油 cost[i]升。你从其中的一个加油站出发&#xff0c;开始时油箱为空。 给定…...

Python(4)Python函数编程性能优化全指南:从基础语法到并发调优

目录 一、Lambda性能优化原理1.1 内联执行优势1.2 并行计算加速 二、工程级优化策略2.1 内存管理机制2.2 类型提示增强 三、生产环境最佳实践3.1 代码可读性平衡3.2 异常处理模式 四、性能调优案例4.1 排序算法优化4.2 数据管道加速 五、未来演进方向5.1 JIT编译优化5.2 类型系…...

本地部署Stable Diffusion生成爆火的AI图片

直接上代码 Mapping("/send") Post public Object send(Body String promptBody) { JSONObject postSend new JSONObject(); System.out.println(promptBody); JSONObject body JSONObject.parseObject(promptBody); List<S…...

qiankun微前端的使用

qiankun使用时注意以下几个点 1&#xff0c;子应用项目框架&#xff08;react&#xff0c;vue&#xff09;使用的打包格式需要为 umd 格式 2&#xff0c;子应用项目最好配置不受同源策略&#xff08;跨域&#xff09;的影响 3&#xff0c;子应用最好使用的路由模式是 histor…...

从国家能源到浙江交通投资,全息技术在能源交通领域的创新应用

一、3D全息技术行业应用参数及设计制作要求 全息投影 全息投影技术通过激光器、全息片等设备&#xff0c;将物体的三维信息记录下来&#xff0c;并在特定条件下再现。应用参数包括投影距离、投影面积、投影亮度等。设计制作要求&#xff1a;高清晰度、高亮度、低噪音、稳定性好…...

PageHiOffice网页组件(WebOffice文档控件)开发集成技巧专题一

PageHiOffice网页组件作为最新一代的WebOffice文档控件&#xff0c;这是目前市场上唯一能做到在Chrome等最新版浏览器中实现内嵌网页运行的商用文档控件&#xff0c;是OA及ERP等系统处理各种文档的福音。从发布到完善已经超过3年&#xff0c;不管是功能性还是稳定性都已经有了长…...

【人工智能】机器学习中的评价指标

机器学习中的评价指标 在机器学习中&#xff0c;评估指标&#xff08;Evaluation Metrics&#xff09;是衡量模型性能的工具。选择合适的评估指标能够帮助我们更好地理解模型的效果以及它在实际应用中的表现。 一般来说&#xff0c;评估指标主要分为三大类&#xff1a;分类、…...

本地安装deepseek大模型,并使用 python 调用

首先进入 ollama 官网 https://ollama.com/点击下载 下载完成后所有都是下一步&#xff0c;就可以 点击搜索 Models &#xff1a; https://ollama.com/search然后点击下载&#xff1a; 选择后复制: ollama run deepseek-r1:32b例如&#xff1a; 让它安装完成后&#xff1…...

Android:蓝牙设置配套设备配对

一、概述 在搭载 Android 8.0&#xff08;API 级别 26&#xff09;及更高版本的设备上&#xff0c;配套设备配对会代表您的应用对附近的设备执行蓝牙或 Wi-Fi 扫描&#xff0c;而不需要 ACCESS_FINE_LOCATION 权限。这有助于最大限度地保护用户隐私。使用此方法执行配套设备&am…...

AI知识补全(二):提示工程(Prompting)是什么?

名人说:人生如逆旅,我亦是行人。 ——苏轼《临江仙送钱穆父》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:AI知识补全(一):tokens是什么? 目录 一、什么是提示工程?二、为什么提示工程如此重要?三、核心提示工程技术1. 少样本学习(Few-Sho…...

Python 变量作用域、global 关键字与闭包作用域深度解析 第三部分

## 三、闭包作用域的存在原因及适用场景 ### 3.1 闭包作用域存在的原因 #### 3.1.1 数据封装与隐藏 闭包可以把数据封装在外部函数的作用域中&#xff0c;只有内部函数能够访问这些数据&#xff0c;这有助于实现数据的隐藏和保护。 python def counter(): count 0 def incre…...

zookeeper使用

下载 官网 链接 1. 2. 然后解压&#xff1a; 启动 先复制一份这个文件&#xff0c; 双击启动 默认占用8080&#xff0c;和Tomcat冲突&#xff0c; 解决方法&#xff1a;链接 然后重启...

【性能优化点滴】odygrd/quill 中一个简单的标记位作用--降低 IO 次数

在 StreamSink 类中&#xff0c;成员变量 _write_occurred 的作用是 跟踪自上次刷新&#xff08;Flush&#xff09;以来是否有写入操作发生&#xff0c;其核心目的是 优化 I/O 性能。以下是详细解析&#xff1a; _write_occurred 的作用 1. 避免不必要的刷新&#xff08;Flush…...

Java面试黄金宝典11

1. 什么是 JMM 内存模型 定义 JMM&#xff08;Java Memory Model&#xff09;即 Java 内存模型&#xff0c;它并非真实的物理内存结构&#xff0c;而是一种抽象的概念。其主要作用是规范 Java 虚拟机与计算机主内存&#xff08;Main Memory&#xff09;之间的交互方式&#x…...

使用BootStrap 3的原创的模态框组件,没法弹出!估计是原创的bug

最近在给客户开发一个CRM系统&#xff0c;其中用到了BOOTSTRAP的模态框。版本是3。由于是刚开始用该框架。所以在正式部署到项目中前&#xff0c;需要测试一下&#xff0c;找到框架中的如下部分。需要说明的是。我用的asp.net mvc框架开发。测试也是在asp.net mvc环境下。 复制…...

【Azure 架构师学习笔记】- Azure Networking(1) -- Service Endpoint 和 Private Endpoint

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Networking】系列。 前言 最近公司的安全部门在审计云环境安全性时经常提到service endpoint&#xff08;SE&#xff09;和priavate endpoint&#xff08;PE&#xff09;的术语&#xff0c;为此做了一些研究储备。 云…...

Excel第41套全国人口普查

2. 导入网页中的表格&#xff1a;数据-现有链接-考生文件夹&#xff1a;网页-找到表格-点击→变为√-导入删除外部链接关系&#xff1a;数据-点击链接-选中连接-删除-确定&#xff08;套用表格格式-也会是删除外部链接&#xff09;数值缩小10000倍&#xff08;除以10000即可&am…...

VUE2导出el-table数据为excel并且按字段分多个sheet

首先在根目录下建一个文件夹export用来存储export.js import * as XLSX from xlsxfunction autoWidthFunc(ws, data) {// 设置每列的最大宽度const colWidth data.map(row > row.map(val > {var reg new RegExp([\\u4E00-\\u9FFF], g) // 检测字符串是否包含汉字if (v…...

PDF文件转Markdown,基于开源项目marker

​ 首先我们来问下deepseek 为啥要选marker呢 基于深度学习&#xff0c;一看就逼格拉满。搞科研必备&#xff0c;效果应该不会太差。 看下官网 https://github.com/VikParuchuri/marker ​ 一看头像是个印度佬&#xff0c;自吹——又快又好。那就试试吧。 安装步骤 安装…...

深入理解 HTML5 Web Workers:提升网页性能的关键技术解析

深入理解 HTML5 Web Workers&#xff1a;提升网页性能的关键技术解析 引言1. 什么是 Web Workers&#xff1f;Web Workers 的特点&#xff1a; 2. Web Workers 的使用方式2.1 创建一个 Web Worker步骤 1&#xff1a;创建 Worker 文件步骤 2&#xff1a;在主线程中调用 Worker 3…...