# Python fastAPI實作Server sent events(SSE) 使用Python fastAPI實作Server sent events(SSE),並取代輪詢(polling)的方式來更改牌桌狀態 ### 輪詢(polling)的缺點 1. Client持續向Server發起請求,這樣不間斷的請求會加大Server的壓力,還可能因為網路延遲而影響數據的時效性 2. 主要可以透過WebSocket、Server sent events這兩種方式來解決。 ### 簡單介紹一下Server sent events 1. 基於HTTP。單向傳播(Server => Client),是WebSocket的輕量替代方案 2. 是一個長連接,因此要設定`media_type = "text/event-stream"`,返回的是字串格式 3. 需要`獨立占用一個連線`,不能跟原本的API共存同一個port ### 原本polling的方式 #### 原本Server端 - 提供一個GET的接口,給前端一直打 ```python @app.get("/games/{game_id}/player/{player_id}/status", response_model=GameStatus) async def get_status(game_id: str, player_id: str): return service.get_status(game_id, player_id) ``` #### 原本Client端 - 透過`useEffect` + `setInterval`的方式,來實作出輪詢 ```react const [gameStatus, setGameStatus] = useState<GameStatus | null>(null); useEffect(() => { // set GameStatus before the refresher triggered GetGameStatus(gameId, username).then((status: GameStatus) => { if (!isEqual(gameStatus, status)) { setGameStatus(status); } }); const intervalId = setInterval(() => { // auto-refresh GameStatus GetGameStatus(gameId, username).then((status: GameStatus) => { if (!isEqual(gameStatus, status)) { setGameStatus(status); } }); }, 1 * 1000); return () => { clearInterval(intervalId); }; }, []); ``` --- ### 後來改用SSE的方式 #### 後來Server端 - 提供一個GET的接口,讓Client進行連接,讓Server持續傳遞資料 - 返回的資料格式 1. id: 表明id 2. event: 消息的類型 3. data: 消息的資料(必須是string) 4. retry: Client重連的時間。單位是毫秒 ```python @app.get("/stream/{game_id}/player/{player_id}/status") async def message_stream(request: Request, game_id: str, player_id: str): async def event_generator(): while True: # If client closes connection, stop sending events if await request.is_disconnected(): break # Checks for new messages and return them to client if any status = service.get_status(game_id, player_id) status_str = json.dumps(status) if not status.get("final_player", None): yield { "event": "new_message", "id": "message_id", "retry": RETRY_TIMEOUT, "data": status_str, } else: yield {"event": "end", "retry": RETRY_TIMEOUT, "data": status_str} await asyncio.sleep(STREAM_DELAY) return EventSourceResponse(event_generator()) ``` #### 後來Client端 - 使用addEventListener方式來處理對應事件的處理方式 ```react let evtSource: EventSource | null = new EventSource(`${BACKEND_SSE_URL}/stream/${gameId}/player/${playerId}/status`); useEffect(() => { if (evtSource === null) { return } evtSource.addEventListener("new_message", function (event) { const data = JSON.parse(String(event.data)); if (!isEqual(gameStatus, data)) { setGameStatus(data); } }) evtSource.addEventListener("end", function(event) { console.log('Handling end....') if (evtSource === null) { return } evtSource.close(); evtSource = null; }); }, []) evtSource.onerror = () => { console.log("on error!") if (evtSource === null) { return } evtSource.close(); evtSource = null; }; ``` ### 總結 - 實作上覺得SSE的開發方式比一般的API複雜許多,需要設定連線終止的條件(`event = end 促使Client關閉連線`) - 還有資料傳給前端的資料格式也跟API不一樣,API可以傳遞json、string等等格式,SSE只能傳遞string格式,再讓前端自己轉成json ### 遇到的問題 - 在實作過程中,遇到蠻多難題的。有人對以下問題有想法的都歡迎提出討論~ 1. 為甚麼不能跟原本的API共存,當SSE與Client進行連接時,後續的API卻沒辦法接收到前端打進來的請求了 - **因為需要`獨立占用一個連線`,所以不能跟原本的API共存同一個port** 2. 當Server端意外的關閉,導致SSE連線斷開,Client端還是會一直在進行retry的動作,就算呼叫了`evtSource.close()`,F12打開卻還是會一直嘗試連接Server端 ``` GET http://127.0.0.1:8081/stream/5ca5bcfc6cc944f6aae5fd8f5d049c55/player/44444/status net::ERR_CONNECTION_REFUSED GET http://127.0.0.1:8081/stream/5ca5bcfc6cc944f6aae5fd8f5d049c55/player/44444/status net::ERR_CONNECTION_RESET ``` - **目前有嘗試在evtSource.onerror中設定close(),並把evtSource設定成null** ```react evtSource.onerror = () => { console.log("error!!!"); if (evtSource === null) { return } evtSource.close(); evtSource = null; }; ``` ### 參考資料 1. https://sairamkrish.medium.com/handling-server-send-events-with-python-fastapi-e578f3929af1 2. https://blog.csdn.net/weixin_44777680/article/details/114692497