[Lv5] Webservの入り口
0. はじめに
課題には書いてないのにレビューで要求されたり、実装するには負担だけどレビューで必須ではなかったりすることがあるので、実装の導線を整理します。
Q. 何をする課題?
A. c++でnginxを作る課題です。具体的には
1. 自作の./webserv を実行した後に
2. Chromeでlocalhost:8080にアクセスできるようにします。
![](https://assets.st-note.com/img/1675594506419-MIyRt7hmjz.png)
Q. 本買ったほうがいい?
A. 特にいりません。
サーバ周りの知識をしっかり埋めたい、などの動機を持って書籍を読むのは素晴らしいことだと思います。
明確に課題が存在している以上、すでにクリアした人のgithubを3人動かして読むのが最短ですし、不明点は検索すれば十分解決できます。
1. Configファイル
Configファイルに何ら指示がないので、好きに作っていいようです。
もともとサーバの仕組みを知っている人ならワクワクするかもしれませんが、サーバのイメージが湧かない我々初学者は、必須設定すら分からず戸惑うはずです。
1つの方針として、nginxのconfigフォーマットをコピーする気持ちで臨むと学びが多いです。(オレオレ実装だと、課題が終わったあとに残るものが自信だけになってしまうかも。)
そして、例えば以下のような雰囲気のConfigファイルが読めれば、課題要件を満たせます。
default.conf
server {
listen 8080 ;
server_name default_server;
root ./docs/;
# sample_command: curl -i localhost:8080
location / {
method GET;
index index.html;
autoindex off;
}
# sample_command: curl -i -X POST -F upfile=@./docs/default.conf localhost:8080/upload/
location /upload/ {
method GET POST;
root /upload/;
autoindex off;
upload_path /upload/;
max_body_size 10000;
}
# sample_command: curl -i localhost:8080/index/
location /index/ {
autoindex on;
}
# sample_command: curl -i localhost:8080/cgi/cgi.sh
location /cgi/ {
method GET POST;
root /cgi-files/;
index cgi.sh;
cgi_path /bin/bash;
# cgi_path /usr/bin/python3;
}
# sample_command: curl -v localhost:8080/delete/upload_file.txt -X DELETE
location /delete/ {
root /upload/;
method DELETE;
}
# sample_command: curl -i localhost:8080/redirect/
location /redirect/ {
return 301 https://42tokyo.jp;
}
}
server {
listen 8081 ;
server_name default_server;
root ./docs/www/;
# sample_command: curl --resolve default_server:8081:0.0.0.0 http://default_server:8081
location / {
index default.html;
}
}
server {
listen 8081 ;
server_name virtual_server;
root ./docs/www/;
# sample_command: curl --resolve virtual_server:8081:0.0.0.0 http://virtual_server:8081
location / {
index virtual.html;
}
}
ここでは3つのserver{}が定義されています。
ポート番号8080のサーバでは、
・/: デフォルトページ(index.html)へのアクセス
・/upload/ : ファイルのアップロード
・/index/ autoindex
・/cgi/ cgi動作
・/delete/ DELETE動作
・/redirect/ リダイレクト動作
を想定したlocationを用意しています。ポート番号8081のサーバでは、
・同じポート番号のサーバを管理できること
・ポート番号は同じだけど、server_name(ホスト名)が違うサーバを管理できること
を想定した設定です。
このConfigファイルを管理できるサーバが作れれば勝ちです。
たとえばこんな構造体でデータが管理できます。
struct Location
{
std::string name;
std::string root;
std::vector<std::string> methods; // GET POST DELETE
bool autoindex;
std::string index; // default file
std::string cgi_path; // execve(cgi_path, X, X)
std::string upload_path;
size_t max_body_size;
std::string redirect;
};
struct Server
{
std::vector<std::string> names;
std::string name;
std::string root;
std::vector<Location> locations;
std::string host;
size_t port;
};
2. ファイル構造(Configに必要な設置物)
・tree
.
├── Makefile
├── docs
│ ├── cgi-files
│ │ ├── cgi.py
│ │ └── cgi.sh
│ ├── conf
│ │ └── default.conf
│ ├── index.html
│ ├── upload
│ │ └── upload_file.txt
│ └── www
│ ├── default.html
│ └── virtual.html
├── includes
├── objs
├── srcs
└── webserv
この構成で最後まで実装します。
3. int main()の流れ
ざっくりいうとこれだけ。
int main(int argc, char **argv){
// Configを読む
// 複数のsocket()を生成
while(1){
// 同期I/Oの多重化
}
}
4. socket()の生成
[man listen]で調べると、socketを使う一連の流れが書かれています。
接続を受け付けるには、以下の処理が実行される。
1.
socket(2) でソケットを作成する。
2.
bind(2) を使ってソケットにローカルアドレスを割り当てて、 他のソケットがこのソケットに connect(2) できるようにする。
3.
listen() を使って、接続要求を受け付ける意志と接続要求を入れるキュー長を指定する。
4.
accept(2) を使って接続を受け付ける。
実装は、
[socket c++]で調べて、気に入ったタイトルを拾えばいいと思う。
default.confをread()した場合
・port8080のsocket = fd3
・port8081のsocket = fd4
が作られることを想定しています。
int main(int argc, char **argv){
// signalをセットする。
// Configを読む。
// ----- socket()の生成 -----
fd3 = socket(8080port)
fd4 = socket(8081port)
while(1){
// ----- 同期I/Oの多重化 -----
}
}
5. select()関数で、同期I/Oの多重化を実装
同期 I/O の多重化なるものを実装するために、select()関数を使います。
今読んでいるこのページは大人気ですから、この瞬間に100人同時にアクセスしているかもしれません。
そして、これを読めている自分は、ひょっとして100人の中から唯一、サーバからのデータ受信権を得た特別な存在なのでしょうか。
自分以外の99人は、読み込みマークぐるぐるで待機中でしょうか。
ページを下までスクロールして全部表示されたら、今度は2人目にデータが送信される仕組みでしょうか。
そうだったら、これは同期I/Oの多重じゃない通信といえるかもしれません。
同期 I/O の多重化は、複数回のaccess()とselect()で実装できます。
身近なところに、この仕組みは存在しています。銀行です。
webserv銀行の日常
来客者に受付番号を割り振る発券機socketがおかれています。
当webserv銀行では、2台の発券機fd3, 発券機fd4を設置しています。
// --- 1回目のselect() ---
あなたは銀行に入り、発券機fd3のスイッチを押しました。
発券機fd3はaccess()で受付番号fd5を作り、あなたに渡します。
お金をGETしたいあなたは、すでに必要書類を渡す準備ができており、受付に呼ばれるのをソファで待ちます。
// --- 2回目のselect() ---
銀行側の書類のrecv()準備ができたので、fd5のあなたを窓口が呼びました。あなたは書類を渡します。
お金の用意ができるまでお待ちください、とソファに戻らされました。
// --- 3回目のselect() ---
fd5のあなたを窓口が呼びました。お金のGET用意ができたようです。
あなたのもとに、無事にお金がsend()されたので、fd5はclose()されました。
レビュー条件では、select()の使い方に3つの指示があります。(自分はこう解釈した)
1. readfdsとwritefdsを同時に判定してください。
2. どのfdも、select()を通らないfdを使わないでください。
3. クライアントと、一通りのrecv()/send()する場合、selectを1回だけ通ってください。
上記銀行の導線は、課題のselect()条件を網羅した仕組みになっています。
さて、具体的な実装に入ります。
select()では、使い方のセオリーが存在しています。
select関数を用いた標準入力の監視【Linux / C言語】
セオリーを要すると、下記の4手順のループで構成されます。
while(1){
1. FD_ZEROで初期化
2. FD_SETで監視するfdを設定
3. select()
4. FD_ISSET + FD_CLRで、応答のあったfdを探知
}
// ----- ループ1週目 -----
readfdsと、writefdsを詰めようと思う。
port8080のsocketはfd3なので、FD_SET(3, readfds)を詰める。readfds = {3}
port8081のsocketはfd4なので、FD_SET(4, readfds)を詰める。readfds = {3, 4}
accept()してあるコネクションは、今はない。select()で待機
2.で待っている間に、クライアントがコンソールから
curl -i localhost:8080
コマンドを打つ。サーバ側で、port8080にアクセスがあったことをどう探知するか。
select()は変化したfdの個数である1を返す。
このとき、readfds = {3}だけ残る。反応がなかったfd4は削除されている。FD_ISSET(3, &readfds) がtrueになり、fd3にアクセスがあったことを知る。
FD_CLR(3, &readfds) でreadfdsからfdを削除しておく。
FD_ISSET(4, &readfds)がfalseになり、fd4にアクセスがなかったことを知る。socket(8080)はコネクションを作る必要がある。
accept(fd3)を実行して、コネクションfd5が生成される。ループ終了。readfds = {}になっている。
// ----- ループ2週目 -----
readfdsと、writefdsを詰める。
port8080のsocketはfd3なので、FD_SET(3, readfds)を詰める。readfds = {3}
port8081のsocketはfd4なので、FD_SET(4, readfds)を詰める。readfds = {3, 4}
fd5のコネクション生成されているので、FD_SET(5, readfds)を詰める。readfds = {3, 4, 5}select()は、コネクションの受信待ちを即座に探知。
変化したfdの個数である1を返す。
このとき、readfds = {5}だけ残る。反応がなかったfd{3, 4}は削除されている。FD_ISSET(5, &readfds) がtrueになり、fd5の準備ができたことを知る。
FD_CLR(5, &readfds) でreadfdsからfdを削除しておく。fd5について、recv()してリクエストメッセージを受け取り、解析する。
// ----- ループ3週目 -----
readfdsと、writefdsを詰める。
port8080のsocketはfd3なので、FD_SET(3, readfds)を詰める。readfds = {3}
port8081のsocketはfd4なので、FD_SET(4, readfds)を詰める。readfds = {3, 4}
fd5のコネクション生成されており、recv()が完了しているので、FD_SET(5, writefds)を詰める。readfds = {3, 4}, writefds = {5}select()は、コネクションの変化を即座に探知。
変化したfdの個数である1を返す。
このとき、writefds = {5}だけ残る。反応がなかったfd{3, 4}は削除されている。FD_ISSET(5, &writefds) がtrueになり、fd5の準備ができたことを知る。
FD_CLR(5, &writefds) でwritefdsからfdを削除しておく。fd5について、send()してレスポンスメッセージを送信する。
fd5とのやりとりがすべて完了したので、close(fd5)で削除。
int main(int argc, char **argv){
// signalをセットする。
// Configを読む。
// ----- socket()の生成 -----
fd3 = socket(8080port)
fd4 = socket(8081port)
while(1){
// ----- 同期I/Oの多重化 -----
// 0.
// FD_ZEROでreadfdsとwritefdsを初期化
// 1.
// readfdsにsocketFDをFD_SET
// readfdsにacceptFDをFD_SET
// writefdsにacceptFDをFD_SET
// 2.
select(maxFD, readfds, writefds, NULL, time_val); // timeは1secでいい
// 3.
// FD_ISSET(socketFD)なら、FD_CLRしてaccept()
// FD_ISSET(acceptFD)で、acceptFDがreadならFD_CLRしてrecv()
// FD_ISSET(acceptFD)で、acceptFDがwriteならFD_CLRしてsend()
}
}
以上で導線が整いました。これで、レビューとの致命的なズレは回避できると思います。
・レビューのあれこれ
レビュワーの判断がすべてですから、ただの個人の見解。
Q. chunked?
A. レビュー項目にその文字は出てきません。
Q. CGIどこまで?
A. cgiファイルが動作するか聞かれています。HelloWorldを出力するだけでも動作はしています。
Q, すべてのfdがselectを通るというが、厳密に?
A.
ファイルのアップロードをするときに、curl -Fオプションで、ファイル
をopenする機会があるかもしれません。
これはサーバとクライアントとのやり取りと関係ないですから、select()の対象にしなくていいかなと思います。
Q. autoindexってどう実装するの
A. htmlに<a>タブを、動的にごりごり書けばいいの?と思いますが、そうでした。階層情報も取得可能です。
この記事が気に入ったらサポートをしてみませんか?