All Articles

GolangでWebSocketサーバ書いた

最近はPHPでステートレスなアプリばかり書いてたので、今更感はありますが息抜きに時間を見つけて書いてみました。参考にしたのは当たり前だけどRFC6455です。これを満たすように実装してみました。

GolangにはすでにWebSocketのパッケージがあるのでそれ使うのがいいです。が、今回は勉強ってことで、httpは使わず生のnetとcrypto/tlsパッケージを使う縛りで、handshakeから実際にコネクションを張ってメッセージフレームをやり取りしたりbroadcastするところまでやりました。

仕様的にGETっぽいリクエストしか処理しないし、計測はしてないけどそのままTCPでやった方がパフォーマンス出るんじゃないかと。あとgoroutineとchannelでの非同期周りをちゃんとやりたかった。作ったのはこちらです。

https://github.com/ysugimoto/aun

接続と待受

WebSocketの処理の流れはJxckさんのエントリがわかりやすいです:

http://jxck.hatenablog.com/entry/20120725/1343174392

なので省略しますが、基本的にリクエスト -> レスポンスの処理は生のTCPソケットでやったので、解析とかも自前でした。というよりも、socketを継続して待ち受けて、適切なところにメッセージングするのが難しかった印象。先に非同期したい処理をchannelと一緒にgoroutineで起動しておいて、その直後にfor - select ループで待ち受ける感じが定石なのかな。以下の様な感じ:

func main() {
    // 受付用channel
    c := make(chan int 1)
    // goroutine起動、引数にchannel渡す(構造体ならメンバにchannelをつけておけばよい)
    go someMethod(c)
    // 待受
    for {
        select {
            case <- c:
               fmt.Println("受け取ったよ!”)
        }
    }
}

// この関数はgoroutineで起動する
func someMethod(c chan int) {
   for {
     // 何かループとか
     c <- 1 // メッセージ送信
   }
}

goroutineで起動する関数は公開しないようにして、何かマーク付けて置いたほうがいいのかも。 あと、for-selectのcase文でbreakするとforスコープまでしか抜けないので、forの上にラベルを貼るか、待受処理を関数化してreturnで一気に抜けるようにするといいと Ten Useful Techniques in Go に書かれています。

参考: http://arslan.io/ten-useful-techniques-in-go

あとは、入室/退室、接続、切断、メッセージ受信/送信のそれぞれの用途のchannelを作って、TCPソケットからメッセージを読み取ったタイミングで適切なchannelに送信すればOKです。今回は各channelに対して統一したインターフェースを要求するようにしたことで、ハンドシェイクでもメッセージでも平文でも上手く扱えたのでこれは良かったかと。

メッセージフレームのエンコード/デコード

RFC6455では下記の形式でメッセージをやり取りするように指定されているので、それに合わせてコツコツ処理するだけです:

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

payload lengthの扱いとmaskingがちょっと面倒なくらいでした。 あとはこのデータを送受信することで、Chrome/Firefoxで動作させることができました。

感想とか

改めてシンプルなプロトコルだなー、と。ファイル数も少なかったし、バイナリは試してないけど、メッセージ自体も小さかったです。

セキュリティ周りは気になるところですが、今回大元の部分を理解できたので、専用にカスタムヘッダで認証したり、メッセージフレームをちょっといじったりしてオリジナルの要件に合わせられるような気もしました。久々に書いてて楽しかったですね。

cliで起動出来るようにしてるので、スポットでWebSocketな双方向通信が必要な時にさっくり使えるかも知れません。まぁ、通常はgolang.org/x/net/websocketを使いましょう。安定。

現場からは以上です。