C言語 socket通信でhttp通信
背景
webアプリを作りたくて始めました.普通のアプリならjavascriptやphpでゴニョゴニョすればいいと思うのですが(よくしらない),FPGAチップのZinqシリーズ上で動くFPGA上のデータをウェブブラウザ経由でモニタリングするものを考えていたため,メモリの直叩きが得意なCでサーバー側のプログラムを作る必要がありました.その第一歩としてまずはhttp通信をCで表現するとどうなるのか?そもそもhttpとは?を理解するべくsocket通信でhttp通信を試してみることにしました.
ネットワークの基礎
ネットワークはOSI参照モデルによって階層分けされており,一番上のアプリケーション層は一つ下のプレゼンテーション層に支えられ...というように成り立っています.今自分が使ってるモノがどの層で動いているのかを意識するとわかりやすいです.
OSI参照モデルはあくまで概念なのでイメージ程度に考えます.
表1 OSI参照モデル
図1 ざっくりしたTCP/IP
図1にざっくりした下層の解説図を表しました.
ネットワーク層:クライアントはサーバーのIPアドレスを頼りに通信を始めます.クライアント-サーバー間にどんな機器が存在するか,どんなデータを送っているかを気にすることなく通信します.
データリンク層:ネットワーク機器は宛先IPアドレスをもとに次にどこにデータを送ればよいか判断しデータを中継します.この時,機器に割り振られたアドレスがMACアドレスと呼ばれるものです.ネットワーク機器はMACアドレスで次の中継先にデータを送るので,中継先とどんなインターフェースでつながれているか,最終目標のIPアドレスは何なのかを気にすることなく通信します.
物理層:ネットワーク機器の端子からは何らかの物理現象でデータを表現し送信します.MACアドレス,IPアドレス,データの中身を気にすることなく,単に信号を光に変換したり,Wi-Fi電波で飛ばしたり,電気信号としてケーブルに流します.
以上のようにしてクライアント-サーバー間で通信を行っています.実際にはIPアドレスを変換したりなんやら勘彌ら複雑ですが気にしないことにします.
socket通信の基礎
C言語でsocketを使うと文字の通信ができるようになります.socket通信はセッション層らしいです.TCP通信では図2のように通信を行います.socketはこの一連の処理をなんやかんややってくれて,read(),write()関数だけで文字のやり取りができるようにしてくれます.
図2 TCP通信の概要
httpの基礎
httpはアプリケーション層で動くプロトコルです.クライアント(webブラウザ)はサーバーに対してデータ送信要求を示す文字列を送り,データが返ってくるという形です.(図3)今回の送信文字列は
GET /test.html HTTP/1.1
Host: 192.168.0.41:80
Connection: close
です.
1行目:メソッド(送信要求),要求オブジェクトURI,プロトコル
2行目:IPアドレス,ポート番号
3行目:ヘッダ,上述のように記述すると送信が終わると接続が切れる.
ローカルIPアドレス192.168.0.41(ポート80)にHTTP ver 1.1 で/test.htmlのデータを要求しています.
図3 http通信の概略
取得するhtmlファイルの大きさが大きいと1度のレスポンスで送り切れないので今回の実験では1度で送り切れるような簡単なtest.htmlを用意しました.
読みだしたtest.htmlファイルも載せておきます.サーバー側ではapacheが動いておりこのファイルをindex.htmlと同じディレクトリに保存して実験しました.
test.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> <p>hello world</p> </body> </html>
実行プログラム
インターネット上をあされば数多の同様なプログラムが出てくると思います.しかしながらたいていのプログラムは名前解決が実装されてたりエレガントにエラー処理がされてたりしてわかりにくいので超絶シンプル記述を意識しました.
httpは究極に単純化するとsocket通信のwrite()でGET要求を送ってread()でデータを読めばいいのです.
今回はあらかじめLAN内にサーバーを立ててtest.htmlを用意し,そこにLinuxPCからアクセスする作戦で行きました.サーバーのアドレスは192.168.0.41を設定しました.
クライアントプログラム
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main() { struct sockaddr_in server; int sock; char buf[1024]; //バッファ(1kB) int n; char request_messe[]="GET /test.html HTTP/1.1\r\nHost: 192.168.100.41:80\r\nConnection: close\r\n\r\n"; //文字数79 /*ソケットの作成*/ sock = socket(AF_INET, SOCK_STREAM, 0); server.sin_family = AF_INET; //よくわからん server.sin_port = htons(80); //接続先ポート番号 server.sin_addr.s_addr = inet_addr("192.168.0.41"); //接続先IPアドレス /*サーバに接続*/ connect(sock, (struct sockaddr *)&server, sizeof(server)); printf("conected\n"); send(sock, request_messe, 77,0); //(ソケット,送信メッセージ,データ文字数,フラグ?) printf("data send\n"); /*サーバからデータを受信*/ memset(buf, 0, sizeof(buf)); //バッフア初期化 n = read(sock, buf, sizeof(buf)); //受信 printf("read:%d\n\n %s\n", n, buf); //表示 /*ソケットの終了*/ close(sock); return 0; }
実行結果
conected data send read:415 HTTP/1.1 200 OK Date: Tue, 05 Apr 2022 00:05:56 GMT Server: Apache/2.4.38 (Raspbian) Last-Modified: Mon, 04 Apr 2022 15:06:42 GMT ETag: "8f-5dbd579d471ae" Accept-Ranges: bytes Content-Length: 143 Vary: Accept-Encoding Connection: close Content-Type: text/html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> <p>hello world</p> </body> </html>