使用生成器包装套接字
在这一步中,你将学习如何使用生成器来包装套接字操作。这是一个非常重要的概念,尤其在异步编程中。异步编程允许你的程序同时处理多个任务,而无需等待一个任务完成后再开始另一个任务。使用生成器包装套接字操作可以使你的代码更高效且更易于管理。
理解问题
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(b'GOT:' + data)
print('Connection closed')
client.close()
在这段代码中,我们使用了 yield
关键字。yield
关键字在 Python 中用于创建生成器。生成器是一种特殊类型的迭代器,它允许你暂停和恢复函数的执行。在这里,yield
用于指示服务器何时准备好接受连接,或者客户端处理程序何时准备好接收或发送数据。然而,手动的 yield
语句将事件循环的内部工作机制暴露给了用户。这意味着用户必须了解事件循环的工作原理,这会使代码更难理解和维护。
创建 GenSocket 类
让我们创建一个 GenSocket
类,用生成器来包装套接字操作。这将使我们的代码更简洁、更易读。通过将套接字操作封装在一个类中,我们可以向用户隐藏事件循环的细节,专注于服务器的高级逻辑。
- 在编辑器中打开
server.py
文件:
cd /home/labex/project
这个命令将当前目录更改为包含 server.py
文件的项目目录。进入正确的目录后,你可以使用你喜欢的文本编辑器打开该文件。
- 在文件末尾、现有函数之前添加以下
GenSocket
类:
class GenSocket:
"""
A generator-based wrapper for socket operations.
"""
def __init__(self, sock):
self.sock = sock
def accept(self):
"""Accept a connection and return a new GenSocket"""
yield 'recv', self.sock
client, addr = self.sock.accept()
return GenSocket(client), addr
def recv(self, maxsize):
"""Receive data from the socket"""
yield 'recv', self.sock
return self.sock.recv(maxsize)
def send(self, data):
"""Send data to the socket"""
yield 'send', self.sock
return self.sock.send(data)
def __getattr__(self, name):
"""Forward any other attributes to the underlying socket"""
return getattr(self.sock, name)
这个 GenSocket
类作为套接字操作的包装器。__init__
方法使用一个套接字对象初始化该类。accept
、recv
和 send
方法执行相应的套接字操作,并使用 yield
来指示操作何时准备好。__getattr__
方法允许该类将任何其他属性转发给底层的套接字对象。
- 现在,修改
tcp_server
和 echo_handler
函数以使用 GenSocket
类:
def tcp_server(address, handler):
sock = GenSocket(socket(AF_INET, SOCK_STREAM))
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind(address)
sock.listen(5)
while True:
client, addr = yield from sock.accept()
tasks.append(handler(client, addr))
def echo_handler(client, address):
print('Connection from', address)
while True:
data = yield from client.recv(1000)
if not data:
break
yield from client.send(b'GOT:' + data)
print('Connection closed')
client.close()
注意,显式的 yield 'recv', sock
和 yield 'send', client
语句已被更简洁的 yield from
表达式所取代。yield from
关键字用于将执行委托给另一个生成器。这使得代码更易读,并向用户隐藏了事件循环的细节。现在,代码看起来更像普通的函数调用,用户不必担心事件循环的内部工作机制。
- 让我们添加一个简单的测试函数,以演示如何使用我们的服务器:
def run_server():
"""Start the server on port 25000"""
tasks.append(tcp_server(('localhost', 25000), echo_handler))
try:
event_loop()
except KeyboardInterrupt:
print("Server stopped")
if __name__ == '__main__':
print("Starting echo server on port 25000...")
print("Press Ctrl+C to stop")
run_server()
这段代码更易读且更易于维护。GenSocket
类封装了 yield
逻辑,使服务器代码能够专注于高级流程,而不是事件循环的细节。run_server
函数在端口 25000 上启动服务器,并处理 KeyboardInterrupt
异常,允许用户通过按 Ctrl+C
停止服务器。
理解优点
yield from
方法有几个优点:
- 代码更简洁:套接字操作看起来更像普通的函数调用。这使得代码更易于阅读和理解,尤其对于初学者来说。
- 抽象化:事件循环的细节对用户隐藏。用户无需了解事件循环的工作原理即可使用服务器代码。
- 可读性强:代码更能表达它在做什么,而不是如何做。这使得代码更具自解释性,更易于维护。
- 可维护性高:对事件循环的更改不需要更改服务器代码。这意味着如果你将来需要修改事件循环,可以在不影响服务器代码的情况下进行。
这种模式是迈向现代 async/await
语法的垫脚石,我们将在下一步中探索。async/await
语法是在 Python 中编写异步代码的更高级、更简洁的方式,理解 yield from
模式将帮助你更轻松地过渡到它。