From dac91796ab90c8addd4f4d7e80eda01ecb2bd8da Mon Sep 17 00:00:00 2001 From: maeken4 Date: Sat, 15 Nov 2025 23:49:47 +0900 Subject: [PATCH] implement minimum server and client --- .gitignore | 2 + Makefile | 47 +++++++++++++ memo.md | 86 +++++++++++++++++++++++ src/client.c | 69 ++++++++++++++++++ src/http_request.c | 171 +++++++++++++++++++++++++++++++++++++++++++++ src/http_request.h | 17 +++++ src/http_server.c | 86 +++++++++++++++++++++++ 7 files changed, 478 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 memo.md create mode 100644 src/client.c create mode 100644 src/http_request.c create mode 100644 src/http_request.h create mode 100644 src/http_server.c 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..498f1ca --- /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)/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..06cb4a4 --- /dev/null +++ b/memo.md @@ -0,0 +1,86 @@ +# 📝課題: 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 + + + +- リクエスト行 + +POST /index.html HTTP/1.0 + +みたいにスペースで区切られる + + + +- headerフィールド +フィールド名:valueの形式 + +--- +HTTPレスポンスは、以下の3つの要素で構成される。 + +ステータス行 +ヘッダーフィールド +ボディ + +### C10k問題 +プロセス作れる数の限界UNIX系ではプロセス数が~32767, 16bit=2^16 diff --git a/src/client.c b/src/client.c new file mode 100644 index 0000000..2bb1d19 --- /dev/null +++ b/src/client.c @@ -0,0 +1,69 @@ +#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; + } + 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(client_fd, request, request_len, 0); // TODO: 0以外のflagの挙動を調べる + + // http responseの読み取り + // TODO: Status line, header, bodyをちゃんと読み取るようにする + int BUF_SIZE = 500; + char buf[BUF_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..b2531fc --- /dev/null +++ b/src/http_request.c @@ -0,0 +1,171 @@ +#include "http_request.h" + +#include +#include +#include + +int parse_and_calc(char* calc_query, int* res) { + // calc_queryはquery=2+3の形式 + char* p = strchr(calc_query, '='); + if (strncmp(calc_query, "query", p - calc_query) != 0) { + return -1; + } + ++p; + // TODO: 2+3の部分。本当はもっとちゃんとやる必要があるが後で + char* end; + int val1 = strtol(p, &end, 10); + ++end; + int val2 = strtol(end, &end, 10); + *res = val1 + val2; + 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(tail->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); + } + free(req->method); + free(req->uri); + free(req->body); + free(req); +} + +void hundle_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) { + // TODO: resource not found errorのresponseを返す。 + } + ++query_param; + + int calc_result; + if (parse_and_calc(query_param, &calc_result) < 0) { + // TODO: 入力が不正エラーのresponseを返す。 + } + 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..569bfaf --- /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 hundle_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..8f45a9b --- /dev/null +++ b/src/http_server.c @@ -0,0 +1,86 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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引数はポインタへのポインタ) + err = getaddrinfo(NULL, "8090", &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; + 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) { + exit(EXIT_FAILURE); + } + + // 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"); + hundle_http_req(input_file_stream, output_file_stream); + fflush(output_file_stream); + close(sock_fd); + } + close(server_fd); +}