この記事は、2015年のGo Advent Calendarの3日目の記事です。
さてGoのHTTPといえばnet/http
において今までどおりのコードでHTTP2対応が出来るようになる変更がGo1.6にて入るようになりますが、それはさておき、突然WebSocketがしゃべりたくなったとしましょう。
WebSocketをサーバで扱う実装といえばnode.jsのものが有名ですが、Goにおいても簡単に扱う事ができます。
ここではgolang.org/x/net/websocket
を用いて説明し、また最後にHTTP/1.1以前しかしゃべることのできないサーバでもWebSocketによるサーバサイドプッシュを実現できるミドルウェアである拙作のgithub.com/mackee/kuiperbelt
の紹介をします。
基本的な使い方
ほぼgodocに書かれている内容どおりで申し訳ないのですが……
使い方はごく簡単で、golang.org/x/net/websocket
をimportした上で以下のinterfaceを持つ関数を作ります。
func (*websocket.Conn)
例えばecho serverであれば、以下のようにします。
import (
"io"
"golang.org/x/net/websocket"
)
func EchoHandler(ws *websocket.Conn) {
io.Copy(ws, ws)
}
*websocket.Conn
はio.ReadWriteCloser
が実装されており、クライアントから来たWebSocketのメッセージは*websocket.Conn.Read
で読むことができ、また*websocket.Conn.Write
で送ることができます。なので、io.Copy
で逐次読みだして同じ内容をまた書き込むことで簡単にecho serverを書くことができます。
これをサーバに組み込むのも簡単です。
import (
"net/http"
)
func main() {
http.Handle("/echo", websocket.Handler(EchoHandler))
err := http.ListenAndServe(":12345", nil)
if err != nil {
panic("ListenAndServe: " + err.Error())
}
}
先ほど作った関数をwebsocket.Handler
という型にキャストしています。websocket.Handler
にはnet/http.Handler
が実装されているので、http.Handle
に食わせることができます。
普通のHTTPサーバを作るときに行うnet/http.HandleFunc
でfunc Handler(w http.ResponseWriter, r *http.Request)
を食わせるのと同じですね。
立てたWebSocketのサーバを検証してみましょう。ブラウザのJavaScriptでWebSocketをしゃべるのも非常に簡単ですが、僕はいつもwscatを使っています。node.jsで動くcliのコマンドなので、npmでインストールしましょう。
ここで注意ですが、上のコードで作ったサーバをwscatで繋ぐ場合
$ wscat -c ws://localhost:12345/echo
とすると思うのですが、これだと403で弾かれてしまいます。原因は、クロスオリジン通信をしようとして弾かれているようですので、以下のようにすると繋げられるようになります。
$ wscat -c ws://localhost:12345/echo -o localhost:12345
また以下の記事のようにコードに変更を加える事でクロスオリジン通信を許可することもできます。
golang - WebSocketをGoで触ってみた - Qiita
その他tips
フレームを指定して送る
websocketには以下のようにフレームに種類があります。
%x0 は継続フレームを表す
%x1 はテキストフレームを表す
%x2 はバイナリフレームを表す
%x3-7 は追加の非制御フレーム用に予約済み
%x8 は接続の close を表す
%x9 は ping を表す
%xA は pong を表す
%xB-F は追加の制御フレーム用に予約済み
RFC6455 — The WebSocket Protocol 日本語訳より
このうち継続フレームやcloseフレーム、ping/pongフレームは何をしなくてもよしなに対応してくれるのですが、*websocket.Conn
で普通にWrite
するとwebsocket.Conn.PayloadType
が使われます。
constでwebsocket.TextFrame
およびwebsocket.BinaryFrame
が定義されているので、これを使って都度セットしてもいいのですが、Write中に別のgoroutineでPayloadTypeを変更されると意図しない種類のフレームとして送られる可能性があります。
なにかうまい方法はないかなと探してみたところ、websocket.Codec
を使う方法が良さそうでしたので紹介します。
websocket.Codec
は以下の様な定義のstructです。
type Codec struct {
Marshal func(v interface{}) (data []byte, payloadType byte, err error)
Unmarshal func(data []byte, payloadType byte, v interface{}) (err error)
}
func (cd Codec) Receive(ws *Conn, v interface{}) (err error)
func (cd Codec) Send(ws *Conn, v interface{}) (err error)
任意のMarsharl/Unmarshalの関数をセットすることで自分で定義した型に沿ってPayloadTypeを変更したり、クライアントから来たフレームのPayloadTypeに応じて変換する方法を変えたりすることができます。
例えば以下のようにオレオレstructを定義し、Marshalを自作してテキストフレームで送るか、バイナリフレームで送るかを指定できます。
type Message struct {
Bs []byte
PayloadType byte
}
var codec = websocket.Codec{
Marshal: func(v interface{}) ([]byte, byte, error) {
m, ok := v.(*Message)
if !ok {
return nil, 0x0, errors.New("marshal error: v is not *Message")
}
return m.Bs, m.PayloadType, nil
},
}
func BinaryHandler(ws *websocket.Conn) {
m := &Message{
Bs: []byte("This is binary message."),
PayloadType: websocket.BinaryFrame,
}
err := codec.Send(ws, m)
if err != nil {
log.Println("WehsocketHandler send error:", err)
}
}
こんなことをしなくても元からwebsocket.Message
というCodecが定義されており、[]byte
のときはBinaryFrameで、string
のときはTextFrameで送ってくれます。
その他にもencoding/json
のタグがついたstructをMarshal/UnmarshalしてTextFrameで送るcodecは元から定義されており、websocket.JSON
で利用できます。
WebSocketにupgradeする前に認証する
WebSocketは以下の手順でコネクションを確立させます。
- クライアントからUpgradeヘッダなどを付けたGETリクエストを送る
- サーバでヘッダを検証し、よければStatusコード 101を返す
- WebSocket開始
1から2までは普通のHTTPと一緒なので、ここに認証を噛ませることができます。例えばHTTPで独自ヘッダをつけたり、query stringによって認証することが可能です。
ヘッダーによる簡単なパスワード認証を行うコードを以下に示します。
const PASSWORD = "hogehogefugafuga"
func AuthHandler(w http.ResponseWriter, r *http.Request) {
pass := r.Header.Get("X-OreOre-Password")
if pass != PASSWORD {
w.WriteHeader(http.StatusForbidden)
return
}
websocket.Handler(websocketHandler).ServeHTTP(w, r)
}
func websocketHandler(ws *websocket.Conn) {
// do something
}
このコードでUpgrade前に接続を切ることができます。JavaScriptのWebSocket APIだと任意のヘッダをリクエストに埋め込むのはできないみたいなので、その代わりにquery stringを用いて同様のことを行うことも可能です。
kuiperbeltについて
以上はGoでWebSocketをしゃべる話でしたが、例えば別の言語でしゃべりたくなったとしましょう。その場合にRubyやPython,PerlといったLLですと、WebSocketのような非同期プロトコルを扱うには工夫が必要です。
僕は仕事でPerlを用いていますので、Perlの例を紹介しますとAnyEventと呼ばれるイベント駆動プログラミングを実現するライブラリを用いてサーバを作る必要があります。その場合、RDBMSを用いるライブラリ(PerlであればDBI)などのブロッキング動作が前提としたライブラリをそのまま使うとはできません。また1つのコネクションを複数のリクエスト/セッションが共有してしまう危険性もあります。
そこで普段通りのブロッキングI/Oな環境でWebSocketを扱う場合、別のミドルウェアにクライアントからのWebSocketの接続を維持してもらい、PerlのサーバからはHTTPのRPCでそのミドルウェアにリクエストを投げ、ミドルウェアはHTTPのBodyに入っているメッセージをWebSocketのメッセージに変換してクライアントに送りつける、というアーキテクチャを考えることができます。
kuiperbeltはそのアーキテクチャを実現するためのミドルウェアです。
ekboというコマンドをインストールすれば使えるので以下のコマンドでインストールしてみてください。
$ go get github.com/mackee/kuiperbelt/cmd/ekbo
まだドキュメントも無くgo vet
などもかけていない汚いコードですが、一応動く状態になっており、リポジトリの_example`以下にPerlと組み合わせて作られた簡単なチャットアプリケーションが実装されています。
cpanmコマンドが入ったPerlをインストールしていただき、このディレクトリでcpanm --installdeps .
で依存モジュールを入れて、plackup -s Starlet --port=12346 app.psgi
とやっていただくとPerl側アプリケーションが立ちますので、別のコンソールでekbo --config=config.yaml
とすると、kuiperbeltが立ち上がります。
http://localhost:12346/
にアクセスするとチャットアプリケーションが開くので試してみてください。
もう一個だけ宣伝
12/16(水)にGotanda.pm #7 vs Yokohama.pm #13でPerl + Goの実運用の話
と題しましてソーシャルゲームでマイクロアーキテクチャ的にPerlで書かれたアプリケーションサーバとGoで書かれたアプリケーションサーバを運用している話をしますので、もしよろしければ聞きに来てください。
次(4日目)は
Go: takeshiyakoさん
Goその2: Maki-Daisukeさん
Goその3: hirotakasterさん
が担当されます! 乞うご期待!