搜尋此網誌
由於從事Client端工作較多年,所以可能相關的領域較為了解,但是程式這種職業,其實能拆分細數成無數分支的程式職業,每個不同的程式區塊都是隔行如隔山,不同公司的習慣與水平都有著巨大的差距,所以看到不同的寫法也許只是習慣不同,而大家也都是希望遊戲領域或是程式領域能發展的更好,所以歡迎討論,但是希望避免那種都沒說明就說別人錯誤的狀況~
精選
- 取得連結
- X
- 以電子郵件傳送
- 其他應用程式
遊戲伺服器2025年初架設與研究part7遊戲中常見的websocket使用,並且在Unity與伺服器中建立連線
那上次已經可以透過POST收送封包,來進行一些基本的操作了
當然如果願意鑽研的話,光是POST這邊就能用出非常多種應用
並且即便只使用POST也足夠應付不少小遊戲的封包收送了
不過這邊也在提供另一個方式,也是在遊戲中極為常見的用法(非遊戲領域就不清楚)
也就是透過Websocket的連線來收送封包
那這邊也是簡單的說一下Websocket的優點
當然這邊不是純討論它的優缺點,所以可能會說的不夠完整
大家也可以自行查詢相關的資料來更加完善這方面的知識
首先它可以主動從伺服器對使用者發送封包(當連線已經建立以後),如果用原本的http收送封包的方式基本上得從使用者主動發送,即便是要資料也要使用者主動問才能獲取
不過websocket則不用,它可以主動從伺服器發送給使用者
舉例來說,現在伺服器假設有公告系統,伺服器有公告了,如果有websocket連線,伺服器就可以直接通知所有使用者公告是什麼。
但是如果沒有websocket,那就是使用者透過類似心跳包來詢問,例如每30秒問一次server有沒有公告,也就是說伺服器可能已經發了公告,但是使用者須要等到下一次的心跳包詢問的時候才能知道伺服器有沒有公告,類似這樣的差別。
所以不是沒辦法做到,而是步驟不同。
第二點是websocket建立連線後可以維持較長的時間(這邊要考慮放置太久都沒送封包有可能連線會被一些自動機制切斷),不像http連線基本上就是收送,連線就斷開了,一直收送就要不斷處理這個連線連接跟丟棄的動作。
第三點則是因為不用等使用者問,所以可以持續送封包給使用者,用來節省一半的延遲(原本封包送到伺服器,再從伺服器回去,會有兩段延遲),舉例來說遊戲常用到的房間機制,這邊可能就由伺服器來運算房間邏輯的話,就可以不斷的傳送房間同步資訊給房間內的所有玩家,不用讓玩家每個自己問了再更新,延遲上至少可以減半。
那這邊就只是概略的提了三點,詳細可以直接去搜尋更多用法。
接下來就直接來開始說明如何簡單的使用。
首先自然是要先安裝
pip install websockets
這邊記得要先切到我們的虛擬環境中再執行安裝,這樣套件就會安裝在虛擬環境中
接下來我們現到伺服器程式碼的專案中新增一個新的.py檔
import asyncio
import websockets
最上面先import
接下來是主要的程式碼的內容
# 儲存所有已連線的使用者(key 是玩家 ID,value 是 websocket)
connected_users = {}
async def handler(websocket):
player_id = None
try:
async for message in websocket:
print(f"[接收訊息] {message}")
data = json.loads(message)
if data["type"] == "login":
player_id = data["playerId"]
connected_users[player_id] = websocket
await websocket.send(json.dumps({
"type": "login_ok",
"message": f"登入成功,歡迎 {player_id}"
}))
print(f"[登入] 玩家 {player_id} 連線")
elif data["type"] == "echo":
await websocket.send(json.dumps({
"type": "echo_reply",
"message": data["message"]
}))
elif data["type"] == "broadcast":
msg = data["message"]
for pid, ws in connected_users.items():
if ws.open:
await ws.send(json.dumps({
"type": "broadcast",
"from": player_id,
"message": msg
}))
else:
await websocket.send(json.dumps({
"type": "error",
"message": "未知的封包類型"
}))
except websockets.exceptions.ConnectionClosed:
print(f"[斷線] 玩家 {player_id} 離線")
finally:
if player_id and player_id in connected_users:
del connected_users[player_id]
print(f"[清除連線] 已移除玩家 {player_id}")
async def main():
async with websockets.serve(handler, "127.0.0.1", 8765):
print("WebSocket Server 已啟動在 ws://127.0.0.1:8765")
await asyncio.Future() # 永遠不結束
asyncio.run(main())
那這邊只是簡單的範例,詳細邏輯請根據自己的需要調整
我這邊就是簡單的登入,然後印log,並且保存連線,並且廣播訊息的處理
然後如果連線中斷,則把連線移除,做一個基本的處理
不過這邊要注意的是client端帶來的資料是要進行判斷處理的
另外一點是
async def handler(websocket):
這個語法本身,如果現在查應該有不少範例還會寫著
async def handler(websocket , path):
不過實際上目前好像是沒有path了,應該是版本到某個版本以後就沒有了
所以這邊發現有跳錯誤,可以檢查一下python的版本
目前新版是沒有path的
那這邊伺服器的程式碼已經準備好了,接下來要處理轉發的部分
轉發的話我們要回到Nginx 設定反向代理
用先前的指令打開Nginx的設定檔進行編輯
例如放在這邊的話/etc/nginx/sites-enabled/your-site.conf
當然請找到自己的設定檔進行編輯
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; location /ws { proxy_pass http://127.0.0.1:8765/; # 傳給 WebSocket Server # 必要 WebSocket 標頭,讓 Nginx 允許 Upgrade proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; # 一些常見補充 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 可加其他 API 或靜態網站處理(例如 Flask 的 API) location /api/ { proxy_pass http://127.0.0.1:6666/; } }
補上websocket要使用的語法
方便對應我也把上下文保留的,大家可以自己比對來放入正確的區域
主要就是找443這個server區塊
然後關鍵就是/ws這邊
IP對應請對應上我們先前在python裡面打的,包含port接口要打對
當然這部分可以自己定義,只是也要避免打到其它服務使用的接口(會衝突)
那接下來沒問題後,我們就可以到Unity內進行websocket的設置處理
首先unity要使用websocket常見的插件應該是使用
NativeWebSocket
可以搜尋了解一下
那我這邊也直接說明要如何匯入專案
首先在Unity專案中打開Package Manager
點選左上角的+號
然後選擇透過網址安裝(有顯示Git URL的那個選項)
接著在上面填寫安裝的網址https://github.com/endel/NativeWebSocket.git#upm
這邊也提供Git的網址讓大家進行確認
https://github.com/endel/NativeWebSocket
安裝好之後就可以使用了(記得匯入專案)
接下來創建新的腳本來進行撰寫相關的程式碼
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
首先是using的部分
接下來創建新的腳本來進行撰寫相關的程式碼
private ClientWebSocket ws;
private CancellationTokenSource cancelToken = new CancellationTokenSource();
private Uri serverUri = new Uri("wss://你的網域/ws");
async void Start()
{
await ConnectWebSocket();
await SendLogin("player_001");
// 測試封包
await SendEcho("Hello Server!");
await SendBroadcast("玩家發送了公告!");
}
void Update()
{
#if !UNITY_WEBGL || UNITY_EDITOR
websocket?.DispatchMessageQueue();
#endif
}
private async Task ConnectWebSocket()
{
ws = new ClientWebSocket();
try
{
await ws.ConnectAsync(serverUri, cancelToken.Token);
Debug.Log("已連接到伺服器");
// 開啟接收協程
StartCoroutine(ReceiveLoop());
}
catch (Exception ex)
{
Debug.LogError("無法連接到伺服器: " + ex.Message);
}
}
private IEnumerator ReceiveLoop()
{
var buffer = new byte[4096];
while (ws.State == WebSocketState.Open)
{
ArraySegment<byte> seg = new ArraySegment<byte>(buffer);
WebSocketReceiveResult result = null;
try
{
result = await ws.ReceiveAsync(seg, cancelToken.Token);
string msg = Encoding.UTF8.GetString(buffer, 0, result.Count);
Debug.Log("收到訊息: " + msg);
}
catch (Exception ex)
{
Debug.LogError("接收失敗: " + ex.Message);
break;
}
yield return null;
}
}
private async Task SendLogin(string playerId)
{
var loginData = new Dictionary<string, object>
{
{ "type", "login" },
{ "playerId", playerId }
};
await SendJson(loginData);
}
private async Task SendEcho(string message)
{
var data = new Dictionary<string, object>
{
{ "type", "echo" },
{ "message", message }
};
await SendJson(data);
}
private async Task SendBroadcast(string message)
{
var data = new Dictionary<string, object>
{
{ "type", "broadcast" },
{ "message", message }
};
await SendJson(data);
}
private async Task SendJson(Dictionary<string, object> dict)
{
string json = JsonUtility.ToJson(new JsonWrapper(dict));
byte[] bytes = Encoding.UTF8.GetBytes(json);
ArraySegment<byte> segment = new ArraySegment<byte>(bytes);
try
{
await ws.SendAsync(segment, WebSocketMessageType.Text, true, cancelToken.Token);
Debug.Log("📤 發送封包: " + json);
}
catch (Exception ex)
{
Debug.LogError("發送失敗: " + ex.Message);
}
}
// JsonUtility 不能直接序列化 Dictionary,所以加個 wrapper
[Serializable]
public class JsonWrapper
{
public string type;
public string playerId;
public string message;
public JsonWrapper(Dictionary<string, object> data)
{
if (data.ContainsKey("type")) type = data["type"].ToString();
if (data.ContainsKey("playerId")) playerId = data["playerId"].ToString();
if (data.ContainsKey("message")) message = data["message"].ToString();
}
}
那這邊主要就是跟server進行串接的部分,可以看到是對應需要的資料來處理
當然剛開始可以不用直接打這麼多,可以用類似底下這樣測試
websocket = new WebSocket("wss://你的網域/ws");
websocket.OnOpen += () => Debug.Log("連線成功!");
websocket.OnMessage += HandleWebSocketMessage;
websocket.OnError += (e) => Debug.Log("錯誤:" + e);
websocket.OnClose += (e) => Debug.Log("連線關閉");
await websocket.Connect();
void Update()
{
#if !UNITY_WEBGL || UNITY_EDITOR
websocket?.DispatchMessageQueue();
#endif
}
private void HandleWebSocketMessage(byte[] bytes)
{
Debug.Log("收到了websocket封包");
}
可以先確保這段可以正常運作,才開始慢慢添加或是調整,慢慢加到跟上面那樣稍微複雜的程度。
那這邊程式碼我們一樣可以放在async void Start()的地方
要發送websocket封包主要的語法是
await websocket.SendText(message);
message就是主要帶過去的內容
接著就是在場景中掛上這個腳本
然後unity按下play
當然我們這邊要確保伺服器這個腳本已經有在運作了
例如想要手動執行的話可以輸入python3 你的腳本名稱.py
就可以讓伺服器開始接收websocket的連線了(Nginx也記得要重啟)
並且這邊也可以延伸開始去測試多個websocket連線,如何讓A玩家的資訊可以同步到例如B玩家C玩家那邊了
另外Websocket的網域開頭,wss對應就是https,所以如果大家還使用http連線的話要測試則是使用ws開頭進行測試
這次就先分享到這邊,如果還有什麼疑問的也可以提出來,或是有什麼講錯的部分也歡迎指正~
留言
張貼留言