iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0
自我挑戰組

chatGPT 帶你從零開始寫 websocket 連線遊戲系列 第 14

D14 實作日 - websocket API server 和 client 端完整實作

  • 分享至 

  • xImage
  •  

今天卡關卡蠻兇的,先上 code,詳細解說之後再補上

server 端

import asyncio
import websockets
import json

# 用於儲存連接的客戶端
connections = {}

# 這個字典用於儲存正在等待回應的請求
pending_requests = {}

# 定義一個處理/foo API的函式
def fooAPIhandler(args):
    try:
        result = sum(args)
        return {"data": result}
    except Exception as e:
        return {"error": str(e)}

# WebSocket 伺服器處理函式
async def server(conn, path):
    print(f'add conn#{conn.id}')
    connections[conn.id] = conn

    async for payload in conn:
        print('payload', payload)
        try:
            request = json.loads(payload)
            api = request.get("api")
            args = request.get("args", [])
            request_id = request.get("id")

            if api == "/foo":
                response = fooAPIhandler(args)
                response["id"] = request_id
                await conn.send(json.dumps(response))
            else:
                await conn.send(json.dumps({"id": request_id, "error": "Unknown API"}))

        except Exception as e:
            print(f"Error handling request: {e}")

# 啟動 WebSocket 伺服器
async def main():
    server_host = "localhost"
    server_port = 8765
    start_server = await websockets.serve(server, server_host, server_port)
    print(f"WebSocket server started on ws://{server_host}:{server_port}")

    await start_server.wait_closed()

if __name__ == "__main__":
    asyncio.run(main())

client 端

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebSocket Client</title>
</head>
<body>
  <script>
    async function initWebSocket(serverURL) {
      return new Promise((resolve, reject) => {
        const socket = new WebSocket(serverURL);

        // 監聽WebSocket的open事件
        socket.addEventListener('open', () => {
          console.log('WebSocket已經成功建立連線並處於OPEN狀態');
          resolve(socket); // 將WebSocket對象傳遞給resolve函式
        });

        // 監聽WebSocket的error事件
        socket.addEventListener('error', (event) => {
          console.error('WebSocket錯誤發生', event);
          reject(event); // 如果發生錯誤,拒絕Promise
        });
      });
    }

    class WebSocketAPIManager {
      constructor(socket) {
        this.socket = socket;
        this.pendingRequests = new Map(); // 使用 Map 來追蹤等待回應的請求
        this.requestId = 1; // 每個請求都有一個唯一的ID,用於匹配回應
        this.socket.onmessage = this.handleMessage.bind(this);
      }

      sendRequest(api, args) {
        console.log('sendRequest', api, args)
        return new Promise((resolve, reject) => {
          const requestId = this.requestId++;
          const request = {
            id: requestId,
            api,
            args,
          };
          this.pendingRequests.set(requestId, {
            request,
            resolve,
            reject,
          });
          this.socket.send(JSON.stringify(request));
        });
      }

      handleMessage(event) {
        const response = JSON.parse(event.data);

        // 找到對應的請求
        const pendingRequest = this.pendingRequests.get(response.id);
        if (!pendingRequest) {
          console.warn(`Received response for unknown request ID: ${response.id}`);
          return;
        }

        // 從 pendingRequests 中刪除這個請求
        this.pendingRequests.delete(response.id);

        // 如果有成功的回應,則呼叫 resolve,否則呼叫 reject
        if (response.data) {
          pendingRequest.resolve(response.data);
        } else {
          pendingRequest.reject(new Error(response.error));
        }
      }
    }

    async function main() {
      const socket = await initWebSocket('ws://localhost:8765/')
      const apiManager = new WebSocketAPIManager(socket);

      try {
        const response = await apiManager.sendRequest('/foo', [1, 2, 3]);
        console.log('Received response:', response);
        const response2 = await apiManager.sendRequest('/foo', [4, 5, 6]);
        console.log('Received response:', response2);
      } catch (error) {
        console.error('Error:', error);
      }
    }

    main();
  </script>
</body>
</html>

卡關的地方

Error: DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.

這是因為 client 端的 socket 在 new 完之後沒有等它的狀態變 OPEN 就馬上呼叫 send() 導致的錯誤

然後 constructor() 是同步的,沒辦法在裡面使用 await, promise 之類的東西。所以等待 socket OPEN 的工作就只能在 constructor() 外面完成了。最後的實作是先用 initWebSocket() 初始化一個 socket 之後再傳入 WebSocketAPIManager 的建構式

client 會連續發送兩個一模一樣的 request

為了修前面的錯誤,在 sendRequest() 加了一些 socket 狀態判定的東西,意外造成連續送出兩個一樣的 request

sendRequest() 一直跳錯誤

if (response.success) { pendingRequest.resolve(response.data); }
後來才發現 handleMessage 內部的判定有問題,我定義的 response 格式裡面沒用到 success,後來改成 response.data 問題就解決了

解說

socket.send(), socket.recv() 整併為一個 request 呼叫搞定。中間需要透過 pendingRequest 保存狀態,同時也把決定 Promise 最終狀態的 resolve, reject 兩個 callback 紀錄下來。


上一篇
D13 實作日 - 聊天室 client 端改寫
下一篇
D15 驅動遊戲前進的是什麼? - 談談遊戲循環、狀態機、事件溯源
系列文
chatGPT 帶你從零開始寫 websocket 連線遊戲31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言