ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python Mastery 练习 8.5 精讲:用生成器实现事件驱动的并发 TCP Echo 服务器

2026/10/5 6:37:05 拓冰建站 浏览量
Python Mastery 练习 8.5 精讲:用生成器实现事件驱动的并发 TCP Echo 服务器 示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载本篇技术指南围绕 Advanced Python Mastery 课程dabeaz 著的Exercise 8.5展开核心主题是用生成器generator充当任务task配合select()系统调用实现一个事件驱动的、可并发处理多个网络连接的 TCP Echo 服务器并在此基础上亲手写一个微型任务调度器。读完本文你将完全掌握send(None)驱动生成器、yield值作为原因/资源协议、基于select()的 I/O 等待队列这三层机制理解现代异步框架如 Python 3.4 加入标准库的asyncio背后的核心思想并能在自己的终端中运行、验证一个支持多客户端并发连接的 echo 服务。关联文档Exercises/soln8_5.md解答、Exercises/ex8_5.md题目仓库中可直接运行的成品位于 Solutions/8_5/multitask.py 与 Solutions/8_5/server.py。背景生成器为什么需要被驱动一个生成器或协程风格的生成器函数永远无法自行执行——它必须由其他代码驱动。例如用于迭代的生成器除非真正执行for循环否则不会产出任何值一组协程除非有人调用它们的send()方法否则永远不会运行。在生成器的高级应用里可以用各种不寻常的方式驱动生成器。Exercise 8.5 分两步a、b演示了这一思想先写一个最朴素的轮询式任务调度器再把它升级成事件驱动 I/O 多路复用的网络服务器调度器。第一部分a生成器作为任务——微型轮询调度器核心代码multitask.py在multitask.py中定义如下代码仓库成品见 Solutions/8_5/multitask.py# multitask.py from collections import deque tasks deque() def run(): while tasks: task tasks.popleft() try: task.send(None) tasks.append(task) except StopIteration: print(Task done)这段代码实现了一个极简的任务调度器它以先进先出FIFO方式循环驱动每个生成器任务从双端队列左侧取出一个任务调用task.send(None)让生成器推进到下一个yield等价于next(task)若正常返回说明任务还没跑完把它放回队列右侧等待下一轮调度若抛出StopIteration说明生成器内部执行完毕打印Task done不再放回。由于collections.deque的popleft/append组合天然形成环形队列多个生成器之间就实现了严格的轮转round-robin调度——这正是协作式多任务的雏形。测试用的生成器函数把下面几个生成器函数追加到multitask.py中# multitask.py ... def countdown(n): while n 0: print(T-minus, n) yield n - 1 def countup(n): x 0 while x n: print(Up we go, x) yield x 1 if __name__ __main__: tasks.append(countdown(10)) tasks.append(countdown(5)) tasks.append(countup(20)) run()注意countdown与countup都含有一个裸yield不产出值、也不接收值它的作用仅仅是把控制权交还给调度器。三个任务countdown(10)、countdown(5)、countup(20)被依次加入tasks队列。运行效果多个生成器的输出交错在一起运行该文件可以看到所有生成器的输出被交错打印T-minus 10 T-minus 5 Up we go 0 T-minus 9 T-minus 4 Up we go 1 T-minus 8 T-minus 3 Up we go 2 T-minus 7 T-minus 2 Up we go 3 T-minus 6 T-minus 1 Up we go 4 T-minus 5 Task done Up we go 5 T-minus 4 Up we go 6 T-minus 3 Up we go 7 T-minus 2 Up we go 8 T-minus 1 Up we go 9 Task done Up we go 10 Up we go 11 Up we go 12 Up we go 13 Up we go 14 Up we go 15 Up we go 16 Up we go 17 Up we go 18 Up we go 19 Task done从输出可以清晰观察到三个现象公平轮转每个任务每轮只推进一个yield三个任务的输出严格交替独立生命周期countdown(5)最先耗尽先打印Task done随后调度器自动把它从队列移除剩余两个任务继续轮转有序收尾countdown(10)次之最后countup(20)独自跑完剩余 10 次并打印Task done。用调度器的视角看每个生成器其实就是一个任务它在yield处主动让出 CPU调度器负责在任务之间切换。这很有趣但还不够有说服力——真正的价值在于下一部分把 I/O 等待也纳入调度。第二部分b生成器服务网络连接——事件驱动并发 Echo 服务器从轮询到事件驱动升级版调度器创建server.py放入以下代码仓库成品见 Solutions/8_5/server.py# server.py from socket import * from select import select from collections import deque tasks deque() recv_wait {} # sock - task send_wait {} # sock - task def run(): while any([tasks, recv_wait, send_wait]): while not tasks: can_recv, can_send, _ select(recv_wait, send_wait, []) for s in can_recv: tasks.append(recv_wait.pop(s)) for s in can_send: tasks.append(send_wait.pop(s)) task tasks.popleft() try: reason, resource task.send(None) if reason recv: recv_wait[resource] task elif reason send: send_wait[resource] task else: raise RuntimeError(Unknown reason %r % reason) except StopIteration: print(Task done)这是第a部分调度器的复杂版本需要仔细研究核心思想在于每个任务不仅会 yield还会说明自己 yield 的原因要接收数据recv还是要发送数据send任务声明意图生成器每次 yield 一个(reason, resource)二元组reason是recv或sendresource是涉及的 socket 对象登记到等待区调度器根据 reason 把resource - task的映射写入recv_wait或send_wait字典sock - task该任务暂时挂起阻塞等待 I/O当tasks队列为空没有任何可运行任务时调用select(recv_wait, send_wait, [])阻塞等待这些 socket 变为可读或可写事件唤醒任务select()返回后把can_recv可读 socket对应的任务从recv_wait.pop(s)取回、can_send可写 socket对应的任务从send_wait.pop(s)取回追加进tasks队列继续运行非法原因报错若 reason 不是recv或send抛出RuntimeError(Unknown reason %r % reason)以尽早暴露协议错误。这里select()的作用是I/O 多路复用单个调度循环同时监视所有挂起任务的 socket谁就绪就唤醒谁从而在没有线程的情况下实现并发。应用代码TCP 服务器与 Echo 处理器在server.py中追加以下代码实现一个简单的 echo 服务器# server.py ... def tcp_server(address, handler): sock socket(AF_INET, SOCK_STREAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.bind(address) sock.listen(5) while True: yield recv, sock client, addr sock.accept() tasks.append(handler(client, addr)) def echo_handler(client, address): print(Connection from, address) while True: yield recv, client data client.recv(1000) if not data: break yield send, client client.send(bGOT: data) print(Connection closed) if __name__ __main__: tasks.append(tcp_server((,25000), echo_handler)) run()逐段解读tcp_server(address, handler)监听 socket 的生产者任务。创建 socket 后依次执行setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)允许端口重用便于重启、bind(address)、listen(5)最大挂起连接数为 5。随后进入无限循环yield recv, sock声明我在等这个监听 socket 可读被调度器挂入recv_wait一旦有连接到达accept()立即返回随即把新的handler(client, addr)任务加入tasks队列继续下一轮等待。echo_handler(client, address)每连接的工作任务。先yield recv, client等待客户端数据收到data后若为空if not data: break表示连接关闭则退出循环并打印Connection closed否则yield send, client等待 socket 可写再把bGOT: data回写给客户端——回显内容带GOT:前缀。__main__入口把tcp_server((, 25000), echo_handler)作为唯一初始任务加入队列并启动run()服务器在 25000 端口上监听所有网络接口表示任意地址。每个新连接都会动态生成一个新的echo_handler生成器任务——这正是每连接一个任务one task per connection的并发模型且全程只有单线程。运行与验证用 nc / telnet / Python 测试并发在一个终端窗口运行服务器bash % python3 server.py然后在另一个终端用telnet或nc连接bash % nc localhost 25000 Hello Got: Hello World Got: World如果没有nc或telnet也可以直接用 Python 内置模块bash % python3 -m telnetlib localhost 25000 Hello Got: Hello World Got: World验证要点每发送一行文本服务器都会回显带GOT:前缀的同一行内容说明收发链路正常同时打开多个客户端连接它们会并发工作、互不阻塞——因为每个连接的任务都在各自的yield处让出控制权调度器通过select()统一等待所有 socket 的 I/O 事件。从手写调度器到 asyncio这条思路的深远意义Exercise 8.5 结尾特别点明这种巧妙的生成器用法日常编码中你不太可能直接去写但它正是asyncioPython 3.4 加入标准库内部使用的机制。换句话说本练习用一个约 60 行的server.py亲手复刻了现代异步事件循环的核心骨架手写实现Exercise 8.5asyncio 中的对应物tasks deque()run()循环事件循环event loop的任务队列与调度task.send(None)驱动生成器协程的推进coro.send(None)/awaityield recv, sock/yield send, sock声明意图协程内await等待 I/O 完成recv_wait/send_wait字典sock → task事件循环的 I/O 等待表select(recv_wait, send_wait, [])底层 I/O 多路复用selectors模块StopIteration捕获 →Task done协程完成、任务回调如果你继续学习 Exercises/ex8_6.md会发现这条路线的自然演进先把裸露的yield封装进GenSocket类配合yield from委托生成器代码更干净再给方法加上coroutine装饰器最后把服务器函数改写为async defawait——成品可参考 Solutions/8_6/asyncserver.py其调度器run()与本文版本几乎一模一样只是应用层换成了 async/await 语法。这证明你在这里手写的调度器与 Python 官方异步框架共享同一套底层思想。小结与练习清单Exercise 8.5 的核心收获可以概括为三句话生成器必须被外部代码驱动send(None)或next()是驱动它的唯一手段StopIteration是它的结束信号yield 值可以携带语义yield recv, sock这种(reason, resource)协议让调度器能根据任务意图把任务挂到不同的等待区select() 等待表 单线程并发调度器无事可做时阻塞在select()上socket 就绪事件自动唤醒对应任务从而用几十行代码实现并发网络服务器。仓库中与本练习相关的可执行资源题目原文Exercises/ex8_5.md解答原文Exercises/soln8_5.md成品代码a 部分Solutions/8_5/multitask.py成品代码b 部分Solutions/8_5/server.py后续演进async/await 版Solutions/8_6/asyncserver.py建议按顺序完成先运行multitask.py观察交错输出再运行server.py并用两个终端验证多客户端并发最后对照 Exercises/ex8_6.md 亲手把服务器改写成yield from与 async/await 版本彻底打通生成器 → 协程 → asyncio的认知链路。赞分享示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载相关推荐Presto 日期与时间函数完全指南运算符、时区转换与常用函数详解Presto 日期与时间函数完全指南运算符、时区转换与常用函数详解 导读 本文以 Presto 官方文档《Date and Time Functions an示例工程教程30天自制C服务器Day03用 epoll 实现高并发 echo 服务器30天自制C服务器Day03用 epoll 实现高并发 echo 服务器 本教程是「30天自制C服务器」系列第 3 天讲解 Linux 高并发的示例工程如何用 Tokio 搭建 TCP echo 服务器并用 tokio::spawn 并发处理连接如何用 Tokio 搭建 TCP echo 服务器并用 tokio::spawn 并发处理连接 你要完成的任务是用 Tokio 搭一个 TCP echo 服务后端异步编程通信并发编程上一篇揭秘InboxLayout核心组件InboxScrollView与InboxLayoutBase原理解析下一篇golang-migrate/migrate 的 PostgreSQL 驱动连接 URL、参数配置与多语句模式实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考