跳到主要內容

精選

遊戲伺服器2025年初架設與研究part11-Websocket與Unity的登入+聊天室精簡版

 那先前使用Websocket的內容經過一段時間研究 也算是有了一些進展 並且先前也有人對於websocket那篇筆記一次講的內容有點太多,會不好理解 導致可能中途就遭遇了一些BUG比較難解決的 那這次就把相關的內容進行了一些精簡 讓大家可以更快速的架設好websocket的內容 當然這邊也可以當作是不同的使用方式 隨著學習各種不同的資訊,也能更清楚地理解這些其實是不同的使用方式。 因此,這方面完全可以多學幾種,以便因應各種不同的情況。 首先是 Unity 專案的準備 我們必須把常用的 Websocket 插件準備好(Unity) https://github.com/endel/NativeWebSocket 這是較多人使用的 Websocket 插件 可以根據Github那邊寫的資訊進行操作 匯入 到Unity專案內 那我這邊也直接提供方式 首先在你的Unity專案中,上方的選單選擇Window 底下選到 Package Manager 打開Package Manager 點選有URL的選項( Install package from git URL ) 接著請在這個URL框內輸入對應的內容 也就是 https://github.com/endel/NativeWebSocket.git#upm 輸入好之後就點擊最右邊的install安裝就好了 安裝好會如圖這樣我們可以看到 Native WebSockets 插件會在這個位置 如果需要更新,可以在右邊的選單點Update即可 這樣在Unity內就可以使用Websocket相關的語法了 接著我們回到伺服器的地方,也就是我們的 Ubuntu server 首先確保在虛擬環境當中進行安裝 websocket 插件 簡單的確認方式就是看現在輸入的位置前方有無 (venv) 確認好之後可以進行安裝pip install websockets 接著請在想要執行的位置新增一個python檔案(.py) 我這邊是在自己電腦的python專案內新增了一個python檔案main.py 內容如下 import asyncio import websockets import json co...

遊戲伺服器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開頭進行測試

這次就先分享到這邊,如果還有什麼疑問的也可以提出來,或是有什麼講錯的部分也歡迎指正~







留言

熱門文章