TCPサーバーを実装したソケット通信【STM32Nucleo】
マイコンをイーサネット対応にしてpingが通った状態でネットワークに参加しただけでは何の意味もありませんが、上位のトランスポート層にTCPやUDPといったプロトコルを搭載することによってサーバーとクライアント間で通信ができるようになります。
ソケット通信は前回解説したOSI参照モデルのトランスポート層に属するTCPプロトコルにしたがった通信手段です。W5500コントローラを搭載したマイコンでTCPソケット通信を実現するためには内蔵したTCP/IPプロトコルスタックをレジスタで操作します。操作自体はメーカーがドライバを提供しているので容易に行われます。
TCPサーバーを構築してソケット通信の実装が整えば、任意のタイミングでシリアル通信のようなデータのやりとりができるようになります。
TCPソケット通信とは
インターネット情報には無数の解説記事が存在するためにここでは詳細の解説は割愛しますが、簡潔にまとめますとソケット通信はサーバーとクライアントが互いに確認しながら通信を行うTCPプロトコルに基づいた方式です。互いにやりとりすることからハンドシェイク方式とも呼ばれます。
ソケット通信は電話による会話に似ていて会話の出入り口である受話器、耳、口に相当するものをソケットと呼びます。相手が応答して初めて会話(通信)が成立し、途中で相手やこちらで切断すると会話も終了します。また、相手が接続中(受話器を上げたままの状態)のときは、こちらからの接続要求に応答しません。
トランスポート層に属するプロトコルには他にUDPがありますが、これはサーバーがクライアントに確認せずに一方的に情報を与える方式です。動画などのストリーミングで内容に多少の不備があっても問題がない多量の情報を送るのに適しています。
下図はソケット通信で標準的なバークレーソケット(BSD unixに実装されたネットワーク用API)に基づいた通信のフローチャートです。サーバー、クライアントともにあらかじめソケットを作成しておき、クライアント側から必要時に接続要求をだして、サーバーが応答して通信を確立します。一度確立すると、どちら側から切断しないかぎり、任意のタイミングで送受信できるのがソケット通信の特徴です。
TCPサーバーの実装
ドライバsoket.cでは標準インターフェースのBSDソケットに準じた関数をまとめていますがここではドライバ内に定義された関数の一部のみを使用したソケット通信のサンプルを紹介しています。
サンプルプログラムではTCPサーバーをサンプリングタイム5msで実行する専用タスクprvTask_Tcpに構成しています。
ドライバのソケット標準関数を使用して一般的なTCPサーバーを作成することもできますが、ここではサンプリングタイム毎にモニターしたソケット状態に応じた処理をドライバのレジスタ操作関数を使って実行します。
ソケットレジスタブロックの設定
① ソケット状態のモニター
state = getSn_SR(SN);
ソケットの状態は主に
「SOCK_CLOSED」ソケット閉鎖
「SOCK_LISTEN」ソケット待機
「SOCK_ESTABLISHED」ソケット確立
「SOCK_CLOSING」ソケット閉鎖中
の4種類があり、これらの状態をソケット番号SNに応じてgetSn_SR(SN)関数でソケットのレジスタ値を読み込みます。
② ソケット生成
srcSocket = socket(SN,Sn_MR_TCP, PORT, SF_IO_NONBLOCK);
プログラム起動直後のソケットは「SOCK_CLOSED」ですのでまずソケットをsocket()関数で生成します。
関数の第1引数はソケット番号SNで、SNは自作関数をまとめたヘッダファイルnet_conf.hで1に定義しています。
第2引数はプロトコルは設定でTCPプロトコルを指定しています。第3引数はPORT番号を指定し、第4引数ではnon-blockを指定し、ノンブロッキングソケットとしています。
③ STABE_LISTENに遷移
LISTEN(SN);
ソケットが作成されるとlisten(SN)関数を実行し、SOCK_LISTEN状態に遷移してクライアントからの接続待ち状態で待機します。
ソケットの状態がSOCK_LISTENの待機時にクライアントのターミナルからIPアドレスおよびポートを指定して接続の要求をし、接続が確立するとSOCK_ESTABLISHEDになります。接続が確立しクライアントのターミナルにサーバーのIPアドレスが表示されると任意のタイミングで送受信ができるようになります。
④ データ送受信
len=recv(SN, (uint8_t*)rcvbuffer, sizeof(rcvbuffer));
データ受信にはrecv関数を使用します。第1引数はソケット番号、第2引数は受信バッファのポインタ、第3引数はデータ長を指定します。データを受信するとデータ長を返します。
send(SN, (uint8_t*)sendbuffer, sizeof(sendvbuffer));
データ送信にはsend関数を使用します。第1引数はソケット番号、第2引数は送信バッファのポインタ、第3引数はデータ長を指定します。
通信用バッファメモリである送信TXおよび受信RXバッファはそれぞれ16Kバイトのブロックで構成されています。
⑤ ソケット制御関数
setSn_CR(SN,Sn_CR_CLOSE);
setSn_CR関数はsocket.c内に定義されている関数でソケットを制御するものです。第1引数はソケット番号、第2引数はソケットの状態を指定します。Sn_CR_CLOSEを指定するとソケットを閉鎖(close)します。
その他ソケットの”OPEN", ”LISTEN", "CONNECT", "DISCON", "SEND", "SEND_MAC", "SEND_KEEP", "RECV"等があります。詳細はドライバのソケット定義ファイルsocket.cを確認してください。
TCPサーバーのソケット状態遷移をまとめると上図のようになります。W5500ではソケット状態は他にもありますが、とりあえず主な4つの状態をコントロールしておけば機能します。
ソケット通信のイメージ
ソケット通信の状態を電話による会話に例えると下図のようなイメージなります。この通信状態を想定しながら実際にソケット通信を実施して動作を確認してみます。
実際のソケット通信
クライアントではターミナルソフトを使用します。ここでもTera Termを使用しています。これに限らず慣れたツールを使用してください。
起動後、TCPソケット通信ですのでサーバーに登録したIPアドレスおよびポート番号を入力し"OK"を押します。
① 接続要求
画面左上にサーバーのIPアドレスが表示されると設定ポートが開いてクライアントとサーバーが接続したことになります。
② ソケット接続確立中
TCPサーバーに対してコマンドを与えて通信できるかを確認します。通信テスト用の応答メッセージを返す処理、ソケットの閉鎖(close)および接続切断(disconnect)するコマンドをまとめたファイルを作成して実行しています。
まず文字列"ABC"を入力します。受信バッファに格納された文字列が"ABC"で一致すると”Matched‼"と応答します。応答はsend関数を使用しています。
文字列"abc"を入力すると"ABC"と一致しませんので”Error"と応答します。
ソケットが接続確立しているところで新たに別の接続を試みます。
すでにポートが開いているために接続が拒否されました。電話では相手の受話器が上がっていて話し中の状態です。
③ ソケットclose
ソケットを閉じるためにコマンド"scls"を入力すると"socket close"メーセージを返してCRレジスタにSn_CR_CLOSEを送信してからソケットを閉鎖(close)します。結果、未接続となります。
④ 接続要求+ソケット接続確立
また新たに接続を試みると今度はソケットが閉じているため再びポートが開き接続できます。
最後に接続自体を遮断するため、コマンド"srst"を入力します。CRレジスタにSn_CR_DISCONを送信し、切断処理(disconnect)を実行します。
⑤ 接続切断
画面左上が未接続となり接続が遮断されたことが確認できます。
以上、テスト用コマンドを入力してソケット通信の様子を確認しました。実際のソケット通信ではプログラミング次第でコマンドをさらに多様化してコマンドにより様々な処理を実施させることになります。
TCPソケット通信はサーバーとクライアント間でリアルタイムでデータをやりとりできることが特徴ですので組み込み機器にTCPサーバーを搭載しておけばIoTとしてリアルタイムでインターネットを介してデータのリモート管理ができるようになります。