Hatena::ブログ(Diary)

みずぴー日記

2011-01-23(日)

サーバを作りながら学ぶWebSocketプロトコル

| サーバを作りながら学ぶWebSocketプロトコル - みずぴー日記 を含むブックマーク

WebSocketって何?

f:id:mzp:20110123214943p:image

WebSocketは、Javascriptサーバとリアルタイム双方向通信をする仕組みです。概要は第1回 WebSocket登場までの歴史:Jettyで始めるWebSocket超入門|gihyo.jp … 技術評論社によくまとまっています。

この記事ではWebSocketサーバを実装しながら、どういうプロトコルかを解説します。サンプルコードはWebSocket Draft 76でechoサーバーを作ってみた - いろいろな何かのものを参考にさせていただいています。ありがとうございます。

※WebSocketプロトコルは現在ドラフトの段階なので、そのうち仕様が変わる可能性があります。この記事は20111/23時点の情報です。

プロトコル概要

WebSocketで通信を行なうおおまかな流れは次のようになります。

  1. クライアントサーバの間でハンドシェイクを行ない、接続を確立する。
  2. データを双方向でやりとりする。

これを順番に説明してきます。

クライアントサーバの間でのハンドシェイク

f:id:mzp:20110123215316j:image

クライアントが接続するたびに、サーバには次のようなリクエストが送信されます。

GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
Sec-WebSocket-Protocol: sample
Upgrade: WebSocket
Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
Origin: http://example.com

^n:ds[4U

※ 赤字の部分はハンドシェイクに使われる情報なので、接続のたびに変化します。

このリクエストに対して、適切なレスポンスを返すことで接続が確立されます。

まず、Sec-WebSocket-Key1Sec-WebSocket-Key2暗号化されているので復号する必要があります。復号は数字部分/スペースの個数で行なうので、例えば12998 5 Y3 1 .P00は1299853100/5=259970620です。

これをコードにすると次のようなります。

def decode(self, s):
  '''ハンドシェイク中のキーを解析する。'''
  # 数字の部分だけをとりだす
  n = filter(lambda c : c.isdigit(), s)
  # スペースだけをとりだす
  m = filter(lambda c : c == ' '   , s)
  # 数字部分 / スペースの個数
  return int(n) / len(m)

次に、復号したSec-WebSocket-Key1(32bits)、同じく復号した Sec-WebSocket-Key2(32bits) 、リクエストのボディ部(128bits) を順に並べてmd5を計算し、これをHTTPレスポンストの本体にして送り返すことでハンドシェイクを確立します。

これをコードにすると次のようになります。

part1 = self.decode(fields['sec-websocket-key1'])
part2 = self.decode(fields['sec-websocket-key2'])

# 値を32bitのビッグエンディアンのバイナリーにする
CHALLENGE = struct.pack('>I', part1)

# 値を32bitのビッグエンディアンのバイナリーにする
CHALLENGE += struct.pack('>I', part2)

CHALLENGE += key

# /chalenge/のMD5 fingerprintを/response/に入れる
RESPONSE = hashlib.md5(CHALLENGE).digest()

あとはWebSocketプロトコルにそったヘッダをつけて、クライアントに送り返します。

HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://example.com
Sec-WebSocket-Location: ws://example.com/demo
Sec-WebSocket-Protocol: sample

8jKS'y:G*Co,Wxa-

以上のことを図にまとめると次のようになります。

f:id:mzp:20110123214945p:image

データ送受信

f:id:mzp:20110123214944p:image

接続を確立したら、データの送受信ができます。WebSocketではデータの送受信はフレームという単位で行います。

テキストフレームは先頭に\x00、末尾に\xFFがつきます。例えば、"hoge"を送りたい場合は、"\x00hoge\xFF"になります。制御用のフレームもあるけど、こっちはなくてもとりえず動作します。

逆にフレームからテキストを取り出す場合は先頭の\x00と末尾の\xFFを除去すればいいので、次のようなコードになります。

data = self.request.recv(1024)

xs = itertools.takewhile(lambda x : ord(x) != 0xFF, data[1:])
self.RAW_DATA = "".join(list(xs))

コード

以上のことをまとめると以下のようなコードになります。

https://gist.github.com/737068

実行例

参考元の記事と同様に実行できます。

f:id:mzp:20110123214942p:image

要するに

  • ハンドシェイクはちょっとややこしいけど、データの送受信は簡単
  • ライブラリ使ったほうが楽だよ。