Chapter8_流
1、from async import Transport, 这个Transport的含义理解?
Transport 是 asyncio 模块中的一个概念,用于抽象底层的传输协议。它主要用于处理与网络连接相关的低级别 I/O 操作。
Transport 提供了一种统一的方式来处理各种传输机制,例如 TCP、UDP、SSL 等。
在 asyncio 中,Transport 通常与 Protocol 搭配使用,Protocol 定义了传输层上的协议逻辑,而 Transport 负责实际的数据传输。通过将它们结合起来,你可以实现自定义的通信协议。
Transport 的作用
Transport 提供了以下几种主要功能:
- 数据传输:发送和接收数据。
- 连接管理:打开和关闭连接。
- 流量控制:控制数据流量,比如暂停和恢复传输。
- 缓冲区管理:管理底层的读写缓冲区。
主要方法
Transport 提供了一些常用的方法来进行数据传输和连接管理,比如:
write(data): 发送数据。writelines(list_of_data): 发送多行数据。close(): 关闭传输。abort(): 立即关闭传输。get_extra_info(name): 获取底层传输的一些额外信息,比如远程地址等。
示例代码
下面是一个简单的例子,展示了如何使用 Transport 和 Protocol 来创建一个 TCP 客户端:
import asyncio class EchoClientProtocol(asyncio.Protocol): def __init__(self, message, loop): self.message = message self.loop = loop def connection_made(self, transport): transport.write(self.message.encode()) print('Data sent:', self.message) def data_received(self, data): print('Data received:', data.decode()) def connection_lost(self, exc): print('The server closed the connection') print('Stop the event loop') self.loop.stop() async def main(loop): message = 'Hello, World!' transport, protocol = await loop.create_connection( lambda: EchoClientProtocol(message, loop), '127.0.0.1', 8888) # The loop will run until the protocol calls loop.stop() loop = asyncio.get_event_loop() loop.run_until_complete(main(loop)) loop.run_forever() loop.close()
在这个例子中,EchoClientProtocol 定义了协议逻辑,包括连接建立、数据接收和连接关闭的处理。
而 Transport 由 loop.create_connection 创建并传递给 Protocol,负责底层的数据传输。
总结
Transport 在 asyncio 中起到了封装底层传输机制的作用,使得我们可以专注于实现协议逻辑,而不必关心具体的传输细节。
通过 Transport 和 Protocol 的结合,asyncio 提供了一种强大且灵活的异步编程模型。
2、对于如下代码,我有若干疑惑点。 [from exmaple8_1.py]
from asyncio import Transport, Future, AbstractEventLoop, Protocol from typing import Optional class HttpGetClientProtocol(Protocol): def __init__(self, host: str, loop: AbstractEventLoop): self._host: str = host self._future: Future = loop.create_future() self._transport: Optional[Transport] = None self._response_buffer: bytes = b'' async def get_response(self): return await self._future def _get_request_bytes(self) -> bytes: requests = f"GET / HTTP/1.1\r\n" \ f"Connection: close\r\n" \ f"Host: {self._host} \r\n\r\n" return requests.encode() # string --> bytes def connection_made(self, transport: Transport): print(f'Connection made to {self._host}') self._transport = transport self._transport.write(self._get_request_bytes()) def data_received(self, data: bytes) -> None: print(f'Data received!') self._response_buffer = self._response_buffer + data def eof_received(self) -> Optional[bool]: self._future.set_result(self._response_buffer.decode()) return False def connection_lost(self, exc: Optional[Exception]) -> None: if exc is None: print('Connection closed without error.') else: self._future.set_exception(exc)
问题1:
对于eof_received()函数, 我的直觉本来如下
def eof_received(self): if 'eof' in self._response_buffer.decode() return True
为什么你要写成上述那样,请详细解释。
eof_received 方法的设计是为了在接收到数据流结束标志(EOF)时进行处理。
在 asyncio.Protocol 中,eof_received 方法的调用并不是通过检查数据内容来判断是否接收到 EOF,而是由底层传输机制在接收到 EOF 标志时自动触发的。
EOF 的工作原理
在 TCP 连接中,当对端关闭它的写入端时,接收端会收到一个 EOF 标志。这是底层传输层协议处理的,与应用层的数据内容无关。因此,我们无需通过检查数据内容来判断是否接收到 EOF。
eof_received 方法的实现
以下是 eof_received 方法的实现:
def eof_received(self): self._future.set_result(self._response_buffer.decode()) return False
详细解释
-
self._future.set_result(self._response_buffer.decode()):- 这行代码将
_response_buffer中的接收到的数据解码为字符串,并将其作为结果设置到self._future中。这样,等待该 Future 的协程就可以获取到完整的响应数据。 - 设置 Future 的结果是为了通知等待该结果的协程,数据已经接收完毕,并且可以处理接收到的数据了。
- 这行代码将
-
return False:- 返回
False表示不需要进一步的 EOF 处理。根据asyncio.Protocol的设计,返回False意味着不会再继续处理 EOF 数据流。
- 返回
为什么不需要检查数据内容
-
底层传输机制:
- EOF 是由底层传输机制处理的,与数据内容无关。
eof_received方法会在底层传输检测到 EOF 时自动调用,因此我们不需要通过检查数据内容来判断是否接收到 EOF。
- EOF 是由底层传输机制处理的,与数据内容无关。
-
数据完整性:
- 在
data_received方法中,所有接收到的数据都会被追加到_response_buffer中。在eof_received方法中,设置 Future 的结果表示数据接收完成,并将缓冲区中的数据解码为字符串并返回。
- 在
为什么不使用 if 'eof' in self._response_buffer.decode()
- 错误的判断方式:
eof是 TCP 传输的底层概念,不会直接出现在应用层数据中。因此,通过检查数据内容中的eof字符串来判断 EOF 是不正确的。- 这种方式依赖于特定的数据内容,而 EOF 的检测应该是独立于数据内容的。
结论
eof_received方法的调用是由底层传输机制在检测到 EOF 时自动触发的,而不是通过检查数据内容。- 在
eof_received方法中设置 Future 的结果,并返回False,表示数据接收完成且不需要进一步的 EOF 处理。 - 不需要通过检查数据内容来判断是否接收到 EOF,因为 EOF 是由底层传输层处理的,与数据内容无关。
问题2:
eof_received 方法会在底层传输检测到 EOF 时自动调用?也即是说像eof_received、connection_made、data_received、connection_lost这种函数命名也不能随便改?
是的,像 eof_received、connection_made、data_received、connection_lost 这些方法的命名是不能随意更改的。
这是因为它们是 asyncio.Protocol 类的标准回调方法,asyncio 框架在处理网络事件时会自动调用这些方法。
方法命名的重要性
这些方法是 asyncio.Protocol 类定义的回调方法。框架依赖于这些方法的名称来识别和处理不同的网络事件。具体来说:
connection_made(self, transport: Transport):- 当连接成功建立时,
asyncio框架会调用这个方法。你可以在这个方法中执行初始化操作,比如保存transport对象,用于后续数据传输。
- 当连接成功建立时,
data_received(self, data: bytes):- 当从网络中接收到数据时,
asyncio框架会调用这个方法。你可以在这个方法中处理接收到的数据。
- 当从网络中接收到数据时,
eof_received(self):- 当接收到 EOF 标志时,
asyncio框架会调用这个方法。你可以在这个方法中处理数据流的结束,比如合并数据并通知等待的任务。
- 当接收到 EOF 标志时,
connection_lost(self, exc: Optional[Exception]):- 当连接被关闭或发生错误时,
asyncio框架会调用这个方法。你可以在这个方法中执行清理操作,并处理异常情况。
- 当连接被关闭或发生错误时,
修改方法名称的后果
如果你修改了这些方法的名称,asyncio 框架将无法识别并调用它们。结果是你的协议实现将无法正确处理相应的网络事件。这会导致程序的行为不符合预期,可能无法接收数据、检测到连接的建立或丢失等。
问题3:
如下_get_requests_bytes(self) -> bytes: 方法又该作何理解呢?
def _get_request_bytes(self) -> bytes: requests = f"GET / HTTP/1.1\r\n" \ f"Connection: close\r\n" \ f"Host: {self._host} \r\n\r\n" return requests.encode()
_get_request_bytes 方法的作用是构建一个 HTTP GET 请求报文,并将其编码为字节串,以便在网络传输中使用。
让我们逐行详细解释这个方法:
详细解释
-
方法签名:
def _get_request_bytes(self) -> bytes:
- 这是一个实例方法,返回类型为
bytes。 - 方法名以
_开头,表示这是一个内部方法,不应在类外部直接调用。
2.构建 HTTP GET 请求字符串:
request = f"GET / HTTP/1.1\r\n" \ f"Connection: close\r\n" \ f"Host: {self._host}\r\n\r\n"
- 使用 Python 的 f-string 语法构建一个符合 HTTP 协议格式的 GET 请求字符串。
f"GET / HTTP/1.1\r\n":请求行,表示使用 HTTP 1.1 协议请求根路径/。f"Connection: close\r\n":请求头,告知服务器在完成请求后关闭连接。f"Host: {self._host}\r\n\r\n":请求头,指定目标主机名。self._host是主机名的占位符,将在实际运行时被具体主机名替换。- 整个字符串使用
\r\n分隔各行,并以\r\n\r\n结束,表示 HTTP 请求头的结束。
3.编码为字节串:
return request.encode()
- 将构建的请求字符串编码为字节串。这是因为网络传输需要使用字节数据而不是字符串。
3、对于如下代码,请问create_connection返回的两个对象到底分别是什么?为什么make_request要写成 async异步? [from exmaple8_2.py]
import asyncio from asyncio import AbstractEventLoop from Chapter8.example8_1 import HttpGetClientProtocol async def make_request(host: str, port: int, loop: AbstractEventLoop) -> str: def protocol_factory(): return HttpGetClientProtocol(host, loop) _, protocol = await loop.create_connection(protocol_factory, host=host, port=port) return await protocol.get_response() async def main(): loop = asyncio.get_running_loop() result = await make_request('www.example.com', 80, loop) print(result) asyncio.run(main())
asyncio.AbstractEventLoop.create_connection 方法返回一个由两个元素组成的元组,分别是传输对象(Transport)和协议对象(Protocol)。
create_connection 返回的两个对象
_, protocol = await loop.create_connection( protocol_factory, host=host, port=port )
在这段代码中:
-
第一个返回值:传输对象(Transport):
- 传输对象负责底层 I/O 操作,如发送和接收数据。它提供了对底层套接字或流的抽象,允许你通过高层 API 进行读写操作。
- 在本例中,传输对象用
_表示,意味着它不被显式使用或引用。
-
第二个返回值:协议对象(Protocol):
- 协议对象是实现具体协议逻辑的类实例。在本例中是
HTTPGetClientProtocol的实例。 - 协议对象处理各种网络事件,如连接建立、数据接收、连接丢失等。
create_connection方法使用传入的protocol_factory函数创建协议对象,并与传输对象关联。
- 协议对象是实现具体协议逻辑的类实例。在本例中是
create_connection 的作用
create_connection 方法会:
- 创建一个传输对象和协议对象。
- 建立一个网络连接(在本例中是 TCP 连接)。
- 将传输对象和协议对象关联,使它们协同工作。
为什么 make_request 要写成 async
async def make_request(host: str, port: int, loop: AbstractEventLoop) -> str: def protocol_factory(): return HTTPGetClientProtocol(host, loop) _, protocol = await loop.create_connection( protocol_factory, host=host, port=port ) return await protocol.get_response()
make_request 方法被定义为异步函数(async def)是为了支持异步操作和非阻塞 I/O。这有几个原因:
-
建立连接是异步操作:
loop.create_connection是一个协程,需要异步等待网络连接建立。- 使用
await等待连接建立,使得事件循环能够在等待期间继续处理其他任务,而不是被阻塞。
-
等待响应是异步操作:
protocol.get_response是一个异步方法,需要等待 HTTP 响应数据接收完成。- 使用
await等待响应数据,使得程序可以在等待期间处理其他任务。
-
非阻塞执行:
- 异步方法允许程序在等待 I/O 操作完成时不阻塞事件循环,从而提高并发性和响应性。
make_request方法需要执行多个异步操作,因此被定义为异步函数,以便在事件循环中进行协同调度。
总结
create_connection返回的两个对象分别是传输对象和协议对象。- 传输对象负责底层 I/O 操作。
- 协议对象处理具体的网络事件和协议逻辑。
make_request被定义为异步函数是因为它需要执行和等待多个异步操作,包括建立连接和接收响应。- 使用
await关键字来异步等待操作完成,确保事件循环在等待期间可以处理其他任务,从而实现非阻塞执行。
- 使用
这种设计方式确保了异步操作能够高效进行,提高了程序的并发性能和响应速度。
4、如何理解如下代码? [example8_3.py]
import asyncio from asyncio import StreamReader from typing import AsyncGenerator async def read_until_empty(stream_reader: StreamReader) -> AsyncGenerator[str, None]: while response := await stream_reader.readline(): #A yield response.decode() async def main(): host: str = 'www.example.com' request: str = f"GET / HTTP/1.1\r\n" \ f"Connection: close\r\n" \ f"Host: {host}\r\n\r\n" stream_reader, stream_writer = await asyncio.open_connection('www.example.com', 80) try: stream_writer.write(request.encode()) #B await stream_writer.drain() responses = [response async for response in read_until_empty(stream_reader)] #C print(''.join(responses)) finally: stream_writer.close() #D await stream_writer.wait_closed() asyncio.run(main())
下面逐行详细解释上述代码。
导入必要的模块和类型
import asyncio from asyncio import StreamReader from typing import AsyncGenerator
asyncio:Python 的标准库模块,用于编写异步 I/O 操作。StreamReader:asyncio提供的类,用于读取数据流。AsyncGenerator:类型提示,用于标注异步生成器函数的返回类型。
定义异步生成器函数 read_until_empty
async def read_until_empty(stream_reader: StreamReader) -> AsyncGenerator[str, None]: while response := await stream_reader.readline(): #A yield response.decode()
-
async def read_until_empty(stream_reader: StreamReader) -> AsyncGenerator[str, None]:- 定义一个异步生成器函数,它接收一个
StreamReader对象作为参数,并返回一个异步生成器,生成字符串。
- 定义一个异步生成器函数,它接收一个
-
while response := await stream_reader.readline()::- 使用
await等待从stream_reader读取一行数据,赋值给response。 :=是海象操作符,用于在表达式内部进行赋值。- 如果读取到的数据行不为空,则进入循环体。
- 使用
-
yield response.decode():- 将读取到的字节数据解码为字符串,并通过
yield生成。 - 生成器会暂停在
yield处,直到下一次迭代请求。
- 将读取到的字节数据解码为字符串,并通过
定义主异步函数 main
async def main(): host: str = 'www.example.com' request: str = f"GET / HTTP/1.1\r\n" \ f"Connection: close\r\n" \ f"Host: {host}\r\n\r\n"
-
host: str = 'www.example.com':- 定义目标主机名。
-
request: str = f"GET / HTTP/1.1\r\nConnection: close\r\nHost: {host}\r\n\r\n":- 构建一个 HTTP GET 请求字符串,用于请求主机的根路径
/。
- 构建一个 HTTP GET 请求字符串,用于请求主机的根路径
建立网络连接
stream_reader, stream_writer = await asyncio.open_connection('www.example.com', 80)
stream_reader, stream_writer = await asyncio.open_connection('www.example.com', 80):- 使用
asyncio.open_connection建立到指定主机和端口的 TCP 连接。 - 返回一个
StreamReader和StreamWriter对象,用于读取和写入数据。
- 使用
发送请求并读取响应
try: stream_writer.write(request.encode()) #B await stream_writer.drain()
-
stream_writer.write(request.encode()):- 将请求字符串编码为字节,并通过
stream_writer发送。
- 将请求字符串编码为字节,并通过
-
await stream_writer.drain():- 等待写入完成,确保数据已经发送到对端。
读取响应数据
responses = [response async for response in read_until_empty(stream_reader)] #C print(''.join(responses))
responses = [response async for response in read_until_empty(stream_reader)]:- 使用异步列表推导式,从
read_until_empty异步生成器中读取所有响应数据,并将其收集到列表responses中。
- 使用异步列表推导式,从
print(''.join(responses)):- 将所有响应数据拼接成一个字符串,并打印输出。
关闭连接
finally: stream_writer.close() #D await stream_writer.wait_closed()
-
finally::- 无论前面代码是否抛出异常,都会执行
finally块中的代码。
- 无论前面代码是否抛出异常,都会执行
-
stream_writer.close():- 关闭
StreamWriter对象,表示不再发送数据。
- 关闭
-
await stream_writer.wait_closed():- 等待传输层关闭,确保连接已经完全关闭。
运行主函数
asyncio.run(main())
asyncio.run(main()):- 使用
asyncio.run运行主异步函数main,启动事件循环并执行异步任务。
- 使用
总结
- 这段代码构建了一个简单的异步 HTTP GET 客户端,连接到指定主机,发送请求并接收响应。
- 通过
StreamReader和StreamWriter进行异步 I/O 操作,使用生成器函数read_until_empty读取所有响应数据。 - 最终,将接收到的响应数据打印输出,并确保在程序结束前关闭连接。
5、追问:await stream_writer.drain(): 等待写入完成,确保数据已经发送到对端。这个方法还不太理解,请展开详细说下
await stream_writer.drain() 是 asyncio 中 StreamWriter 类的一个方法,用于确保数据已经被成功写入到传输层(通常是 TCP 套接字),并且等待缓冲区中的数据被发送到对端。这对于流量控制和数据传输的正确性非常重要。
详细解释
背景知识
在网络编程中,数据传输并不是即时的,尤其在涉及到 I/O 操作的时候。数据写入缓冲区是一个非阻塞操作,但传输层需要时间将这些数据实际发送到对端。为了避免写入过多的数据超出缓冲区的容量,需要一种机制来控制流量,确保数据能够被顺利发送。
stream_writer.write(data)
stream_writer.write(data)方法将数据写入到StreamWriter的内部缓冲区。这是一个非阻塞操作,意味着它不会等待数据被真正发送到对端。- 数据写入到缓冲区后,
StreamWriter会异步地将这些数据通过底层传输层(通常是 TCP 套接字)发送出去。
await stream_writer.drain()
drain()方法用于等待缓冲区中的数据被发送出去。- 当缓冲区满时,
drain()方法会暂停写操作,直到缓冲区有足够的空间来容纳新的数据。这种机制有助于防止发送过多数据导致缓冲区溢出。 - 通过
await关键字,drain()方法会异步等待缓冲区中的数据被处理,确保在继续写入新的数据之前,前面的数据已经被成功发送。
使用场景
await stream_writer.drain() 主要用于以下场景:
- 流量控制:在连续发送大量数据时,使用
drain()可以防止缓冲区溢出,确保数据按顺序发送并被对端接收。 - 保证数据传输完整性:在发送关键数据后,通过
drain()等待数据被成功发送,可以确保数据已经到达传输层,增加传输的可靠性。
6、追问:最好结合流程图来说明writer.drain()的作用,哪里是缓冲区,哪里是传输层,这样比较形象,好理解
通过一个流程图来详细说明 await stream_writer.drain() 的作用,以及缓冲区和传输层在数据传输过程中的关系。
+-------------------+ +----------------------+ +------------------+
| Your Application | | StreamWriter Buffer | | Transport Layer |
+-------------------+ +----------------------+ +------------------+
| | |
| writer.write(chunk) | |
+-------------------------> | |
| | |
| await writer.drain() | |
+-------------------------> | |
| | |
| v v
+-------------------+ +----------------------+ +------------------+
| Write Data to | ---> | Store in Buffer | ---> | Send over Socket |
| StreamWriter | | (Non-blocking) | | (Blocking) |
+-------------------+ +----------------------+ +------------------+
| | |
| | |
| <----------------------- + <----------------------- +
| | |
v v v
+-------------------+ +----------------------+ +------------------+
| Continue Execution| | Buffer is drained | | Data is sent to |
| (when drained) | | (await returns) | | destination |
+-------------------+ +----------------------+ +------------------+
流程说明
-
数据写入缓冲区:
- 在代码中,当你调用
writer.write(chunk)时,数据被写入到StreamWriter的内部缓冲区。这是一个非阻塞操作,也就是说,写操作会立即返回,而不等待数据被真正发送到对端。 - 缓冲区(Buffer):这是
StreamWriter的内部内存区域,用于暂存将要发送的数据。
- 在代码中,当你调用
-
等待缓冲区数据发送:
- 当你调用
await writer.drain()时,程序会异步等待,直到缓冲区中的数据被完全发送到传输层。这一步确保所有暂存的数据都已经从缓冲区发送出去,防止缓冲区溢出。 - 传输层(Transport Layer):这是底层的 TCP 连接,负责实际的数据传输。通过调用
drain(),确保缓冲区中的数据已经被 TCP 传输层接收并处理。
- 当你调用
代码流程解释
import asyncio async def send_data(writer, data): for chunk in data: writer.write(chunk) # 将数据写入缓冲区 await writer.drain() # 等待缓冲区的数据被发送到传输层 writer.close() await writer.wait_closed() async def main(): host = 'www.example.com' port = 80 reader, writer = await asyncio.open_connection(host, port) data = [b'chunk1', b'chunk2', b'chunk3'] # 示例数据块 await send_data(writer, data) asyncio.run(main())
-
writer.write(chunk):- 将数据块写入
StreamWriter的缓冲区。由于这是一个非阻塞操作,程序会立即继续执行,而不等待数据被发送。
- 将数据块写入
-
await writer.drain():- 异步等待,直到缓冲区中的数据被完全发送到传输层。这一步确保所有写入缓冲区的数据都已经被 TCP 连接接收并准备发送。
drain()方法的作用是防止缓冲区溢出,确保数据按顺序发送,尤其是在处理大量数据或快速写入数据时。
-
writer.close()和await writer.wait_closed():- 关闭
StreamWriter,并等待传输层完全关闭连接,确保所有数据都被发送并且连接安全关闭。【顺序不能乱】
- 关闭
总结
- 缓冲区:
StreamWriter的内部区域,用于暂存数据,支持非阻塞写入操作。 - 传输层:底层的 TCP 连接,负责实际的数据传输。
await writer.drain()用于确保缓冲区中的数据被完全发送到传输层,防止缓冲区溢出。
在这个上下文中,"drained" 意味着缓冲区已经被完全清空,所有数据都已经被成功地发送到了底层的传输层,即 TCP 连接。
在异步 I/O 编程中,"drained" 表示数据的发送操作已经完成,缓冲区现在是空的,可以继续写入新的数据。
这种异步机制通过 await writer.drain() 实现流量控制,确保高效的数据传输,同时保证程序的并发性能和响应性。
7、关于关闭连接这一部分: 这两个顺序能颠倒吗,为什么?
finally: stream_writer.close() #D await stream_writer.wait_closed()
在这段代码中,stream_writer.close() 和 await stream_writer.wait_closed() 的顺序不能颠倒。原因如下:
-
stream_writer.close():这一步是关闭写入流,意味着不再向连接中写入数据。如果在关闭之前仍然写入数据,可能会导致未发送的数据丢失或者连接异常关闭。 -
await stream_writer.wait_closed():这一步是等待写入流完全关闭,确保所有的写入操作已经完成并且连接已经被完全关闭。如果在等待关闭之前已经关闭了写入流,就不需要再等待关闭了,因为已经没有数据需要发送了。
因此,stream_writer.close() 应该在 await stream_writer.wait_closed() 之前调用,以确保在关闭连接之前所有的写入操作都已经完成。
8、AsyncGenerator[str, None]: 这种写法是什么意思?返回值是str或者None?
AsyncGenerator[str, None] 是一种类型注解,用于标识一个异步生成器(async generator)的类型。
这种写法不是表示返回值是 str 或 None,而是描述了异步生成器的生成项类型和返回类型。让我们详细解释一下:
AsyncGenerator[str, None] 的含义
AsyncGenerator[str, None]表示这个异步生成器:- 生成项类型(yield type)是
str:即每次生成器生成一个值时,这个值的类型是str。 - 返回类型(return type)是
None:当生成器完成执行时,它的返回类型是None。
- 生成项类型(yield type)是
9、如下代码如何理解 [example8_5.py]
import asyncio import sys from asyncio import StreamReader async def create_stdin_reader() -> StreamReader: stream_reader = asyncio.StreamReader() protocol = asyncio.StreamReaderProtocol(stream_reader) loop = asyncio.get_running_loop() await loop.connect_read_pipe(lambda: protocol, sys.stdin) return stream_reader
首先,这段代码使用了Python的asyncio库,它提供了异步编程的支持。异步编程可以让程序在等待某些操作完成时(比如IO操作)去执行其他操作,提高了效率。
import asyncio import sys from asyncio import StreamReader
这里导入了一些需要用到的模块和类。sys模块提供了一些与Python解释器交互的功能。StreamReader是asyncio库中的一个类,用于异步读取数据流。
async def create_stdin_reader() -> StreamReader:
这一行定义了一个异步函数create_stdin_reader, 它的返回值类型是StreamReader。async和await是Python中异步编程的关键字。
stream_reader = asyncio.StreamReader()
protocol = asyncio.StreamReaderProtocol(stream_reader)
这两行创建了一个StreamReader对象和一个StreamReaderProtocol对象。StreamReader用于读取数据流, 而StreamReaderProtocol是一种与底层传输层交互的协议。
loop = asyncio.get_running_loop()
这一行获取当前运行的事件循环。事件循环是asyncio库中的一个重要概念, 它负责管理和调度异步任务的执行。
await loop.connect_read_pipe(lambda : protocol, sys.stdin)
这一行是关键所在。loop.connect_read_pipe是一个异步方法, 它将StreamReaderProtocol协议连接到sys.stdin(标准输入流)。
lambda : protocol是一个lambda函数, 它返回protocol对象, 用于在需要时创建协议对象。await关键字用于等待这个异步操作完成。
return stream_reader
最后,函数返回创建的stream_reader对象。
总的来说,这段代码的作用是创建一个StreamReader对象,并将其连接到标准输入流(sys.stdin)。
这样,我们就可以使用异步的方式从标准输入中读取数据了。这在构建一些异步命令行程序或交互式程序时会很有用。
虽然代码中涉及了一些新的概念,如asyncio、事件循环、协议等,但它们都是为了实现异步编程而设计的。
10、如下代码报错的原因理解
import asyncio from asyncio import StreamReader from util import delay import sys async def create_stdin_reader() -> StreamReader: stream_reader = asyncio.StreamReader() protocol = asyncio.StreamReaderProtocol(stream_reader) loop = asyncio.get_running_loop() await loop.connect_read_pipe(lambda: protocol, sys.stdin) return stream_reader async def main(): stdin_reader = await create_stdin_reader() while True: delay_time = await stdin_reader.readline() asyncio.create_task(delay(int(delay_time))) asyncio.run(main())
这段代码在 Windows 上报错的原因是 Windows 并不支持 loop.connect_read_pipe 方法。
这是因为 Windows 上的标准输入流 sys.stdin 不支持传统的管道概念。
在 Unix-like 系统中,像终端这样的输入输出设备被视为管道文件。
因此,loop.connect_read_pipe 可以将协议连接到这个管道文件上,以实现异步读取输入。
但在 Windows 上,标准输入流是一个常规文件,无法使用 connect_read_pipe 方法。
在Windows上,loop.connect_read_pipe 会引发错误,因为 Windows 对 asyncio 和 IO 流处理有一些限制。
要在 Windows 上处理这个问题,可以使用线程来处理标准输入, 然后将其传递给 asyncio 事件循环。
以下是修改后的代码,添加了线程来读取标准输入,而不改变原有代码的目标:
import asyncio from asyncio import StreamReader from util import delay import sys import threading async def create_stdin_reader() -> StreamReader: stream_reader = asyncio.StreamReader() loop = asyncio.get_running_loop() def read_input(): while True: line = sys.stdin.readline() loop.call_soon_threadsafe(lambda: stream_reader.feed_data(line.encode())) threading.Thread(target=read_input, daemon=True).start() return stream_reader async def main(): stdin_reader = await create_stdin_reader() while True: delay_time = await stdin_reader.readline() asyncio.create_task(delay(int(delay_time))) asyncio.run(main())
尽量通俗易懂地解释这段代码。
import asyncio from asyncio import StreamReader from util import delay import sys import threading
这部分是导入需要用到的模块。asyncio是Python中用于异步编程的库, StreamReader是从中导入的一个类。
sys模块提供了与Python解释器交互的功能。threading模块用于创建和管理线程。
async def create_stdin_reader() -> StreamReader: stream_reader = asyncio.StreamReader() loop = asyncio.get_running_loop()
这是一个异步函数,用于创建一个StreamReader对象,该对象用于异步读取数据流。l
oop是获取当前运行的事件循环,事件循环是asyncio中负责管理和调度异步任务的核心。
def read_input(): while True: line = sys.stdin.readline() loop.call_soon_threadsafe(lambda: stream_reader.feed_data(line.encode()))
这是一个普通函数,它的作用是持续从标准输入(sys.stdin)中读取数据,并将读取到的数据(line)传递给stream_reader对象进行处理。
call_soon_threadsafe是一个线程安全的方法,用于将任务安全地提交到事件循环中。
threading.Thread(target=read_input, daemon=True).start()
这一行创建了一个新线程, 并在其中运行read_input函数。
daemon=True表示这个线程是一个后台线程,当主线程结束时,它也会自动结束。
return stream_reader
函数返回创建的stream_reader对象。
async def main(): stdin_reader = await create_stdin_reader() while True: delay_time = await stdin_reader.readline() asyncio.create_task(delay(int(delay_time)))
这是主函数,它首先获取create_stdin_reader函数返回的stdin_reader对象。
然后进入一个无限循环,每次循环都会从stdin_reader中异步读取一行输入(这就是我们之前创建的线程的作用), 并根据读取到的数字(延迟时间)创建一个新的异步任务delay。
asyncio.run(main())
这一行启动了asyncio事件循环,并在其中运行main函数。
总的来说,这段代码的作用是创建一个新线程持续从标准输入读取数据,将读取到的数据传递给StreamReader对象进行处理。主程序则通过StreamReader异步读取这些数据,并根据读取到的数字创建延迟任务。
虽然代码中涉及了一些新概念,如线程、事件循环等,但它们都是为了实现异步读取标准输入的目的而设计的。
11、python中,请解释下什么tty这个包是什么?是cooked模式、raw模式、cbreak模式的理解 【代码不能跑通,tty模块python也有(后面的解释貌似不太对),这里之后细究,先熟悉下后面结论即可】
下面分别解释tty 模块中的 cooked 模式、raw 模式和 cbreak 模式,并给出在 Windows 下的使用示例。
首先,tty 模块提供了一种在终端上操作 tty (远程通信终端) 的方式,允许你控制终端的各种模式和设置。
- Cooked 模式
- 概念:这是默认模式,读取输入时只有在用户按下回车键后,整行数据才会被获取。这种模式提供了行缓冲和行编辑功能。
- 通俗解释:就像我们平时在命令行下输入命令一样,只有敲击回车后,输入的整行内容才会被执行。在输入过程中,可以使用方向键、删除键等进行编辑。
- Windows 示例:在 Windows 下,默认情况下就是 Cooked 模式,无需特殊设置。
- Raw 模式
- 概念:在这种模式下,每个输入的字符会被立即传递给程序,而不需要按回车键。数据不经过任何处理,直接从终端传递到程序。
- 通俗解释:你每敲一个键,程序就会立即收到这个字符,而不需要等待整行输入完成。这种模式通常用于编写文本编辑器或命令行界面。
Windows 示例
import msvcrt def enable_raw_mode(): import os msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) def disable_raw_mode(): import os msvcrt.setmode(sys.stdin.fileno(), os.O_TEXT) try: enable_raw_mode() print("Enter some characters (press Ctrl+C to exit): ") while True: ch = msvcrt.getch() if ch == b'\x03': # Ctrl+C break print(ch.decode(), end='') except KeyboardInterrupt: pass finally: disable_raw_mode()
在这个示例中,我们使用 msvcrt.setmode() 函数将标准输入流设置为二进制模式,实现 Raw 模式的功能。程序会立即读取并打印出每个输入的字符,直到用户按下 Ctrl+C 退出。
3.Cbreak 模式
- 概念:这种模式类似于 raw 模式,但它保留了一些 minimal 处理,例如处理中断和退出字符。
- 通俗解释:在这种模式下,程序可以立即获取用户输入的字符,但同时也能响应一些特殊的控制字符,比如 Ctrl+C 用于中断程序。
Windows 示例
import msvcrt def enable_cbreak_mode(): import os msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) def disable_cbreak_mode(): import os msvcrt.setmode(sys.stdin.fileno(), os.O_TEXT) try: enable_cbreak_mode() print("Enter some characters (press Ctrl+C to exit): ") while True: ch = msvcrt.getch() if ch == b'\x03': # Ctrl+C break print(ch.decode(), end='') except KeyboardInterrupt: pass finally: disable_cbreak_mode()
这个示例与 Raw 模式的示例非常相似,只是我们将模式名称改为了 Cbreak 模式。在 Windows 下,由于没有 tty 模块,无法完全区分 Raw 模式和 Cbreak 模式,但我们可以通过捕获 Ctrl+C 来模拟 Cbreak 模式的行为。
总结:
- tty 模块允许你控制终端的各种模式和设置,包括 cooked 模式、raw 模式和 cbreak 模式。
- Cooked 模式是默认模式,只有在用户按下回车键后,整行输入才会被获取。这种模式提供了行缓冲和行编辑功能。
- Raw 模式会立即获取每个输入的字符,不需要等待回车键。这种模式通常用于编写文本编辑器或命令行界面。
- Cbreak 模式类似于 raw 模式,但它保留了对某些特殊字符(如 Ctrl+C)的处理功能。
在 Windows 下,由于没有 tty 模块,我们需要使用 msvcrt 模块来实现类似的功能。通过设置标准输入流的模式,我们可以模拟 Raw 模式和 Cbreak 模式的行为。不过,与 Unix 系统相比,Windows 的控制台 I/O 操作存在一些局限性和差异。
12、 shutil包的作用,shutil.get_terminal_size()的作用?
shutil 是 Python 内置的一个模块,主要用于高级文件操作, 如复制、移动、删除等。但是它也包含了一些其他实用函数,其中之一就是 get_terminal_size()。
shutil.get_terminal_size() 函数的作用是获取当前终端窗口的大小,以行和列的形式返回一个元组 (lines, columns)。其中,lines 表示终端窗口的行数,columns 表示终端窗口的列数(宽度)。
这个函数通常用于需要根据终端窗口的大小调整输出格式或布局的情况。例如,在命令行工具或文本用户界面(TUI)中,我们可以使用它来确保输出不会超出终端窗口的边界,或者根据窗口大小动态调整布局。
下面是一个简单的示例,演示如何使用 get_terminal_size() 函数:
import shutil # 获取终端窗口的大小 lines, columns = shutil.get_terminal_size() print(f"Terminal size: {lines} lines, {columns} columns") # 根据终端大小打印一行星号 print("*" * columns)
在这个例子中,我们首先使用 shutil.get_terminal_size() 获取终端窗口的行数和列数。然后,我们打印出这些值,并根据列数打印出一行星号。
输出可能看起来像这样:
Terminal size: 80 lines, 24 columns # [in mypc] ************************
需要注意的是,get_terminal_size() 函数在不同的操作系统和终端环境下可能有不同的实现方式, 因此其准确性和可用性也可能有所不同。
如果无法获取终端大小,它会返回合理的默认值。
总的来说,shutil.get_terminal_size() 函数提供了一种简单的方式来获取当前终端窗口的大小,
这对于需要根据终端大小调整输出或布局的程序非常有用。它是 Python 标准库中的一个实用工具函数。

浙公网安备 33010602011771号