Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/*
build/*
47 changes: 47 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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)
86 changes: 86 additions & 0 deletions memo.md
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions src/client.c
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

サーバー側の名前がhttp_server.cなのでhttp_client.cだとわかりやすいと思いました!

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#define _GNU_SOURCE
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#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"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeepAlive ではないのでConnection: closeをつけているのが丁寧で良いと思いました!

"\r\n",
SERVER_ADDR, SERVER_PORT);
send(client_fd, request, request_len, 0); // TODO: 0以外のflagの挙動を調べる
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

サーバー側のコネクションがcloseしているとここでプロセスが落ちるのでMSG_NOSIGNALをつけてベットエラーハンドリングをした方が良いかもしれないです。

MSG_NOSIGNAL (Linux 2.2 以降)
ストリーム指向のソケットで相手側が接続を切断した時に、エラーとして SIGPIPE を送信しないように要求する。
https://ja.manpages.org/send/2

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendは例えばカーネルの送信バッファーに十分な空きがない時にはデータが一部しか送信されない可能性があるようです。
なのでデータを送り切るまでループすると良いと思います。

https://stackoverflow.com/questions/8900474/when-will-send-return-less-than-the-length-argument


// 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より大きいと動かない
Comment on lines +63 to +64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mallocで動的にメモリを確保してレスポンスを受け取る書き方も試しても良いかもしれません。

buf[response_len] = '\0';

close(client_fd);
printf("%s", buf);
}
171 changes: 171 additions & 0 deletions src/http_request.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#include "http_request.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strncmpの第三引数の比較する文字列の長さをp - calc_queryで決めているので例えばque=2+3の場合でも0を返すことになるので、queryという文字列の長さを指定するのが良さそうです。

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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node->value ですかね?

}
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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tailが実際は末尾ではなくてiteratorを表しているのでnodeのような名前にすると読みやすい気がしました。
動かしてないですが以下のようなイメージです。

    node = req->header;
    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);
Comment on lines +121 to +123
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req->http_verのfreeが漏れてると思います。

free(req);
}

void hundle_http_req(FILE* in, FILE* out) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleのtypoでしょうか?

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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// 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);
}
17 changes: 17 additions & 0 deletions src/http_request.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include <stdio.h>

struct HTTPRequest {
char* http_ver;
char* method;
char* uri;
struct HTTPHeaderField* header;
char* body;
};

struct HTTPHeaderField {
char* field;
char* value;
struct HTTPHeaderField* next;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinkedListのような感じでヘッダーを最後の行まで見る方法は思いつかなかったので勉強になりました。

};

void hundle_http_req(FILE* http_req, FILE* http_res);
Loading