#WebSocketクライアント
今更感がかなりありますが、WebSocketクライアントを少し検証したので、久しぶりに投稿します。
今回は普段WebSocket通信を行う場合、ほとんどの処理をブラウザが行ってくれます。その処理の中身をちょっとだけ実践してみようと思います。
#用意したもの
・tornadoサーバー(さくらクラウド)
・クライアントPC(マイMac)
#WebSocketとは
念のため、WebSocketについてです。
HTTP通信で使用したコネクションをそのままデータ通信に使用するようなプロトコルです。
そのため、HTTP通信でSwitching protocolを行うハンドシェイクを行います。
#サーバー側
tornadoでサーバー起動させましたが、ソースは
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>
<head></head>
<body></body>
</html>
htmlは空っぽです。
このサーバーを起動させると8080ポートでlistenし、index.htmlを返却します。
WebSocketは/wsになります。
#クライアント
クライアントは自分のPCから行いました。
python3.4です。
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を確認して、データの並びを見てみるといいと思います。
#実行
サーバーを起動後、クライアントからアクセスしてみると、
サーバー側
$ 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
クライアント
$>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を指定しなくてもおそらく問題ないと思いますが、今回はわかりやすいように指定しました。
いつも通りソースをのせるだけになりましたが、何かの役に立てればと思います。