[Lv5] Webservの入り口

0. はじめに

課題には書いてないのにレビューで要求されたり、実装するには負担だけどレビューで必須ではなかったりすることがあるので、実装の導線を整理します。

Q. 何をする課題?
A. c++でnginxを作る課題です。具体的には
1. 自作の./webserv を実行した後に
2. Chromeでlocalhost:8080にアクセスできるようにします。

自作サイトを表示する「仕組み」を作ります

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;
	}
}
  1. ここでは3つのserver{}が定義されています。

  2. ポート番号8080のサーバでは、
    ・/: デフォルトページ(index.html)へのアクセス
    ・/upload/ : ファイルのアップロード
    ・/index/ autoindex
    ・/cgi/ cgi動作
    ・/delete/ DELETE動作
    ・/redirect/ リダイレクト動作
    を想定したlocationを用意しています。

  3. ポート番号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) を使って接続を受け付ける。

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/listen.2.html

実装は、
[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週目 -----

  1. readfdsと、writefdsを詰めようと思う。
    port8080のsocketはfd3なので、FD_SET(3, readfds)を詰める。readfds = {3}
    port8081のsocketはfd4なので、FD_SET(4, readfds)を詰める。readfds = {3, 4}
    accept()してあるコネクションは、今はない。

  2. select()で待機

  3. 2.で待っている間に、クライアントがコンソールから
    curl -i localhost:8080
    コマンドを打つ。

  4. サーバ側で、port8080にアクセスがあったことをどう探知するか。

  5. select()は変化したfdの個数である1を返す。
    このとき、readfds = {3}だけ残る。反応がなかったfd4は削除されている。

  6. FD_ISSET(3, &readfds) がtrueになり、fd3にアクセスがあったことを知る。
    FD_CLR(3, &readfds) でreadfdsからfdを削除しておく。
    FD_ISSET(4, &readfds)がfalseになり、fd4にアクセスがなかったことを知る。

  7.  socket(8080)はコネクションを作る必要がある。
    accept(fd3)を実行して、コネクションfd5が生成される。

  8. ループ終了。readfds = {}になっている。

// ----- ループ2週目 -----

  1. 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}

  2. select()は、コネクションの受信待ちを即座に探知。
    変化したfdの個数である1を返す。
    このとき、readfds = {5}だけ残る。反応がなかったfd{3, 4}は削除されている。

  3. FD_ISSET(5, &readfds) がtrueになり、fd5の準備ができたことを知る。
    FD_CLR(5, &readfds) でreadfdsからfdを削除しておく。

  4.  fd5について、recv()してリクエストメッセージを受け取り、解析する。

// ----- ループ3週目 -----

  1. 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}

  2. select()は、コネクションの変化を即座に探知。
    変化したfdの個数である1を返す。
    このとき、writefds = {5}だけ残る。反応がなかったfd{3, 4}は削除されている。

  3. FD_ISSET(5, &writefds) がtrueになり、fd5の準備ができたことを知る。
    FD_CLR(5, &writefds) でwritefdsからfdを削除しておく。

  4.  fd5について、send()してレスポンスメッセージを送信する。

  5.  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>タブを、動的にごりごり書けばいいの?と思いますが、そうでした。階層情報も取得可能です。

この記事が気に入ったらサポートをしてみませんか?