はじめに
C言語(g++でビルドするけれど)でHTTPでデータを送る方法を調べていました。
POSTでセンサーデータを送りたいなー、と思っていた所、libcurl というライブラリをC/C++で使うことができることを知ったので、頑張って動かしてみました。あっさり動いたので、いちおうメモしておきます。
自分のコード:こちら
参考にしたtutorial: こちら
POST リクエストをC言語で送ってみる
準備
libcurl だけではcurl/curl.h がないと言われたので、apt update してから
$ sudo apt install libcurl4-openssl-dev
でインストールしました。
POST リクエストを受け取るサーバ
いつもの
$ python -m http.server 8000
でサーバを立てようとしたのですが、POSTを受け取るにはCGIで値を受け取るようになど分からなかったので、やめました。
https://docs.python.org/ja/3/library/http.server.html
今回は趣向を変えて、golang で用意しました。goの環境はapt で入りました。
$ sudo apt install golang-go
$ go version
go version go1.13.8 linux/amd64
一式は /usr/share/go 以下にインストールされた、っぽいです。GOROOTとかGOPATHとか環境変数の設定していないけれどいいのか、、、分かりませんが、先に進みます。
このコードをそのまま使いました。下記に、GOのコードの作者の解説があります。多謝。
ビルドして実行します。
$ go build main.go
$ ./main
別ターミナルでPOSTしてみます。
$ curl -X POST -d "foo=1000&hoge=aiueo" http://127.0.0.1:8080/sample1
サーバを立ち上げている方のターミナルで、受け取ったことが表示されます。
$ ./main
hoge aiueo
foo 1000
めでたしめでたし。尚、curl の使い方は下記を参考にしました。
libcurl を使用したC言語でのHTTP POSTするclientを実装
パラメータを送る
ずばり下記を参考に書いてみました。
手順としては、
- curl_global_init(...) を最初に呼ぶ。socket 以下のHWよりのソフトの準備かな?
- curl_easy_init(...) でハンドラを作る。socket のイメージだけと違うかな。
- curl_easy_setopt(...) でTCP client としての接続先サーバと送るメッセージの準備。SSLとかも設定できる。なdocumentを参照。何度もこの関数を呼んで設定する。ドキュメントを読もう。
- curl_easy_perform(...) でいざ通信を実行。これはblocking モード。non-blocking では別の関数を使う。
- curl_easy_cleanup(...) で作ったハンドラをクリアする。
- curl_global_cleanup()を最後に呼ぶ。
で動いた。実際はsetopt と perform を繰り返し使うのかな。エラーが起きて再接続するときは curl_easy_init でハンドラを作りなして。それぞれドキュメントがあり、使い方コードもあってとても参考になりました。
下記のコードであっさりと動きました。
#include <curl/curl.h>
int main(void)
{
curl_global_init(CURL_GLOBAL_ALL);
// https://curl.se/libcurl/c/curl_global_init.html
printf("libcurl version %s\n", curl_version());
CURL *easy_handle = curl_easy_init();
/* set URL to operate on */
if(easy_handle){
CURLcode res = curl_easy_setopt(easy_handle, CURLOPT_URL, "http://127.0.0.1:8080/sample1");
if(res == CURLE_OK){
printf("OK set url\n");
}else{
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
res = curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDS, "foo=1000&hoge=aiueo");
if(res == CURLE_OK){
printf("OK setopt CURLOPT_POSTFIELDS\n");
}else{
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
res = curl_easy_perform(easy_handle);
if(res == CURLE_OK){
printf("OK easy_perform\n");
}else{
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
curl_easy_cleanup(easy_handle);
}
curl_global_cleanup();
return 0;
}
ビルドは下記で。pkg-config を使うと少しかっこいいのかな。
$ g++ main.cc `pkg-config --libs libcurl`
$ ./a.out
libcurl version libcurl/7.68.0 OpenSSL/1.1.1f zlib/1.2.11 brotli/1.0.7 libidn2/2.2.0 libpsl/0.21.0 (+libidn2/2.2.0) libssh/0.9.3/openssl/zlib nghttp2/1.40.0 librtmp/2.3
OK set url
OK setopt CURLOPT_POSTFIELDS
OK easy_perform
さきほどのサーバに同じような反応が見られます。
サーバを立てていないとエラーの内容をきちんと返してくれました。
OK set url
OK setopt CURLOPT_POSTFIELDS
curl_easy_perform() failed: Couldn't connect to server
バイナリデータを送る
POSTリクエストでは、先程パラメータをJSONのような辞書形式のパラメータで送る以外に、直接バイナリデータを送ることもできます。その場合、libcurl では、curl_easy_setopt でデータの長さとデータの配列のポインタを渡します。
ポイントとしてはデータの長さはuint16_t で定義した変数だとダメで、きちんと curl_off_t を使いましょう。こちらを参考にしました。
#include <curl/curl.h>
int main(void)
{
curl_global_init(CURL_GLOBAL_ALL);
printf("libcurl version %s\n", curl_version());
CURL *easy_handle = curl_easy_init();
/* set URL to operate on */
if(easy_handle){
CURLcode res = curl_easy_setopt(easy_handle, CURLOPT_URL, "http://127.0.0.1:8080/bytearray");
if(res != CURLE_OK){
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
// Here, length and pointer of byte array is passed to easy_handel
uint32_t data_num = 1024*1024;
curl_off_t length_of_data = data_num*2;
uint16_t* data = (uint16_t*)malloc(data_num*sizeof(uint16_t));
res = curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDSIZE_LARGE, length_of_data);
if(res != CURLE_OK){
fprintf(stderr, "curl_easy_setopt(CURLOPT_POSTFIELDSIZE) failed: %s\n", curl_easy_strerror(res));
}
res = curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDS, reinterpret_cast<uint8_t*>(data));
if(res != CURLE_OK){
fprintf(stderr, "curl_easy_setopt(CURLOPT_POSTFIELDS) failed: %s\n", curl_easy_strerror(res));
}
res = curl_easy_perform(easy_handle);
if(res != CURLE_OK){
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
free(data); data = NULL;
curl_easy_cleanup(easy_handle);
}
curl_global_cleanup();
return 0;
}
受け取るサーバ側も変更します。http.HandleFunc("/bytearray", myhandler) で新しい設定するハンドラーで、リクエストのbody を読むことで、送られてきたバイナリデータを取得できます。
func myhandler(w http.ResponseWriter, request *http.Request) {
fmt.Println("bindata_handler")
d, err := ioutil.ReadAll(request.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
tmpfile, err := os.Create("./" + "data.bin")
defer tmpfile.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
tmpfile.Write(d)
w.WriteHeader(200)
return
}
無事に動きました。
まとめ
とりあえず、HTTP POST リクエストを受け取る簡易サーバをローカルに立てて、libcurl を使用したCコードで POST リクエストを遅れることが確認できた。これ、RasPiとかで使えるかな。。
今年はとりあえず幸先よくスタートできたぞ。
(2024/1/2)