自分用めも

初心者ちっくなプログラムネタを中心に、自分用の覚え書きをメモっていくための場所です。

Nginxでecho(golang)でWebSocket 「golang.org/x/net/websocket」編

前回で単純なhtmlのHello worldは出来たので、今度はwebsocketを疎通してみます。

Golangでwebsocketを扱うにあたり、echoのサンプルでも取り上げられている二つのパッケージを試してみることにしました。
WebSocket Recipe | Echo - High performance, minimalist Go web framework

  1. golang.org/x/net/websocket
  2. github.com/gorilla/websocket

上がgo公式なのですが機能が少ない上2019年現在で3年ほど?メンテされておらず、
今は下のgolliraを使うことが推奨されているようです。
本採用はgollira予定ですが、今回はテストで上のnet/websocketを使います。

このテストはタイトル通り、
「nginx」「echo(golang)」「golang.org/x/net/websocket」でどう繋げるかということに主眼をおいています。
メインロジックには以下のサンプルチャットを使っています。
GitHub - AlexanderGrom/go-websocket-chat: Simple Websocket Chat in Golang

Nginx側

前回構築したgo-echoのhttpサーバのうち、
パス「/ws」をwebsocket接続するように修正します。
Unixドメインソケットはhttpと使い回しで良いようです。
参考:WebSocket proxying

vim /srv/www/nginx.conf
server {
	#~略~
	location /ws {
		proxy_pass http://goecho_socket_server;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
		proxy_read_timeout 6h;
	}
	#~略~
}

タイムアウトを設定しない場合、60秒で切断されます。
ここでは6時間に設定しています。

go-echo側

今回使わせてもらった「go-websocket-chat」のうち、main.go以外を使います。
mein.goについては自前で前回構築した物に取り入れます。

package main

//~略~

// ハンドラ用のレシーバ関数
func (s *Server) handleWebScoket(c echo.Context) error {
	s.Handler().ServeHTTP(c.Response(), c.Request())
	return nil
}

go-websocket-chatの独自構造体「Server」はシングルトンであるように設計されているようです。
またイベントハンドラも専用の関数があります。
これをgo-echoのwebsocketサンプルの形式と合わせるため、Serverにレシーバを生やします。

// WebSocket ------
func main() {
	//~略~
	s := NewServer()
	go s.Listen()
	e.GET("/ws", s.handleWebScoket)
	//~略~
}

あとはmain関数でServerインスタンスを作成し、
レシーバをイベントハンドラとして登録すればokです。

これでサンプルのread.meにあるhtmlを設置すれば、簡易チャットが立ち上がります。

サンプルチャットについて

本題とは逸れるのですが、このサンプルチャットはGolangの「チャンネル型」を理解する上で、個人的に非常に参考になりました。

ざっくり書くと、処理は以下のようになります。

  1. func main(起動時に一回のみ実行)
    → Serverインスタンスを作成
    → 「/ws」パス(サンプルのままだと「/」パス)へのリクエストをServerのハンドラに紐づけ
    → Serverゴルーチンの待ち受け処理開始(forの無限ループを行うサブプロセス)
  2. Serverのハンドラ(server.go Handler)
    → リクエストがあったらClientインスタンス作成
    → Clientゴルーチンの待ち受け処理開始

Serverインスタンスはシングルトンなため、常に一つだけ存在します。
Clientインスタンスは「/ws」に接続があるたびに新規作成され、ゴルーチン(サブプロセス)として常駐します。

チャットですから、クライアントAさんがチャットを打ったときには、クライアントBさんにメッセージを流す必要があります。
別々のサブプロセスであるゴルーチン間でメッセージをやり取りする仕組みが、チャンネル型の変数になります。

  1. Aさん、Bさんが「/ws」に接続
  2. それぞれにClientインスタンスが作成、Clientゴルーチン開始
  3. Aさんが書き込み「こんにちは」
  4. Clientゴルーチンがwebsocketからのメッセージ(本例ではJSONデータ)を拾って解析、エラーがなければClient.readチャンネルにメッセージを送信
  5. ClientゴルーチンはClient.readチャンネルに来たメッセージをServerゴルーチン(別プロセス)のServer.SendAllチャンネルに送る
  6. ServerゴルーチンはClientからServer.SendAllチャンネルにメッセージが来たら、管理下の全Clientゴルーチンのチャンネルにメッセージを送る(Client.Write)
  7. ClientゴルーチンはClient.Writeメッセージを受け取ったらwebsocketクライアント(ブラウザ)にJSON形式でメッセージを送る
  8. Aさん、Bさんの画面に「こんにちは」と表示される

完全に自分用レベルの粗いメモになってしまいましたが…とりあえずここまで。