diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c58ac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/* +build/* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aa036f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +CC := gcc +CFLAGS := -std=c11 -Wall -Wextra -O2 -g -I src -MMD -MP +LDFLAGS := +LDLIBS := + +SRCDIR := src +OBJDIR := build +BINDIR := bin + +SRCS_SERVER := $(SRCDIR)/http_server.c $(SRCDIR)/http_request.c +SRCS_CLIENT := $(SRCDIR)/http_client.c + +OBJS_SERVER := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SRCS_SERVER)) +OBJS_CLIENT := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SRCS_CLIENT)) + +TARGET_SERVER := $(BINDIR)/http_server +TARGET_CLIENT := $(BINDIR)/http_client + +DEPS := $(OBJS_SERVER:.o=.d) $(OBJS_CLIENT:.o=.d) + +.PHONY: all clean + +all: $(TARGET_SERVER) $(TARGET_CLIENT) + +# create bin and build dirs as order-only prerequisites +$(BINDIR): + mkdir -p $(BINDIR) + +$(OBJDIR): + mkdir -p $(OBJDIR) + +# pattern rule to compile .c -> build/%.o +$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR) + $(CC) $(CFLAGS) -c $< -o $@ + +# link targets +$(TARGET_SERVER): $(OBJS_SERVER) | $(BINDIR) + $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ + +$(TARGET_CLIENT): $(OBJS_CLIENT) | $(BINDIR) + $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ + +# include dependency files if present +-include $(DEPS) + +clean: + rm -rf $(BINDIR) $(OBJDIR) diff --git a/memo.md b/memo.md new file mode 100644 index 0000000..dc53264 --- /dev/null +++ b/memo.md @@ -0,0 +1,103 @@ +# 📝課題: C言語 + system callでHTTP Serverを作ってみよう +``` +GET /calc?query=2+10 HTTP/1.1 +``` +に対して +``` +HTTP/1.1 200 OK +Content-Length: 2 + +12 +``` + +を返すようなもの +## ヒント +- manを使う +- system callのエラーは必ず対処する +- メモリは動的に確保しよう +- クライアントとサーバーの処理が別で必要 + +## 余力があれば挑戦するとよいこと +- IPv4+v6両対応 +- CPU性能を最大限活用できるようにする +- non-blocking化 +- マルチスレッド化 +- 通信タイムアウトの設定 +- SSL対応 +- signalを受け取ったら全コネクションが正常終了するようにする + + + +### 自分の実装計画 + +まず1プロセスで単純なHTTPサーバーを作る。 +- socket, bind, listen, acceptで通信確立 +- request lineの解析 +- headerの解析 +- bobyから計算 + +時間があったら... +CI/CDをちゃんと設定する +→次にワーカープロセスを使った実装 +→次にマルチスレッド +→次にepollによる実装 + +## 調べたもの + +### bind(2) +addrをsocketのfdに結び付ける +0: success +-1: error + +sockaddr_in構造体=ポート番号 + +### HTTP リクエスト +HTTPリクエストは、以下の3つの要素で構成される。 + +- リクエスト行 +- ヘッダーフィールド +- ボディ +ヘッダーフィールド、ボディは省略可能。 +ヘッダーフィールドとボディの間は、空行を1行挟む。 + +https://qiita.com/gunso/items/94c1ce1e53c282bc8d2f#2http%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%AE%E6%A7%8B%E6%88%90 +https://www.rfc-editor.org/rfc/rfc9112.html + + +### HTTP レスポンス +- HTTPリクエストと同様に、 +``` +POST /index.html HTTP/1.0 +``` +みたいにスペースで区切られる + +- headerフィールド +フィールド名:valueの形式 + +- HTTPレスポンスは、以下の3つの要素で構成される。 + - ステータス行 + - ヘッダーフィールド + - ボディ + +### C10k問題 +プロセス作れる数の限界UNIX系ではプロセス数が~32767, 16bit=2^16 + +### bindとTIME_WAITについて +TCPにおいて、切断後、一定時間はソケット状態を維持するため、アプリを再起動したときにすぐに同じポートを再利用できない。`SO_REUSEADDR`を設定するとうまくいく +``` +// bindが解放されなくなる対策 +const int one = 1; +// 第2引数で設定したいレイヤー、第3引数でそのレイヤーで設定したいオプション、第4引数でそのオプションの値を入れる。 +setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); +``` +https://qiita.com/bamchoh/items/1dd44ba1fbef43b5284b +https://bombrary.github.io/blog/posts/socket01-file-tcp/ + +## 頂いたコメント +- clientからhttp_requestを送るときにconnectまで成功して、いざsend使用する直前に切断されてしまった場合、そのままsendするとSIGPIPEシグナルが飛んできてプロセスが終了してしまう。 + >サーバー側のコネクションがcloseしているとここでプロセスが落ちるのでMSG_NOSIGNALをつけてベットエラーハンドリングをした方が良いかもしれないです。 + + >MSG_NOSIGNAL (Linux 2.2 以降) + ストリーム指向のソケットで相手側が接続を切断した時に、エラーとして SIGPIPE を送信しないように要求する。 + https://ja.manpages.org/send/2 + diff --git a/src/http_client.c b/src/http_client.c new file mode 100644 index 0000000..a2280a6 --- /dev/null +++ b/src/http_client.c @@ -0,0 +1,82 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#define SERVER_ADDR "127.0.0.1" +#define SERVER_PORT "8090" + +int main() { + struct addrinfo hints, *result, *res_p; + int err, client_fd = -1; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_INET; // IPv4 + hints.ai_socktype = SOCK_STREAM; // TCP + + err = getaddrinfo(SERVER_ADDR, SERVER_PORT, &hints, &result); + if (err != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); + exit(EXIT_FAILURE); + } + + for (res_p = result; res_p; res_p = res_p->ai_next) { + client_fd = socket(res_p->ai_family, res_p->ai_socktype, res_p->ai_protocol); + if (client_fd < 0) { + fprintf(stderr, "create socket failed: %s (errno=%d)\n", strerror(errno), errno); + continue; + } + + if (connect(client_fd, res_p->ai_addr, res_p->ai_addrlen) < 0) { + fprintf(stderr, "create connection failed: %s (errno=%d)\n", strerror(errno), errno); + continue; + } + // connect成功 + break; + } + // TODO: ここいらない? + if (client_fd < 0) { + printf("create connection error"); + exit(EXIT_FAILURE); + } + freeaddrinfo(result); + + // http requestの書き込み + char request[512]; + int request_len = snprintf(request, sizeof(request), + "GET /calc?query=12+24 HTTP/1.1\r\n" + "Host: %s:%s\r\n" + "User-Agent: simple-c-http-client/1.0\r\n" + "Connection: close\r\n" + "\r\n", + SERVER_ADDR, SERVER_PORT); + // send: 送り切るまで + int sent = 0; + while (sent < request_len) { + int n = send(client_fd, request + sent, request_len - sent, MSG_NOSIGNAL); + if (n < 0) { + if (errno == EINTR) continue; // interrupted system call + perror("send"); + close(client_fd); + exit(1); + } + sent += n; + } + + // http responseの読み取り + // TODO: Status line, header, bodyをちゃんと読み取るようにする + // TODO: 動的にメモリ確保をする + int BUF_SIZE = 500; + char buf[BUF_SIZE]; + // TODO: ここもBUFF_SIZE受け取れているとは限らない... + int response_len = recv(client_fd, buf, BUF_SIZE, 0); // BUF_SIZEより大きいと動かない + buf[response_len] = '\0'; + + close(client_fd); + printf("%s", buf); +} diff --git a/src/http_request.c b/src/http_request.c new file mode 100644 index 0000000..af03975 --- /dev/null +++ b/src/http_request.c @@ -0,0 +1,214 @@ +#include "http_request.h" + +#include +#include +#include +#include +#include + +int parse_and_calc(char* calc_query, int* res) { + if (calc_query == NULL || res == NULL) { + return -1; + } + // calc_queryはquery=2+3の形式 + // // queryがqueでもよくなってしまっている。あと=がない場合にまずい + // char* p = strchr(calc_query, '='); + // if (strncmp(calc_query, "query", p - calc_query) != 0) { + // return -1; + // } + char* key = "query="; + size_t key_length = strlen(key); + if (strncmp(calc_query, key, key_length) != 0) { + return -1; + } + char* p = calc_query + key_length; + + // 2+3の形式をチェック + char* end; + long val1 = strtol(p, &end, 10); + if (errno == ERANGE || val1 < INT_MIN || val1 > INT_MAX) { + return -1; + } + if (*end != '+') { + return -1; + } + ++end; + long val2 = strtol(end, &end, 10); + if (errno == ERANGE || val2 < INT_MIN || val2 > INT_MAX) { + return -1; + } + if (*end != '\0') { + return -1; + } + long sum = val1 + val2; + if (sum > INT_MAX || sum < INT_MIN) { + return -1; + } + *res = (int)sum; + return 0; +} + +struct HTTPHeaderField* read_header_field(FILE* in) { + char buf[256]; + char* p; + p = fgets(buf, sizeof(buf), in); + if (p == NULL) { + exit(EXIT_FAILURE); + } + if (strlen(buf) <= 2) { + return NULL; + } + + struct HTTPHeaderField* ret; + ret = malloc(sizeof(*ret)); + p = strchr(buf, ':'); + *p = '\0'; + // :の後の半角スペースをスキップ + p += 2; + ret->field = malloc(strlen(buf) + 1); + strcpy(ret->field, buf); + // value + ret->value = malloc(strlen(p) + 1); + strcpy(ret->value, p); + + ret->next = NULL; + return ret; +} + +struct HTTPRequest* parse_request(FILE* in) { + // 戻り値にするためheap上に確保してポインタを返す + struct HTTPRequest* req; + req = malloc(sizeof(struct HTTPRequest)); + + // read request line + int MAX_STR_LEN = 300; + char buf[MAX_STR_LEN]; + char* err = fgets(buf, MAX_STR_LEN, in); + if (err == NULL) { + return NULL; + } + char *method_end, *uri_end; + + // read method + method_end = strchr(buf, ' '); + *method_end = '\0'; + req->method = malloc(strlen(buf) + 1); + strcpy(req->method, buf); + char* uri = ++method_end; + + // read uri + uri_end = strchr(uri, ' '); + *uri_end = '\0'; + req->uri = malloc(strlen(uri) + 1); + strcpy(req->uri, uri); + char* http_ver = ++uri_end; + + // read http_ver + req->http_ver = malloc(strlen(http_ver) + 1); + strcpy(req->http_ver, http_ver); + + // Request headerの処理 + req->header = read_header_field(in); + struct HTTPHeaderField *tail = req->header, *node; + // 後続にbodyがあるか + long long content_length = -1; + while ((node = read_header_field(in)) != NULL) { + if (strcmp(node->field, "Content-Length") == 0) { + char* end; + // この長さも確認したほうがよい + content_length = strtol(node->value, &end, 10); + } + tail->next = node; + tail = node; + } + + // Request bodyの処理 + if (content_length < 0) { + req->body = NULL; + return req; + } + + req->body = malloc(content_length); + int num = fread(req->body, content_length, 1, in); + if (num < content_length * 1) { + fprintf(stderr, "parse request body faild"); + } + return req; +} + +void free_request(struct HTTPRequest* req) { + // struct HTTPHeaderField *node, *tail; + + // tail = req->header; + // while (tail) { + // node = tail; + // tail = tail->next; + // free(node->field); + // free(node->value); + // free(node); + // } + + struct HTTPHeaderField *node = req->header, *next_node; + while (node) { + next_node = node->next; + free(node->field); + free(node->value); + free(node); + node = next_node; + } + free(req->method); + free(req->uri); + free(req->body); + free(req); +} + +void handle_http_req(FILE* in, FILE* out) { + struct HTTPRequest* http_req = parse_request(in); + + // -------- debug + printf("%s%s%s", http_req->method, http_req->uri, http_req->http_ver); + struct HTTPHeaderField* node = http_req->header; + while (node != NULL) { + printf("%s: %s", node->field, node->value); + node = node->next; + } + if (http_req->body != NULL) printf("%s", http_req->body); + // -------- debug + + char* query_param; + query_param = strchr(http_req->uri, '?'); + + // pathが正しいか + // if (strncmp(http_req->uri, "/calc", query_param - http_req->uri) != 0) { + // /caでも通っちゃう!!! + // // TODO: resource not found errorのresponseを返す。 + // } + char* calc_path = "/calc"; + if (strncmp(http_req->uri, calc_path, strlen(calc_path)) != 0) { + fprintf(out, "HTTP/1.1 404 Not Found"); + } + ++query_param; + + int calc_result; + if (parse_and_calc(query_param, &calc_result) < 0) { + fprintf(out, "HTTP/1.1 400 Bad Request"); + } + free_request(http_req); + + char body[64]; + int body_len = snprintf(body, sizeof(body), "%d\n", calc_result); + // output streamに書き込む + fprintf(out, + "HTTP/1.1 200 OK\r\n" + "Content-Length: %d\r\n" + "\r\n" + "%s", + body_len, body); + // debug + printf( + "HTTP/1.1 200 OK\r\n" + "Content-Length: %d\r\n" + "\r\n" + "%s", + body_len, body); +} diff --git a/src/http_request.h b/src/http_request.h new file mode 100644 index 0000000..2997c10 --- /dev/null +++ b/src/http_request.h @@ -0,0 +1,17 @@ +#include + +struct HTTPRequest { + char* http_ver; + char* method; + char* uri; + struct HTTPHeaderField* header; + char* body; +}; + +struct HTTPHeaderField { + char* field; + char* value; + struct HTTPHeaderField* next; +}; + +void handle_http_req(FILE* http_req, FILE* http_res); diff --git a/src/http_server.c b/src/http_server.c new file mode 100644 index 0000000..3bcea9e --- /dev/null +++ b/src/http_server.c @@ -0,0 +1,101 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HOST "localhost" +#define PORT "8090" + +int main() { + // この段階ではhintsはスタック領域上に確保、resultはどこも指していないポインタ + struct addrinfo hints, *result, *res_p; + int err, server_fd = -1; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_INET; // IPv4 + hints.ai_socktype = SOCK_STREAM; // TCP + hints.ai_flags = AI_PASSIVE; // bind用 + + // result(ポインタ)へのポインタを渡す(getaddrinfoの第4引数はポインタへのポインタ) + // resultは利用可能なアドレス情報(addrinfo)の連結リスト + err = getaddrinfo(HOST, PORT, &hints, &result); + if (err != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); + exit(EXIT_FAILURE); + } + + for (res_p = result; res_p; res_p = res_p->ai_next) { + // clientと通信を確立する用のソケットを作成 + server_fd = socket(res_p->ai_family, res_p->ai_socktype, res_p->ai_protocol); + if (server_fd < 0) { + fprintf(stderr, "create socket failed: %s (errno=%d)\n", strerror(errno), errno); + continue; + } + + // bindが解放されなくなる対策 + const int one = 1; + // 第2引数で設定したいレイヤー、第3引数でそのレイヤーで設定したいオプション、第4引数でそのオプションの値を入れる。 + setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); + // socketにポート番号をバインドする + err = bind(server_fd, res_p->ai_addr, res_p->ai_addrlen); + if (err < 0) { + fprintf(stderr, "bind failed: %s (errno=%d)\n", strerror(errno), errno); + close(server_fd); + continue; + } + + // listen状態にする + int QUEUE_CONN_NUM = 10; + err = listen(server_fd, QUEUE_CONN_NUM); + if (err < 0) { + fprintf(stderr, "listen failed: %s (errno=%d)\n", strerror(errno), errno); + exit(EXIT_FAILURE); + } + break; + } + freeaddrinfo(result); + + if (server_fd < 0) { + exit(EXIT_FAILURE); + } + for (;;) { + printf("Waiting connect...\n"); + // これらの情報は使わないから要らない? + struct sockaddr addr; + socklen_t addrlen; + + // listen socketは接続を受け付けるだけで、接続済みのソケットは別になる + int sock_fd = accept(server_fd, &addr, &addrlen); + if (sock_fd < 0) { + if (errno == EINTR) { + // EINTRはシグナルで割り込まれただけなので再試行する + continue; + } + perror("accept"); + } + + // fdopen: ファイルディスクリプタを扱いやすいFILE*でラップする + FILE* input_file_stream = fdopen(sock_fd, "r"); + int dup_fd = dup(sock_fd); + if (dup_fd < 0) { + perror("dup"); + fclose(input_file_stream); + continue; + } + FILE* output_file_stream = fdopen(dup_fd, "w"); + handle_http_req(input_file_stream, output_file_stream); + // fcloseに統一 + // fflush(output_file_stream); + // close(sock_fd); + fclose(input_file_stream); + fclose(output_file_stream); + } + close(server_fd); +}