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.html
と main.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/
にアクセスしますと、まずはサーバからのご挨拶があります。
フォームのインプットフィールドにクライアントからのご挨拶を入れて、"送信"をクリックしてみます。
ちゃんとメッセージが戻(echo)されてきていますね。
gorilla を使った WebSocket
こちらは、サーバ側だけ記載しておきます。準標準ライブラリを使っていた方が長い目では良さそうな感じはしますが、gorilla の方がいろいろ進んではいるようです。
(2021/08/28)
準標準ライブラリの実装と同様の実装を 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
の初期化処理があるくらいで、データの受信/送信などのお作法も大きく変わらず、このレベルの実装では大きな違いは無さそうです。
パスへの割り当ては、ハンドラではななくハンドラ関数での方で行っています。
クライアントから突っついてみます。結果は先程と同様になります。
どちらを使っても手軽に実装できるようになっていますね。