メモ置き場

メモ置き場です.開発したものや調べたことについて書きます.

[tex: ]

libwebsocketを使ってZynqにWebsocketサーバを実装してみる:C言語編

ZynqとWebsocketを組み合わせてリアルタイムモニターシステムを構築したという記事を書いた.Websocketサーバをどのように実装したかについてメモを残しておく.

このgithubのコードを基本としてサーバを実装した.

このページでは,上記コードを例にとってどのようにWebsocketサーバを実装すればよいかについて述べる.

Websocketとは

Websocketとは,サーバとクライアント側で双方向に通信ができる通信規格である.普通のHTTP通信だとクライアント側からの要求に答える形でのみサーバは情報を送ることができるが,Websocketではサーバ側のタイミングで自由にクライアントへ情報を送ることができる.

Websocketでは,まずサーバ側とクライアント側で通信のコネクションを確立する.ソケット通信と同じように,クライアント側のハンドシェイク要求に応じる形でコネクションが確立される.普通はブラウザからの要求に従ってコネクションが確立されるので,HTTPのGETでコネクションができる.
確立したコネクションを使って,クライアント・サーバ間の双方向通信を行う.一回コネクションが確立してしまえば,そのコネクションを利用して(つまり通信ごとにコネクションを貼り直すということをせずに)通信をするので,コストのかからない通信プトロコルとなる.通信はTCP/IPを使っているみたい.

f:id:okchan08:20190520120633p:plain

したがって,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を呼び出しコールバックを生成している.