---
# System prepended metadata

title: Python fastAPI實作Server sent events(SSE)

---

# 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
