ラズパイでWebSocketの仕組みを理解する

ラズパイをWebSocketサーバーにして、同じローカルネットワーク内にいるMacBからPythonやJavaScriptでデータ通信できるか試してみた。

あくまでWebSocketの理解を深めるのが目的である。

この記事の前半ではHTMLとJavaScrpitを使ってクライントを作り、できるだけ簡単にWebSocketの通信を実現し、WebSocketの全体像をつかみやすくした。後半ではPythonでWebSocketクライアントを作り、HTTPヘッダーの内容まで足をふみ込んでいく。

ラズパイにWebSocketサーバーをインストール

PythonのライブラリにWebSocketサーバーがあるので、それをラズパイにインストールしてラズパイをWebSocketサーバーにしていく。

$ sudo pip install websocket-server

ラズパイからWebSocketでブラウザにメッセージを送る


画像の拡大

websocket-sever のインストールができたところで、ラズパイからWebSocketでブラウザにメッセージを送ってみよう。

WebSocketサーバープログラム

次の簡単なWebSocketサーバーをPythonで作った。ラズパイからWebSocketでブラウザへ「Hello world! This is Raspberry Pi!」を送信するプログラムである。

from websocket_server import WebsocketServer

def sendMessage(client, server):
    server.send_message_to_all("Hello world! This is Raspberry Pi!")

server = WebsocketServer(5555, host="192.168.100.136")
server.set_fn_new_client(sendMessage)
server.run_forever()

hostにはラズパイのIPアドレスを設定する。IPアドレスを調べるには、$ ifconfig を実行して wlan0inet に書かれている。

JavaScriptでWebSocketクライアントの作成

次に、ラズパイWebSocketサーバーへ通信するために、WebSocketクライアントをJavaScriptで作ってみた。このコードはMacで動かすものとする。

<html>
<head>
    <script src="http://code.jquery.com/jquery-latest.min.js">
    </script>
    <script>
        $(function () {
            var ws = new WebSocket("ws://192.168.100.136:5555/");
            ws.onmessage = function (message) {
                document.getElementById("recived").innerHTML = message.data;
            }
        })
    </script>
</head>
<body>
    <div id="recived"></div>
</body>
</html>

このコードをたとえば websocket-client.html として保存し、それをSafariで表示させてみよう。

ラズパイで設定したメッセージ「Hello world! This is Raspberry Pi!」が、ブラウザに表示されればWebSocketの通信成功である。

JavaScriptでブラウザからラズパイへWebSocketメッセージを送る

JavaScriptでブラウザからラズパイへWebSocketメッセージを送る
JavaScriptでブラウザからラズパイへWebSocketメッセージを送る

今度は逆に、クライアントからサーバーへWebSocketでメッセージを送れるようにしてみよう。

ラズパイのサーバープログラム websocket-sever.py を少し改造する。

from websocket_server import WebsocketServer

def sendMessage(client, server):
    server.send_message_to_all("Hello world! This is Raspberry Pi!")

def receivedMessage(client, server, message):
    print message
    server.send_message(client, "recived message: " + message)
    
server = WebsocketServer(5555, host="192.168.100.136")
server.set_fn_new_client(sendMessage)
server.set_fn_message_received(receivedMessage)
server.run_forever()

クライアント側の websocket-client.html からラズパイへWebSocketでメッセージを送信できるように修正した。

<html>
<head>
    <script src="http://code.jquery.com/jquery-latest.min.js">
    </script>
    <script>
        $(function () {
            var ws = new WebSocket("ws://192.168.100.136:5555/");
            ws.onmessage = function (message) {
                document.getElementById("recived").innerHTML = message.data;
            }

            $('#btn').on('click', function () {
                ws.send($('#btn').text());
            });
        })
    </script>
</head>
<body>
    <div id="recived"></div>
    <button id="btn">Hi, I'm from Mac.</button>
</body>
</html>

ブラウザで表示し、ボタンを押すと次のようにメッセージを送れたと思う。


画像の拡大

こちらの記事もオススメ!

PythonでHTTPヘッダーを書いて、WebSocketクライアントを作ってみた

今度はよりWebSocket通信の理解を深めるため、JavaScriptではなくPythonを使ったWebSocketクライアントを作ってみよう。Pythonのscoketライブラリを使って、HTTPヘッダーを手作りしてみる。

WegSocketのヘッダーの構成

ところでWebSocketは次のようにHTTPリクエストを使ってWebSocketのハンドシェイクする形になっている。

GET / HTTP/1.1
Host: 192.168.100.136
Connection: Upgrade
Sec-WebSocket-Key: vTu2gqiamRGo9gXd9dKbXg==
Sec-WebSocket-Version: 13
Upgrade: websocket

Sec-WebSocket-Keyは16byteのランダムな値をbased64でエンコードしたもの。これはサーバーとクライアントとのハンドシェイクの受信を立証するために必要なものだ。

上記のHTTPリクエストをWebSocketサーバーへ投げると、次のようにレスポンスが返ってくる。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Od7wpaRgvxXMSnWrv2qkQqREX7I=

これは、これからWebSocket通信に切り替わるメッセージ。このハンドシェイク後、WebSocketでのデータ通信が行えるようになる。

WebSocketのデータフレーム

さらに、WebSocketのデータフレームがどうなっているのか見てみよう。

WebSocketのデータフレーム
WebSocketのデータフレーム

RFC6455より

流石にこのデータフレームを1から作り上げていくのはシンドイ。今回は「へぇ、こんな感じなんだ」で流して、オープンソースを参考にクライアントを作成する。

最終的にPythonで書いたWebSocketクライアントプログラムがこちら。

# -*- coding: utf-8 -*-
import time
import os
import base64
import socket
import struct
import array
try:
    import thread
except ImportError:
    import _thread as thread

## python3で動作

def ws_client(host, port, header):
    print('++++++++++++HEADER START+++++++++++++++++')
    print(header + '+++++++++++++HEADER END++++++++++++++++++')
    address = (host, port)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(address)
    s.send(header.encode())

    response = b''
    while True:
        recv = s.recv(1)
        if not recv:
            break
        response += recv
        if b'\r\n\r\n' in response:
            return response.decode(), s

def make_ws_data_frame(data):
    FIN = 0x80  # 128(8bit分を確保、そしてFINフラグは1である -> 0b10000000)
    RSV1 = 0x0
    RSV2 = 0x0
    RSV3 = 0x0
    OPCODE = 0x1
    # [OPCODE]
    # 0x0 継続フレーム
    # 0x1 テキストフレーム
    # 0x2 バイナリフレーム
    # 0x8 Closeフレーム(接続切断)
    # 0x9 ping
    # 0xA pong

    # FINと同様の考えでMASKから8bit分確保する
    MASK = 0x80
    payload = 0x0

    frame = struct.pack('B', FIN | RSV1 | RSV2 | RSV3 | OPCODE)

    # frame=b'\x81' -> 0b10000001
    # print(bin(int(frame.hex(), 16)))

    data_len = len(data)
    #    print(data_len)
    if data_len <= 125:
        payload = struct.pack('B', MASK | data_len)
    elif data_len < 0xFFFF:
        payload = struct.pack('!BH', 126 | MASK, data_len)
    else:
        # B(unsigned char, 1byte) Q(unsigned long long, 8byte)
        payload = struct.pack('!BQ', 127 | MASK, data_len)

    frame += payload
    #print(bin(int(frame.hex(), 16)))
    # 0b10000001 111111100000000011111110

    masking_key = os.urandom(4)
    #print(masking_key)
    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)]


if __name__ == "__main__":
    HOST = '192.168.100.136'
    PORT = 5555
    HEADER = 'GET / HTTP/1.1\r\n'
    HEADER += 'Host: ' + HOST + '\r\n'
    HEADER += 'Connection: Upgrade\r\n'
    HEADER += 'Sec-WebSocket-Key: ' + base64.b64encode(
        os.urandom(16)).decode('UTF-8') + '\r\n'
    HEADER += 'Sec-WebSocket-Version: 13\r\n'
    HEADER += 'Upgrade: websocket\r\n'
    HEADER += '\r\n'

    res, client = ws_client(HOST, PORT, HEADER)

    print(res)

    if header != '':

        def run(*args):
            frame, frame_len = make_ws_data_frame("Hello, I'm from Mac!")
            client.send(frame)
            time.sleep(1)
            #            client.close()
            print("thread terminating...")

        thread.start_new_thread(run, ())

        response = b''
        while True:
            recv = client.recv(1024)
            print(recv)
            if not recv:
                break
            response += recv
            if b'\r\n' in response:
                print(response.decode())
                response = b''

データフレームのプログラムはこちらを参考

Pythonでプログラムを実行すれば、ラズパイWebSocketサーバーと通信できるはずだ。JavaScriptではあれほど簡単だったWebScocket通信も、実際は複雑な処理を内部で行っている。

とは言え、データフレーム以外はHTTPリクエストと同じなので難しくないと思う。WebSocketでの通信は今後ますます普及していくので、ラズパイなどで遊びながら仕組みを理解すると良い。

Raspberry Pi Zero W - ヘッダー ハンダ付け済み - ラズベリー・パイ ゼロ W ワイヤレス
Raspberry Pi Zero W - ヘッダー ハンダ付け済み - ラズベリー・パイ ゼロ W ワイヤレス

Amazon

最後まで読んでいただきありがとうございました。

「この記事が参考になったよ」という方は、ぜひ記事をシェアをしていただけるととても嬉しいです。

今後も有益な記事を書くモチベーションにつながりますので、どうかよろしくお願いいたします。↓↓↓↓↓↓↓

あなたにおすすめ