🐙

LwIPにおけるHTTP Client

2024/07/03に公開

前回のDNS Clientの記事で、LwIPを使用してDNSでホスト名からIPアドレスを取得する方法を説明しました。
今回はLwIPのアプリケーションとして、HTTPのGETメソッドを送るAPIが用意されているので、この使い方に関して説明していきます。
アクセスするHTTP Serverは下記のリンク先です。前回のDNS Clientの記事で使用したサイトを使います。
http://httpbin.org/
また、上記のServerへアクセスする際は暗号通信を使用しないのでご注意ください(例えば、テスト的に送るHTTP GETメソッドに個人情報など、悪意ある第三者に知られると問題のある情報は送らないようにしてください)。
また、このサイトと本記事のサンプルコードを使用することにおける、セキュリティ上のトラブル等においては一切責任は取りませんのでご了承ください。

DNS Clientの記事と同様、HTTPの説明をした後にLwIPのAPIの説明をして、サンプルの実装を示していきます。

HTTP

HTTP(HyperText Transfer Protocol)は、Google ChromeやEdge、FireFox等のブラウザを使用してWebページにアクセスするために作成されたプロトコルです。現在はWebページへのアクセスだけでなく、外部APIの呼び出しや、サーバやミドルウェアの監視等を行うこともあります。組み込み開発で扱うとしたら、このあたりの用途が多いのかもしれません。

HTTPのバージョン

HTTPにはバージョンがあり、HTTP/0.9, HTTP/1.0, HTTP/1.1, HTTP/2, HTTP/3などがあります。昨今ではHTTP/1.1以降が使われることが多いです。

HTTPの通信シーケンス

HTTPのプロトコルの特徴として、ステートレスな通信であることが挙げられます。これは前回のHTTPの通信における状態等を保存せずに通信を行うことを意味します。
 例として、HTTPのGETメソッドを送る場合を考えます(GETメソッドについては後述しますので詳しい説明は割愛します。ここではWebページを取得するための通信と考えてください)。下記の図のようにブラウザからWebページの取得要求を送り、それを受信したサーバがWebページのデータを返信して、それで通信を完了とする方式です。

上記で示した通信を行った後の通信は、このWebページの取得のための通信の影響を受けません。

HTTPメッセージのフォーマット

HTTPで送られるデータ(HTTPメッセージ)のフォーマットに関しても説明しておきます。
HTTPメッセージのフォーマットは下記のようになっています。

上記の図において、ヘッダセクションとトレーラセクションはHTTPメッセージにおける通信設定やコンテンツに格納されているデータに関する情報が入っています。コンテンツは通信相手に送りたいデータを格納します。主にWebページのデータや、アップロードするファイルのデータが格納されることが多いです。
また、ヘッダセクションのないHTTPメッセージは無効となりますが、トレーラセクションは無くても問題ありません。

HTTPのリクエストとレスポンス

HTTPのメッセージのフォーマットを説明しましたが、実際にどのようなメッセージを送っているかが無いと分かりづらいので、実際に送るデータとなるHTTPのリクエストとレスポンスのデータフォーマットについても説明します。
今回のサンプルはHTTP1.1を使用しているので、HTTP1.1のフォーマットに関しての説明となります。

HTTP リクエスト

HTTP リクエストは最初に送信されるHTTPメッセージのことで、クライアントからサーバに送信されるものを指します。下記に例を示します

【HTTP リクエストの一例】

GET / HTTP/1.1
Host: www.example.com

HTTP1.1のリクエストは改行をデータの区切りとして認識します。また、リクエストの最後の行は改行のみとする決まりになっています。そのため、上記のデータでも空行を最後に入れています。
そして、HTTP1.1のリクエストで何を要求しているかを示すのは一行目の情報(リクエストライン)です。このリクエストラインのデータを一つずつ見ていきます。

  • 「GET」は、送っているサーバーに対してWebページを要求するリクエストであることを示します。
  • 「/」はリクエストを送るパスを示します。送るサーバーのどのパスに対してリクエストを送っているのかを示します。
  • 「HTTP/ 1.1」は通信で使用するHTTPのバージョンを指定しています。

よって、上記の例であれば送信先のサーバにHTTP1.1を使用してWebページの情報を送ってほしいとリクエストをしています。
また、二行目からはリクエストヘッダフィールドというフィールドが続きます。例ではHostと呼ばれるフィールドを指定しています。このフィールドがないと、サーバーが無効なHTTPリクエストを送ってきたと認識することが多いです。実際にウェブブラウザが送るHTTPリクエストは三行目以降も存在するのですが、今回は仕組みを理解するのが先決なので省略しております。

HTTP レスポンス

HTTPリクエストへ返答する形でサーバからクライアントへ送られるものを、HTTP レスポンスと呼びます。一部を省略した例を下記に示します。

【HTTP レスポンス一例】

HTTP/1.1 200 OK
~以下省略(レスポンスヘッダとwebページのデータ)~

HTTP1.1のレスポンスも改行をデータの区切りとして認識し、最後の行は改行のみとする決まりになっています。
重要なのは一行目のステータスラインと呼ばれるデータなので、このデータを一つずつ説明していきます。

  • 「HTTP/1.1」はバージョンを示すものです。
  • 「200」はステータスコードで、リクエストを処理できたかどうかを示します。200番台は正常なソースコードを示しますので、正常に処理されています。
  • 「OK」はリーズンフレーズと呼ばれており、ステータスコードの説明となります。
    仕様であるRFC2616を見る限り、アスタリスクで示されているため、無くても問題ないようです。ただ、このあたりは通信相手によっては確認する場合もありますので、注意が必要です。

上記で説明した通り、一例として示したレスポンスは正常にHTTPの通信を終えた事を示すものです。通常、ステータスコードが200番台(200~299)であれば正常に通信が行われたと考えてよいでしょう。ステータスコードが400番台であれば、サーバーから何らかのエラーが返ってきていると思われます。例えば、ブラウザでウェブページが見つからない際に「404 Not Found」というメッセージが出るのを目にしたことがある方も多いと思いますが、この404もステータスコードの一種です。

HTTP のリクエストメソッド

HTTPのリクエストに設定する、実行したいアクションを示すものをリクエストメソッドと呼びます。いわば、HTTPの述語のようなものです。ここでは代表的なものを挙げます。もっと詳しい内容を知りたい方は仕様となるRFC7231をご参照ください。

GET

HTTPリクエストのリクエストラインで指定したパスにあるデータの取得要求を行う

HTTPリクエストのリクエストラインで指定したパスにあるデータの取得要求を行う。ただし、HTTPレスポンスには

POST

HTTPリクエストのリクエストラインで指定したパスに、データを送る

PUT

HTTPリクエストのリクエストラインで指定したパスにあるデータを、リクエストで送ったデータに置き換える

DELETE

HTTPリクエストのリクエストラインで指定したパスにあるデータを削除する

LwIPのHTTP Client API

ここからはLwIPで定義されている。HTTPのGETを送るAPIを説明します。他のリクエストメソッドを送るAPIは現状用意されていないようです。

httpc_get_file

HTTPのGETリクエストを送るAPIです。仕様はこちら
です。IPアドレスを指定する必要があるため、あらかじめHTTPのGETリクエストを送るIPアドレスを取得しておく必要があります。

項目1 項目2 説明
機能 HTTPのGETリクエストを送る関数
引数 server_addr HTTP GETリクエストを送るサーバーのIPアドレス
port HTTP GETリクエストを送る時に使用するポート番号
uri GETリクエストで要求するファイルパス
settings GETリクエストを送る際の設定情報です。API呼び出し前に設定しておきます
recv_fn HTTP GETリクエストのレスポンスを受信したときによばれる関数のポインタ
callback_arg 「recv_fn」が呼ばれる際に指定される引数
connection HTTP GETリクエストを送った際のステート情報。API側でステート情報のポインタが指定される
返り値 エラーコード。正常終了したらERR_OK(= 0)を返す

第四引数の「setting」は、APIを送る前に設定します。ここで指定する変数の型「httpc_connection_t」のメンバは下記のようになります。

メンバ 説明
proxy_addr プロキシサーバーのIPアドレス
proxy_port プロキシサーバーのポート番号
use_proxy プロキシサーバーを使うかどうか。1以上の値を入れることで使うと判断される。
altcp_allocator メモリ確保の実装をユーザが定義する場合に使用する。基本的に使用しないので説明は割愛
result_fn リクエストのやり取りが終了した際に呼ばれる関数ポインタ。HTTPの通信で使ったTCPのコネクションを切断した直後に呼ばれる
headers_done_fn HTTPレスポンスのヘッダを受信した際に呼ばれる関数ポインタ。関数の返り値をERR_OK以外に指定することで処理を中止できる

上記のメンバにおける、「result_fn」「headers_done_fn」の関数仕様は下記となります。「headers_done_fn」の関数内で「pbuf *hdr」のメモリを開放しておかないとメモリリークの危険性があるようなので、注意が必要です。

typedef void (*httpc_result_fn)(void *arg, httpc_result_t httpc_result, u32_t rx_content_len, u32_t srv_res, err_t err);

typedef err_t (*httpc_headers_done_fn)(httpc_state_t *connection, void *arg, struct pbuf *hdr, u16_t hdr_len, u32_t content_len);
result_fnの引数 説明
arg 「httpc_get_file」の第六引数で指定したポインタ
httpc_result HTTPリクエストを送った際の結果を示す値。列挙型
rx_content_len HTTPレスポンスのヘッダを除いた受信データサイズ
srv_res HTTPレスポンスのステータスコード
err LwIPのTCP通信におけるエラー値
headers_done_fnの引数 説明
connection HTTP GETリクエストを送った際のステート情報
arg 「httpc_get_file」の第六引数で指定したポインタ
hdr 受信したHTTPレスポンスのヘッダのデータ本体を指すポインタ
hdr_len 受信したHTTPレスポンスのヘッダのサイズ
content_len 受信したHTTPレスポンスのヘッダを除くデータのサイズ

第五引数の「recv_fn」は、HTTPレスポンスを受信した際に呼ばれる関数です。関数仕様は下記となっています。また、この関数内で「pbuf *p」のメモリを開放しておかないとメモリリークの危険性があるようなので、注意が必要です。

typedef err_t (*altcp_recv_fn)(void *arg, struct altcp_pcb *conn, struct pbuf *p, err_t err);

第七引数の「connection」は、APIの処理の中で、ステート情報のインスタンスのポインタが代入されます。よって、空のポインタのポインタを指定します。

httpc_get_file_dns

HTTPのGETリクエストを送るAPIです。仕様はこちらです。こちらは「httpc_get_file」とは違い、ドメイン名を引数に指定します。内部的にDNS Clientによる名前解決を行った後、HTTPのGETリクエストを送ります。

項目1 項目2 説明
機能 HTTPのGETリクエストを送る関数
引数 server_name HTTP GETリクエストを送るサーバーのドメイン名
port HTTP GETリクエストを送る時に使用するポート番号
uri GETリクエストで要求するファイルパス
settings GETリクエストを送る際の設定情報です。API呼び出し前に設定しておきます
recv_fn HTTP GETリクエストのレスポンスを受信したときによばれる関数のポインタ
callback_arg 「recv_fn」が呼ばれる際に指定される引数
connection HTTP GETリクエストを送った際のステート情報。API側でステート情報のポインタが指定される
返り値 エラーコード。正常終了したらERR_OK(= 0)を返す

「server_name」以外の引数に関しては、「httpc_get_file」と同様ののため、説明は省略します。

サンプルソースコード

説明したAPIを使用したサンプルソースコードを示します。ここではLwIPの初期化とDHCPによるIPアドレスの取得のソースコードにかんしては省略しています。LwIP初期化とIPアドレスの初期化についてはこちらをご参照ください。

#include "lwip/dns.h"
#include "lwip/dhcp.h"
#include "lwip/apps/http_client.h"

/* HTTP Clientの管理情報。LwIPのスレッドから参照するので、HTTP Clientの通信が終わるまで保持しておく必要がある。 */
static httpc_connection_t connection; 

static void result_fn(void *arg, httpc_result_t httpc_result, u32_t rx_content_len, u32_t srv_res, err_t err)
{
     /* HTTP GETリクエストの結果を確認するソースコードをここに書く */
}
static err_t headers_done_fn(httpc_state_t *connection, void *arg, struct pbuf *hdr, u16_t hdr_len, u32_t content_len)
{
	/* 送られてきたヘッダ情報が入った「hdr」を確認するソースコードをここに書く */

    pbuf_free(hdr);
    return ERR_OK;
}

static err_t recv_fn(void *arg, struct altcp_pcb *conn, struct pbuf *p, err_t err)
{
	/* 送られてきたレスポンスデータ「p」を確認するソースコードをここに書く */

    pbuf_free(p);
    return ERR_OK;
}

err_t send_httpRequestGET_LwIP()
{
    httpc_state_t* httpState;
    err_t err;

    connection.use_proxy = 0;
    connection.headers_done_fn = headers_done_fn; /* NULL指定でも問題ない */
    connection.result_fn = result_fn; /* NULL指定でも問題ない */

    err = httpc_get_file_dns("httpbin.org", 80, "/get", &connection, recv_fn, NULL, &httpState); /* httpStateは指定せず、NULLとしても問題ない */

    return err;
}

上記のソースコードにおいて、「httpc_get_file_dns」で指定する第七引数はNULLを指定しても問題ないです。また、引数で指定している各関数ポインタに関してもNULLを指定しても動作します(もちろん、受信したレスポンスコードやレスポンスに含まれるデータを確認する必要があるので、定義したほうが良いです)。

参考資料

https://datatracker.ietf.org/doc/html/rfc2616#page-39
https://gihyo.jp/admin/serial/01/http3/0001
https://developer.mozilla.org/ja/docs/Web/HTTP/Methods

Discussion