WebSocket
一,为什么需要 WebSocket?
因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。
二,WebSocket 简介
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易,一种在单个TCP连接上进行全双工通讯的协议。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
WebSocket 是独立的、创建在 TCP 上的协议,和 HTTP 的唯一关联是使用 HTTP 协议的101状态码进行协议切换。
# 基于WebSocket的url示例
ws://example.com:80/some/path
三,WebSocket 优点
1)较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;
2)更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
3)保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;
4)更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;
5)可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。
四,心跳检测和重连机制?
网络中的接收和发送数据都是使用 Socket 实现的。但是如果此套接字已经断开,那发送数据和接收数据的时候就一定会有问题。可是如何判断这个套接字是否还可以使用呢?这个就需要在系统中创建心跳机制。所谓 “心跳” 就是定时发送一个自定义的结构体(心跳包或心跳帧),让对方知道自己 “在线”,以确保链接的有效性。而所谓的心跳包就是客户端定时发送简单的信息给服务器端告诉它我还在而已。代码就是每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息,如果服务端几分钟内没有收到客户端信息则视客户端断开。在 WebSocket 协议中定义了 心跳 Ping 和 心跳 Pong 的控制帧。如果发现链接断开了就需要重连,这个就是WebSocket的心跳检测和重连机制。
重连机制里服务端只接受消息:
import asyncio
import websockets
async def hello(websocket, path):
while True:
name = await websocket.recv()
print(f"< {name}")
start_server = websockets.serve(hello, "localhost", 8765)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
五,示例代码
1.一对一短链接示例
# 用于搭建webscocket服务器,在本地8765端口启动,接收到消息后会在原消息前加上`I got your message:再返回去
# ----------- server 端 server.py-----------
import asyncio
import websockets
async def echo(websocket, path):
async for message in websocket:
message = "I got your message: {}".format(message)
await websocket.send(message)
asyncio.get_event_loop().run_until_complete(websockets.serve(echo, 'localhost', 8765))
asyncio.get_event_loop().run_forever()
# ----------- client 端 client.py-----------
import asyncio
import websockets
async def hello(uri):
async with websockets.connect(uri) as websocket:
await websocket.send("hello world")
print("< HELLO WORLD")
while True:
recv_text = await websocket.recv()
print("> {}".format(recv_text))
asyncio.get_event_loop().run_until_complete(hello('ws://localhost:8765'))
# 先执行`server.py`,然后执行`client.py`,`client.py`的输出结果如下:
< Hello world!
> I got your message: Hello world!
> 2022-08-24 15:11:50
await websockets.server.serve(ws_handler, host=None, port=None)
ws_handler:函数名称,该函数用来处理一个连接,它必须是一个协程函数,接收2个参数:WebSocketServerProtocol、path。
host:ip地址
port:端口
2.一对一长连接示例
# ----------- server 端 -----------
import asyncio
import websockets
async def producer_handler(websocket, path):
while True:
message = input('please input:')
await websocket.send(message)
start_server = websockets.serve(producer_handler, 'localhost', 8765)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
# ----------- client 端 -----------
import asyncio
import websockets
async def consumer_handler():
async with websockets.connect('ws://localhost:8765') as websocket:
async for message in websocket:
print(message)
asyncio.get_event_loop().run_until_complete(consumer_handler())
3.一对多长连接示例
# ----------- server 端 -----------
import asyncio
import logging
import websockets
logging.basicConfig()
USERS = set()
async def notify_users():
# 对注册列表内的客户端进行推送
if USERS: # asyncio.wait doesn't accept an empty list
message = input('please input:')
await asyncio.wait([user.send(message) for user in USERS])
async def register(websocket):
USERS.add(websocket)
await notify_users()
async def unregister(websocket):
USERS.remove(websocket)
await notify_users()
async def counter(websocket, path):
# register(websocket) sends user_event() to websocket
await register(websocket)
try:
# 处理客户端数据请求 (业务逻辑)
async for message in websocket:
print(message)
finally:
await unregister(websocket)
asyncio.get_event_loop().run_until_complete(websockets.serve(counter, 'localhost', 6789))
asyncio.get_event_loop().run_forever()
# ----------- client 端 -----------
import asyncio
import websockets
async def consumer_handler():
async with websockets.connect('ws://localhost:6789') as websocket:
async for message in websocket:
print(message)
asyncio.get_event_loop().run_until_complete(consumer_handler())
4.websockets-routes
实现路由自由(安装 pip install websockets-routes)
# 希望开灯的uri为/light/on, 关灯为/light/off,其它的uri返回错误,这里就需要用到路由了
# ----------- server 端 -----------
import asyncio
import websockets
import websockets_routes
# 初始化一个router对象
router = websockets_routes.Router()
@router.route("/light/{status}") #添加router的route装饰器,它会路由uri。
async def light_status(websocket, path):
async for message in websocket:
print("got a message:{}".format(message))
print(path.params['status'])
await asyncio.sleep(2) # 假装模拟去操作开关灯
if (path.params['status'] == 'on'):
await websocket.send("the light has turned on")
elif path.params["status"] == 'off':
await websocket.send("the light has turned off")
else:
await websocket.send("invalid params")
async def main():
# rooter是一个装饰器,它的__call__函数有三个参数,第一个参数是self。
# 所以这里我们需要使用lambda进行一个转换操作,因为serv的wshander函数只能接收2个参数
async with websockets.serve(lambda x, y: router(x, y), "127.0.0.1", 8765):
print("======")
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())
# ----------- clint 端 -----------
import asyncio
import websockets
async def hello():
try:
async with websockets.connect('ws://127.0.0.1:8765/light/on') as websocket:
light_addr = '00-12-4b-01'
await websocket.send(light_addr)
recv_msg = await websocket.recv()
print(recv_msg)
except websockets.exceptions.ConnectionClosedError as e:
print("connection closed error")
except Exception as e:
print(e)
await hello()
# 效果如下
ws://127.0.0.1:8765/light/on #连接正常,收到消息:the light has turned on
ws://127.0.0.1:8765/light/off#连接正常,收到消息:the light has turned off
ws://127.0.0.1:8765/light/1#连接正常,收到消息:invalid params
ws://127.0.0.1:8765/switch/on#连接异常,收到异常消息:connection closed error
5.安全示例
# 安全的WebSocket连接可以提高机密性和可靠性,因为它们可以降低不良代理干扰的风险。WSS需要像HTTPS这样的TLS证书。
# ----------- server 端 -----------
import asyncio
import pathlib
import ssl
import websockets
async def hello(websocket, path):
name = await websocket.recv()
print(f"< {name}")
greeting = f"Hello {name}!"
await websocket.send(greeting)
print(f"> {greeting}")
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(pathlib.Path(__file__).with_name('localhost.pem'))
start_server = websockets.serve(
hello, 'localhost', 8765, ssl=ssl_context)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
# ----------- clint 端 -----------
import asyncio
import pathlib
import ssl
import websockets
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.load_verify_locations(pathlib.Path(__file__).with_name('localhost.pem'))
async def hello():
async with websockets.connect(
'wss://localhost:8765', ssl=ssl_context) as websocket:
name = input("What's your name? ")
await websocket.send(name)
print(f"> {name}")
greeting = await websocket.recv()
print(f"< {greeting}")
asyncio.get_event_loop().run_until_complete(hello())
6.同步示例
服务端
# WebSocket服务器可以从客户端接收事件,处理它们以更新应用程序状态,并在客户端之间同步结果状态。这是一个示例,任何客户端都可以递增或递减计数器。更新将传播到所有连接的客户端。asyncio的并发模型保证更新被序列化。
import asyncio
import json
import logging
import websockets
logging.basicConfig()
STATE = {'value': 0}
USERS = set()
def state_event():
return json.dumps({'type': 'state', **STATE})
def users_event():
return json.dumps({'type': 'users', 'count': len(USERS)})
async def notify_state():
if USERS: # asyncio.wait doesn't accept an empty list
message = state_event()
await asyncio.wait([user.send(message) for user in USERS])
async def notify_users():
if USERS: # asyncio.wait doesn't accept an empty list
message = users_event()
await asyncio.wait([user.send(message) for user in USERS])
async def register(websocket):
USERS.add(websocket)
await notify_users()
async def unregister(websocket):
USERS.remove(websocket)
await notify_users()
async def counter(websocket, path):
# register(websocket) sends user_event() to websocket
await register(websocket)
try:
await websocket.send(state_event())
async for message in websocket:
data = json.loads(message)
if data['action'] == 'minus':
STATE['value'] -= 1
await notify_state()
elif data['action'] == 'plus':
STATE['value'] += 1
await notify_state()
else:
logging.error(
"unsupported event: {}", data)
finally:
await unregister(websocket)
asyncio.get_event_loop().run_until_complete(websockets.serve(counter, 'localhost', 6789))
asyncio.get_event_loop().run_forever()
客户端:
<!DOCTYPE html>
<html>
<head>
<title>WebSocket demo</title>
<style type="text/css">
body {
font-family: "Courier New", sans-serif;
text-align: center;
}
.buttons {
font-size: 4em;
display: flex;
justify-content: center;
}
.button, .value {
line-height: 1;
padding: 2rem;
margin: 2rem;
border: medium solid;
min-height: 1em;
min-width: 1em;
}
.button {
cursor: pointer;
user-select: none;
}
.minus {
color: red;
}
.plus {
color: green;
}
.value {
min-width: 2em;
}
.state {
font-size: 2em;
}
</style>
</head>
<body>
<div class="buttons">
<div class="minus button">-</div>
<div class="value">?</div>
<div class="plus button">+</div>
</div>
<div class="state">
<span class="users">?</span> online
</div>
<script>
var minus = document.querySelector('.minus'),
plus = document.querySelector('.plus'),
value = document.querySelector('.value'),
users = document.querySelector('.users'),
websocket = new WebSocket("ws://127.0.0.1:6789/");
minus.onclick = function (event) {
websocket.send(JSON.stringify({action: 'minus'}));
}
plus.onclick = function (event) {
websocket.send(JSON.stringify({action: 'plus'}));
}
websocket.onmessage = function (event) {
data = JSON.parse(event.data);
switch (data.type) {
case 'state':
value.textContent = data.value;
break;
case 'users':
users.textContent = (
data.count.toString() + " user" +
(data.count == 1 ? "" : "s"));
break;
default:
console.error(
"unsupported event", data);
}
};
</script>
</body>
</html>