golang.org/x/net/websocketの使い方とkuiperbelt

この記事は、2015年のGo Advent Calendarの3日目の記事です。

さてGoのHTTPといえば`net/http`において[今までどおりのコードでHTTP2対応が出来るようになる変更](http://deeeet.com/writing/2015/11/19/go-http2/)がGo1.6にて入るようになりますが、それはさておき、突然WebSocketがしゃべりたくなったとしましょう。

WebSocketをサーバで扱う実装といえばnode.jsのものが有名ですが、Goにおいても簡単に扱う事ができます。
ここでは`golang.org/x/net/websocket`を用いて説明し、また最後にHTTP/1.1以前しかしゃべることのできないサーバでもWebSocketによるサーバサイドプッシュを実現できるミドルウェアである拙作の`github.com/mackee/kuiperbelt`の紹介をします。

## 基本的な使い方

ほぼ[godoc](https://godoc.org/golang.org/x/net/websocket)に書かれている内容どおりで申し訳ないのですが……

使い方はごく簡単で、`golang.org/x/net/websocket`をimportした上で以下のinterfaceを持つ関数を作ります。

```go
func (*websocket.Conn)
```

例えばecho serverであれば、以下のようにします。

```go
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を書くことができます。

これをサーバに組み込むのも簡単です。

```go
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](https://github.com/websockets/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](http://qiita.com/m0a/items/f6405bc29073a7609050)

## その他tips

### フレームを指定して送る
websocketには以下のようにフレームに種類があります。

```
%x0 は継続フレームを表す
%x1 はテキスト­フレームを表す
%x2 はバイナリ­フレームを表す
%x3-7 は追加の非­制御フレーム用に予約済み
%x8 は接続の close を表す
%x9 は ping を表す
%xA は pong を表す
%xB-F は追加の制御フレーム用に予約済み
```

[RFC6455 — The WebSocket Protocol 日本語訳](http://www.hcn.zaq.ne.jp/___/WEB/RFC6455-ja.html#_xref-0-0)より

このうち継続フレームやcloseフレーム、ping/pongフレームは何をしなくてもよしなに対応してくれるのですが、`*websocket.Conn`で普通に`Write`すると`websocket.Conn.PayloadType`が使われます。
constで`websocket.TextFrame`および`websocket.BinaryFrame`が定義されているので、これを使って都度セットしてもいいのですが、Write中に別のgoroutineでPayloadTypeを変更されると意図しない種類のフレームとして送られる可能性があります。
なにかうまい方法はないかなと探してみたところ、`websocket.Codec`を使う方法が良さそうでしたので紹介します。

`websocket.Codec`は以下の様な定義のstructです。

```go
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を自作してテキストフレームで送るか、バイナリフレームで送るかを指定できます。

```go
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は以下の手順でコネクションを確立させます。

1. クライアントからUpgradeヘッダなどを付けたGETリクエストを送る
2. サーバでヘッダを検証し、よければStatusコード 101を返す
3. WebSocket開始

1から2までは普通のHTTPと一緒なので、ここに認証を噛ませることができます。例えばHTTPで独自ヘッダをつけたり、query stringによって認証することが可能です。

ヘッダーによる簡単なパスワード認証を行うコードを以下に示します。

```go

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と組み合わせて作られた簡単なチャットアプリケーションが実装されています。

https://github.com/mackee/kuiperbelt/tree/master/_example

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](http://gotanda-pm.connpass.com/event/23106/)で`Perl + Goの実運用の話`と題しましてソーシャルゲームでマイクロアーキテクチャ的にPerlで書かれたアプリケーションサーバとGoで書かれたアプリケーションサーバを運用している話をしますので、もしよろしければ聞きに来てください。


次(4日目)は
Go: [takeshiyako](http://qiita.com/takeshiyako)さん
Goその2: [Maki-Daisuke](http://qiita.com/Maki-Daisuke)さん
Goその3: [hirotakaster](http://qiita.com/hirotakaster)さん
が担当されます！　乞うご期待！