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

  • 44
    いいね
  • 1
    コメント

この記事は、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.Connio.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.HandleFuncfunc 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は以下の手順でコネクションを確立させます。

  1. クライアントからUpgradeヘッダなどを付けたGETリクエストを送る
  2. サーバでヘッダを検証し、よければStatusコード 101を返す
  3. 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と組み合わせて作られた簡単なチャットアプリケーションが実装されています。

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

次(4日目)は
Go: takeshiyakoさん
Goその2: Maki-Daisukeさん
Goその3: hirotakasterさん
が担当されます! 乞うご期待!

この投稿は Go Advent Calendar 20153日目の記事です。