關於 web service, unity, blogger 等軟體工程筆記

Golang server + WebSocket protocol + Unity C# client

Edit icon 沒有留言
go+websocket+unity

研究資料傳輸方案,運用在工作專案中可能性的筆記。嘗試使用 Golang 開發伺服器 (Server),經由 WebSocket protocol 與使用 Unity C# 開發的用戶端 (Client) 串接。實作一個的應答伺服器 (Echo server),用戶端傳什麼給伺服器,伺服器就回應什麼給用戶端。

架構圖

Why WebSocket

為什麼用 WebSocket?因為我們需要 socket 來對接伺服器以及用戶端,來達到即時互動且雙向資料傳輸。TCP socket 從頭自己開始刻覺得太麻煩了,要定義處理那一堆 header, packets, and data frames。

此外,用戶端未來有計畫發佈在網頁平台 (Web platform),必須使用瀏覽器開啟。而在瀏覽器 (browser) 唯一的 socket 解決方案,現階段就只有 Websocket 了。那些 wiki 上寫的 WebSocket 好處,並不是主要的考量。

至於之前研究過的 Photon engine,Unity 第三方連線方案,嗯,在目前的工作專案上完全不再考慮的範圍之內,除了授權費,架構不符所需以及對它還不夠了解外,還有私心,研究 HTML5 新技術 WebSocket。

WebSocket protocol

細節請參考 rfc6455: The WebSocket Protocol 文件,這邊只提到用戶端送出交握 (Handshaking) 中的部分資料。

WebSocket handshaking 採用 HTTP header,故可以自行加入應用層級的擴充資料,一開始用戶端先傳以下資料:

GET /echo HTTP/1.1
Host: 127.0.0.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: echo, super-echo
Sec-WebSocket-Version: 13

其中 Sec-WebSocket-Protocol 標示用戶端所支援的應用層的 protocols,在這次測試中並沒有使用到。Origin 標示用戶端來源位置,用來保護用戶避免 Cross-origin script attack,在非瀏覽器的用戶端,這值可能不會送,這次因為該值問題,導致 Unity 用戶端一直連不上 Golang 寫的伺服器,研究卡住一陣子。

Server using Golang

為什麼使用 Golang 來架設伺服器?因為相較於其他熱門的語言 (Javascript, Java, C++, C#, Python, and Ruby 等等…) 來說,目前較熟悉這語言。

很容易地在 Golang 社群上找到範例程式,經過加工處理後的伺服器程式碼如下:

package main

import (
"fmt"
"io"
"net/http"

"golang.org/x/net/websocket"
)

func EchoServer(ws *websocket.Conn) {
var err error
for {
var reply string

if err = websocket.Message.Receive(ws, &reply); err != nil {
if err != io.EOF {
fmt.Println("Can't receive, err: %v", err)
}
break
}

msg := "Received: " + reply

if err = websocket.Message.Send(ws, msg); err != nil {
if err != io.EOF {
fmt.Println("Can't send, err: %v", err)
}
break
}
}
}

func EchoHandler(h websocket.Handler) http.Handler {
s := websocket.Server{
Handler: h,
Handshake: nil,
}
return s
}

func main() {
http.Handle("/echo", EchoHandler(EchoServer))

err := http.ListenAndServe(":80", nil)
if err != nil {
panic("ListenAndServe failed, err: " + err.Error())
}
}

不使用 websocket.Handler,主要是該 handler 預設的 hadeshaking function,會檢查 header Origin 是否為空值。使用 Unity Editor 測試時,因非瀏覽器用戶端,其 websocket client handshake 時不會傳送 header Origin,進而沒有辦法與 此伺服器連線,當時在這裡卡了許久。

改成自行建立 websocket.Server,不設定任何的 Handshake 檢查,便可讓 Unity editor websocket client 完成連線。未來若要自訂自己的 hadeshaking,可以實作 func handshake(*websocket.Config, *http.Request) error。

在這測試專案中,僅使用 websocket.Message 僅傳輸字串,也可以改用 websocket.Json 來處理 JSON 格式,或是建立 websocket.Codec 實作自己應用層的資料處理 (marshal/ unmarshal)。

package websocket
type Codec struct {
Marshal func(v interface{}) (data []byte, payloadType byte, err error)
Unmarshal func(data []byte, payloadType byte, v interface{}) (err error)
}

甚至更進階,除了原先 protocol OpCode 已定義的 %x1 文字以及 %x2 二進位的資料外 (see rfc6455, section 5.2),自行定義 %x3-7 的格式吧。

Client using C# in Unity

Unity 5.5.0 還沒有原生支援 WebSocket,從網路上可以找到 C# Websocket-sharp 的 WebSocket 實作,但對於 Unity WebGL 平台目前沒有辦法支援。

從 Unity asset store 上的 WebSocket 範例,則是 Unity 官方提供的簡易範例,有額外包一層架構,針對 WebGL 平台特別實作,而非 WebGL 平台則使用上述 Websocket-sharp 的實作。

採用 Websocket-sharp 的連線測試程式碼:

using UnityEngine;
using WebSocketSharp;

public class WebsocketTest : MonoBehaviour
{
void Start()
{
var ws = new WebSocket("ws://127.0.0.1:80/echo");

ws.OnError += (sender, e) =>
{
Debug.LogException(e.Exception);
};

ws.OnMessage += (sender, e) =>
{
Debug.Log("Says: " + e.Data);
};

ws.OnClose += (sender, e) =>
{
Debug.Log("Closed");
};

ws.OnOpen += (sender, e) =>
{
ws.Send("Hello");
ws.Send("Siyuan");
ws.Send("Wang");
};

ws.Connect();
}
}

採用 Assetstore 上的範例的連線測試程式碼:

using System;
using System.Collections;
using UnityEngine;

public class WebSocketTest : MonoBehaviour {

IEnumerator Start()
{
var ws = new WebSocket(new Uri("ws://127.0.0.1:80/echo"));
yield return ws.Connect();
this.StartCoroutine(this.Receiver(ws));
ws.SendString("Hello");
ws.SendString("Siyuan");
ws.SendString("Wang");
}

IEnumerator Receiver(WebSocket ws)
{
while (true)
{
if (ws.error != null)
{
Debug.LogError("Error: " + ws.error);
break;
}

var reply = ws.RecvString();
if (reply != null)
{
Debug.Log("Received: " + reply);
}

yield return null;
}
}
}

WebSocket 研究大致上到這個階段,已經能夠實作用戶端以及伺服端透過 WebSocket protocol 互傳資料,接下來便是根據遊戲需求,實作應用層的資料傳輸以及處理了。

Reference

沒有留言: