聊天室、協作編輯、即時看盤,這些功能的共通點是伺服器要能主動把資料推給瀏覽器,而不是等瀏覽器來問。HTTP 的請求-回應模型做不到這件事,硬要做就得靠輪詢,每隔幾秒打一次 API,既浪費頻寬又有延遲。WebSocket 就是為了解決這個問題而生的:一條 TCP 連線上跑全雙工通訊,兩邊都能隨時送資料。
這篇用 Python 寫後端、原生 JavaScript 寫前端,建一個能在多個瀏覽器分頁之間互傳訊息的聊天室。程式碼都是可以直接跑的,而且特別避開了 websockets 套件新版本上的幾個地雷——網路上很多教學還在用舊版 API,照抄到新環境會直接報錯。
WebSocket 到底是什麼
WebSocket 是一個獨立的網路協定(ws:// 或加密的 wss://),握手階段借用 HTTP 升級連線,握手完成後就脫離 HTTP,變成一條雙向的長連線。
跟輪詢比起來,它的好處很實際:
- 伺服器能主動推播,不必等客戶端發問
- 連線建立後省掉每次 HTTP 標頭的開銷
- 延遲低,適合聊天、通知、即時資料這類場景
要注意的是 WebSocket 不會自動重連,連線斷了就是斷了,這部分要自己處理,文末會提。
環境準備
後端只需要 Python 3.7 以上和 websockets 套件:
1 | pip install websockets |
前端不用任何框架,一個 HTML 檔加一段 JavaScript 就夠。
這裡先講一個踩過的坑。websockets 在 13.0 之後改了 API:連線處理函式的第二個參數 path 被棄用,到 14.0 直接移除;同時推薦的伺服器啟動方式也從 asyncio.get_event_loop() 換成了 asyncio.run()。如果你照著舊教學寫 async def handler(websocket, path),在新版上會跳 DeprecationWarning,更新到 14 以後甚至連線都接不起來。下面的程式碼用的是新版寫法。
後端實作
建一個 server.py:
1 | import asyncio |
幾個重點:
from websockets.asyncio.server import serve, broadcast用的是新版 asyncio 實作的匯入路徑。舊的import websockets; websockets.serve(...)走的是 legacy 實作,雖然還能用但已不建議。chat只接一個參數websocket,這是 13.0 之後的正確簽名。- 廣播沒有自己寫迴圈一個個
send,而是用套件內建的broadcast()。原因是:手寫迴圈時如果某個連線中途斷掉,send會丟例外,整個迴圈就中斷,後面的人都收不到訊息。broadcast()內部會跳過已關閉的連線,不會因為一個壞掉的連線拖垮整批廣播。 connected.discard()比connected.remove()安全,集合裡沒有該元素時不會丟KeyError。- 啟動用
asyncio.run(main())搭配async with serve(...),連線生命週期和關閉清理都交給 context manager 管,不用自己拼 event loop。
前端實作
建一個 index.html:
1 |
|
這裡有個很多教學會寫錯的地方:onerror 的回呼參數。
WebSocket 的 error 事件依規範是一個 generic Event,不是 ErrorEvent,所以它沒有 message 屬性。如果你寫 'WebSocket錯誤: ' + error.message,畫面上會顯示「WebSocket錯誤: undefined」,反而誤導人。瀏覽器基於安全考量也不會在這個事件裡透露錯誤細節。
要知道連線為什麼掛掉,正確的做法是看 close 事件的 code(例如 1006 通常代表非正常關閉),上面 onclose 就把代碼一併顯示出來了。
跑起來測試
啟動伺服器:
1 | python server.py |
然後在瀏覽器直接打開 index.html,再開幾個分頁模擬不同的人。在任一個分頁送訊息,其他分頁應該都會立刻收到「用戶 xxxxx: ...」。終端機那邊也會印出連線、收訊、斷線的日誌。
如果送出後其他分頁沒反應,先看終端機有沒有 DeprecationWarning 或 TypeError——那幾乎都是 handler 簽名或啟動方式還停在舊版的徵兆。
從這裡再往上走
這個聊天室能跑,但離正式環境還差不少。幾個值得補的方向:
- **改用
wss://**:正式環境一定要走加密連線,否則訊息在路上是明文。通常會在 Nginx 之類的反向代理上掛 TLS,再轉發到後端的ws。 - 加身分驗證:握手時帶 token 驗證身分,現在這版任何人連上就能發言,而且「用戶」名稱只是物件的記憶體位址,並非真正的帳號。
- 重連機制:前端在
onclose裡做指數退避重連,斷線後自動補上,使用者才不會默默掉線。 - 結構化訊息:純文字夠示範,但要傳「誰、什麼時間、什麼類型」這種資料,改用 JSON 會清楚很多,前後端各自
JSON.parse/json.dumps。 - 水平擴展:單機的
connected集合撐不了多台伺服器。要擴展時,得靠 Redis Pub/Sub 之類的外部訊息匯流排,讓跨機器的連線也能互通。
把這個能跑的版本當底,照上面幾點一條條加,就能慢慢長成一個真正能上線的即時系統。重點是先讓最小可用版本動起來——用對的 API、廣播別被單一斷線拖垮、錯誤事件別亂讀屬性,這三件事做對了,後面的功能才好往上疊。




