mbed に WebSocketサーバーを載せてみた。

WebSocket関連で検索するのがほぼ日課みたいなものとなっています。
そんなある日、mbedというものを見つけました。

mbedとは?

私も触り始めたばかりで偉そうに説明できるほどではありませんが、第一印象として「簡単」ということ。
54mmx26mmの小さな基盤にARMのCPUが搭載されたものです。
青い基盤のもの(mbed NXP LPC1768)と、黄色い基盤のもの(mbed NXP LPC11U24)ものとがあります。(他にもいくつか種類があるみたいです。)
青い方はARM Cortex-M3を搭載しており、黄色い方はスペックを落として低消費電力化したものとなっており、ARM Cortex-M0を搭載しています。アキバではマルツ千石電商秋月電子などで入手可能です。 開発用IDEクラウドで用意されており、いつでもどこでもブラウザ上で開発ができるというのが素晴らしいところです。なお、オフラインでの開発環境もできるみたいです。
その他の詳しい内容はここでは割愛させていただきます。

WebSocketサーバーを載せる

mbed用にWebSocketクライアントが開発されているのが見つけるきっかけでした。
しかし、ソースを見てみるとhybi-00(hixi-76)の古いプロトコルバージョンを使用して作成されたもので、RFCに対応していないものでした。 ですので、RFCに対応したWebSocketを載せてみようと思います。
@masato_kaさんのブログのコメントにてwebsockets_hello_world_ethernetをインポートすればというコメントが有りましたのでインポートしてみるとRFCに対応したWebSocketライブラリが使用されていました。
ただ、WebSocketクライアントを載せた場合は別途サーバーが必要となります。そこで、WebSocketサーバーをmbedに載せればブラウザーから直接データのやり取りが行えるようになるのではと思い挑戦してみたら出来たというお話です。

ソース

SHA1ハッシュの計算を行うにあたり、NetServiceSourceからsha1.h, sha1config.h, sha1.cのソースを使用させていただいています。
私は、C++はまったくといっていいほど触ったことがないため、いろいろとわからないままで組んでおりますのでご理解の程。警告は読んでわからないものは放置しています。
main.cppのソースを以下に掲載します。

なお、今回のプロジェクトファイル一式は下記のリンクからダウンロードできます。
mbed_WebSocketServer.zip

ほとんどコピペなソースなので主要なonLinkSocketEvent関数部分のみ載せます
void onLinkSocketEvent(TCPSocketEvent e) {
    switch (e) {
        case TCPSOCKET_CONNECTED:
            printf("TCP Socket Connected\n");
            break;
        case TCPSOCKET_WRITEABLE:
            //Can now write some data...
            printf("TCP Socket Writable\n");
            break;
        case TCPSOCKET_READABLE:
            //Can now read dome data...
            printf("TCP Socket Readable\n");
            // Read in any available data into the buffer
            char buff[1024];
            while ( int len = link->recv(buff, 1024) ) {
                // And send straight back out again
                //link->send(buff, len);
                
                if (wsState == 0) {
                    // ハンドシェイクステート
                    buff[len]=0; // make terminater
                    printf("%s\n", (char*)buff);
                    for (int i = 0; i < len; i++) {
                        if (buff[i] == 'K' && buff[i + 1] == 'e' && buff[i + 2] == 'y') {
                            for (int j = i + 1; j < len; j++) {
                                if (buff[j] == '\r') {
                                    i += 5;
                                    int keyLen = j - i;
                                    char strKey[keyLen + 1];
                                    strKey[keyLen] = 0;
                                    // Sec-WebSocket-Keyフィールドの値をstrKeyに取得
                                    strncpy(strKey, buff + i, keyLen);
                                    // Acceptデータ作成用の固定GUID文字列
                                    char guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

                                    // Sec-WebSocket-Acceptデータを作成
                                    strcat(strKey, guid);
                                    unsigned char hash[20];
                                    sha1((unsigned char*)strKey,strlen((char*)strKey),hash);
                                    string accept = encode64((char*)hash, 20);

                                    // ハンドシェイクレスポンスを作成
                                    string hsRes = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ";
                                    hsRes += accept;
                                    hsRes += "\r\n\r\n";
                                    // ハンドシェイクレスポンスデバッグ出力
                                    printf("%s\n", hsRes.c_str());
                                    
                                    // ハンドシェイクレスポンスをクライアント(ブラウザー)に送信
                                    link->send(hsRes.c_str(), hsRes.size());
                                    // データ送受信ステートに移行する
                                    wsState = 1;
                                    return;
                                }
                            }
                        }
                    }
                } else {
                    // データ受信ステート
                    // ここでクライアントから送られてきたデータを処理する。
                    // 今回は送られてきたデータをデバッグ出力し、
                    // そのままクライアント(ブラウザー)に送信する。
                    // なお単純にエコーで返す場合は以下の1行だけでいい。
                    // link->send(buff, len);
                    bool fin = (buff[0] & 0x80) == 0x80;
                    int opcode = buff[0] & 0x0f;
                    if(opcode == 9) {
                        // Ping(opcode = 0x9)が送られた場合は、Pong(opcde = 0xA)を返す 
                        buff[0]++;
                        link->send(buff, len);
                        return;
                    }
                    int dataLen = buff[1] & 0x7f;
                    if (!fin || dataLen > 125) {
                        // 今回は、FINフラグが立っていないフレーム、
                        // または126バイト以上のデータは扱わないことにする
                        link->close();
                        return;
                    }
                    int i = 0;
                    // アンマスクを行う
                    for (i = 0; i < dataLen; i++) {
                        buff[6 + i] = buff[6 + i] ^ buff[2 + (i % 4)];
                    }
                    if(opcode == 1) {
                        // 送られてきたフレームがテキストフレームの場合
                        // 送られてきたテキストデータをデバッグ出力
                        char dispData[dataLen + 1];
                        strncpy(dispData, buff + 6, dataLen);
                        dispData[dataLen] = 0;
                        printf("%s", dispData);
                    }
                    // 送信フレームの作成
                    char sendData[2 + dataLen + 1];
                    sendData[0] = buff[0];
                    sendData[1] = buff[1] & 0x7f;
                    for (i = 0; i < dataLen; i++) {
                        sendData[2 + i] = buff[6 + i];
                    }
                    sendData[2 + dataLen] = 0;
                    // クライアント(ブラウザー)に送信
                    link->send(sendData, 2 + dataLen);
                }
            }
            break;
        case TCPSOCKET_CONTIMEOUT:
            printf("TCP Socket Timeout\n");
            break;
        case TCPSOCKET_CONRST:
            printf("TCP Socket CONRST\n");
            break;
        case TCPSOCKET_CONABRT:
            printf("TCP Socket CONABRT\n");
            break;
        case TCPSOCKET_ERROR:
            printf("TCP Socket Error\n");
            link->close();
            break;
        case TCPSOCKET_DISCONNECTED:
            printf("TCP Socket Disconnected\n");
            // wsStateをリセット
            wsState = 0;
            link->close();
            break;
        default:
            printf("DEFAULT\n");
    }
}

LANのモジュラ・ジャックの増設

mbed単体ではLANケーブルを繋げることは困難ですので(直接ハンダ付けすれば出来なくは無いですが)、LANの(RJ45)モジュラ・ジャックを増設する必要があります。
LANのモジュラ・ジャックは千石電商などでキットとして販売されていますのでこちらを購入したほうがいいでしょう。 また、☆Board Orangeというmbed用のベースボードのも販売されています。このボードはmicroSD、USB(Host)、キャラクターLCDなどのI/Oインターフェイスを簡単に増設できるベースボードとなっています。このベースボードも千石電商などで入手可能です。(mbed用のベースボードは他にもあるようです。)

準備

mbedとPCをUSBとLAN(クロス)ケーブルで接続します。
プログラムのソースは固定IPで接続するように組んでいます。DHCPでIPを自動で割り振る場合は割り振られたmbedのIPアドレスが分かる手段を整えてください。
また、Windowsの方はmbedのシリアルポートドライバーインストールし、Tera Termなどのシリアル通信モニターソフトを起動します。MacLinuxの方はドライバーは不要なようですので、シリアル通信モニターのみ用意し起動します。

実行

サンプルプログラムは固定IPで接続するように組んでいます。DHCPで接続したい場合は適宜ソースを修正してください。
また、1:1での接続のみ対応しています。
なお、ブラウザープロトコルバージョンhybi-07以上に対応したChromeまたはFirefox(Windowsの場合)で接続してください。 コンパイルしてできたbinファイルをmbed(USBで接続するとマスストレージとして認識されます)にコピーまたは移動します。
コピーまたは移動したらmbedのリセットボタンを押し実行します。
シリアル通信モニターソフトの画面にListeningと表示されたら、プロジェクトファイル一式内にあるmbedWebSocketTest.htmを開きます。
(なお、Chromeで開く場合は、Chromeが起動している場合はすべて閉じたあとにChromeを --allow-file-access-from-files オプション付きで起動したあとに開いてください。)
開いたら、テキストボックスに半角で文字列を入力しエンターキーを押したら送信されます。
送信されたデータはシリアル通信モニターソフトの画面に
received data:送信データ
と表示されます。
また、mbed側では受診したデータをブラウザーにそのまま返しますので、ブラウザーのテキストボックスの下にも送信された文字列が表示されます。

最後に

今回は、WebSocketサーバーを実装しテキストデータの送受信を行いました。WebSocketプロトコルはバイナリデータの送受信にも対応していますので(Chromeはすでにバイナリデータの送受信に対応していますが、Firefoxはバージョン11から対応します。)、画像データなどの送受信も可能です。