今回は複数の接続に耐えられる温湿度サーバーの作成を行います。
前回のコードでは下記のような課題がありました。
- 温湿度サーバーに接続できるクライアントは1つだけ
- 1度接続解除するとサーバーが終了する
そこで前回作成したプログラムをさらに改良して、温湿度サーバーを複数の接続に応答できるようにします。
ちなみに下記順で見ると、BME280の回路接続方法、温湿度サーバーの作成方法やクライアントアプリの作成方法といった、一通りのことがわかりますので、時間があれば最初からどうぞ。
- 【第1回】Raspberry Piで温湿度センサー(BME280)を動かしてみよう
- 【第2回】プログラムで温湿度センサー(BME280)を操作してみよう!C言語編
- 【第3回】温湿度を計測するサーバーの作成
- 【第4回】温湿度サーバーを複数接続に対応する方法 ← 今ココ
- 【第5回】温湿度を取得するクライアントアプリの作成(iOS版)
温湿度サーバーの複数接続対応
今回は複数接続に対応するわけですが、大きく分けて2つのパートに分かれます。
- 複数クライアントの接続を受け付ける
- 接続したクライアントの処理をスレッド化して並列処理できるようにする
というわけで、まずは複数クライアントの接続を受け付ける対応をします。
作成したコードは下記に格納しているので、解説と合わせて見ると分かりやすいかもしれません。
このリポジトリ内の"example/server/multi_connection/multi_server.c"が今回解説するコードにあたります。
複数クライアントの接続受け付け
前回コードでソケットAPIのaccept()を使ってクライアントからの接続を受け付けていました。しかし、前回のコードでは1回しかaccept()していなかったため、1回のクライアント接続しか処理できないわけです。(見やすくするためサンプルコードからエラー処理などを省いています)
clitSock = accept(servSock, (struct sockaddr *)&clitSockAddr, &clitLen)
while(1)
{
memset(buf, 0, sizeof(BUFSIZE));
if ((recvMsgSize = recv(clitSock, buf, BUFSIZE, 0)) < 0)
{
fprintf(stderr, "Failed recv().\n");
break;
}
else if(recvMsgSize == 0)
{
fprintf(stderr, "connection closed by foreign host.\n");
break;
}
get_temp(&comp_data) == 0
memset(buf, 0, sizeof(BUFSIZE));
sprintf(buf, "%0.2lf deg C, %0.2lf hPa, %0.2lf%%\n", comp_data.temperature, comp_data.pressure * 0.01, comp_data.humidity);
if ((sendMsgSize = send(clitSock, buf, strlen(buf), 0)) < 0)
{
fprintf(stderr, "Failed send().\n");
break;
}
else if(sendMsgSize == 0)
{
fprintf(stderr, "connection closed by foreign host.\n");
break;
}
}
close(clitSock);
したがって、accept()を複数回行えば、それだけクライアントの接続を受け付けることができるというわけですね。つまり、accept()をループしてあげれば良いのです、このように。
while(1)
{
clitSock = accept(servSock, (struct sockaddr *)&clitSockAddr, &clitLen)
while(1)
{
memset(buf, 0, sizeof(BUFSIZE));
if ((recvMsgSize = recv(clitSock, buf, BUFSIZE, 0)) < 0)
{
fprintf(stderr, "Failed recv().\n");
break;
}
else if(recvMsgSize == 0)
{
fprintf(stderr, "connection closed by foreign host.\n");
break;
}
get_temp(&comp_data) == 0
memset(buf, 0, sizeof(BUFSIZE));
sprintf(buf, "%0.2lf deg C, %0.2lf hPa, %0.2lf%%\n", comp_data.temperature, comp_data.pressure * 0.01, comp_data.humidity);
if ((sendMsgSize = send(clitSock, buf, strlen(buf), 0)) < 0)
{
fprintf(stderr, "Failed send().\n");
break;
}
else if(sendMsgSize == 0)
{
fprintf(stderr, "connection closed by foreign host.\n");
break;
}
}
close(clitSock);
}
たったこれだけで複数クライアントからの接続要求を受け付けることができます。
ただし、このコードだと1台目に接続したクライアントの接続が切れるまで、2台目のクライアントに対して温湿度データを送ることができません。(recv/send部分がループしてるので、2台目のaccept()に到達するのは1台目の接続が切れてからになる)
そこで、次の「接続したクライアントの処理をスレッド化」が必要になります。
接続したクライアントの処理をスレッド化
ここではクライアントと接続したらクライアント応答用のスレッドを生成し、そのスレッド内で温湿度データをクライアントに送るようにします。
というわけで今回はPOSIXスレッド(pthread)を使ってスレッドを生成し、その中で温湿度を送信する処理を行うようにします。
スレッド化する手順としてはこんな感じ。
- スレッドに渡したい情報の構造体を定義
- スレッド内で処理したいコードを関数化
- pthread_create()を呼び出してスレッド生成
この3つを行うだけで簡単にスレッド化できます。
スレッドに渡したい情報の構造体を定義
まずはスレッド内で使いたい情報を渡すために、情報受け渡し用の構造体を作成します。
今回のケースでは、clitSock(accept()の戻り値であるクライアントのソケットFD)とclitSockAddr(クライアントのアドレス情報)を渡したいので、この2つをまとめる構造体を定義します。
こんなかんじ。
struct clientdata {
int sock;
struct sockaddr_in saddr;
};
スレッド内で処理したいコードを関数化
今回のケースでいえば、クライアントとの送受信処理を関数化したいので、このように送受信の部分を関数化します。
void* get_temp_thread(void* pArg)
{
int recvMsgSize, sendMsgSize;
struct clientdata *cdata = pArg;
struct drv_bme280_data comp_data;
char buf[BUFSIZE];
while(1)
{
memset(buf, 0, sizeof(BUFSIZE));
if ((recvMsgSize = recv(cdata->sock, buf, BUFSIZE, 0)) < 0)
{
fprintf(stderr, "Failed recv().\n");
break;
}
else if(recvMsgSize == 0)
{
fprintf(stderr, "connection closed by foreign host.\n");
break;
}
if(get_temp(&comp_data) == 0)
{
memset(buf, 0, sizeof(BUFSIZE));
sprintf(buf, "%0.2lf deg C, %0.2lf hPa, %0.2lf%%\n", comp_data.temperature, comp_data.pressure * 0.01, comp_data.humidity);
}
if((sendMsgSize = send(cdata->sock, buf, strlen(buf), 0)) < 0)
{
fprintf(stderr, "Failed send().\n");
break;
}
else if(sendMsgSize == 0)
{
fprintf(stderr, "connection closed by foreign host.\n");
break;
}
}
close(cdata->sock);
free(cdata);
return NULL;
}
ここで注意したいのはリソースの解放忘れです。親プロセスから渡されたリソースを解放する処理を追加しています。これを忘れるとメモリリークとなってしまうので注意しなければなりません。
今回は子スレッド側で解放していますが、親スレッド側で子スレッドの処理が終了するのを待って解放するという方法もあります。
pthread_create()を呼び出してスレッド生成
あとはpthread_create()を呼び出してスレッド生成してあげれば、各クライアントを並列して処理することができます。
今まではaccept()した後にそのまま送受信処理を行っていましたが、下記のようにpthread_create()でスレッド生成して、送受信する関数の処理を行うようにしました。
while(1) {
clit = malloc(sizeof(struct clientdata));
clitLen = sizeof(clit->saddr);
if ((clit->sock = accept(servSock, (struct sockaddr *) &clit->saddr, &clitLen)) < 0) {
fprintf(stderr, "Failed accept().\n");
close(servSock);
break;
}
printf("Connected from %s.\n", inet_ntoa(clitSockAddr.sin_addr));
if (pthread_create(&th, NULL, get_temp_thread, clit) != 0) {
fprintf(stderr, "Failed pthread_create().\n");
break;
}
if (pthread_detach(th) != 0) {
fprintf(stderr, "Failed pthread_detach().\n");
break;
}
}
また、生成したスレッドの処理を親スレッドで待つ必要はないため、pthread_detach()によってスレッドをデタッチ状態にして切り離しておきます。親スレッドで待ち合わせなどをしたい場合はpthread_join()を使いましょう。
以上で複数クライアントからの接続・要求に耐えられるようになりました。
プログラムの動作確認
まずはBME280の操作プログラムと、今回の複数接続に対応したサーバーのサンプルコードをダウンロードします。
git clone https://github.com/radical-kei/BME280.git
コードの取得ができたら、下記コマンドを実行してディレクトリを移動します。
cd BME280/example/server/multi_connection
ディレクトリ移動が済んだら、下記コマンドを実行して実行ファイルを生成します。(CmakeLists.txtは以前に紹介したクロスコンパイラ環境でコンパイルするようセットしています。コンパイル出来ない場合は自身の環境に合わせてコンパイラをCmakeLists.txtにセットしてください)
mkdir build
cd build
cmake ..
make
"temp_multi_server"という実行体が生成されるので、ラズパイに転送してください。転送が終わったらラズパイのターミナルを開いて"temp_multi_server"を実行します。実行するとこんな感じで待ち状態に入ります。
pi@pizero:~ $ ./temp_multi_server
▋
あとは他のPCからnetcatコマンドを使って、実際に温湿度が取得出来るか確認します。WSL上のUbuntuやMacのターミナルを開いて下記コマンドを実行します。
nc <ラズパイのホスト名 or IPアドレス> <ポート番号>
サンプルコードはポート番号"50000"を開いているので"50000"としてください。
下記のように動作するはずです。
左側の2つがMacのターミナルで、右側がラズパイのターミナルです。Macのターミナルでreturnを入力するたびに温湿度が取得できています。また、複数接続してもすべてのクライアントで温湿度が取得できていますし、一度切断して再接続しても温湿度を取得できています。
最後にMacのターミナルでControl+Cで抜けて、ラズパイのターミナルでもControl+Cを入力することで終了できます。
まとめ
以上で簡単にサーバーを複数接続に対応することができました。これでサーバーサイドとしては十分な機能を有していると言えます。
あとはクライアント側をどう作るかなので、次回はクライアント側のソフトを作成します。とりあえずiOS用のアプリを作成して、iPhone上で温湿度データを取得できるようにする予定です。
コメント