第2の人生の構築ログ

自分の好きなことをやりつつ、インカムもしっかりと。実現していく過程での記録など。読書、IT系、旅行、お金に関係する話などの話題。

Go 言語で WebSocket を使ってみる

Webにおいて双方向通信を行うためのプロトコルの一つである WebSocket 使う必要があったので、まずはどのようなものか、Go 言語でシンプルに実装してみます。

実装するライブラリにはいろいろありそうですが、ここでは、golang.org/x で提供される準標準パッケージを使った場合と、gorilla を使った場合で試してみます。

golang.org/x を使った WebSocket

まずは準標準ライブラリの golang.org/x を使います。サーバ側の実装と、クライアントからその実装を試すための実装、2つを用意します。

ファイルの構成は以下の通りです。

$ tree
.
├── go.mod
├── go.sum
├── main.go
└── public
    ├── index.html
    └── main.js

サーバ側の実装は main.go 一本で、WebSocket のクライアント側は、public ディレクトリ配下の index.htmlmain.js で実装します。

サーバ側の実装です。

main.go:

package main

import (
    "fmt"
    "log"
    "net/http"

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

func webSocketHandler(ws *websocket.Conn) {
    defer ws.Close()

    // 初回メッセージを送信
    err := websocket.Message.Send(ws, "Server: Hello, Client!")
    if err != nil {
        log.Println(err)
    }

    for {
        msg := ""
        err := websocket.Message.Receive(ws, &msg)
        if err != nil {
            log.Println(err)
            break
        }

        err = websocket.Message.Send(ws,
            fmt.Sprintf("Server: '%s' received.", msg))
        if err != nil {
            log.Println(err)
            break
        }
    }
}

func main() {
    files := http.FileServer(http.Dir("public"))
    http.Handle("/", files)
    http.Handle("/ws", websocket.Handler(webSocketHandler))


    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

2つのハンドラを用意していますが、1つ目は / のパスに割り当てた静的ファイルを配信する部分です。この静的ファイルで WebSocket のクライアント側を実装します。静的フィアルの配信には、http.Handler を実装した http.FileServer 関数を使っています。

2つ目の/wsパスに割り当てているハンドラが WebSocket のサーバ側の実装です。  

ハンドラに指定しているwebsocket.Handler 型の引数はwebsocket.Connのポインタを引数にもつ関数func(*Conn)となっており、実際の実装の部分は、webSocketHandler 関数の部分になります。 ちなみに、websocket.Handler 型は以下のように定義されており、ServeHTTP 実装しています。

type Handler func(*Conn)

func checkOrigin(config *Config, req *http.Request) (err error) {
    config.Origin, err = Origin(config, req)
    if err == nil && config.Origin == nil {
        return fmt.Errorf("null origin")
    }
    return err
}

// ServeHTTP implements the http.Handler interface for a WebSocket
func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    s := Server{Handler: h, Handshake: checkOrigin}
    s.serveWebSocket(w, req)
}

クライアントへのメッセージの送信は、websocket.Message.Send で、クライアントからの受信は、websocket.Message.Receive で行われ、単純に最初にサーバからメッセージを送っておき、その後は無限ループの中でメッセージの受信、受信後にその内容を送る、といったことをやっています。echoサーバですね。

クライアントの実装です。Inputボックスに何かメッセージを入れて送信ボタンを押下すると上部に送信したメッセージを表示させるというものです。

public/index.html:

<!doctype html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>WebSocket example</title>
</head>
<body>
    <p id="output"></p>

    <input type="text" id="input">
    <input type="submit" id="btn" value="送信">

    <script type="text/javascript" src="main.js"></script>
</body>
</html>

public/main.js:

document.addEventListener('DOMContentLoaded', () => {
  let loc = window.location;
  let uri = 'ws:';
  if (loc.protocol === 'https:') {
    uri = 'wss:';
  }
  uri += '//' + loc.host;
  uri += loc.pathname + 'ws';

  const ws = new WebSocket(uri);
  ws.onopen = function()  {
    console.log('Connected');
  }

  ws.onmessage = function(evt) {
    let out = document.getElementById('output');
    out.innerHTML += evt.data + '<br />';
  }

  const btn = document.getElementById('btn');
  btn.addEventListener('click', () => {
    ws.send(document.getElementById('input').value);
  });
});

実行してみます。http://localhost:8080/ にアクセスしますと、まずはサーバからのご挨拶があります。

f:id:dr_taka_n:20210823214741p:plain

フォームのインプットフィールドにクライアントからのご挨拶を入れて、"送信"をクリックしてみます。

f:id:dr_taka_n:20210823215301p:plain

ちゃんとメッセージが戻(echo)されてきていますね。

f:id:dr_taka_n:20210823215347p:plain

gorilla を使った WebSocket

こちらは、サーバ側だけ記載しておきます。準標準ライブラリを使っていた方が長い目では良さそうな感じはしますが、gorilla の方がいろいろ進んではいるようです。

github.com

(2021/08/28)
f:id:dr_taka_n:20210823215532p:plain:w500

準標準ライブラリの実装と同様の実装を gorilla でも行ってみます。main.goのみの変更でそれ以外(クライアント)は同じものを使います。

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

// デフォルト値で初期化
var upgrader = websocket.Upgrader{}

func webSocketHandleFunc(w http.ResponseWriter, r *http.Request) {
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("upgrade:", err)
        return
    }
    defer ws.Close()

    err = ws.WriteMessage(websocket.TextMessage, []byte(`Server (gorilla): Hello, Client!`))
    if err != nil {
        log.Println("WriteMessage:", err)
        return
    }

    for {
        mt, message, err := ws.ReadMessage()
        if err != nil {
            log.Println("ReadMessage:", err)
            break
        }
        err = ws.WriteMessage(mt, []byte(fmt.Sprintf("Server (gorilla): '%s' received.", message)))
        if err != nil {
            log.Println("WirteMessage:", err)
            break
        }
    }
}

func main() {
    files := http.FileServer(http.Dir("public"))
    http.Handle("/", files)
    http.HandleFunc("/ws", webSocketHandleFunc)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

gorilla を使うと、WebSocket が通常の HTTP プロトコルの拡張であるということを実感します。(websocket.Upgrader{}.Upgrade) 多少、メソッド名の違いなどありますが websocket.Upgrader の初期化処理があるくらいで、データの受信/送信などのお作法も大きく変わらず、このレベルの実装では大きな違いは無さそうです。

パスへの割り当ては、ハンドラではななくハンドラ関数での方で行っています。

クライアントから突っついてみます。結果は先程と同様になります。

f:id:dr_taka_n:20210827161452p:plain

どちらを使っても手軽に実装できるようになっていますね。