背景
- 在實際專案中,可能會通過前端框架使用 WebSocket 和後端進行通訊
- 這裡就來詳細講解下 FastAPI 是如何操作 WebSocket 的
模擬 WebSocket 客戶端
#!usr/bin/env python # -*- coding:utf-8 _*- """ # author: 小菠蘿測試筆記 # blog: https://www.cnblogs.com/poloyy/ # time: 2021/10/5 5:26 下午 # file: 46_websocket.py """ import uvicorn from fastapi import FastAPI, WebSocket from fastapi.responses import HTMLResponse app = FastAPI() html = """ <!DOCTYPE html> <html> <head> <title>小菠蘿聊天室</title> </head> <body> <h1>小菠蘿聊天室</h1> <form action="" onsubmit="sendMessage(event)"> <input type="text" id="messageText" autocomplete="off"/> <button>Send</button> </form> <ul id='messages'> </ul> <script> // 載入頁面,自動建立一個 WebSocket 連線 var ws = new WebSocket("ws://localhost:8080/ws"); // 收到訊息 ws.onmessage = function(event) { // 獲取輸入框的值 var messages = document.getElementById('messages') // 建立一個 li 元素 var message = document.createElement('li') // 接收 event 的 data var content = document.createTextNode(event.data) message.appendChild(content) messages.appendChild(message) }; // 傳送訊息方法 function sendMessage(event) { var input = document.getElementById("messageText") ws.send(input.value) input.value = '' event.preventDefault() } </script> </body> </html> """ # 返回一段 HTML 程式碼給前端 @app.get("/") async def get(): return HTMLResponse(html) @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): # 1、ws 連線 await websocket.accept() while True: # 2、接收客戶端傳送的內容 data = await websocket.receive_text() # 3、服務端傳送內容 await websocket.send_text(f"小菠蘿收到的訊息是: {data}") if __name__ == '__main__': uvicorn.run(app="46_websocket:app", reload=True, host="127.0.0.1", port=8080)
啟動 uvicorn 伺服器,訪問 127.0.0.1:8080/
客戶端、服務端建立 WebSocket 連線成功
傳送聊天資訊
每發一條訊息,均會顯示在列表中
可以在其他地方使用 WebSocket
- Depends
- Security
- Cookie
- Header
- Path
- Query
在依賴項中使用 WebSocket
from typing import Optional import uvicorn from fastapi import FastAPI, WebSocket, Cookie, Query, status, Depends from fastapi.responses import HTMLResponse app = FastAPI() html = """ <!DOCTYPE html> <html> <head> <title>Chat</title> </head> <body> <h1>小菠蘿聊天室</h1> <form action="" onsubmit="sendMessage(event)"> <label>Item ID: <input type="text" id="itemId" autocomplete="off" value="foo"/></label> <label>Token: <input type="text" id="token" autocomplete="off" value="some-key-token"/></label> <button onclick="connect(event)">Connect</button> <hr> <label>Message: <input type="text" id="messageText" autocomplete="off"/></label> <button>Send</button> </form> <ul id='messages'> </ul> <script> var ws = null; function connect(event) { var itemId = document.getElementById("itemId") var token = document.getElementById("token") ws = new WebSocket("ws://localhost:8080/items/" + itemId.value + "/ws?token=" + token.value); ws.onmessage = function(event) { var messages = document.getElementById('messages') var message = document.createElement('li') var content = document.createTextNode(event.data) message.appendChild(content) messages.appendChild(message) }; event.preventDefault() } function sendMessage(event) { var input = document.getElementById("messageText") ws.send(input.value) input.value = '' event.preventDefault() } </script> </body> </html> """ @app.get("/") async def get(): return HTMLResponse(html) async def get_cookie_or_token( websocket: WebSocket, session: Optional[str] = Cookie(None), token: Optional[str] = Query(None) ): # 模擬:如果 session 和 token 都為空,則關閉 websocket if session or token: return session or token await websocket.close(code=status.WS_1008_POLICY_VIOLATION) @app.websocket("/items/{item_id}/ws") async def websocket_depends( websocket: WebSocket, item_id: str, q: Optional[str] = None, # 依賴項 cookie_or_token: str = Depends(get_cookie_or_token) ): # 1、建立 websocket 連線 await websocket.accept() while True: # 2、接收客戶端傳送的內容 data = await websocket.receive_text() # 3、服務端傳送內容 await websocket.send_text(f"cookie or token value is:{cookie_or_token}") if q: # 4、如果有傳查詢引數 q,則再發一條 await websocket.send_text(f"query param value is:{q}") # 5、最後再發一條資訊 await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") if __name__ == '__main__': uvicorn.run(app="46_websocket:app", reload=True, host="127.0.0.1", port=8080)
傳送聊天資訊
不帶查詢引數 q
帶查詢引數 q
當 WebSocket 連線關閉時
await websocket.receive_text() 將引發 WebSocketDisconnect 異常,這不是期望看到的結果
處理斷開連線和多個客戶端
from typing import List import uvicorn from fastapi import FastAPI, WebSocket, WebSocketDisconnect, status from fastapi.responses import HTMLResponse app = FastAPI() html = """ <!DOCTYPE html> <html> <head> <title>Chat</title> </head> <body> <h1>WebSocket Chat</h1> <h2>Your ID: <span id="ws-id"></span></h2> <form action="" onsubmit="sendMessage(event)"> <input type="text" id="messageText" autocomplete="off"/> <button>Send</button> </form> <ul id='messages'> </ul> <script> var client_id = Date.now() document.querySelector("#ws-id").textContent = client_id; var ws = new WebSocket(`ws://localhost:8080/ws/${client_id}`); ws.onmessage = function(event) { var messages = document.getElementById('messages') var message = document.createElement('li') var content = document.createTextNode(event.data) message.appendChild(content) messages.appendChild(message) }; function sendMessage(event) { var input = document.getElementById("messageText") ws.send(input.value) input.value = '' event.preventDefault() } </script> </body> </html> """ # 返回一段 HTML 程式碼給前端 @app.get("/") async def get(): return HTMLResponse(html) # 處理和廣播訊息到多個 WebSocket 連線 class ConnectionManager: def __init__(self): self.active_connections: List[WebSocket] = [] async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) def disconnect(self, websocket: WebSocket): self.active_connections.remove(websocket) async def send_personal_message(self, message: str, websocket: WebSocket): await websocket.send_text(message) async def broadcast(self, message: str): for connection in self.active_connections: await connection.send_text(message) manager = ConnectionManager() @app.websocket("/ws/{client_id}") async def websocket_endpoint(client_id: str, websocket: WebSocket): # 1、客戶端、服務端建立 ws 連線 await manager.connect(websocket) # 2、廣播某個客戶端進入聊天室 await manager.broadcast(f"{client_id} 進入了聊天室") try: while True: # 3、服務端接收客戶端傳送的內容 data = await websocket.receive_text() # 4、廣播某個客戶端傳送的訊息 await manager.broadcast(f"{client_id} 傳送訊息:{data}") # 5、服務端回覆客戶端 await manager.send_personal_message(f"服務端回覆{client_id}:你傳送的資訊是:{data}", websocket) except WebSocketDisconnect: # 6、若有客戶端斷開連線,廣播某個客戶端離開了 manager.disconnect(websocket) await manager.broadcast(f"{client_id} 離開了聊天室") if __name__ == '__main__': uvicorn.run(app="48_websocket_handler:app", reload=True, host="127.0.0.1", port=8080)
- 模擬一個小型聊天室的場景
- 新的客戶端進來,所有人都會收到新客戶端進入聊天室的訊息
- 某個客戶端傳送訊息,所有人都能看到
- 某個客戶端退出了(關閉瀏覽器),所有人都會收到該客戶端退出聊天室的訊息
瀏覽器開啟三個 tab 均訪問 127.0.0.1:8080
關掉其中一個客戶端(tab)