マイコンに実装したWebSocketによる双方向通信【STM32Nucleo】
前回の記事「HTTPプロトコルで構成したWEBサーバーを搭載したマイコンシステム」ではマイコンにWEBサーバーを搭載して専用のアプリを使用しなくてもPCやスマホ等のブラウザからアクセスすることでより身近に機器のIoT化に発展させる方法を解説しました。
ブラウザは通常HTTPプロトコルに従って機器と通信しているのですが、基本的に何かを要求するのはブラウザ側からだけです。機器側はブラウザからの要求に応じてサーバーとして応答するだけですので例えば、機器内のデータをブラウザに送信してリアルタイムで表示させることはできません。
HTTPプロトコルだけで機器内のデータをブラウザに送信して定期的にページを更新したり、疑似的に動的な表示をさせることは可能(Comet, SSE等)なのですが、HTTPではちょっとしたデータを送るにも大きなサイズのヘッダを付加させるために無駄も多い通信プロトコルです。
そこで、このHTTPの欠点を補う別のプロトコルであるWebSockeを使うとブラウザと機器間であたかもTCPソケット通信のようにリアルタイムで双方向通信ができるようになるため、マイコンにWebSocketを実装したいと思います。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を開始するためのコードは大体フォーマットは決まっていて、WebSocketオブジェクトを生成することから始めます。オブジェクト名は任意に指定できます。下記のサンプル例ではwsocketとしています。
下記のJavaScript記述した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キーからアクセスキーを生成するところです。
囲んだ24文字のコードがWebSocketコネクションのためにブラウザが発行したキーです。 一例としてサーバーは受信したGETリクエストの空白前のヘッド部に”Sec-WebSocket-Key”があればWebSocketリクエストと認識し、その後に続く24文字分のキー(xxx...xxx==)を抽出します。
この次が最大のヤマ場であるリクエストのWebSocketキーに対するアクセスキーを生成する部分です。
アクセスキーを言葉で表現すると「リクエストで与えられたブラウザが生成したキーに固定値の"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"を連結して、SHA-1ハッシュと呼ばれる暗号化を行い、Base64エンコードを行った値」となっています。
アクセスキーの定義を初めて目にするといったい何のことかはわかりにくいのですが、解説すると、ブラウザから与えられたキーにGUIDと呼ばれるコードを連結させたあとに、SHA1方式で暗号化して20桁のハッシュ値を生成し、さらにBASE64エンコードで符号化するということです。
初めてアクセスキー生成にチャレンジしたとき、まずハッシュ、SHA1やBASE64といった言葉が何であるか分からなかったため、さっぱり理解できなかったです。これらは暗号・符号化のための用語やツールだとわかってからは何をすべきかようやくわかったのですが、暗号化の内容自体は無機質な性質のものでいまだによくわかりません。
ハッシュ値およびBASE64エンコード値生成手順
- ブラウザが生成するキー:
リクエストヘッダからhqkH4S/djHSSovAPaDdycg==のみ抽出する - GUID連結(標準関数strcatを使用):
抽出したキーにGUIDを連結し、hqkH4S/djHSSovAPaDdycg==258EAFA5-E914-47DA-95CA-C5AB0DC85B11とする - ハッシュ値SHA1(20桁40文字16進数表示)生成(ハッシュ化):
GUID連結したキーをSHA1ハッシュ化すると1c10aa3dd498c5bfb39a95c5c10277e6770f28c1(バイナリ/HEX)が得られる - ハッシュ値20桁をBASE64エンコード(符号化):
20桁のSHA1ハッシュ値をBASE64と呼ばれる符号化をするとHBCqPdSYxb+zmpXFwQJ35ncPKME=(28文字テキスト)が得られる
アクセスキーを生成する手順は上記の流れですが、SHA1ハッシュ値を生成するアルゴリズムは難解のため、これすべてを自力でするのではなくどこかの汎用的なライブラリを利用すべきです。
BASE64エンコードに関しては内容はそう難解ではありませんが、これもライブラリ情報は多いため、利用するほうが得策かもしれません。
ブラウザとサーバー間でやり取りするキーはテキストですがハッシュ化処理はバイナリ(HEX)で行っていることを意識してください。
WebSocketキーに対するアクセスキーの正当性はネットで利用できる変換ツールを使って確認できます。
WebSocketコネクション確立
アクセスキーが生成できたら、WebSocketコネクション確立のためのHTTPアップグレードレスポンスをブラウザへ返します。
レスポンスのフォーマットは下例のとおりでよいと思います。レスポンス一行目のステータスラインは"HTTP/1.1 101 Switching Protocols"と"HTTP/1.1 101 OK"のどちらでも機能するようです。
生成したアクセスキーが有効であることがブラウザで認識されるとWebSocketコネクションが確立し、WebSocketプロトコルによる双方向通信が始まります。いわゆるサーバーとクライアント(ブラウザ)間でハンドシェイクのやり取りが成立したということになります。
確立するとopenイベントが発火し、定義したイベント内容が実行されます。例えばonopenイベントで”Websocket Connect!!"のメッセージを表示させる処理など記述しておけばよいでしょう。
WebSocketによるデータの送受信方法
WebSocketコネクションが確立すると任意のタイミングでブラウザ、サーバー間で双方向通信ができるようになります。WebSocket通信では送受信データにはテキストのみならずバイナリも扱うことができます。
データはWebSocketデータフレームとよばれるフォーマットに従ったものを送受信時に扱います。
データフレームの詳細は下記リンクのサイトで確認してみてください。
引用:RFC6455 The WebSocket Protocol
一度に送るデータが127文字以下でテキストかバイナリに限定する場合は比較的単純です。
データフレームのフォーマットは1バイト単位のブロックで構成されています。1番目のバイトブロックは通信データが最後のパケットであるかの指定およびデータの種類を指定します。2番目のバイトブロックではデータのマスク有無およびデータ長を指定します。
データのマスク有無ですが、これは決まり事でブラウザからのデータはマスクを付加し、ブラウザへのデータはマスクを付けないことになっています。
言葉での説明はわかりにくいのですが、送受信するデータの具体的な適用例で確認すれば理解しやすいと思いますので、送信、受信の場合で解説していきます。
ブラウザからサーバーへのデータ送信
具体的な例としてブラウザがテキスト文字列"test"を送信する場合で確認します。送受信はTCPソケット通信で行われます。
ブラウザからの送信ではsendメソッドを使用します。
sendメソッド wsocket.send(”test") を実行
4文字テキストの場合、ブラウザは10バイト分のWebSocketデータフレームを送信します。サーバー側で受信するTCPデータバッファをdata_buffer[]とすると実際にこの受信バッファに読み込まれるデータの例は以下のようになります。
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となっています。マスクキーも送信されてきますがここでは意味はありません。
サーバー側で受信した先頭のバイトブロックdata_buffer[0]が0x88であればブラウザでcloseメソッドを実行したことがわかります。
WebSocketのopcodeには、他に0x9(Ping), 0xA(Pong)がよく使用されます。切断したときの処理などに利用できます。
実際にやり取りされる具体的なデータで確認していくとすぐに理解できたのではないでしょうか。次はサーバーからの送信を確認していきます。
WebSocket通信ではTCPソケット通信と同様に任意のタイミングでブラウザからのデータを受信することになるため、TCP受信処理においてWebSocketデータに適切に対応させて、想定外のエラーを発生させないようにしておきます。
サーバーからブラウザへのデータ送信
今度はサーバー内のテキスト文字列"test"をブラウザに送信する場合で動作を確認します。サーバーからの場合はマスクを必要としないのできわめてシンプルです。
サーバー内のテキストデータ*str_send="test"をブラウザに送信して表示させる場合で確認していきます。
4文字テキストを送信する場合はマスクは使用せず、WebSocketデータフレームの1番目と2番目バイトブロックに送信するデータのフォーマットを指定して、つづいてテキストデータを送信するだけです。
HTTPプロトコルに比べて送信時は特にヘッダが小さいため送信する情報量が小さくてすむのが特徴です。送信データが小さいものほど差は顕著です。
ブラウザへはTCPのsend関数(TCP処理系により異なる)で送信します。
ブラウザではデータを受信したときにonmessageイベントが発火しますので、テキストを表示させるなど実施したい処理を設定しておいてください。
これらの送信ブロックを任意のタイミングで送信してブラウザに反映させることができるのがWebSoketの特徴です。
WebSocketテストプログラム
これまでの内容を実機で確認する場合の配線はこれまでと同じです。
ブラウザからサーバーのIPアドレス/ポートを指定するとボディメッセージに記述したHTMLページが開きます。
前回の記事と同様にファイルシステムのないマイコンの場合ですので、プログラミングにおいてサーバからブラウザへ送信するHTMLはすべてハードコーディングして配列に格納したものです。
WebSocketコネクションを確立するために"Connect"ボタンを押します。このサンプルでは”Connect"ボタンを押してからWebSocketリクエストを送っています。
WebSocketコネクションが確立すると発火するopenイベント内に"Websocket Connectioned!!"を表示するようにしています。
HTMLのスライダーを操作するとJavaSriptで設定したデータ範囲の数値が"Slider output"に表示されます。これはブラウザ側で生成した値で送信するデータですのでWebSocketコネクション確立にかかわらず表示されます。
WebSocketコネクションが確立した状態でスライダーを操作すると同時に"NUCLEO Loopback"に数値が表示されます。これはサーバーが受信したデータをそのままブラウザに送り返しているデータですので双方のデータはほぼリアルタイムに連動しているところがWebSocketによる双方向通信の特徴です。
"Close"ボタンを押すとcloseメソッドを実行してコネクションを切断するようになっています。Closeイベントが発火すると"Websocket DisConnected.."を表示するようにしています。
ひとたびWebSocketコネクションが確立するとWebsocketプロトコルによる双方向通信は比較的シンプルに実現できます。ただ、実際の運転においてはTCPソケット通信に比べて通信が不安定になりがちですので安定した通信を実現するにはあと一工夫必要です。
次のデモ動画はサーバー内の変数をブラウザに送って表示させるものです。送信のサンプリングレートは100msです。
これくらいの表示速度であれば動作を伴うセンサー値などのリアルタイムモニターに応用できるのではないでしょうか。
送信のサンプリングレートが150msより高速になると不安定でよく停止や切断をするようになりました。数値の桁が変わっても停止します。そこで対策としてブラウザへの送信データのバイトブロックを3回に分けていたのをすべて1つにまとめて1回の送信にし、数値の桁にかかわらずデータ長を3に固定すると改善しました。
このデモ動画ではサーバー内でカウント値を3桁の固定長の数字(アスキーコード)に変換して以下の送信関数を100ms周期で実行してブラウザに送っています。
websocket_send(文字列に変換した3桁数値);
送信周期を100ms以下にすると不安定で、組み込み機器のリアルタイム通信としてはこの速度では少し物足りずWebSocketの利点が活かされていないようですが、現状ではこんなものかもしれません。フリーズや切断する原因を特定して可能であればより安定して高速な通信の実現を今後の課題としておきます。
WebSocketの技術はどちらかといえば、組み込み系のものではなく、HTMLやJavaScriptを駆使するWEBエンジニアが得意とする分野のものであり、これを組み込みに適用しようとしたから苦心しただけのような気はします。とはいえ、マイコンプログラミングでは必須のデバッガやパケットモニターを使用すれば、ブラウザとマイコン間でやりとりされるデータが実際に確認できるためにデバッグとともに理解が深まるともいえます。
WebSocket通信はHTTPプロトコルと比較して、やりとりするパケットが小さいために使用するメモリの消費も小さくなる傾向はあります。Nucleo-F103RBのようなメモリの小さなマイコンでも十分機能しています。今回のサンプルプログラムでのメモリ消費はRAMで約9k、Flashで約28k程度です。
今回のサンプル回路はNucleoにイーサネットコントローラW5500を接続しただけの単純なものですが、組み込みというよりはソフトウェアよりの技術ですのでTCP通信のみならず、HTTP通信さらにはWebSocket通信と無限にアプリケーションが広がります。奥が深いので一度試してみてください。