WebSocketクライアント

#WebSocketクライアント
今更感がかなりありますが、WebSocketクライアントを少し検証したので、久しぶりに投稿します。
今回は普段WebSocket通信を行う場合、ほとんどの処理をブラウザが行ってくれます。その処理の中身をちょっとだけ実践してみようと思います。

#用意したもの
・tornadoサーバー（さくらクラウド）
・クライアントPC（マイMac）

#WebSocketとは
念のため、WebSocketについてです。
HTTP通信で使用したコネクションをそのままデータ通信に使用するようなプロトコルです。
そのため、HTTP通信でSwitching protocolを行うハンドシェイクを行います。


#サーバー側
tornadoでサーバー起動させましたが、ソースは

```py3
import tornado.ioloop
import tornado.web
import tornado.websocket
from tornado import options

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        self.render("index.html")

class WebSocket(tornado.websocket.WebSocketHandler):
    def open(self):
        print("open websocket connection")

    def on_message(self, message):
        print(message)

    def on_close(self):
        print("close websocket connection")

app = tornado.web.Application([
    (r"/", MainHandler),
    (r"/ws", WebSocket),
])

if __name__ == "__main__":
    options.parse_command_line()
    app.listen(8080)
    tornado.ioloop.IOLoop.instance().start()
```


```html:index.html
<html>
<head></head>
<body></body>
</html>
```

htmlは空っぽです。
このサーバーを起動させると8080ポートでlistenし、index.htmlを返却します。
WebSocketは/wsになります。

#クライアント
クライアントは自分のPCから行いました。
python3.4です。

```py3
import time
import os
import base64
import socket
import struct
import array


def make_ws_data_frame(data):
    FIN = 0x80
    RSV1 = 0x0
    RSV2 = 0x0
    RSV3 = 0x0
    OPCODE = 0x1
    MASK = 0x80
    payload = 0x0

    frame = struct.pack('B', FIN | RSV1 | RSV2 | OPCODE)
    data_len = len(data)
    if data_len <= 125:
        payload = struct.pack('B', MASK | data_len)
    elif data_len < 0xFFFF:
        payload = struct.pack('!BH', 126 | MASK, data_len)
    else:
        payload = struct.pack('!BQ', 127 | MASK, data_len)

    frame += payload
    masking_key = os.urandom(4)
    mask_array = array.array('B', masking_key)
    unmask_data = array.array('B', data.encode('UTF-8'))

    for i in range(data_len):
        unmask_data[i] = unmask_data[i] ^ masking_key[i % 4]

    mask_data = unmask_data.tobytes()
    frame += masking_key
    frame += mask_data

    return [frame, len(frame)]

HTTP_GET = """GET / HTTP/1.1\r
Connection: Keep-Alive\r
\r\n"""

ws_upgrade_header = {
    'Upgrade' : 'websocket',
    'Connection' : 'Upgrade',
    'Sec-WebSocket-Key' : base64.b64encode(os.urandom(16)).decode('UTF-8'),
    'Sec-WebSocket-Version' : '13',
}

sock = socket.create_connection(['my.domain.com', 8080])

sock.send(HTTP_GET.encode('UTF-8'))

recv_buf = ""
recv_buf = sock.recv(4096)

print(recv_buf.decode('UTF-8'))

headers = "GET /ws HTTP/1.1\r\nHost: my.domain.com\r\n"
for key in ws_upgrade_header:
    headers += key + ": " + ws_upgrade_header[key] + "\r\n"

headers += "\r\n"

print(headers)

sock.send(headers.encode('UTF-8'))

time.sleep(1)

recv_buf = sock.recv(4096)
print(recv_buf.decode('UTF-8'))

time.sleep(1)

message, message_len = make_ws_data_frame('Hello')

sock.send(message)

sock.close()

```

相変わらずエラー処理はしていません。
WebSocketデータマスクの処理などは、tornadoを参考にしました。
RFCを確認して、データの並びを見てみるといいと思います。

#実行
サーバーを起動後、クライアントからアクセスしてみると、

サーバー側

```bash
$ python3 server.py 
[I 150222 15:43:15 web:1811] 200 GET / (XXX.XXX.XXX.XXX) 1.82ms
open websocket connection
Hello
close websocket connection

```

クライアント

```bash
$>python3 ws_client.py 
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: TornadoServer/4.0.2
Date: Sun, 22 Feb 2015 06:43:15 GMT
Etag: "7ead2de92161d299969a9eb938ab1bd212bbeb90"
Content-Length: 43

<html>
<head></head>
<body></body>
</html>

GET /ws HTTP/1.1
Host: XXX.XXX.XXX.XXX
Sec-WebSocket-Version: 13
Connection: Upgrade
Sec-WebSocket-Key: /LfLVpUsMF7UevDCeOSwhw==
Upgrade: websocket


HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: b0eu6R73OLCL/9hkzEcIjA8eguQ=
```

プロトコルがswitchしているのがわかるかと思います。
サーバー側にはクライアントから送信した「Hello」が受信されているのを確認できました。

#最後に
当たり前ですが、クライアント側の最初のGETメッセージで「Connection: Close」を指定するとWebSocketハンドシェイクできません。
Connectionを指定しなくてもおそらく問題ないと思いますが、今回はわかりやすいように指定しました。
いつも通りソースをのせるだけになりましたが、何かの役に立てればと思います。