Skip to content

Latest commit

 

History

History
464 lines (309 loc) · 28.9 KB

File metadata and controls

464 lines (309 loc) · 28.9 KB

十一、使用asyncio构建通信通道

通信通道是计算机科学领域应用并发的重要组成部分。在本章中,我们将介绍传输的基本理论,传输是asyncio模块提供的类,用于抽象各种形式的通信通道。我们还将介绍 Python 中一个简单的回显服务器-客户机逻辑的实现,以进一步说明asyncio和并发在通信系统中的使用。本例的代码将作为本书后面出现的一个高级示例的基础。

本章将介绍以下主题:

  • 通信通道的基本原理及其异步编程的应用
  • 如何使用asyncioaiohttp在 Python 中构建异步服务器
  • 如何异步向多个服务器发出请求并处理异步文件读写

技术要求

以下是本章的先决条件列表:

传播渠道生态系统

通信信道用于表示不同系统之间的物理布线连接和便于计算机网络的数据逻辑通信。在本章中,我们将只关注后者,因为这是一个与计算相关的问题,并且与异步编程的思想更为密切。在本节中,我们将讨论通信通道的一般结构,以及该结构中与异步编程特别相关的两个特定元素。

通信协议层

大多数通过通信信道完成的数据传输过程以开放系统互连OSI模型协议层的形式进行。OSI 模型列出了系统间通信过程中的主要层和主题。

下图显示了 OSI 模型的一般结构:

OSI model structure

如上图所示,在数据传输过程中有七个主要的通信层,具有不同程度的计算级别。我们将不详细介绍每一层的用途和具体功能,但了解媒体层和主机层背后的一般思想仍然很重要。

三个底层包含相当低级别的操作,这些操作与通信通道的底层流程交互。物理层和数据链路层中的操作包括编码方案、接入方案、低级错误检测和纠正、位同步等。这些操作用于实现和指定在传输数据之前处理和准备数据的逻辑。另一方面,网络层通过确定接收者的地址和数据传输的路径来处理在计算机网络中从一个系统(例如,服务器)到另一个系统(例如,客户端)的数据包转发。

另一方面,顶层处理高级数据通信和操作。在这些层中,我们将重点关注传输层,因为asyncio模块在实现通信信道时直接使用传输层。该层通常被视为媒体层和主机层(例如,客户端和服务器)之间的概念过渡,负责沿不同系统之间的端到端连接发送数据。此外,由于数据包(由网络层准备)在传输过程中可能由于网络错误而丢失或损坏,因此传输层还负责通过错误检测码中的方法检测这些错误。

其他主机层实现处理、解释和提供从另一个系统发送的数据的机制。从传输层接收数据后,会话层处理身份验证、授权和会话恢复过程。然后表示层转换相同的数据并将其重新组织为可解释的表示。最后,应用层以用户友好的格式显示数据。

通信信道异步编程

考虑到异步编程的本质,编程模型可以提供一些功能来补充有效促进通信通道的过程,这并不奇怪。以 HTTP 通信为例,服务器可以同时异步处理多个客户端;在等待特定客户机发出 HTTP 请求时,它可以切换到另一个客户机并处理该客户机的请求。类似地,如果客户端需要向多个服务器发出 HTTP 请求,并且必须等待来自某些服务器的大型响应,则它可以处理更轻量级的响应,已处理并首先发送回客户端的。下图显示了服务器和客户端如何在 HTTP 请求中异步交互的示例:

Asynchronous, interleaved HTTP requests

异步 IO 中的传输和协议

asyncio模块提供许多不同的传输类别。本质上,这些类是前一节讨论的传输层功能的实现。您已经知道,传输层在通信通道中起着不可或缺的作用;因此,传输类为asyncio(以及开发人员)提供了对实现我们自己的通信通道过程的更多控制。

asyncio模块将传输抽象与异步程序的实现结合起来。具体来说,即使传输是通信通道的核心元素,为了利用传输类和其他相关的通信通道工具,我们需要启动并调用事件循环,它是asyncio.AbstractEventLoop类的一个实例。然后,事件循环本身将创建传输并管理低级通信过程。

需要注意的是,asyncio中已建立的通信通道中的transport对象始终与asyncio.Protocol类的实例相关联。顾名思义,Protocol类指定了通信通道使用的底层协议;对于与另一个系统建立的每个连接,将从此类创建一个新的协议对象。协议对象与transport对象紧密合作时,可以从transport对象调用各种方法;在这一点上,我们可以实现通信通道的特定内部工作。

因此,在构建连接通道时,我们通常需要关注asyncio.Protocol子类及其方法的实现。换句话说,我们使用asyncio.Protocol作为父类来派生满足我们的通信通道需求的子类。为此,我们在自己的自定义协议子类中覆盖asyncio.Protocol基类中的以下方法:

  • Protocol.connection_made(transport):当连接到另一个系统时,会自动调用此方法。transport参数保存与连接关联的transport对象。同样,每个transport需要与协议配对;我们通常将该transport对象作为该特定协议对象的属性存储在connection_made()方法中。
  • Protocol.data_received(data):当我们连接的一个系统发送其数据时,会自动调用此方法。注意,data参数保存发送的信息,通常以字节表示,因此在进一步处理data之前,应该使用 Python 的encode()函数。

接下来,让我们考虑从 Type T0 中的传输类的重要方法。所有传输类都从父传输类asyncio.BaseTransport继承,我们有以下常用方法:

  • BaseTransport.get_extra_info():顾名思义,该方法返回调用transport对象的额外通道特定信息。结果可以包括有关套接字、管道和与该传输关联的子流程的信息。在本章后面,我们将调用BaseTransport.get_extra_info('peername'),以获取传输所经过的远程地址。

  • BaseTransport.close():此方法用于关闭正在调用的transport对象,之后不同系统之间的连接将停止。传输的相应协议将自动调用其connection_lost()方法。

在众多传输类的实现中,我们将重点关注asyncio.WriteTransport类,它再次继承了BaseTransport类的方法,并另外实现了用于促进只写传输功能的其他方法。在这里,我们将使用WriteTransport.write()方法,该方法将写入我们希望发送到通过transport对象与之通信的其他系统的数据。作为asyncio模块的一部分,该方法不是阻塞功能;相反,它以异步方式缓冲并发送写入的数据。

asyncio 服务器客户机的总体情况

您已经了解到异步编程,特别是asyncio,可以极大地改善通信通道的执行。您还看到了在实现异步通信通道时需要使用的特定方法。在我们深入研究 Python 的一个工作示例之前,让我们简要地讨论一下我们要完成的工作的总体情况,或者换句话说,我们的程序的总体结构。

如前所述,我们需要实现一个子类asyncio.Protocol来指定通信通道的底层组织。同样,每个异步程序的核心都有一个事件循环,因此我们还需要在协议类的上下文之外创建一个服务器,并在程序的事件循环内启动该服务器。这个过程将建立整个服务器的异步体系结构,并且可以通过asyncio.create_server()方法完成,我们将在下面的示例中介绍。

最后,我们将使用AbstractEventLoop.run_forever()方法永远运行异步程序的事件循环。与实际的服务器类似,我们希望在服务器遇到问题之前保持服务器运行,在这种情况下,我们将优雅地关闭服务器。下图说明了整个过程:

Asynchronous program structure in communication channels

Python 示例

现在,让我们看一个具体的 Python 示例,它实现了一个促进异步通信的服务器 https://github.com/PacktPublishing/Mastering-Concurrency-in-Python ,并导航到Chapter11文件夹。

启动服务器

Chapter11/example1.py文件中,让我们看看EchoServerClientProtocol类,如下所示:

# Chapter11/example1.py

import asyncio

class EchoServerClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def data_received(self, data):
        message = data.decode()
        print('Data received: {!r}'.format(message))

在这里,我们的EchoServerClientProtocol类是asyncio.Protocol的一个子类。如前所述,在这个类中,我们需要实现connection_made(transport)data_received(data)方法。在connection_made()方法中,我们只需通过get_extra_info()方法(使用'peername'参数)获取所连接系统的地址,打印出包含该信息的消息,最后将transport对象存储在类的属性中。为了在data_received()方法中打印出类似的消息,我们再次使用decode()方法从字节数据中获取字符串对象。

让我们转到脚本的主程序,如下所示:

# Chapter11/example1.py

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()

我们正在使用熟悉的asyncio.get_event_loop()函数为异步程序创建事件循环。然后,我们通过让事件循环调用create_server()方法为我们的通信创建一个服务器;此方法从asyncio.Protocol类中获取一个子类,即我们服务器的地址(在本例中,它是我们的本地主机:127.0.0.1),最后是该地址的端口(通常为8888

请注意,此方法不会创建服务器本身;它只启动异步创建服务器的过程,并返回将完成该过程的协同路由。出于这个原因,我们需要将从该方法返回的协程存储在一个变量中(在我们的例子中是coro),并让我们的事件循环运行该协程。在使用服务器对象的sockets属性打印出一条消息后,我们将永远运行事件循环,以保持服务器运行,除非调用了KeyboardInterrupt异常。

最后,在我们的程序结束时,我们将处理脚本中的房屋清理部分,它将优雅地关闭服务器。这通常是通过让服务器对象调用close()方法(启动服务器的关闭过程)并使用事件循环在服务器对象上运行wait_closed()方法来完成的,以确保服务器正确关闭。最后,我们关闭事件循环。

安装 Telnet

在运行示例 Python 程序之前,我们必须安装 Telnet 程序,以便正确模拟客户端和服务器之间的连接通道。Telnet 是一个提供终端命令的程序,可促进双向、交互式、面向文本的通信协议。如果您的计算机上已经有 Telnet,只需跳到下一节;否则,请在本节中查找适合您的系统的信息。

在 Windows 系统中,已安装 Telnet,但可能未启用。要启用它,您可以利用“打开或关闭 Windows 功能”窗口并确保选中 Telnet 客户端框,或者运行以下命令:

dism /online /Enable-Feature /FeatureName:TelnetClient

Linux 系统通常预装 Telnet,因此如果您拥有 Linux 系统,只需转到下一节。

在 macOS 系统中,您的计算机上可能已经安装了 Telnet。如果没有,则需要通过软件包管理软件 Homebrew 执行此操作,如下所示:

brew install telnet

请注意,macOS 系统确实预装了 Telnet 的替代品,称为 Netcat。如果您不希望在 macOS 计算机上安装 Telnet,只需在下面的示例中使用nc命令而不是telnet,您将获得相同的效果。

模拟连接通道

运行以下服务器示例有多个步骤。首先,我们需要运行脚本来启动服务器,您将从中获得以下输出:

> python example1.py
Serving on ('127.0.0.1', 8888)

请注意,程序将一直运行,直到您调用Ctrl+C组合键。当程序仍在一个终端(这是我们的服务器终端)上运行时,打开另一个终端并在指定端口(8888连接到服务器(127.0.0.1);此服务器将作为我们的客户端终端:

telnet 127.0.0.1 8888

现在,您将在服务器和客户端终端中看到一些更改。最有可能的是,您的客户端终端将具有以下输出:

> telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.

这是来自 Telnet 程序的接口,这表明我们已成功连接到本地服务器。更有趣的输出是在我们的服务器终端上,它将类似于以下内容:

> python example1.py
Serving on ('127.0.0.1', 8888)
Connection from ('127.0.0.1', 60332)

回想一下,这是我们在EchoServerClientProtocol类中特别在connection_made()方法中实现的信息消息。同样,在服务器和新客户机之间建立连接时,将自动调用此方法以启动通信。从输出消息中,我们知道客户端正在从服务器127.0.0.1的端口60332发出请求(与正在运行的服务器相同,因为它们都是本地的)。

我们在EchoServerClientProtocol类中实现的另一个特性是data_received()方法。具体来说,我们打印从客户端发送的解码数据。要模拟这种类型的通信,只需在客户端中键入一条消息,然后按返回(*输入,*用于 Windows)键。您将不会看到客户端终端输出中的任何更改,但是服务器终端应该打印出一条消息,正如我们协议类的data_received()方法中所指定的那样。

例如,当我从客户端发送消息Hello, World!时,以下是我的服务器终端输出:

> python example1.py
Serving on ('127.0.0.1', 8888)
Connection from ('127.0.0.1', 60332)
Data received: 'Hello, World!\r\n'

\r\n字符只是消息字符串中包含的返回字符。使用我们当前的协议,您可以向服务器发送多条消息,甚至可以让多个客户端向服务器发送消息。要实现这一点,只需打开另一个终端并再次连接到本地服务器。您将从服务器终端看到,不同的客户端(来自不同的端口)已连接到服务器,而我们的服务器与旧客户端的原始通信仍在维护中。这是异步编程的另一个结果,它允许多个客户机与同一服务器无缝通信,而无需使用线程或多处理。

将消息发送回客户端

因此,在我们当前的示例中,我们能够让异步服务器接收、读取和处理来自客户端的消息。但是,为了使我们的通信通道有用,我们还希望从服务器向客户端发送消息。在本节中,我们将把服务器更新为 echo 服务器,根据定义,echo 服务器将从特定客户机接收的所有数据发送回客户机。

为此,我们将使用asyncio.WriteTransport类中的write()方法。检查EchoServerClientProtocol类的data_received()方法中的Chapter11/example2.py文件,如下所示:

# Chapter11/example2.py

import asyncio

class EchoServerClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def data_received(self, data):
        message = data.decode()
        print('Data received: {!r}'.format(message))

        self.transport.write(('Echoed back: {}'.format(message)).encode())

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()

在接收到来自transport对象的数据并打印出来后,我们向transport对象写入相应的消息,该消息将返回到原始客户端。通过运行Chapter11/example2.py脚本并模拟我们在上一个示例中使用 Telnet 或 Netcat 实现的相同通信,您将看到在客户机终端中键入消息后,客户机从服务器接收回显消息。以下是我在启动通信通道并输入Hello, World!消息后的输出:

> telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Hello, World!
Echoed back: Hello, World!

本质上,这个示例说明了我们可以通过自定义asyncio.Protocol类实现的双向通信通道的功能。在运行服务器时,我们可以获取从连接到服务器的各种客户端发送的数据,处理数据,最后将所需结果发送回相应的客户端。

关闭运输工具

有时,我们会希望强制关闭通信通道中的传输。例如,即使使用异步编程和其他形式的并发,您的服务器也可能被来自多个客户端的持续通信所淹没。另一方面,不希望让服务器完全处理一些已发送的请求,并在服务器达到最大容量时立即拒绝其余的请求。

因此,我们可以在协议中指定在成功通信后关闭每个连接,而不是为每个连接到服务器的客户端保持通信打开。我们将使用BaseTransport.close()方法强制关闭调用的transport对象,这将停止服务器与特定客户端之间的连接。同样,我们正在修改Chapter11/example3.pyEchoServerClientProtocol类的data_received()方法,如下所示:

# Chapter11/example3.py

import asyncio

class EchoServerClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def data_received(self, data):
        message = data.decode()
        print('Data received: {!r}'.format(message))

        self.transport.write(('Echoed back: {}'.format(message)).encode())

        print('Close the client socket')
        self.transport.close()

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()

运行脚本,尝试连接到指定的服务器,并键入一些消息,以便查看我们实现的更改。在我们当前的设置中,在客户端连接并向服务器发送消息后,它将接收回显消息,并且它与服务器的连接将关闭。以下是我在使用协议的当前实现模拟此过程后获得的输出(同样来自 Telnet 程序的接口):

> telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Hello, World!
Echoed back: Hello, World!
Connection closed by foreign host.

与 aiohttp 的客户端通信

在前面的章节中,我们介绍了使用asyncio模块实现异步通信通道的示例,主要是从通信过程的服务器端的角度。换句话说,我们一直在考虑处理和处理来自外部系统的请求。然而,这只是等式的一个方面,我们还需要探索通信的客户端。在本节中,我们将讨论应用异步编程向服务器发出请求。

正如您很可能猜到的,此过程的最终目标是通过异步向外部系统发出请求来有效地从这些系统收集数据。我们将重新讨论 web 抓取的概念,这是一个自动化对各种网站的 HTTP 请求并从其 HTML 源代码中提取特定信息的过程。如果您没有阅读过第 5 章并发 Web 请求,我强烈建议您在继续本节之前仔细阅读,因为该章涵盖了 Web 抓取的基本思想以及其他相关的重要概念。

在本节中,还将向您介绍另一个支持异步编程选项的模块:aiohttp(代表异步 I/O HTTP)。该模块提供了简化 HTTP 通信过程的高级功能,并与asyncio模块无缝配合,以便于异步编程。

安装 aiohttp 和 aiofiles

aiohttp模块不是随 Python 发行版预装的;但是,与其他软件包类似,您可以使用pipconda命令轻松安装模块。我们还将安装另一个模块aiofiles,它有助于异步文件写入。如果使用pip作为包管理器,只需运行以下命令:

pip install aiohttp
pip install aiofiles

如果要使用 Anaconda,请运行以下命令:

conda install aiohttp
conda install aiofiles

和往常一样,要确认您已经成功安装了软件包,请打开 Python 解释器并尝试导入模块。在这种情况下,请运行以下代码:

>>> import aiohttp
>>> import aiofiles

如果包已成功安装,则不会出现错误消息。

获取网站的 HTML 代码

首先,让我们看看如何通过aiohttp从单个网站发出请求并获取 HTML 源代码。请注意,即使只有一个任务(一个网站),我们的应用仍然是异步的,异步程序的结构仍然需要实现。现在,导航到Chapter11/example4.py文件,如下所示:

# Chapter11/example4.py

import aiohttp
import asyncio

async def get_html(session, url):
    async with session.get(url, ssl=False) as res:
        return await res.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await get_html(session, 'http://packtpub.com')
        print(html)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

让我们先考虑一下协同程序。我们正在上下文管理器中从aiohttp.ClientSession类启动一个实例;请注意,我们还将async关键字放在这个声明的前面,因为整个上下文块本身也将被视为一个协程。在这个区块内,我们正在呼叫并等待get_html()协同程序处理并返回。

将注意力转向get_html()协同程序,我们可以看到它包含一个会话对象和一个我们想要从中提取 HTML 源代码的网站 URL。在这个函数中,我们创建了另一个异步上下文管理器,用于发出GET请求并存储服务器对res变量的响应。最后,我们返回存储在响应中的 HTML 源代码;由于响应是从aiohttp.ClientSession类返回的对象,其方法是异步函数,因此调用text()函数时需要指定await关键字。

当您运行该程序时,Packt 网站的整个 HTML 源代码都将打印出来。例如,以下是我的输出的一部分:

HTML source code from aiohttp

异步写入文件

大多数情况下,我们希望通过向多个网站发出请求来收集数据,而简单地打印出响应 HTML 代码是不合适的(出于许多原因);相反,我们希望将返回的 HTML 代码写入输出文件。本质上,这个过程是异步下载,它也在流行下载管理器的底层架构中实现。为此,我们将使用aiofiles模块,与aiohttpasyncio结合使用。

导航到Chapter11/example5.py文件。首先,我们来看一下download_html()协同程序,如下所示:

# Chapter11/example5.py

async def download_html(session, url):
    async with session.get(url, ssl=False) as res:
        filename = f'output/{os.path.basename(url)}.html'

        async with aiofiles.open(filename, 'wb') as f:
            while True:
                chunk = await res.content.read(1024)
                if not chunk:
                    break
                await f.write(chunk)

        return await res.release()

这是上一个示例中的get_html()协同程序的更新版本。我们不再使用aiohttp.ClientSession实例发出GET请求并打印返回的 HTML 代码,而是使用aiofiles模块将 HTML 代码写入文件。例如,为了方便异步文件写入,我们使用aiofiles中的异步open()函数在上下文管理器中读取文件。此外,我们使用响应对象的content属性的read()函数,异步地读取返回的 HTML 块;这意味着在读取当前响应的1024字节后,执行流将被释放回事件循环,并发生任务切换事件。

本例中的main()协同程序和主程序与上一例中的程序保持相对一致:

async def main(url):
    async with aiohttp.ClientSession() as session:
        await download_html(session, url)

urls = [
    'http://packtpub.com',
    'http://python.org',
    'http://docs.python.org/3/library/asyncio',
    'http://aiohttp.readthedocs.io',
    'http://google.com'
]

loop = asyncio.get_event_loop()
loop.run_until_complete(
    asyncio.gather(*(main(url) for url in urls))
)

main()协同路由接收 URL 并将其与aiohttp.ClientSession实例一起传递给download_html()协同路由。最后,在我们的主程序中,我们创建一个事件循环,并将指定 URL 列表中的每个项目传递给main()协同路由。运行程序后,您的输出应类似于以下内容,尽管运行程序所需的时间可能有所不同:

> python3 example5.py
Took 0.72 seconds.

此外,名为output(位于Chapter11文件夹内)的子文件夹将填充我们 URL 列表中每个网站下载的 HTML 代码。同样,这些文件是通过前面讨论过的aiofiles模块的功能异步创建和写入的。如您所见,为了比较此程序及其相应的同步版本的速度,我们还跟踪运行整个程序所需的时间。

现在,转到Chapter11/example6.py文件。此脚本包含当前程序的同步版本的代码。具体地说,它会按顺序向各个网站发出 HTTPGET请求,文件写入过程也按顺序实现。此脚本生成以下输出:

> python3 example6.py
Took 1.47 seconds.

虽然它实现了相同的结果(下载 HTML 代码并将其写入文件),但我们的顺序程序比异步程序花费的时间要多得多。

总结

数据传输过程中有七个主要的通信层,计算级别各不相同。媒体层包含与通信通道底层过程交互的相当低级别的操作,而主机层处理高级数据通信和操作。在这七个层中,传输层通常被视为媒体层和主机层之间的概念过渡,负责沿不同系统之间的端到端连接发送数据。异步编程可以提供一些功能,以补充有效简化通信通道的过程。

在服务器方面,asyncio模块将传输抽象与异步程序的实现结合起来。具体而言,asyncio通过其BaseTransportBaseProtocol类提供了不同的方式来定制通信信道的底层架构。与aiohttp模块一起,asyncio提供了客户端通信过程的效率和灵活性。aiofiles模块可以与其他两个异步编程模块一起工作,也有助于促进异步文件的读取和写入。

现在,我们探讨了并发编程中三个最大、最重要的主题:线程、多处理和异步编程。我们已经展示了它们如何应用于各种编程问题,并在速度上提供了显著的改进。在本书的下一章中,我们将从死锁开始讨论并发编程通常给开发人员和程序员带来的问题。

问题

  • 什么是沟通渠道?它与异步编程有什么联系?
  • OSI 模型协议层的两个主要部分是什么?它们各自有什么用途?
  • 什么是传输层?为什么它对沟通渠道至关重要?
  • asyncio如何促进服务器端通信通道的实现?
  • asyncio如何促进客户端通信渠道的实施?
  • 什么是aiofiles

进一步阅读

有关更多信息,请参阅以下链接: