Open12

C言語でHTTPサーバ実装 | 周辺知識の学びを書き散らす

dallPdallP

Big EndianとLittle Endian

Big Endianはデータのバイトを最初から順番通りに配置する
Little Endianはデータのバイトを最後から逆順に配置する

エンディアンとは

マルチバイトデータをコンピュータの記憶装置または通信路に対して配置する(= 記録または伝送する)際に用いられるルールのこと。バイトオーダー。

エンディアンが問題となるケース

閉じたシステム、または同一のシステム間においては特に問題は起きない。

異なるシステム間でのデータのやり取りが発生する場合には、リトルエンディアンで送信されたデータをビッグエンディアンで解釈してしまうといったケースにおいて重大な問題となる。

ネットワークバイトオーダー

上記問題を解決するため、例えばTCP/IPプロトコルスタックにおいてはビッグエンディアンに統一されている。
TCP/IPにおけるネットワークバイトオーダーはビッグエンディアンである、と言うことができる。

それに対して、ネットワーク内の各ノード内におけるバイトオーダーはホストバイトオーダーと言われ、内蔵するCPUの種類によって異なるとのこと。

詳しく調べていないが、M1やM2 Macでお世話になる(今はどうか知らん)Rosettaはこの辺に関連するものっぽい?

参考

https://wa3.i-3-i.info/diff112endiannes.html
https://ja.wikipedia.org/wiki/エンディアン
https://e-words.jp/w/エンディアン.html
https://atmarkit.itmedia.co.jp/icd/root/72/116970472.html

dallPdallP

fd: ファイルディスクリプタ

UnixライクなOSにおいて、OSが開いているファイルを特定するための識別子のこと。
日本語だと「ファイル記述子」と言われたりもする。

突っ込んで調べてないが、UnixライクOSにおいては所謂ファイルはもちろんのこと、マウスなどのデバイス、ネットワーク通信におけるソケット、標準入出力などもすべてファイルとして扱う。

これにより、「ファイル」の実態にかかわらず、統一されたファイル入出力インターフェースで操作ができる。

参考

https://lpi.or.jp/lpic_all/linux/intro/intro10.shtml

dallPdallP

Socket(ソケット)とはなにか

異なるノード間でネットワーク通信を行う際の(※)、各ノードにおける通信口のようなもの。
クライアントとサーバ、それぞれで用意されたソケットを接続することで通信が可能となる。

物理的な機械についてる、ケーブルの差込口みたいなものだと思えば良いと思う。
USBとかHDMIとか。

※ 必ずしも異なるノード間の場合に限らないと思われるが、HTTPサーバの実装について検討しているので一旦考えない

ソケットの構造

プロトコルファミリーとソケットタイプ、そしてプロトコルを指定することでソケットを生成できる。

端的に言えば、ことインターネット通信においては「TCPとUDPどっち使う?」をここで決める。
それならごちゃごちゃ指定させず、それだけ引数に取れば良くない?と思わないこともないが、僕には理解できない深謀遠慮があるのだろう。

プロトコルファミリー

要するにそのソケットで何をしたいか?(インターネット通信なのか、ローカルプロセス間通信なのか、はたまた別の通信なのか)を指定する。
プロトコルはよく言われるところの(何かを行う際の)「規約」であり、すなわちプロトコルファミリーはより大きな事象を成すための「規約群」とでも言えるか。

IBMによればTCPはプロトコルであり、TCP/IPはプロトコルファミリーであるっぽいが、「TCP/IPプロトコル」なんて言われたりもするのであんまり深く考えてはいけないかもしれない。
また、C言語のソケット生成における「プロトコルファミリー」は、上述のIBMの定義よりももっと曖昧なものを指しているようにも思える。この時点で「TCP/IP使いたいです!」と宣言しているというよりは、「インターネット接続したいです!」と言ったレベルでのグループのことをプロトコルファミリーと呼んでいるような感じ?

そもそも「TCP/IP」という言葉は、その字面から想像できる(= TCPとIPの組み合わせ)以上の範囲(= IPを中心とする標準的な通信プロトコル)を総称して使われる模様。
参考: https://e-words.jp/w/TCP-IP.html

なので、「TCP/IP」にはUDPはもちろん、HTTPやSMTPなども含まれてくる。
他にもインターネット・プロトコル・スイートなどと呼ばれる。はじめからこっちで呼んでくれ。

それを考えればIBMの定義は正しく、上述した「TCP/IP使いたいです!」と「インターネット接続したいです!」は同義であると考えられる。

備考: アドレスファミリー

似た単語にアドレスファミリーがある。
参考にリンクを乗っけているが、当時はプロトコルファミリーに複数のアドレスファミリーが定義されるような運用を想定していたところ、そうなる世界線には至らず、事実上1プロトコルファミリー = 1アドレスファミリーな状態の模様。

その流れでここでもアドレスファミリーを指定してしまう例が多いとのこと。

ソケットタイプ

どのような伝送制御を行うかを指定する。
ストリーム型なのか、ダイアグラム型なのか、それ以外のものなのか。

プロトコル

具体的に使いたいプロトコルを指定する。
インターネット通信目的でストリーム型の伝送制御なら実質TCPだし、ダイアグラム型の伝送制御なら実質UDPだし、ということで、慣例的に「自動選択」を意味する 0 を与えることが多いみたい?

ただ、具体的に IPPROTO_TCP のように指定することもできる。

参考

https://stackoverflow.com/questions/6729366/what-is-the-difference-between-af-inet-and-pf-inet-in-socket-programming
https://www.ibm.com/docs/ja/cics-ts/5.4?topic=concepts-tcpip-protocols
https://www.tenkaiken.com/short-articles/c言語ソケットプログラム/

dallPdallP

サーバサイドのソケットライフサイクル(?)

ソケットの生成・利用・破棄をざっくり。

生成

上で詳述したのでさっくり。

int server_fd = socket(PF_INET, SOCK_STREAM, 0)

PF_INETやSOCK_STREAMはマクロ

socket関数は生成されたソケットのファイルディスクリプタを返却する。
これは、この後段で生成されたソケットに対しての操作を行う際に必要となる。

アドレスの紐づけ

ソケットに対してローカルのIPアドレス・ポートを紐づける。
クライアントからの接続をどのアドレス・どのポートで受け付けるかを指定する。

struct sockaddr_in serv_addr = { .sin_family = AF_INET ,
						 .sin_port = htons(4221),
						 .sin_addr = { htonl(INADDR_ANY) },
						};

bind(server_fd, (struct sockaddr *) &serv_addr, sizeof(serv_addr))

AF_INET, INADDR_ANYはマクロ。

server_fdに対して、sockaddr_in構造体で示されるアドレス・ポートをbindする。

htonsやhtonlは Host Byte Order to Network Byte Order で、エンディアンの変換を行ってくれている。
sとlはshortとlongを表す。

bindは成功時に0、失敗時に-1を返却するので、それに応じて処理を分岐する。

ポートを開く

ソケットに紐づけられたポートを開き、クライアントからの接続を受け入れられる状態にする。
この時点ではまだ、実際にクライアントからの接続要求を受けても接続されない。

int connection_backlog = 5;
listen(server_fd, connection_backlog)

connection_backlogは、次項でacceptされていない「保留中の接続要求」を保持するキューの最大長とのこと。

クライアントからの接続を受け付ける

接続要求待ち状態に入り、正常に受け付けできた(= 接続できた)場合は、その接続専用のソケットを作成する。

struct sockaddr_in client_addr;
client_addr_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *) &client_addr, &client_addr_len);

失敗した場合は-1を返す。

成功した場合は、ここで得たclient_fdを用いてレスポンスを返すことができる。

ソケットを破棄する

close関数を用いる。
この関数自体はソケットのためだけのものではなく、より大きな概念であるファイル(またはfd)に対しての操作として用意されている。

ソケットの文脈で用いられた場合は、そのfdに関連するソケットをシャットダウンし、関連するリソース(?)を解放するという動作になる。

https://www.ibm.com/docs/ja/zos/2.2.0?topic=functions-close-close-file

close(server_fd);
dallPdallP

データの送受信

クライアントとの接続ができたら、recvsendでデータの送受信ができる。

recv

クライアントからのデータを受け取る。
データを格納するための変数は自ら用意する必要がある。

#define RECV_BUF_SIZE 1024

char recv_buf[RECV_BUF_SIZE];

recv(client_fd, recv_buf, RECV_BUF_SIZE, 0);

なぜバッファサイズをrecvの引数に指定する必要があるのか?

調査中

確定的な答えは見つけられなかったが、おそらく第二引数のrecv_bufはあくまでもデータ格納先となるメモリ領域の始点を指定しているだけなので、そこからどこまでをデータ格納領域として扱ってよいか、を第三引数で指定している?

バッファのサイズについて

データを受け取る際にバッファサイズは、クライアントが送るデータ容量が不明な中でどのように指定するべきなのか?
下記読んだら理解できそう。一旦保留。

https://stackoverflow.com/questions/2862071/how-large-should-my-recv-buffer-be-when-calling-recv-in-the-socket-library

defineについて

改めて書きながら気になったので調べたが、#define自体はどこに書いても(e.g. 関数内に書いても)良いらしい。
これはdefineはコンパイルの前段階でプリプロセッサによって処理されるものであり、コンパイラが気にする文法とは無関係であるための模様。
ただ、それ故に関数のスコープを超えてもdefineによる置換が発生してしまうため、関数内などに記載するのはバッドプラクティスとされている?

send

クライアントにデータを送信する。

char *response = "HTTP/1.1 200 OK\r\n\r\n";
send(client_fd, response, strlen(response), 0);

上記ではHTTPステータスラインのみ送信しているが、
キャリッジリターン CRLFで区切ることでレスポンスヘッダとレスポンスボディも送信できる。

CRLF

キャリッジリターン: カーソルを行頭に戻す

ラインフィード: カーソルを次の行に移す

の組み合わせらしい。

dallPdallP

レスポンスヘッダ・レスポンスボディを送信する

上述の通り、レスポンスをCRLFで区切ることでヘッダ・ボディも送信できる。

char response[1024] = "HTTP/1.1 200 OK\r\n"; // ステータスライン

strcat(response, "Content-Type: text/plain\r\n"); // ここからレスポンスヘッダ
strcat(response, "Content-Length: 4\r\n");
strcat(response, "\r\n");
strcat(response, "hoge"); // ここからレスポンスボディ

send(client_fd, response, strlen(response), 0);

HTTPメッセージのヘッダとボディの間には1行空けることになっているので、"\r\n"のみを差し込んでいる部分がある。

200以外のステータスコードを送信する

ステータスラインをそのようにするだけで、実際それをどう扱うかはクライアントに任せる。
以下は404の例。

char response[1024] = "HTTP/1.1 404 Not Found\r\n";
dallPdallP

C言語におけるダブルクォーテーションとシングルクォーテーションの違い

普段自分が書いているJS/TSではいずれもただ単に文字列を表すが、C言語では明確に役割が異なる。

ダブルクォーテーションは(基本的に)文字列が格納されたアドレス、シングルクォーテーションは文字リテラルを表す。

dallPdallP

便利系文字列操作メソッド

JS/TSみたいに文字列オブジェクトに便利メソッドが生えてるみたいなことはないので、標準で用意されている以下のような関数を多用する。

strlen

size_t strlen(const char *string);

stringの長さを返却する。Null文字は長さに含まれない。

strcmp

int strcmp(const char *string1, const char *string2);

string1とstring2の「大小」を比較する。
大文字・小文字を含めて一致する場合には0が返却され、一致しない場合には差分が返却される。

どうも「差分」の計算方法は処理系により異なるようなので、基本的に返却値が0か否かを見て、両者が等しいかどうかだけを見るに留めるほうが良さそう?

strncmp

int strncmp(const char *string1, const char *string2, size_t n);

strcmpの文字数指定バージョン。
string1とstring2、それぞれの先頭からn文字分を比較する。

strcpy

char strcpy(char *string1, const char *string2)

string1にstring2をNull文字('\0')までコピーする。
string1はNull文字まで込みの大きさで定義しておく必要がある。

コピー後の文字列を返却する。

strchr

char *strchr(const char *string, int c);

stringの先頭から文字リテラルcが表す文字を検索し、見つかった位置をポインタで返却する。
見つからなかった場合はNULLを返却する。

strstr

char *strstr(const char *string1, const char *string2);

strchrの文字列検索版。
string1の先頭からstring2を検索し、見つかった位置をポインタで返却する。
見つからなかった場合はNULLを返却する。

strcat

char *strcat(char *string1, const char *string2);

string1の末尾にstring2を結合する。
Null文字も連結するため、string1はそれも含めた大きさである必要がある。
連結された文字列を返却する。

strerror

char *strerror(int errnum);

errnumに対応するエラーメッセージ文字列のポインタを返却する。

errno

C言語の標準関数利用時に発生したエラーの種別を格納するグローバル変数。
#include <errno.h>を行うことによって変数として利用できる。例↓

printf("Socket creation failed: %s...\n", strerror(errno));
dallPdallP

型定義

typedefを用いて型名と構造を定義することができる。

typedef char HTTPVersion[16];
typedef char RequestPath[1024];
typedef enum
{
  GET,
  PUT,
  POST,
  DELETE,
} HTTPMethod;

typedef struct 
{
  HTTPVersion version;
  RequestPath path;
  HTTPMethod method;
} HTTPHead;

のようなイメージ。

dallPdallP

const修飾子

変数宣言時につけることで再代入を防ぐ。

const uint8_t = MAX_METHOD_SIZE = 7;

また、ポインタにより参照される値を関数スコープ内で変更できないようにするためにも用いられる。

char *strcat(char *str1, const char *str2);

strcat関数では第一引数*str1が参照する値は関数により変更されるが、第二引数の参照値は変更されない。

static修飾子

通常はモジュール外から参照できてしまう変数や関数を、そのモジュール内でのみ参照できるようにする。

static int num = 1;

関数内で用いられると、通常は関数の終了時に消滅してしまう値をプログラムの終了まで保持できる。

#include <stdio.h>

int my_func()
{
    static int count;

    count++;

    return count;
}

int main(void)
{
    int count = 0;
    
    count = my_func();
    printf("%d\n", count); // 1

    count = my_func();
    printf("%d\n", count); // 2

    return 0;
}
dallPdallP

リクエストを並列で受け付ける方法

マルチプロセス、マルチスレッド、非同期I/Oをベースにしたシングルスレッドなどあるが、一旦最もシンプルなマルチプロセスのアプローチで実装してみる。

一旦動くものができたので掲載しておく。

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define BUF_SIZE 1024
#define CLRF "\r\n"
#define HTTP_VERSION "HTTP/1.1"
#define HTTP_OK HTTP_VERSION " 200 OK"
#define HTTP_NOT_FOUND HTTP_VERSION " 404 Not Found"
#define CONTENT_TYPE_KEY "Content-Type:"
#define CONTENT_TYPE_TEXT CONTENT_TYPE_KEY " text/plain"
#define CONTENT_LENGTH_KEY "Content-Length:"

typedef char RequestPath[1024];
typedef char HTTPVersion[16];
typedef char HTTPHost[256];
typedef char UserAgent[256];
typedef char Accept[1024];
typedef char HTTPBody[1024];

typedef enum
{
	GET,
	POST,
	END_OF_HTTP_METHOD,
} HTTPMethod;

const static struct
{
	HTTPMethod method;
	const char *str;
} conversion[] = {
		{GET, "GET"},
		{POST, "POST"}};

typedef struct
{
	HTTPMethod method;
	RequestPath path;
	HTTPVersion version;
	HTTPHost host;
	UserAgent user_agent;
	Accept accept;
} HTTPHead;

typedef struct
{
	HTTPHead head;
	HTTPBody body;
} HTTPRequest;

HTTPMethod conv_str_http_method_to_enum(const char *str)
{
	for (int i = 0; i < END_OF_HTTP_METHOD - 1; i++)
		if (strcmp(conversion[i].str, str) == 0)
		{
			return conversion[i].method;
		}

	return -1;
}

uint8_t recv_request(const int client_fd, char *request_buffer, int bufsize)
{
	request_buffer[bufsize - 1] = '\0';
	if (recv(client_fd, request_buffer, bufsize - 1, 0) < 0)
	{
		return -1;
	}
	return 0;
}

uint8_t read_status_line(const char *request, HTTPRequest *parsed_request)
{
	const uint8_t MAX_METHOD_SIZE = 7;
	char method[MAX_METHOD_SIZE];

	sscanf(request, "%s %s %s", method, parsed_request->head.path, parsed_request->head.version);
	parsed_request->head.method = conv_str_http_method_to_enum(method);

	return 0;
}

char *read_http_header(char *request_headers, HTTPRequest *parsed_request)
{
	static char key[32];
	static char value[1024];

	sscanf(request_headers, "%s %s", key, value);

	if (strcmp(key, "Host:") == 0)
	{
		strcpy(parsed_request->head.host, value);
	}
	else if (strcmp(key, "User-Agent:") == 0)
	{
		strcpy(parsed_request->head.user_agent, value);
	}
	else if (strcmp(key, "Accept:") == 0)
	{
		strcpy(parsed_request->head.accept, value);
	}

	return strchr(request_headers, '\n') + 1;
}

uint8_t read_http_headers(const char *request, HTTPRequest *parsed_request)
{
	char request_copy[strlen(request) + 1];
	strcpy(request_copy, request);

	char *beginning_of_headers = strchr(request_copy, '\n');
	beginning_of_headers++;

	while (beginning_of_headers[0] != '\r' && beginning_of_headers[1] != '\n')
		beginning_of_headers = read_http_header(beginning_of_headers, parsed_request);

	return 0;
}

uint8_t read_request_body(const char *request, HTTPRequest *parsed_request)
{
	char *beginning_of_body = strstr(request, CLRF CLRF);
	strcpy(parsed_request->body, beginning_of_body);
	return 0;
}

uint8_t parse_http_request(const char *request, HTTPRequest *parsed_request)
{
	read_status_line(request, parsed_request);
	read_http_headers(request, parsed_request);
	read_request_body(request, parsed_request);
	return 0;
}

uint8_t respond_200(const int client_fd, const HTTPRequest *request, char *response)
{
	sprintf(response, HTTP_OK CLRF CLRF);

	if (send(client_fd, response, strlen(response), 0) < 0)
	{
		printf("Send failed.\n");
		return 1;
	}

	return 0;
}

uint8_t respond_request_path(const int client_fd, const HTTPRequest *request, char *response)
{
	const char *content = request->head.path + strlen("/echo/");

	char content_length[8];
	sprintf(content_length, "%lu", strlen(content));
	sprintf(response, "%s" CLRF "%s" CLRF CONTENT_LENGTH_KEY "%s" CLRF CLRF "%s", HTTP_OK, CONTENT_TYPE_TEXT, content_length, content);

	if (send(client_fd, response, strlen(response), 0) < 0)
	{
		printf("Send failed.\n");
		return 1;
	}

	return 0;
}

uint8_t respond_ua(const int client_fd, const HTTPRequest *request, char *response)
{
	char content_length[8];
	sprintf(content_length, "%lu", strlen(request->head.user_agent));

	sprintf(response, "%s" CLRF "%s" CLRF CONTENT_LENGTH_KEY "%s" CLRF CLRF "%s", HTTP_OK, CONTENT_TYPE_TEXT, content_length, request->head.user_agent);

	if (send(client_fd, response, strlen(response), 0) < 0)
	{
		printf("Send failed.\n");
		return 1;
	}

	return 0;
}

uint8_t respond_not_found(const int client_fd, const HTTPRequest *request, char *response)
{
	sprintf(response, HTTP_NOT_FOUND CLRF CLRF);

	if (send(client_fd, response, strlen(response), 0) < 0)
	{
		printf("Send failed.\n");
		return 1;
	}

	return 0;
}

uint8_t handle_request(const int client_fd, const char *request, char *response)
{
	HTTPHead head = {
			.method = 0,
			.path = "",
			.version = "",
			.host = "",
			.user_agent = "",
			.accept = "",
	};

	HTTPRequest req = {
			.head = head,
			.body = "",
	};

	parse_http_request(request, &req);

	if (strcmp(req.head.path, "/") == 0)
	{
		return respond_200(client_fd, &req, response);
	}
	else if (strncmp(req.head.path, "/echo/", strlen("/echo/")) == 0)
	{
		return respond_request_path(client_fd, &req, response);
	}
	else if (strcmp(req.head.path, "/user-agent") == 0)
	{
		return respond_ua(client_fd, &req, response);
	}

	return respond_not_found(client_fd, &req, response);
}

int main()
{
	// Disable output buffering
	setbuf(stdout, NULL);

	int server_fd;

	server_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (server_fd == -1)
	{
		printf("Socket creation failed: %s...\n", strerror(errno));
		return 1;
	}

	int reuse = 1;
	if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse)) < 0)
	{
		printf("SO_REUSEPORT failed: %s \n", strerror(errno));
		return 1;
	}

	struct sockaddr_in serv_addr = {
			.sin_family = AF_INET,
			.sin_port = htons(4221),
			.sin_addr = {htonl(INADDR_ANY)},
	};

	if (bind(server_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0)
	{
		printf("Bind failed: %s \n", strerror(errno));
		return 1;
	}

	int connection_backlog = 5;
	if (listen(server_fd, connection_backlog) != 0)
	{
		printf("Listen failed: %s \n", strerror(errno));
		return 1;
	}

	while (1)
	{
		printf("Waiting for a client to connect...\n");

		struct sockaddr_in client_addr;
		socklen_t client_addr_len = sizeof(client_addr);

		int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);

		if (client_fd == -1)
		{
			printf("Failed to accept connection.\n");
			return 1;
		}

		printf("Client connected\n");

		if (!fork())
		{
			close(server_fd);

			char request_buffer[BUF_SIZE];

			if (recv_request(client_fd, request_buffer, BUF_SIZE) != 0)
			{
				printf("Failed to receive request.\n");
				return 1;
			}

			char response_buffer[BUF_SIZE];

			handle_request(client_fd, request_buffer, response_buffer);

			close(client_fd);

			exit(0);
		}

		close(client_fd);
	}

	close(server_fd);

	return 0;
}