今天卡關卡蠻兇的,先上 code,詳細解說之後再補上
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())
<!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>
這是因為 client 端的 socket 在 new 完之後沒有等它的狀態變 OPEN 就馬上呼叫 send() 導致的錯誤
然後 constructor() 是同步的,沒辦法在裡面使用 await, promise 之類的東西。所以等待 socket OPEN 的工作就只能在 constructor() 外面完成了。最後的實作是先用 initWebSocket() 初始化一個 socket 之後再傳入 WebSocketAPIManager 的建構式
為了修前面的錯誤,在 sendRequest() 加了一些 socket 狀態判定的東西,意外造成連續送出兩個一樣的 request
if (response.success) { pendingRequest.resolve(response.data); }
後來才發現 handleMessage 內部的判定有問題,我定義的 response 格式裡面沒用到 success,後來改成 response.data
問題就解決了
把 socket.send()
, socket.recv()
整併為一個 request 呼叫搞定。中間需要透過 pendingRequest 保存狀態,同時也把決定 Promise 最終狀態的 resolve, reject 兩個 callback 紀錄下來。