# 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