Winsock I/O 模型:性能和可扩展性的关键

2024-05-24 13:28

本文主要是介绍Winsock I/O 模型:性能和可扩展性的关键,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

引言

Select模型

简介

主要特点

优点

缺点

工作原理

示例用法

WSAAsyncSelect异步I/O模型

简介

工作原理

主要步骤

优点

缺点

示例代码

WSAEventSelect事件选择模型

简介

工作原理

主要步骤

优点

缺点

示例代码

重叠I/O模型

简介

工作原理

主要优势

应用场景

示例代码

完成端口模型

简介

工作原理

主要步骤

线程池

应用场景

示例代码

结论


引言


        在网络编程领域,输入/输出(I/O)模型是数据传输的基础架构。在Windows系统中,Winsock(Windows Sockets API)提供了多种I/O模型以支持不同的网络通信需求。

        本文将详细介绍Winsock提供的五种主要I/O模型:select模型、WSAAsyncSelect异步I/O模型、WSAEventSelect事件选择模型、重叠I/O模型和完成端口模型。

Select模型

简介

        Select模型是Windows Socket中最基本的一种同步I/O模型。它通过使用Select函数,开发者可以监视一组socket的状态变化,例如可读性、可写性、错误状态等。Select模型使用轮询机制,让开发者在一个线程内管理多个socket,有效减少资源的负担。然而,由于Select模型的低效轮询机制,在处理大规模并发连接时会面临性能瓶颈。

主要特点
  • **简单易用:**Select模型的API简单易用,易于理解和使用。
  • **多socket管理:**Select模型可以同时监视多个socket,并等待其中任何一个变为可读、可写或发生异常。
  • **跨平台支持:**Select模型在大多数操作系统上都得到了广泛的支持,包括Windows、Linux和macOS。
优点
  • **易于使用:**Select模型的简单API和直接的方法使其成为开发人员的理想选择。
  • **资源效率:**通过在单个线程内管理多个socket,Select模型最大限度地减少了资源消耗和开销。
  • **跨平台兼容性:**Select模型在不同平台上的广泛支持确保了其在各种环境中的适用性。
缺点
  • **轮询效率低下:**Select模型的轮询机制,即顺序检查每个socket的状态,会导致性能下降,尤其是在处理大量socket时。
  • **并发限制:**Select模型的单线程特性可能会限制并发性,从而阻碍对高水平并发I/O操作要求苛刻的应用程序的性能。
  • **可扩展性问题:**随着连接和I/O操作数量的增加,Select模型的轮询机制可能会不堪重负,导致可扩展性问题。
工作原理

        Select模型通过使用称为“fd_set”的数据结构来存储要监视的socket描述符来工作。fd_set结构包含三个集合:

  • **readfds:**包含可读socket描述符。
  • **writefds:**包含可写socket描述符。
  • **exceptfds:**包含遇到错误的socket描述符。

        开发人员可以将感兴趣的socket描述符添加到相应的fd_set中。然后,他们调用Select函数,并将fd_set作为参数传递。Select函数将阻塞,直到至少一个socket变为可读、可写或遇到错误。返回后,Select函数会更新fd_set结构,指示哪些socket已过渡到准备状态。然后,开发人员可以检查修改后的fd_set来处理相应的I/O操作。

示例用法

以下代码片段演示了使用Select模型监视两个socket的基本用法:

#include <winsock.h>int main() {// 初始化WinsockWSADATA wsaData;int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);if (iResult != 0) {printf("WSAStartup failed: %d\n", iResult);return 1;}// 创建socketSOCKET sock1 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock1 == INVALID_SOCKET) {printf("socket failed: %d\n", WSAGetLastError());WSACleanup();return 1;}SOCKET sock2 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock2 == INVALID_SOCKET) {printf("socket failed: %d\n", WSAGetLastError());closesocket(sock1);WSACleanup();return 1;}// 绑定socketsockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8080);serverAddr.sin_addr.s_addr = INADDR_ANY;int iBindResult1 = bind(sock1, (SOCKADDR*)&serverAddr, sizeof(serverAddr));if (iBindResult1 == SOCKET_ERROR) {printf("bind failed: %d\n", WSAGetLastError());closesocket(sock1);closesocket(sock2);WSACleanup();return 1;}int iBindResult2 = bind(sock2, (SOCKADDR*)&serverAddr, sizeof(serverAddr));if (iBindResult2 == SOCKET_ERROR) {printf("bind failed: %

WSAAsyncSelect异步I/O模型

简介

        WSAAsyncSelect 异步 I/O 模型提供了一种异步的方式来通知应用程序 socket 的状态变化。与 Select 模型的轮询机制不同,WSAAsyncSelect 使用消息队列来传递通知,使应用程序无需主动查询 socket 的状态即可获知其变化。

工作原理

        WSAAsyncSelect 模型的核心是将 socket 与一个消息队列关联起来。当 socket 的状态发生变化时,例如有数据可读或可写,系统就会向该消息队列发送一条消息。应用程序可以通过处理消息队列中的消息来响应相应的 I/O 操作。

主要步骤
  1. **创建消息队列:**应用程序首先需要创建一个消息队列,用于接收来自系统的通知消息。
  2. **关联 socket 和消息队列:**使用 WSAAsyncSelect 函数将 socket 与消息队列关联起来。
  3. **设置事件:**应用程序可以设置 WSAAsyncSelect 函数的参数,指定要通知的事件类型,例如可读、可写或错误。
  4. **处理消息:**应用程序需要有一个消息处理循环来不断地从消息队列中获取消息。
  5. **关闭 socket:**当应用程序不再需要使用 socket 时,需要使用 closesocket 函数关闭 socket 并取消其与消息队列的关联。
优点
  • **异步通知:**应用程序无需主动查询 socket 的状态,可以提高应用程序的响应速度和效率。
  • **减少资源占用:**应用程序无需使用轮询机制来监视 socket 状态,可以减少 CPU 资源的占用。
  • **易于实现:**WSAAsyncSelect 模型的 API 相对简单,易于理解和实现。
缺点
  • **消息队列延迟:**由于依赖消息队列传递通知,可能会存在消息处理的延迟。
  • **可扩展性问题:**在高并发情况下,消息队列可能会成为性能瓶颈。
示例代码

以下代码演示了如何使用 WSAAsyncSelect 模型来监视一个 socket:

#include <winsock.h>int main() {// 初始化 WinsockWSADATA wsaData;int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);if (iResult != 0) {printf("WSAStartup failed: %d\n", iResult);return 1;}// 创建 socketSOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock == INVALID_SOCKET) {printf("socket failed: %d\n", WSAGetLastError());WSACleanup();return 1;}// 创建消息队列MSGQUEUE hMsgQueue = CreateMsgQueue(NULL, 0, 0);if (hMsgQueue == NULL) {printf("CreateMsgQueue failed: %d\n", GetLastError());closesocket(sock);WSACleanup();return 1;}// 关联 socket 和消息队列WSAAsyncSelect(sock, hMsgQueue, WM_SOCKET_NOTIFY);// 设置事件WSAEventSelect(sock, FD_READ);// 消息处理循环MSG msg;while (GetMessage(&msg, NULL, 0, 0)) {switch (msg.message) {case WM_SOCKET_NOTIFY: {WPARAM wParam = msg.wParam;LPARAM lParam = msg.lParam;// 处理 socket 事件switch (wParam) {case FD_READ:// 处理可读事件break;case FD_WRITE:// 处理可写事件break;case FD_CLOSE:// 处理连接关闭事件break;case FD_ERROR:// 处理错误事件break;}break;}default:DefWindowProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);}}// 关闭 socketclosesocket(sock);// 关闭消息队列CloseMsgQueue(hMsgQueue);// 清理 WinsockWSACleanup();return 0;
}

WSAEventSelect事件选择模型

简介

        WSAEventSelect 模型是 Winsock 提供的另一种事件驱动的异步 I/O 方式。与 WSAAsyncSelect 模型不同,WSAEventSelect 模型使用事件对象来表示 socket 的状态变化,而不是使用消息队列。这种模型允许开发者为每个 socket 创建一个事件对象,并将 socket 的状态变化与这些事件关联。随后,开发者可以使用 WaitForMultipleObjects 等函数来等待任何一个事件的触发。这种模型使得事件管理更加简洁,并且可以提供出色的性能表现。

工作原理

        WSAEventSelect 模型的核心是将 socket 与一个或多个事件对象关联起来。当 socket 的状态发生变化时,例如有数据可读或可写,系统就会将该事件的状态设置为已触发。应用程序可以使用 WaitForMultipleObjects 等函数来等待任何一个事件的触发。当一个事件被触发时,应用程序可以根据该事件的类型来执行相应的 I/O 操作。

主要步骤
  1. **创建事件对象:**应用程序首先需要为每个 socket 创建一个事件对象。
  2. **关联 socket 和事件对象:**使用 WSAEventSelect 函数将 socket 与事件对象关联起来。
  3. **设置事件:**应用程序可以设置 WSAEventSelect 函数的参数,指定要通知的事件类型,例如可读、可写或错误。
  4. **等待事件:**应用程序可以使用 WaitForMultipleObjects 等函数来等待任何一个事件的触发。
  5. **处理事件:**当一个事件被触发时,应用程序可以使用 GetEventObjectIdentity 函数来确定哪个 socket 的状态发生了变化,然后根据该事件的类型来执行相应的 I/O 操作。
  6. **关闭 socket:**当应用程序不再需要使用 socket 时,需要使用 closesocket 函数关闭 socket 并取消其与事件对象的关联。
优点
  • **简洁的事件管理:**WSAEventSelect 模型使用事件对象来表示 socket 的状态变化,使得事件管理更加简洁。
  • **出色的性能表现:**WSAEventSelect 模型可以提供出色的性能表现,因为它避免了消息队列的开销。
  • **易于扩展:**WSAEventSelect 模型易于扩展,因为它允许为每个 socket 创建多个事件对象。
缺点
  • **编程复杂度略高:**与 Select 模型相比,WSAEventSelect 模型的编程复杂度略高,因为它需要创建和管理事件对象。
  • **需要额外的内存分配:**事件对象的创建需要额外的内存分配。
示例代码

以下代码演示了如何使用 WSAEventSelect 模型来监视一个 socket:

#include <winsock.h>int main() {// 初始化 WinsockWSADATA wsaData;int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);if (iResult != 0) {printf("WSAStartup failed: %d\n", iResult);return 1;}// 创建 socketSOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock == INVALID_SOCKET) {printf("socket failed: %d\n", WSAGetLastError());WSACleanup();return 1;}// 创建事件对象WSAEVENT hEvent = WSAEventCreate(NULL, TRUE, TRUE, NULL);if (hEvent == NULL) {printf("WSAEventCreate failed: %d\n", GetLastError());closesocket(sock);WSACleanup();return 1;}// 关联 socket 和事件对象WSAEventSelect(sock, hEvent, FD_READ | FD_WRITE);// 等待事件DWORD dwWaitResult = WaitForMultipleObjects(1, &hEvent, TRUE, INFINITE);if (dwWaitResult == WAIT_FAILED) {printf("WaitForMultipleObjects failed: %d\n", GetLastError());WSACloseEvent(hEvent);closesocket(sock);WSACleanup();return 1;}// 处理事件WSAEVENT hTriggeredEvent = NULL;DWORD dwEventCount = 0;BOOL bSuccess = WSAEnumEvents(sock, NULL, &hTriggeredEvent, 1, &dwEventCount);if (bSuccess && dwEventCount > 0) {DWORD dwEventFlags;bSuccess = WSAEventGetInfo(hTriggeredEvent, 0, NULL, &dwEventFlags);

重叠I/O模型

简介

        重叠 I/O 模型是 Windows 独有的一种先进的异步 I/O 技术。它允许 I/O 操作在后台执行,应用程序无需等待 I/O 操作完成即可继续处理其他任务。与传统的阻塞 I/O 模型相比,重叠 I/O 模型可以显著提高应用程序的性能,尤其是在处理大量 I/O 操作的网络应用程序中。

工作原理

        重叠 I/O 模型的核心是使用 OVERLAPPED 结构体来管理 I/O 操作。OVERLAPPED 结构体包含以下成员:

  • hEvent:用于通知应用程序 I/O 操作完成的事件句柄。
  • Internal:保留供系统内部使用。
  • Offset:用于指示 I/O 操作要从文件或缓冲区的哪个位置开始。
  • InternalHigh:保留供系统内部使用。
  • Union:包含指向用于 I/O 操作的缓冲区的指针。

        应用程序可以使用 WSARecvWSASend 等函数来发起重叠 I/O 操作。这些函数会将 I/O 操作的参数和 OVERLAPPED 结构体作为参数传递。系统会将 I/O 操作排队并将其置于后台执行。当 I/O 操作完成时,系统会将 hEvent 事件设置为已触发状态,并通知应用程序。

主要优势
  • **提高性能:**重叠 I/O 模型可以显著提高应用程序的性能,因为应用程序无需等待 I/O 操作完成即可继续处理其他任务。
  • **提高响应速度:**重叠 I/O 模型可以提高应用程序的响应速度,因为应用程序可以同时处理多个 I/O 操作。
  • **降低资源占用:**重叠 I/O 模型可以降低应用程序对 CPU 资源的占用,因为 I/O 操作是在后台执行的。
应用场景

        重叠 I/O 模型适用于需要处理大量 I/O 操作的应用程序,例如:

  • **网络应用程序:**Web 服务器、客户端-服务器应用程序、P2P 应用程序等。
  • **文件 I/O 密集型应用程序:**数据库应用程序、视频编辑软件、大型文件传输应用程序等。
示例代码

以下代码演示了如何使用重叠 I/O 模型从套接字中接收数据:

#include <winsock.h>int main() {// 初始化 WinsockWSADATA wsaData;int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);if (iResult != 0) {printf("WSAStartup failed: %d\n", iResult);return 1;}// 创建套接字SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock == INVALID_SOCKET) {printf("socket failed: %d\n", WSAGetLastError());WSACleanup();return 1;}// 绑定套接字sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8080);serverAddr.sin_addr.s_addr = INADDR_ANY;int iBindResult = bind(sock, (SOCKADDR*)&serverAddr, sizeof(serverAddr));if (iBindResult == SOCKET_ERROR) {printf("bind failed: %d\n", WSAGetLastError());closesocket(sock);WSACleanup();return 1;}// 监听套接字int iListenResult = listen(sock, 5);if (iListenResult == SOCKET_ERROR) {printf("listen failed: %d\n", WSAGetLastError());closesocket(sock);WSACleanup();return 1;}// 接受连接sockaddr_in clientAddr;int clientAddrLen = sizeof(clientAddr);SOCKET clientSock = accept(sock, (SOCKADDR*)&clientAddr, &clientAddrLen);if (clientSock == INVALID_SOCKET) {printf("accept failed: %d\n", WSAGetLastError());closesocket(sock);WSACleanup();return 1;}// 创建事件对象WSAEVENT hEvent = WSAEventCreate(NULL, TRUE, TRUE, NULL);

完成端口模型

简介

        完成端口模型是 Windows Sockets 中最复杂但也是最强大的 I/O 模型。它充分利用了多核处理器的能力,提供了一个可伸缩的高性能 I/O 操作解决方案。与其他 I/O 模型相比,完成端口模型具有以下优势:

  • **高性能:**完成端口模型可以充分利用多核处理器的能力,显著提高 I/O 操作的性能。
  • **可伸缩性:**完成端口模型可以处理大量并发连接,并随着硬件的升级而扩展。
  • **低延迟:**完成端口模型可以提供低延迟的 I/O 操作,非常适合对延迟敏感的应用程序。
工作原理

        完成端口模型的核心是使用 CreateIoCompletionPort 函数创建一个或多个完成端口。完成端口是一个内核对象,用于存储已完成 I/O 操作的通知。应用程序可以使用 AssociateSocket 函数将一个或多个套接字与完成端口关联起来。

        当一个套接字上的 I/O 操作完成时,系统会将一个 IO_COMPLETION_RESULT 结构体发送到与该套接字关联的完成端口。应用程序可以使用 GetQueuedCompletionPort 函数来检索完成端口队列中的通知。

主要步骤
  1. **创建完成端口:**使用 CreateIoCompletionPort 函数创建一个或多个完成端口。
  2. **关联套接字:**使用 AssociateSocket 函数将一个或多个套接字与完成端口关联起来。
  3. **发起 I/O 操作:**使用 WSASendWSARecv 等函数发起 I/O 操作,并将 OVERLAPPED 结构体传递给这些函数。
  4. **检索完成通知:**使用 GetQueuedCompletionPort 函数检索完成端口队列中的通知。
  5. **处理完成通知:**根据 IO_COMPLETION_RESULT 结构体中的信息处理完成的 I/O 操作。
线程池

        完成端口模型通常与线程池技术结合使用。线程池是一种管理线程的技术,可以有效地分配处理器资源进行并行计算。在完成端口模型中,应用程序可以使用线程池来处理完成的 I/O 操作。每个线程池中的线程都可以从完成端口队列中检索通知并处理完成的 I/O 操作。

应用场景

完成端口模型适用于需要处理大量并发 I/O 操作的应用程序,例如:

  • **网络应用程序:**Web 服务器、高性能网络应用、游戏服务器等。
  • **文件 I/O 密集型应用程序:**数据库应用程序、视频编辑软件、大型文件传输应用程序等。
示例代码

以下代码演示了如何使用完成端口模型从套接字中接收数据:

#include <winsock.h>int main() {// 初始化 WinsockWSADATA wsaData;int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);if (iResult != 0) {printf("WSAStartup failed: %d\n", iResult);return 1;}// 创建完成端口HANDLE hCompletionPort = CreateIoCompletionPort(NULL, NULL, 0, 0);if (hCompletionPort == NULL) {printf("CreateIoCompletionPort failed: %d\n", GetLastError());WSACleanup();return 1;}// 创建套接字SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock == INVALID_SOCKET) {printf("socket failed: %d\n", WSAGetLastError());CloseHandle(hCompletionPort);WSACleanup();return 1;}// 绑定套接字sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8080);serverAddr.sin_addr.s_addr = INADDR_ANY;int iBindResult = bind(sock, (SOCKADDR*)&serverAddr, sizeof(serverAddr));if (iBindResult == SOCKET_ERROR) {printf("bind failed: %d\n", WSAGetLastError());closesocket(sock);CloseHandle(hCompletionPort);WSACleanup();return 1;}// 监听套接字int iListenResult = listen(sock, 5);if (iListenResult == SOCKET_ERROR) {

结论


        Winsock为Windows下的网络编程提供了多样化的I/O模型,每种模型都适用于不同场景的需求。无论是简单的select模型,还是高效的完成端口模型,了解它们的工作方式、优缺点对于开发高质量的网络应用程序都至关重要。掌握这些I/O模型,将有助于你建构出更稳定、更高性能的网络通信解决方案。

这篇关于Winsock I/O 模型:性能和可扩展性的关键的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/998533

相关文章

一份LLM资源清单围观技术大佬的日常;手把手教你在美国搭建「百万卡」AI数据中心;为啥大模型做不好简单的数学计算? | ShowMeAI日报

👀日报&周刊合集 | 🎡ShowMeAI官网 | 🧡 点赞关注评论拜托啦! 1. 为啥大模型做不好简单的数学计算?从大模型高考数学成绩不及格说起 司南评测体系 OpenCompass 选取 7 个大模型 (6 个开源模型+ GPT-4o),组织参与了 2024 年高考「新课标I卷」的语文、数学、英语考试,然后由经验丰富的判卷老师评判得分。 结果如上图所

大语言模型(LLMs)能够进行推理和规划吗?

大语言模型(LLMs),基本上是经过强化训练的 n-gram 模型,它们在网络规模的语言语料库(实际上,可以说是我们文明的知识库)上进行了训练,展现出了一种超乎预期的语言行为,引发了我们的广泛关注。从训练和操作的角度来看,LLMs 可以被认为是一种巨大的、非真实的记忆库,相当于为我们所有人提供了一个外部的系统 1(见图 1)。然而,它们表面上的多功能性让许多研究者好奇,这些模型是否也能在通常需要系

人工和AI大语言模型成本对比 ai语音模型

这里既有AI,又有生活大道理,无数渺小的思考填满了一生。 上一专题搭建了一套GMM-HMM系统,来识别连续0123456789的英文语音。 但若不是仅针对数字,而是所有普通词汇,可能达到十几万个词,解码过程将非常复杂,识别结果组合太多,识别结果不会理想。因此只有声学模型是完全不够的,需要引入语言模型来约束识别结果。让“今天天气很好”的概率高于“今天天汽很好”的概率,得到声学模型概率高,又符合表达

智能客服到个人助理,国内AI大模型如何改变我们的生活?

引言 随着人工智能(AI)技术的高速发展,AI大模型越来越多地出现在我们的日常生活和工作中。国内的AI大模型在过去几年里取得了显著的进展,不少独创的技术点和实际应用令人瞩目。 那么,国内的AI大模型有哪些独创的技术点?它们在实际应用中又有哪些出色表现呢?此外,普通人又该如何利用这些大模型提升工作和生活的质量和效率呢?本文将为你一一解析。 一、国内AI大模型的独创技术点 多模态学习 多

OpenCompass:大模型测评工具

大模型相关目录 大模型,包括部署微调prompt/Agent应用开发、知识库增强、数据库增强、知识图谱增强、自然语言处理、多模态等大模型应用开发内容 从0起步,扬帆起航。 大模型应用向开发路径:AI代理工作流大模型应用开发实用开源项目汇总大模型问答项目问答性能评估方法大模型数据侧总结大模型token等基本概念及参数和内存的关系大模型应用开发-华为大模型生态规划从零开始的LLaMA-Factor

模型压缩综述

https://www.cnblogs.com/shixiangwan/p/9015010.html

AI赋能天气:微软研究院发布首个大规模大气基础模型Aurora

编者按:气候变化日益加剧,高温、洪水、干旱,频率和强度不断增加的全球极端天气给整个人类社会都带来了难以估计的影响。这给现有的天气预测模型提出了更高的要求——这些模型要更准确地预测极端天气变化,为政府、企业和公众提供更可靠的信息,以便做出及时的准备和响应。为了应对这一挑战,微软研究院开发了首个大规模大气基础模型 Aurora,其超高的预测准确率、效率及计算速度,实现了目前最先进天气预测系统性能的显著

Java中如何优化数据库查询性能?

Java中如何优化数据库查询性能? 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将深入探讨在Java中如何优化数据库查询性能,这是提升应用程序响应速度和用户体验的关键技术。 优化数据库查询性能的重要性 在现代应用开发中,数据库查询是最常见的操作之一。随着数据量的增加和业务复杂度的提升,数据库查询的性能优化显得尤为重

PyTorch模型_trace实战:深入理解与应用

pytorch使用trace模型 1、使用trace生成torchscript模型2、使用trace的模型预测 1、使用trace生成torchscript模型 def save_trace(model, input, save_path):traced_script_model = torch.jit.trace(model, input)<

关于文章“python+百度语音识别+星火大模型+讯飞语音合成的语音助手”报错的修改

前言 关于我的文章:python+百度语音识别+星火大模型+讯飞语音合成的语音助手,运行不起来的问题 文章地址: https://blog.csdn.net/Phillip_xian/article/details/138195725?spm=1001.2014.3001.5501 1.报错问题 如果运行中报错,且报错位置在Xufi_Voice.py文件中的pcm_2_wav,如下图所示