libwebsocketを使ってZynqにWebsocketサーバを実装してみる:C言語編
ZynqとWebsocketを組み合わせてリアルタイムモニターシステムを構築したという記事を書いた.Websocketサーバをどのように実装したかについてメモを残しておく.
このgithubのコードを基本としてサーバを実装した.
このページでは,上記コードを例にとってどのようにWebsocketサーバを実装すればよいかについて述べる.
Websocketとは
Websocketとは,サーバとクライアント側で双方向に通信ができる通信規格である.普通のHTTP通信だとクライアント側からの要求に答える形でのみサーバは情報を送ることができるが,Websocketではサーバ側のタイミングで自由にクライアントへ情報を送ることができる.
Websocketでは,まずサーバ側とクライアント側で通信のコネクションを確立する.ソケット通信と同じように,クライアント側のハンドシェイク要求に応じる形でコネクションが確立される.普通はブラウザからの要求に従ってコネクションが確立されるので,HTTPのGETでコネクションができる.
確立したコネクションを使って,クライアント・サーバ間の双方向通信を行う.一回コネクションが確立してしまえば,そのコネクションを利用して(つまり通信ごとにコネクションを貼り直すということをせずに)通信をするので,コストのかからない通信プトロコルとなる.通信はTCP/IPを使っているみたい.
したがって,Websocketを使うとサーバ側から好きなタイミングで情報を送れるので,上記のようなリアルタイムモニターを作る場合には都合がいい.
Websocket通信の流れ
まずクライアントからサーバマシンへのアクセスがある.最初のアクセスはHTTPで行われ,htmlファイルをクライアントへ返す.htmlファイルにはWebsocket通信を行うJavaScriptが書かれていて,その内容に従ってWebsocket通信が開始される.
JavaScriptでWebsocket通信を行うときは次のように記述する.
var ws = new WebSocket("ws://" + DEFAULT_DOMAIN + ":" + DEFAULT_PORT, "example-protocol"); // DEFAULT_DOMAINにはサーバーのIPアドレス,DEFAULT_PORTにはWebsocketサーバが動いているポート番号
第2引数は,Websocketサーバで定義されているプロトコルを指定する.ここではexample-protocol
というプロトコルを指定している.libwebsocketsを使った場合にプロトコルを定義する方法はあとで述べる.
libwebsokcetsとは
WebsocketをC言語で実装するためのオープンソースライブラリ.Zynqなどの組み込みではC言語を使うことが多いので,こちらのライブラリを使った.
libwebsocketのインストール方法
Ubuntuのパッケージ管理システムに入っているので
sudo apt-get install libwebsockets-dev
でインストールできる.新しいバージョンとかを使いたい場合は,ソースからビルドする方法がある.
git clone https://github.com/warmcat/libwebsockets.git
とすると,インストールできる.コンパイルするときは-I/usr/local/inclue -L/usr/local/lib -lwebsockets
をつける.
libwebsocketを使ったWebsocketサーバの内容
libwebsocketではwsiというstructでコネクションを管理する.サーバでは通信が確立されるごとにwsiをメモリ領域に確保して管理する必要がある.今回は複数クライアントと接続することを想定していないので,クライアントとの接続が確立されるごとに新しいwsiを確保することにする.本来ならwsiの配列などに保持するといった処理を行う.
libwebsocketは,サーバー側からクライアントにデータを送信する場合lws_write
という関数を使う.サーバー側では,適当なタイミングでlws_write
を呼び出す処理を書くことになる.
自作コールバック関数とプロトコルの宣言
libwebsocketsでは,Websocket通信開始時に指定されたプロトコルで定義された処理が実行される.プロトコルには1つのコールバック関数が紐付けられていて,その中にプロトコルで行う処理を書く.プロトコルの定義はlws_protocols
構造体の配列に書き込む.
static struct lws_protocols protocols[] = { /* 初めのプロトコルは必ずHTTPのものである必要がある */ { "http-only", /* name */ callback_http, /* callback */ 0, /* No per session data. */ 0, /* rx buffer size*/ }, { "example-protocol", //JavaScriptなどから呼び出す際のプロトコル名を書く callback_example, 0, EXAMPLE_RX_BUFFER_LENGTH, }, { NULL, NULL, 0, 0 } /* プロトコル宣言の終了を表す */ };
lws_protocols構造体
lws_protocols
構造体は次のフィールドを持つ.
lws_protocols = { const char* name, // 定義したプロトコルの名前 lws_callback_function *function, // このプロトコルが呼ばれたときに実行するコールバック関数. //コールバックはlibwebsocketsが自動で実行してくれる. //コールバック関数の中に各自行いたい処理を書く.lws_callback_functionはintのtypedef. size_t per_session_data_size, // 接続ごとに確保されるメモリのサイズ.確保されたメモリは接続が切れると自動で開放される. size_t rx_buffer_size // 受信データのサイズ };
コールバック関数
コールバック関数は,次の引数を持たせて定義する必要がある.
lws_callback_function( struct lws *lwi, // コールバックが発生したWebsocket通信に紐付いたlws構造体へのポインタが渡される. enum lws_callback_reasons reason, // コールバックが発生した理由. void *user, // 通信セッションごとに確保されたメモリ領域へのポインタ. void *in, コールバックの中身に応じたデータが入っている.クライアントからのデータなどが含まれることが多い. // inポインタの先頭からデータが詰まっているわけではなく, // LWS_SEND_BUFFER_PRE_PADDINGのオフセットからlen分だけデータが入っている. size_t len // inポインタに含まれているデータの長さ. );
lws_callback_reasons
lws_callback_reasons
のうち,サンプルコードで使われている主要なものを列挙しておく*1.
- LWS_CALLBACK_HTTP
HTTPのリクエストが来たことを表す.Websocketコネクションの状態を更新するために生じたコールバックを意味しないので注意.このコールバックが来たときはlws_serve_http_file
関数を呼び出して初めのウェブページを返すことが多い.
- LWS_CALLBACK_RECEIVE
クライアントからデータが送られてきたことを示す.
- LWS_CALLBACK_SERVER_WRITEABLE/LWS_CALLBACK_CLIENT_WRITEABLE
コネクションが設立した状態で,lws_callback_on_writable
関数を呼び出すと,このタイプのコールバックになる.
サーバー側のプログラムではLWS_CALLBACK_SERVER_WRITEABLE
が,クライアント側のプログラムではLWS_CALLBACK_CLIENT_WRITEABLE
になる.
サンプルコードの構造
サンプルコードは,C言語でサーバーとクライアントを用意しその間で通信を行っている.サーバープログラムは,ウェブブラウザからの通信も受け付けており,ウェブブラウザにWebsocket通信でデータを送信することもできる.
サーバープログラム
サーバー側のプログラムでは,他のクライアントとの接続待ちを行い,コネクション要求やコールバックが生じた場合の処理を行う.
接続に関する情報はlws_context_creation_info
構造体に保持しておく.更に,生成したlws_context_creation_info
を用いてWebsocket通信のハンドラlws_context
を生成する.以下に例を示す.
struct lws_context_creation_info info; memset( &info, 0, sizeof(info) ); info.port = 7100; // Websocket通信を行うポート.JavaScriptで指定するポートと同一にする. info.protocols = protocols; // protocolsは,予め宣言したlws_protocols配列へのポインタ info.gid = -1; info.uid = -1; struct lws_context *context = lws_create_context( &info );
lws_context
を用いて,Websocketの通信を行う.Loop処理にはlws_service
関数を用いる.この関数は新しいコネクション要求をacceptしたり各種のコールバック関数を呼び出す.第1引数にはlws_context
,第2引数にはタイムアウトの時間をmsec単位で指定する.
while( 1 ) { lws_service( context, /* timeout_ms = */ 1000000 ); }
クライアントへのデータ送信
コールバック関数内で各クライアントへデータを送信する処理を書く.サンプルコードでは,callback_example
で,LWS_CALLBACK_SERVER_WRITEABLEでコールバックされたときの処理に対応する.クライアントへデータを送信するにはlws_write
関数を呼び出す.
lws_write( struct lws *wsi, unsigned char *buf, // 送信データが含まれたメモリの先頭アドレスを渡す.unsigned charの配列として // 確保された配列に書き込まれたデータを渡すこと. size_t len, // 送信データの長さ enum lws_wirte_protocol protocol // 送信方法 // テキスト形式で送信するか,バイナリで送信するか選べる // サンプルコードではテキスト形式で送信する. )
buf
の長さは注意する必要がある.Websocketサーバが送信するデータは,ユーザーが詰めたデータとWebsocketがつけるヘッダやフッタ情報が含まれる.したがって,buf
は送信したいデータ長+αで宣言する必要がある.具体的には次のようにする.
unsigned char data[ LWS_SEND_BUFFER_PRE_PADDING + DATA_LENGTH_THAT_YOU_WANT_TO_SEND + LWS_SEND_BUFFER_POST_PADDING];
クライアントのプログラム
Websocketサーバーへ接続し,定期的にコールバックを生成しているプログラム.Websocketサーバーを構築する場合には必須ではないので注意.長くなってきたので詳細は割愛する.
クライアントプログラムではwhile
文の中で,lws_callback_writable
を呼び出しコールバックを生成している.