マイコンに実装したWebSocketによる双方向通信【STM32Nucleo】

前回の記事「HTTPプロトコルで構成したWEBサーバーを搭載したマイコンシステム」ではマイコンにWEBサーバーを搭載して専用のアプリを使用しなくてもPCやスマホ等のブラウザからアクセスすることでより身近に機器のIoT化に発展させる方法を解説しました。

ブラウザは通常HTTPプロトコルに従って機器と通信しているのですが、基本的に何かを要求するのはブラウザ側からだけです。機器側はブラウザからの要求に応じてサーバーとして応答するだけですので例えば、機器内のデータをブラウザに送信してリアルタイムで表示させることはできません。

HTTPプロトコルだけで機器内のデータをブラウザに送信して定期的にページを更新したり疑似的に動的な表示をさせることは可能(Comet, SSE等)なのですが、HTTPではちょっとしたデータを送るにも大きなサイズのヘッダを付加させるために無駄も多い通信プロトコルです。

そこで、このHTTPの欠点を補う別のプロトコルであるWebSockeを使うとブラウザと機器間であたかもTCPソケット通信のようにリアルタイム双方向通信ができるようになるため、マイコンにWebSocketを実装したいと思います。WebSocket通信は敷居が高くちょっとしたアプリには導入しにくいところを具体的な事例で解説し、身近に利用できることを目指しています。

HTTPプロトコル
WebSocketプロトコル

WebSocketとは

めかのとろ

WebSocketを一言で表すと、小さなサイズのデータフレームリアルタイムに双方向通信ができるHTTPの欠点を補ったプロトコルです。

めかのとろ

WebSocketはそう新しいプロトコルでもないのですが、使用するにはブラウザが対応していることが前提です。現在では身近なブラウザではほぼ大部分が対応しているために問題はありません。

めかのとろ

WebSocketの汎用的な解説詳細は専門のWEBエンジニアが得意とするところで、ここではどちらかといえば番外で応用する側ですので組み込みマイコンシステムで実現するにあたり特有な部分に限定して解説します。

めかのとろ

WebSocketを実装するにあたっては組み込み系のエンジニアにとってはHTMLとJavaScriptの使い勝手が組み込みプログラミングとは少し違うため慣れていない場合は苦労するのではないでしょうか。

コラム

世に出回っているWebsocket情報のほとんどがWEB上での記事に関するもので、組み込みマイコンにWebSocketを適用した例はWiFiモジュールESPシリーズを搭載したAruduino系のものしか見当たりません。
WEB上のものであれば、ファイルシステムが利用できるために、Node.jsのようなプラットフォームが使用できますが、ファイルシステムのない組み込みマイコンでは採用できません。また、Arduino系の開発環境であれば情報も豊富でライブラリが利用できますが、STM32マイコンのCortex-M3系では適用例があまりみあたらず、ライブラリ等はあることはありますが情報も少なく使いこなすことは困難のため、すべてを新規に構築する必要があるところが苦労した点です。

WebSocketを確立するまでの流れ

めかのとろ

WebSocketを開始するきっかけクライアント側のブラウザからHTMLの中に埋め込んだJabaScriptで記述したWebSocketリクエストを送ることです。これはHTMLページを起動するときにリクエストを発行してもよいし、ページ内に作成した接続開始ボタンを押したときにリクエストを発行する仕様にしてもよいです。

めかのとろ

WebSocket通信はしくみを理解するのはそう難しくはないですが、初めての実装は結構大変です。実装での最大のヤマはブラウザで発行されたWebSocketキーからブラウザへ返すアクセスキーを生成するところです。

めかのとろ

アクセスキーさえ生成できれば、あとはHTMLとJavaScriptを駆使するだけですので組み込み系の人にとってはちょっとなじみは薄いかもしれませんがWEBプログラミングに精通している人ならばなんのことはないと思われます。

めかのとろ

それではWebSocket通信のコネクション確立までの手順を確認していきます。

めかのとろ

以下の図がブラウザからサーバーにアクセスするところから始まるWebSocketのコネクション確立までの流れです。

めかのとろ

① まず通常のようにサーバーにアクセスするためにブラウザにIPアドレス/ポートを指定するとブラウザからサーバーへHTTPリクエストが送られます。

めかのとろ

② サーバーはブラウザからGETメソッドを受けて、レスポンスを返すのですが、その中にJavaScriptで記述したWebSocketを起動するためのリクエストを埋め込んでおきます。大事なポイントはここで一旦通信を切断しておくことです。

めかのとろ

③ 数秒おいてからブラウザはサーバーから送られたレスポンス内のJavaScriptを実行してWebSocketキーを含んだHTTPアップグレードリクエストをサーバーに送ります。

めかのとろ

④ サーバーではブラウザからのリクエストがWebSocketであることを認知するとWebSocketキーを抽出してアクセスキーを生成し、ブラウザに返します。

めかのとろ

⑤ ブラウザではサーバーから返されたアクセスキーが有効なものであると認定できるとWebSocketコネクションを確立し、オープンイベントを発火します。

めかのとろ

コネクション確立までの流れは以上です。これでサーバーとブラウザはWebsocketプロトコルによる双方向通信ができるようになります。あとはノンブロッキングのソケット通信となります。流れ自体は簡単なので理解はできると思いますがこれを実装するには結構な壁がありますので順次解説していきます。

コラム

イベント駆動型プログラミングでイベントが起こることを発火というそうです。

WebSocketリクエストを埋め込んだウェブページ

めかのとろ

WebSocketを開始する場合はブラウザからHTTPリクエストを受け取った後にレスポンスのメッセージボディにJavaScriptでWebSocketリクエストを埋め込むところが通常の場合と異なります。

WebSocketリクエストを埋め込んだHTTPレスポンス
めかのとろ

WebSocketを開始するためのコードは大体フォーマットは決まっていて、WebSocketオブジェクトを生成することから始めます。オブジェクト名任意に指定できます。下記のサンプル例ではwsocketとしています。

めかのとろ

下記のJavaScript記述したWebSocketリクエストのコードをリクエストのヘッダー部に埋め込んでおきます。

WebSocketリクエストコード
ポイント

WebSocketイベントは組み込みプログラミングにおける割り込みのようなものでWebSocketオブジェク生成時に同時に登録します。あらかじめイベントハンドラ内に定義しておいた内容は、WebSocket実行時、下記のイベントのたびに発生します。

  • openイベント:onopenイベントハンドラプロパティ 
    WebSocketのコネクションが開かれたときに発生
  • closeイベント:oncloseイベントハンドラプロパティ
    WebSocketのコネクションが切断したときに発生
  • messageイベント:onmessageイベントハンドラプロパティ
    WebSocketを通してデータを受信したときに発生
  • errorイベント:onerrorイベントハンドラプロパティ
    WebSocketのコネクションがエラーにより切断したときに発生
ポイント

WebSocketメソッドはブラウザ側から任意のタイミングでデータ送信およびWebSocketコネクション切断を行う場合に実行します。

  • sendメソッド:wsocket.send(data)はデータを送信するためのもの
  • closeメソッド:wsocket.close()はWebSocketコネクションを切断するためのもの
めかのとろ

WebSocketイベントとメソッドはJavaScriptのコードで記述し、アプリケーションに応じた処理内容にします。

ポイント

その他、接続状態を確認するためのプロパティreadyStateがあり、接続状態をモニターして切断時の処理などに利用できます。

0: CONNECTING まだコネクションが確立されていない状態
1: OPEN コネクションが確立されている状態
2: CLOSING コネクションが閉じる過程にある状態
3: CLOSED コネクションが閉じている状態
例:var connectionstate=wsocket.readyState //0 - 3

WebSocketキーからアクセスキー生成

めかのとろ

ブラウザ側でWebSocketリクエストコードを実行するとサーバー側へWebSocketキーを含んだHTTPアップグレードリクエストを送信します。

めかのとろ

WebSocketコネクション確立のための最大のポイントがブラウザから渡されたWebSocketキーからアクセスキーを生成するところです。

ブラウザが発行するWebSocketキー
めかのとろ

囲んだ24文字のコードがWebSocketコネクションのためにブラウザが発行したキーです。 一例としてサーバーは受信したGETリクエストの空白前のヘッド部に”Sec-WebSocket-KeyがあればWebSocketリクエストと認識し、その後に続く24文字分のキー(xxx...xxx==)を抽出します。

めかのとろ

この次が最大のヤマ場であるリクエストのWebSocketキーに対するアクセスキーを生成する部分です。

めかのとろ

アクセスキーを言葉で表現すると「リクエストで与えられたブラウザが生成したキーに固定値の"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"を連結して、SHA-1ハッシュと呼ばれる暗号化を行い、Base64エンコードを行った値」となっています。

ポイント

アクセスキーの定義を初めて目にするといったい何のことかはわかりにくいのですが、解説すると、ブラウザから与えられたキーにGUIDと呼ばれるコードを連結させたあとに、SHA1方式で暗号化して20桁のハッシュ値を生成し、さらにBASE64エンコードで符号化するということです。

コラム

初めてアクセスキー生成にチャレンジしたとき、まずハッシュSHA1BASE64といった言葉が何であるか分からなかったため、さっぱり理解できなかったです。これらは暗号・符号化のための用語やツールだとわかってからは何をすべきかようやくわかったのですが、暗号化の内容自体は無機質な性質のものでいまだによくわかりません。

ハッシュ値およびBASE64エンコード値生成手順

  1. ブラウザが生成するキー:
    リクエストヘッダからhqkH4S/djHSSovAPaDdycg==のみ抽出する
  2. GUID連結(標準関数strcatを使用):
    抽出したキーGUIDを連結し、hqkH4S/djHSSovAPaDdycg==258EAFA5-E914-47DA-95CA-C5AB0DC85B11とする
  3. ハッシュ値SHA1(20桁40文字16進数表示)生成(ハッシュ化):
    GUID連結したキーSHA1ハッシュ化すると1c10aa3dd498c5bfb39a95c5c10277e6770f28c1(バイナリ)が得られる
  4. ハッシュ値20桁をBASE64エンコード(符号化):
    20桁のSHA1ハッシュ値BASE64と呼ばれる符号化をするとHBCqPdSYxb+zmpXFwQJ35ncPKME=(28文字テキスト)が得られる
めかのとろ

アクセスキーを生成する手順は上記の流れですが、SHA1ハッシュ値を生成するアルゴリズムは難解のため、これすべてを自力でするのではなくどこかの汎用的なライブラリを利用すべきです。

めかのとろ

BASE64エンコードに関しては内容はそう難解ではありませんが、これもライブラリ情報は多いため、利用するほうが得策かもしれません。

ポイント1

ブラウザとサーバー間でやり取りするキーはテキストですがハッシュ化処理はバイナリで行っていることを意識してください。

ポイント2

WebSocketキーに対するアクセスキーの正当性はネットで利用できる変換ツールを使って確認できます。

WebSocketコネクション確立

めかのとろ

アクセスキーが生成できたら、WebSocketコネクション確立のためのHTTPアップグレードレスポンスをブラウザへ返します。

めかのとろ

レスポンスのフォーマットは下例のとおりでよいと思います。レスポンス一行目のステータスラインは"HTTP/1.1 101 Switching Protocols"と"HTTP/1.1 101 OK"のどちらでも機能するようです。

アクセスキーを付加したHTTPアップグレードレスポンス
めかのとろ

生成したアクセスキーが有効であることがブラウザで認識されるとWebSocketコネクションが確立し、WebSocketプロトコルによる双方向通信が始まります。いわゆるサーバーとクライアント(ブラウザ)間でハンドシェイクのやり取りが成立したということになります。

めかのとろ

確立するとopenイベントが発火し、定義したイベント内容が実行されます。例えばonopenイベントで”Websocket Connect!!"のメッセージを表示させる処理など記述しておけばよいでしょう。

WebSocketによるデータの送受信方法

めかのとろ

WebSocketコネクションが確立すると任意のタイミングでブラウザ、サーバー間で双方向通信ができるようになります。WebSocket通信では送受信データにはテキストのみならずバイナリも扱うことができます。

めかのとろ

データはWebSocketデータフレームとよばれるフォーマットに従ったものを送受信時に扱います。

めかのとろ

データフレームの詳細は下記リンクのサイトで確認してみてください。

引用:RFC6455 The WebSocket Protocol

WebSocketデータフレーム
めかのとろ

一度に送るデータが127文字以下でテキストかバイナリに限定する場合は比較的単純です。

めかのとろ

データフレームのフォーマットは1バイト単位のブロックで構成されています。1番目のバイトブロックは通信データが最後のパケットであるかの指定およびデータの種類を指定します。2番目のバイトブロックではデータのマスク有無およびデータ長を指定します。

めかのとろ

データのマスク有無ですが、これは決まり事でブラウザからのデータマスクを付加し、ブラウザへのデータマスクを付けないことになっています。

めかのとろ

言葉での説明はわかりにくいのですが、送受信するデータの具体的な適用例で確認すれば理解しやすいと思いますので、送信、受信の場合で解説していきます。

ブラウザからサーバーへのデータ送信

めかのとろ

具体的な例としてブラウザがテキスト文字列"test"を送信する場合で確認します。送受信はTCPソケット通信で行われます。

めかのとろ

ブラウザからの送信ではsendメソッドを使用します。

sendメソッド wsocket.send(”test") を実行

めかのとろ

4文字テキストの場合、ブラウザは10バイト分のWebSocketデータフレームを送信します。サーバー側で受信するTCPデータバッファをdata_buffer[]とすると実際にこの受信バッファに読み込まれるデータの例は以下のようになります。

ブラウザが”text”を送信した場合のデータフレーム
めかのとろ

4文字テキストデータ単独パケットなので1番目バイトブロックdata_buffer[0]先頭ビットFINは1、テキストデータのためopcodeは1となるので16進数表記で0x81となります。

めかのとろ

2番目バイトブロックdata_buffer[1]ブラウザからのデータでマスク付きなので先頭ビットMASKは1、データのpayload長は4なので16進数表記で0x84となります。

めかのとろ

3-6番目バイトブロックdata_buffer[2]-[5]ブラウザから付加されたマスクキーですこれらは同じデータを再度送っても都度変わります。

めかのとろ

7番目以降のバイトブロックdata_buffer[6]-[9] の文字数分がブラウザから送信するテキストデータにマスクでコード化されたものです。コード化したデータはマスクキーを使用して復号することで抽出データunmasked_str[i]を取得します。

めかのとろ

抽出データunmasked_str[i]Mask_Key[i]masked_data[i]XOR(論理演算)によりUnMask(復号)して取得します。

 unmasked_str[i]=Mask_Key[i % 4]^masked_data[i]よりデータ取得

復号したデータ
めかのとろ

ブラウザは任意のテキストデータやバイナリデータ以外にも送信するコードがあります。その一例としてcloseメソッドでwsocket.close()を実行した場合のやり取りされるデータを確認してみます。

めかのとろ

この場合のブラウザから送信されるデータ列は実データを含まない6バイト分のデータフレームです。最初のバイトブロックのopcodeがcloseメソッドを実行したときは0x8となっています。マスクキーも送信されてきますがここでは意味はありません。

ブラウザでcloseメソッドを実行した場合のデータフレーム
めかのとろ

サーバー側で受信した先頭のバイトブロックdata_buffer[0]が0x88であればブラウザでcloseメソッドを実行したことがわかります。

めかのとろ

WebSocketのopcodeには、他に0x9(Ping), 0xA(Pong)がよく使用されます。切断したときの処理などに利用できます。

めかのとろ

実際にやり取りされる具体的なデータで確認していくとすぐに理解できたのではないでしょうか。次はサーバーからの送信を確認していきます。

ポイント

WebSocket通信ではTCPソケット通信と同様に任意のタイミングでブラウザからのデータを受信することになるため、TCP受信処理においてWebSocketデータに適切に対応させて、想定外のエラーを発生させないようにしておきます。

サーバーからブラウザへのデータ送信

めかのとろ

今度はサーバー内のテキスト文字列"test"をブラウザに送信する場合で動作を確認します。サーバーからの場合はマスクを必要としないのできわめてシンプルです。

めかのとろ

サーバー内のテキストデータ*str_send="test"をブラウザに送信して表示させる場合で確認していきます。

めかのとろ

4文字テキストを送信する場合はマスクは使用せずWebSocketデータフレームの1番目と2番目バイトブロックに送信するデータのフォーマットを指定して、つづいてテキストデータを送信するだけです。

めかのとろ

HTTPプロトコルに比べて送信時は特にヘッダが小さいため送信する情報量が小さくてすむのが特徴です。送信データが小さいものほど差は顕著です。

サーバーが"text"を送信した場合のデータフレーム
めかのとろ

ブラウザへはTCPのsend関数(TCP処理系により異なる)で送信します。

サーバーからの送信コマンド
めかのとろ

ブラウザではデータを受信したときにonmessageイベントが発火しますので、テキストを表示させるなど実施したい処理を設定しておいてください。

めかのとろ

これらの送信ブロックを任意のタイミングで送信してブラウザに反映させることができるのがWebSoketの特徴です。

WebSocketテストプログラム

めかのとろ

これまでの内容を実機で確認する場合の配線はこれまでと同じです。

めかのとろ

ブラウザからサーバーのIPアドレス/ポートを指定するとボディメッセージに記述したHTMLページが開きます。

ポイント

前回の記事と同様にファイルシステムのないマイコンの場合ですので、プログラミングにおいてサーバからブラウザへ送信するHTMLはすべてハードコーディングして配列に格納したものです。

めかのとろ

WebSocketコネクションを確立するために"Connect"ボタンを押します。このサンプルでは”Connect"ボタンを押してからWebSocketリクエストを送っています。

めかのとろ

WebSocketコネクションが確立すると発火するopenイベント内に"Websocket Connectioned!!"を表示するようにしています。

めかのとろ

HTMLのスライダーを操作するとJavaSriptで設定したデータ範囲の数値が"Slider output"に表示されます。これはブラウザ側で生成した値で送信するデータですのでWebSocketコネクション確立にかかわらず表示されます。

めかのとろ

WebSocketコネクションが確立した状態でスライダーを操作すると同時に"NUCLEO Loopbackck"に数値が表示されます。これはサーバーが受信したデータをそのままブラウザに送り返しているデータですので双方のデータはほぼリアルタイムに連動しているところがWebSocketによる双方向通信の特徴です。

めかのとろ

"Close"ボタンを押すとcloseメソッドを実行してコネクションを切断するようになっています。Closeイベントが発火すると"Websocket DisConnected.."を表示するようにしています。

めかのとろ

ひとたびWebSocketコネクションが確立するとWebsocketプロトコルによる双方向通信は比較的シンプルに実現できます。ただ、実際の運転においてはTCPソケット通信に比べて通信が不安定になりがちですので安定した通信を実現するにはあと一工夫必要です。

めかのとろ

次のデモ動画はサーバー内の変数をブラウザに送って表示させるものです。送信のサンプリングレートは100msです。

めかのとろ

これくらいの表示速度であれば動作を伴うセンサー値などのリアルタイムモニターに応用できるのではないでしょうか。

ポイント

送信のサンプリングレートが150msより高速になると不安定でよく停止や切断をするようになりました。数値の桁が変わっても停止します。そこで対策としてブラウザへの送信データのバイトブロックを3回に分けていたのをすべて1つにまとめて1回の送信にし数値の桁にかかわらずデータ長を3に固定すると改善しました。

このデモ動画ではサーバー内でカウント値を3桁の固定長の数字(アスキーコード)に変換して以下の送信関数を100ms周期で実行してブラウザに送っています。

websocket_send(文字列に変換した3桁数値);

めかのとろ

送信周期を100ms以下にすると不安定で、組み込み機器のリアルタイム通信としてはこの速度では少し物足りずWebSocketの利点が活かされていないようですが、現状ではこんなものかもしれません。フリーズや切断する原因を特定して可能であればより安定して高速な通信の実現を今後の課題としておきます。

コラム1

WebSocketの技術はどちらかといえば、組み込み系のものではなく、HTMLやJavaScriptを駆使するWEBエンジニアが得意とする分野のものであり、これを組み込みに適用しようとしたから苦心しただけのような気はします。とはいえ、マイコンプログラミングでは必須のデバッガパケットモニターを使用すれば、ブラウザとマイコン間でやりとりされるデータが実際に確認できるためにデバッグとともに理解が深まるともいえます。

コラム2

WebSocket通信はHTTPプロトコルと比較して、やりとりするパケットが小さいために使用するメモリの消費も小さくなる傾向はあります。Nucleo-F103RBのようなメモリの小さなマイコンでも十分機能しています。今回のサンプルプログラムでのメモリ消費はRAMで約9k、Flashで約28k程度です。

今回のサンプル回路はNucleoにイーサネットコントローラW5500を接続しただけの単純なものですが、組み込みというよりはソフトウェアよりの技術ですのでTCP通信のみならず、HTTP通信さらにはWebSocket通信と無限にアプリケーションが広がります。奥が深いので一度試してみてください。