この記事の続きになります。前回の記事でDHCP Clientの処理がどうなっているかわかったので、HTTP Serverの実装を見ていきます。
前々回の記事にも示したとおり、HTTP Serverの実装は、サンプルのソースコード「httpserver_netconn.c」で実装されている、「http_server_netconn_thread()」にて実装されています。この関数はFreeRTOSのスレッド(orタスク)として動作するように設定されていますので、このスレッドがHTTP serverの機能を担っている、ということになるかと思います。
今回も前回の記事と同様、最初に使用しているプロトコルの説明をして、その後にサンプルソースコードの実装を見ていきます。
HTTPとは
HTTP(HyperText Transfer Protocol)は、Google ChromeやEdge、FireFox等のブラウザを使用してWebページにアクセスするために作成されたプロトコルです。現在はWebページへのアクセスだけでなく、外部APIの呼び出しや、サーバやミドルウェアの監視等を行うこともあるそうです。組み込み開発で扱うとしたら、このあたりの用途が多いのかもしれません。
HTTPにはバージョンがあり、HTTP/0.9, HTTP/1.0, HTTP/1.1, HTTP/2, HTTP/3などがあります。昨今ではHTTP/1.1以降が使われることが多いです。
HTTPのプロトコルの特徴として、ステートレスな通信であることが挙げられます。これは前回のHTTPの通信における状態等を保存せずに通信を行うことを意味します。
具体的な例を挙げるとすれば、下記の図のようにウェブブラウザからウェブページのリクエストを送り、リクエストを送られたサーバがウェブページのデータを返信して、それで通信を完了とする方式です。
また、今回のサンプルはHTTP1.1を使用しているので、このHTTP1.1で送られるデータのフォーマットに関しても説明しておきます。
HTTP 1.1のフォーマット
まずは、HTTPにおけるリクエスト(webブラウザから、webページを取得するためにサーバへ送るリクエスト)のデータを見ていきます。
HTTP リクエスト
下記にHTTP リクエストの例を示します。
【HTTP リクエストの一例】
GET / HTTP/1.1
~以下省略~
HTTP1.1のリクエストで何を要求しているかを示すのは一行目の情報です。二行目からのデータに関しても通信次第でしようするのですが、サンプルソースでも使用していないようなので、この記事では省略します。
次にデータを一つずつ見ていきます。
まず、「GET」というのは、送っているサーバーに対してWebページを要求するリクエストであることを示します。次に続く「HTTP/ 1.1」というのは通信で使用するHTTPのバージョンを指定しています。
よって、上記の例であれば送信先のサーバにHTTP1.1を使用してWebページの情報を送ってほしいとリクエストをしています。
HTTPレスポンス
次に、このリクエストへ返答する形でサーバから送られてくるHTTP レスポンスの一例を下記に示します。
【HTTP レスポンス一例】
HTTP/1.1 200 OK
~以下省略(レスポンスヘッダとwebページのデータ)~
こちらに関しても、重要なのは一行目なので、その他のデータは省略します。ウェブページのデータに関しては、送られてきたレスポンスの一番最後にあります。
次にデータを一つずつ見ていきます。
まず、「HTTP/1.1」はリクエストと同様、バージョンを示すものです。
次に「200」という数字が続きます。これはステータスコードで、リクエストを処理できたかどうかを示します。200番台は正常なソースコードを示しますので、正常に処理されています。ブラウザでウェブページが見つからない際に「404 Not Found」というメッセージが出るのを目にしたことがある方も多いと思いますが、この404もステータスコードの一種です。
最後に「OK」という文字が続きます。これはリーズンフレーズと呼ばれており、ステータスコードの説明となります。仕様であるRFC2616を見る限り、アスタリスクで示されているため無くても問題ないようです。
http_server_netconn_thread()
ここからはサンプルソースコードを見ていきます。
static void http_server_netconn_thread(void *arg)
{
struct netconn *conn, *newconn;
err_t err, accept_err;
/* Create a new TCP connection handle */
conn = netconn_new(NETCONN_TCP);
if (conn!= NULL)
{
/* Bind to port 80 (HTTP) with default IP address */
err = netconn_bind(conn, NULL, 80);
if (err == ERR_OK)
{
/* Put the connection into LISTEN state */
netconn_listen(conn);
while(1)
{
/* accept any icoming connection */
accept_err = netconn_accept(conn, &newconn);
if(accept_err == ERR_OK)
{
/* serve connection */
http_server_serve(newconn);
/* delete connection */
netconn_delete(newconn);
}
}
}
}
}
こちらの実装の中で、netconn〜という名前の関数は、全てLwIPのAPIとなります。機能としては、LwIPのTCP/IPのプロトコルスタックでの通信の準備を行い、HTTP Serverの機能を担っているhttp_server_serve関数を呼び出しています。
この関数内で呼ばれているnetconn_new、netconn_bind、netconn_listen、netconn_accept、netconn_delete がTCP/IPの通信を行うための制御を行うものとなります。よって、何をしているかを一つずつ見ていき、そのあとにHTTP Serverの機能を担っているhttp_server_serveを見ていきます。
netconn_new
netconn_newは、引数で設定したnetconn_type型の変数と対応するプロトコルで通信するための構造体を作成します。このときはTCPのIPv4で通信するので、引数を「NETCONN_TCP」に設定しています。
netconn_bind
netconn_bindは、第一引数で指定したnetconn型の構造体と、第二引数で指定したIPアドレス、第三引数で指定したポート番号を紐づけます。
第二引数にはNULLを指定することで、どのIPアドレスでも第一引数で指定したnetconn型の構造体変数の設定で通信するようにしています(APIの仕様書に従うなら、IP4_ADDR_ANYか IP6_ADDR_ANYを使う方が良いかもしれません)。
第三引数は、HTTPの通信を行うので、80を指定しています。ポート番号は使用するプロトコルごとに決められており、HTTPは80となります。
netconn_listen
netconn_listenは引数で指定したnetconn型の構造体変数を、接続待ち状態に設定します。
netconn_listenを実施することで、netconn_bindで設定したIPアドレス、ポート番号でTCPのコネクション確率要求を待ちます。TCPの通信では、通信先とコネクションを確立する、つまり接続先と通信の準備が整っているかを確認したのち、通信を行う仕様となっています。そのため、通信前には必ずコネクションを確立させる必要があります。
このサンプルコードでは、IPアドレスを指定していないので、IPアドレスの指定はせず、ポート番号が80の接続を待つことになります。
netconn_accept
netconn_acceptは、受信したコネクション確率要求を承認します。これを行うことで、コネクション確率要求をしてきたデバイスと通信することができます。
第一引数は、netconn_listenで接続待ち状態にしたデバイスを指定し、第二引数で指定しているnetconn型の構造体変数にはコネクション確率要求を送ってきたデバイスの情報を格納します。
netconn_delete
netconn_deleteは、現在通信を行っている接続状態を破棄します。
引数には、接続先のデバイスを格納した情報を入れます。
よって、サンプルコードでは、netconn_acceptの第二引数と同じ変数を指定しています。
http_server_serve()
この関数では、送られてきたHTTPのリクエストに沿って処理を行います。実装は下記の通りです。
static void http_server_serve(struct netconn *conn)
{
struct netbuf *inbuf;
err_t recv_err;
char* buf;
u16_t buflen;
struct fs_file file;
/* Read the data from the port, blocking if nothing yet there.
We assume the request (the part we care about) is in one netbuf */
recv_err = netconn_recv(conn, &inbuf);
if (recv_err == ERR_OK)
{
if (netconn_err(conn) == ERR_OK)
{
netbuf_data(inbuf, (void**)&buf, &buflen);
/* Is this an HTTP GET command? (only check the first 5 chars, since
there are other formats for GET, and we're keeping it very simple )*/
if ((buflen >=5) && (strncmp(buf, "GET /", 5) == 0))
{
/* Check if request to get ST.gif */
if (strncmp((char const *)buf,"GET /STM32F4xx_files/ST.gif",27)==0)
{
fs_open(&file, "/STM32F4xx_files/ST.gif");
netconn_write(conn, (const unsigned char*)(file.data), (size_t)file.len, NETCONN_NOCOPY);
fs_close(&file);
}
/* Check if request to get stm32.jpeg */
else if (strncmp((char const *)buf,"GET /STM32F4xx_files/stm32.jpg",30)==0)
{
fs_open(&file, "/STM32F4xx_files/stm32.jpg");
netconn_write(conn, (const unsigned char*)(file.data), (size_t)file.len, NETCONN_NOCOPY);
fs_close(&file);
}
else if (strncmp((char const *)buf,"GET /STM32F4xx_files/logo.jpg", 29) == 0)
{
/* Check if request to get ST logo.jpg */
fs_open(&file, "/STM32F4xx_files/logo.jpg");
netconn_write(conn, (const unsigned char*)(file.data), (size_t)file.len, NETCONN_NOCOPY);
fs_close(&file);
}
else if(strncmp(buf, "GET /STM32F4xxTASKS.html", 24) == 0)
{
/* Load dynamic page */
DynWebPage(conn);
}
else if((strncmp(buf, "GET /STM32F4xx.html", 19) == 0)||(strncmp(buf, "GET / ", 6) == 0))
{
/* Load STM32F4xx page */
fs_open(&file, "/STM32F4xx.html");
netconn_write(conn, (const unsigned char*)(file.data), (size_t)file.len, NETCONN_NOCOPY);
fs_close(&file);
}
else
{
/* Load Error page */
fs_open(&file, "/404.html");
netconn_write(conn, (const unsigned char*)(file.data), (size_t)file.len, NETCONN_NOCOPY);
fs_close(&file);
}
}
}
}
/* Close the connection (server closes in HTTP) */
netconn_close(conn);
/* Delete the buffer (netconn_recv gives us ownership,
so we have to make sure to deallocate the buffer) */
netbuf_delete(inbuf);
}
この関数の機能としては、netconn_recv関数でHTTPリクエストを受信し、受け取ったHTTPリクエストの一行目のリクエストラインを読み出し、読み出した文字列と対応する処理を行います。送られてくるのはGETメゾッドだけを想定しており、受信したGETメゾッドと対応するデータがあれば、netconn_writeを使って送信しています。
実装を見る限りでは送信するデータファイル形式になっており、fs_open()で逐一読み出して送っている形のようです。
また、HTTPのプロトコルで説明した通り、HTTPはステートレスな通信のため、リクエストに対するデータの送信後にnetconn_close関数とnetbuf_delete関数を用いて通信を終了します。
netconn_recv
netconn_recvは、第一引数で指定したnetconn型の構造体の設定によるTCPの通信で、受信したデータを取得します。
受信したデータは、第二引数で指定したnetbuf型のポインタのポインタが指し示すインスタンスに格納されます。
netbuf_data
netbuf_dataは、第一引数で指定したnetbuf型のポインタの指すインスタンスから、通信するデータとデータのサイズを取得します。
第二引数に通信するデータを指すポインタのポインタ、第三引数にデータサイズが格納されます。
netconn_write
netconn_writeは、第一引数で指定したnetconn型の構造体の設定によるTCPの通信で、データを送信します。
第二引数に送信データのポインタ、第三引数に送信データのサイズを指定します。
第四引数では、送信時のオプション設定です。ソースコード上では「NETCONN_COPY」を指定しているので、送信データは一度LwIPのスタック内のバッファにコピーされたのち、送信します。
他の第四引数の設定としては、受信したデータをすぐに上位アプリケーションに渡す設定のフラグPSH flagを設定して送信する「NETCONN_MORE」、一度の送信で送信できるだけ送信する「NETCONN_DONTBLOCK」があります。
netconn_close
netconn_closeは、第一引数で指定したnetconn型の構造体の設定によるTCPのコネクションを終了します。
DynWebPage()
http_server_serveで呼び出しているサンプルソースコードです。「GET /STM32F4xxTASKS.html」というリクエストラインのHTTPリクエストを受信した場合に呼ばれます。実装は下記のとおりです。
void DynWebPage(struct netconn *conn)
{
portCHAR PAGE_BODY[512];
portCHAR pagehits[10] = {0};
memset(PAGE_BODY, 0,512);
/* Update the hit count */
nPageHits++;
sprintf(pagehits, "%d", (int)nPageHits);
strcat(PAGE_BODY, pagehits);
strcat((char *)PAGE_BODY, "<pre><br>Name State Priority Stack Num" );
strcat((char *)PAGE_BODY, "<br>---------------------------------------------<br>");
/* The list of tasks and their status */
osThreadList((unsigned char *)(PAGE_BODY + strlen(PAGE_BODY)));
strcat((char *)PAGE_BODY, "<br><br>---------------------------------------------");
strcat((char *)PAGE_BODY, "<br>B : Blocked, R : Ready, D : Deleted, S : Suspended<br>");
/* Send the dynamically generated page */
netconn_write(conn, PAGE_START, strlen((char*)PAGE_START), NETCONN_COPY);
netconn_write(conn, PAGE_BODY, strlen(PAGE_BODY), NETCONN_COPY);
}
この関数では、ページのアクセス回数と現在動作しているタスクの情報を、表示するウェブページに追加してHTTPレスポンスを送信しています。
「osThreadList」が、タスクの情報を文字列で返すFreeRTOSのAPI「vTaskList」を呼び出しており、これによって取得した情報をHTTPレスポンスで送信するwebページに追加しています。
osStatus osThreadList (uint8_t *buffer)
{
#if ( ( configUSE_TRACE_FACILITY == 1 ) && ( configUSE_STATS_FORMATTING_FUNCTIONS == 1 ) )
vTaskList((char *)buffer);
#endif
return osOK;
}
サンプルソースコードにおける、LwIPでのTCP/IPの通信フロー
サンプルソースコードにおける、LwIPでのTCP/IPの通信フローをまとめます。
- netconn_newを、引数に「NETCONN_TCP」を指定して呼び出す。これにより、TCP用の通信制御用の構造体の作成を行う
- netconn_bindを、第一引数でnetconn_new()で生成した構造体を指定し、第二引数で通信に使うIPアドレス、第三引数で通信に使うポート番号を指定して呼び出す。これにより、通信制御用の構造体と、IPアドレスとポート番号を紐づける
- netconn_listenを、引数にnetconn_new()で生成した構造体を指定して呼び出す。これにより、TCPのコネクションの確立要求を待つ
- netconn_acceptを、第一引数にnetconn_new()で生成した構造体を指定し、第二引数で空のnetconn型構造体を指定する。これにより、コネクションの確立要求があったデバイスを承認して接続する。接続先の情報は第二引数で指定したnetconn型構造体に格納される
-
netconn_recvとnetconn_writeを使ってデータの送受信を行う
- netconn_recvは第一引数でnetconn_new()で生成した構造体を指定し、第二引数でnetbuf型のポインタのポインタを指定する。受信データは第二引数で指定したnetbuf型のポインタのポインタが指し示すインスタンスに格納される。
- netconn_writeは第一引数でnetconn_new()で生成した構造体を指定し、第二引数に送信データのポインタ、第三引数に送信データのサイズを指定する。第四引数では、送信時のオプション設定を指定する。
- 通信が終了したら、netconn_closeを、引数にnetconn_accept()の第二引数に格納された構造体を指定して呼び出す。引数で指定したnetconn型の構造体の設定によるTCPのコネクションを切断する(削除は行わない)。
- netconn_deleteを、引数にnetconn_accept()の第二引数に格納された構造体を指定して呼び出す。現在通信を行っている接続状態を完全に削除する。
また、上記のフローは、不特定多数のデバイスがTCPのコネクションの確立要求を送ってくる、いわゆるサーバーのような振る舞いをする場合のものです。
ウェブブラウザなどの特定のサーバーへ通信する、いわゆるクライアントのような振る舞いをする場合においては、フローが異なってくるので注意が必要です。この場合は、netconn_bind, netconn_listen, netconn_acceptを呼び出すフローの代わりに、netconn_connectを呼び出すようなフローに変えれば問題ないと思います。こちらに関しても別途違う記事で説明していこうかと思います。
LwIPにおけるHTTP Serverの通信フロー
サンプルソースコードにおける、LwIPでのHTTP Serverの通信フローをまとめます。
- TCPのコネクション確率要求を送ってきたHTTP Clientと、TCPのコネクションを確立(「サンプルソースコードにおける、LwIPでのTCP/IPの通信フロー」で説明したので、詳細は省略)
- netconn_recvを呼び出し、HTTP ClientからくるHTTPリクエストを受信する
- netbuf_dataを呼び出し、第二引数に指定した通信するデータを指すポインタのポインタと第三引数に指定したデータサイズを参考に、受信したHTTPリクエストのデータを取り出す
- 3で取り出したデータをパースして、HTTP リクエスト内容を解析する
HTTPリクエストの解析結果を基に、HTTPレスポンスとして送るデータを作成する。 - netconn_writeでHTTPレスポンスを送信する
- netconn_closeを呼び出し、TCPのコネクションを切断する
上記のようにLwIPにはHTTP専用で使用するAPIは無く、TCPの通信を行うAPIを駆使してHTTPの通信を行っています。
また、昨今使用されている大抵のウェブブラウザは暗号化したHTTPの通信(HTTPS)をすることが推奨されていますが、このサンプルは簡略化のため暗号化通信には対応していません。ウェブブラウザからアクセスする形を想定するならば、暗号化通信に対応する必要があります。
参考文献
・マスタリングTCP/IP 入門編
https://www.ohmsha.co.jp/book/9784274224478/
・RFC2616
https://datatracker.ietf.org/doc/html/rfc2616#page-39
・gihyo.jp HTTP3
https://gihyo.jp/admin/serial/01/http3/0001