安装
websocket不是python的自带库,需要下载安装:
pip install websockets
案例
client:
import asyncio
import websockets
async def hello():
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
# 发送消息给服务器
message = "Hello, WebSocket!"
await websocket.send(message)
print(f"发送消息: {message}")
# 接收服务器的回复
response = await websocket.recv()
print(f"收到回复: {response}")
if __name__ == "__main__":
asyncio.run(hello())
serve:
import asyncio
import websockets
async def echo(websocket):
"""处理单个客户端的连接,接收消息并原样返回"""
async for message in websocket:
print(f"收到客户端消息: {message}")
# 将消息回显给客户端
await websocket.send(f"服务器回显: {message}")
async def main():
# 启动 WebSocket 服务器,绑定到本地 8765 端口
async with websockets.serve(echo, "localhost", 8765):
print("WebSocket 服务器已启动,监听 ws://localhost:8765")
await asyncio.Future() # 保持服务器运行
if __name__ == "__main__":
asyncio.run(main())
每一个async的作用--链接。
1. async with websockets.connect(uri) as websocket:
-
异步上下文管理器 。进入该上下文时,会执行
websockets.connect(uri)的异步操作(建立 WebSocket 连接),并且等待 连接建立完成,期间当前协程会暂停,让出事件循环。 -
退出上下文时,会自动等待连接关闭的异步操作完成。
-
async with保证了在进入和退出时正确地处理异步资源的获取与释放。
2. await websocket.send(message)
-
等待 发送操作完成。
websocket.send是一个异步方法,它将消息写入缓冲区并可能等待网络发送完成。使用await后,当前协程(调用send()的协程)会暂停,直到数据发送完毕或出错,才会返回当前协程,期间事件循环可以执行其他任务。 -
注意:这里的"等待发送完成"并不一定意味着数据已经到达对方,而是指数据已进入内核缓冲区,可以继续发送后续消息。
3. await websocket.recv()
-
等待 接收服务器的回复。这是一个异步阻塞操作:如果没有消息到达,当前协程 (调用recv()的协程)会暂停,让出事件循环;当收到完整消息后,协程恢复并返回消息内容。
-
这体现了异步 I/O 的核心:在等待网络数据时,CPU 可以处理其他任务。
4. async for message in websocket:
-
异步迭代 。
websocket对象是一个异步可迭代对象,每次迭代都会等待下一条消息的到来。 -
循环体会在收到消息时执行,当没有消息时,当前协程暂停,事件循环可以运行其他客户端的协程。
-
这种写法避免了手动调用
recv()和循环控制,是处理消息流的简洁方式。
-- async 的核心目的就是让协程在"没事干"(比如等待网络、磁盘 I/O)的时候主动让出事件循环的控制权,从而让其他就绪的协程得以执行。
--await 则是调用了某个异步接口(async def修饰)之后,异步接口没有完成之前,当前执行的协程不会往下执行,需要等待异步接口完成,期间如果异步接口需要等待,则当前协程则会让出事件循环控制权。
| 关键字/语法 | 作用 |
|---|---|
async def |
定义协程函数,调用返回协程对象 |
async with |
异步上下文管理器,进入和退出时自动等待异步操作 |
async for |
异步迭代器,每次迭代自动等待下一个元素 |
await |
挂起当前协程,等待一个可等待对象(协程、任务、Future、异步方法等)完成,期间事件循环运行其他任务 |
这样每次调用一个接口的时候都要加上一个async就很麻烦,既然已经使用async def定义协程函数了,为什么协程函数里边还要在每一个异步函数之前加上await,async with已经修饰这代码块了,为什么代码快中还要在调用的每一个函数前加上await?
1. async def 只是定义了一个协程工厂
当你写:
async def hello():
...
你只是定义了一个协程函数 。调用它(例如 hello())并不会执行函数体内的任何代码,而是立即返回一个协程对象 。这个协程对象需要被事件循环调度(比如通过 await 或 asyncio.create_task)才能真正开始执行。
换句话说:async def 只是让函数具备了"可暂停"的能力,但不会自动运行。
2. await 是实际执行异步操作的指令
在协程内部,当你调用一个异步函数 (比如 websocket.send(message)),它返回的是一个可等待对象 (awaitable),通常是另一个协程对象。如果你不加 await,这个协程对象就被丢弃了,里面的代码永远不会执行。这就像你写了一个函数却没有调用它。
因此,必须使用 await 来告诉事件循环:"请执行这个异步操作,并在它完成之前,我可以让出控制权。" 正是 await 触发了真正的 I/O 操作,并让协程在等待期间挂起。
接口
websocket.serve
async with websockets.serve(self.handler, self.host, self.port):
print(f"WebSocket 服务器已启动,监听 {self.host}:{self.port}")
await asyncio.Future()
既然是服务端,那么应该不用指定ip,为什么serve还要指定ip; asyncio.Future()的作用为什么可以保持服务器运行?
问题1:为什么服务端还需要指定 IP?
在网络编程中,服务端必须明确指定要监听的地址和端口 。这是因为一台计算机可能拥有多个网络接口(网卡)(例如:本地回环 127.0.0.1、局域网 IP、公网 IP 等)。通过指定 IP,你可以控制服务端仅接受来自特定接口的连接:
-
"localhost"或"127.0.0.1":只允许本机上的客户端连接,外部机器无法访问。常用于本地开发或测试。 -
"0.0.0.0":监听所有可用的网络接口,允许任何能到达该主机的客户端连接(包括本机和远程)。 -
具体的局域网 IP:只允许通过该 IP 地址访问。
问题2:await asyncio.Future() 为什么能保持服务器运行?
理解 asyncio.Future
asyncio.Future 是一个占位符对象,代表一个将来会完成 的异步操作的结果。它类似于并发编程中的"Promise"。默认情况下,一个新建的 Future 对象处于未完成 状态,除非你手动调用 .set_result() 或 .set_exception() 来让它完成。