忍者ブログ

Home > hello httpd

hello httpd Archive

[PR]

  • 2024-11-21 (Thu)
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。

  • Comments (Close):
  • TrackBack (Close):

Hello, httpd! C言語でWebServerを作ってみよう [第1回目]

さて、久々の勝手に連載企画を始めてみます。
(ホントは先週に始まってる予定だったんですが…忙しすぎました)

お題は最近興味津々のWebサーバの実装です。
特にリリースとかは目指しませんが、
この企画を通してApacheとかlightyのソースを読める力をつけたいと思ってます。

それでは第1回目startです!
今回は最初なので雛形となる超シンプルなサーバを作ります。
今後、このソースを拡張していくことになります。

ということで、完成品がこちら↓
※ ようやく始めたGitHubに置いてます

hello.c

make
./hello 8080

で起動できます。

今回実装したのは、「クライアント(同時接続1)に対してhtmlファイルを応答するだけのサーバ」です。
特に難しいことはやってませんが、一応中身を説明しときます。

ソース解説

     1	#include <stdio.h>
     2	#include <stdlib.h>
     3	#include <string.h>
     4	#include <netinet/in.h>
     5	#include <fcntl.h>
     6	#include <errno.h>
     7	
     8	#define DOCUMENT_ROOT "./htdocs"
     9	#define BUF_SIZE 1024
    10	
    11	#define DEBUG

冒頭ではヘッダファイルの読み込みと、
ドキュメントルート、バッファサイズ、デバッグモードの指定をしています。

    13	int main(int argc, char *argv[]) {
    14	
    15		struct sockaddr_in server;
    16		int sockfd;
    17		int port = (argc == 2) ? atoi(argv[1]) : 80;
    18	
    19		// socket作成
    20		if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
    21			perror("socket");
    22			exit(EXIT_FAILURE);
    23		}

ここからmain関数に入ります。
まず、変数の定義とポートの指定(※)をしています。
※ コマンドライン引数がなければ80番を使用
次にTCPのsocketを作成しています。

    25		// Port, IPアドレスを設定(IPv4)
    26		memset(&server, 0, sizeof(server));
    27		server.sin_family = AF_INET;	
    28		server.sin_addr.s_addr = INADDR_ANY;
    29		server.sin_port = htons(port);
    30		
    31		// socketをすぐ再利用できるように
    32		char opt = 1;
    33		setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt));
    34	
    35		// ディスクリプタとPortを結び付ける
    36		if (bind(sockfd, (struct sockaddr *) &server, sizeof(server)) < 0) {
    37			perror("bind");
    38			exit(EXIT_FAILURE);
    39		}

お次は、作成したsocketをportと結び付けます。(bind)

一度clientから接続されると、プログラムを終了した後も一定時間bindできなくなるので、
setsockoptですぐ再利用できるように指定しています。
(が、効いてないようです。なんでだろ?macのせい…?)

    41		// listen準備
    42		if (listen(sockfd, SOMAXCONN) < 0) {
    43			perror("listen");
    44			exit(EXIT_FAILURE);
    45		}

portをlisten可能な状態にします。
listen()の第2引数は接続待ちできる数です。
SOMAXCONNはデフォルト値で、環境毎に異なります。

さて、これで準備は完了です。

    47		// メインループ
    48		while (1) {
    49			struct sockaddr_in client;
    50			int newfd;
    51	
    52			char buf[BUF_SIZE] = "";
    53			char method[BUF_SIZE] = "";
    54			char url[BUF_SIZE] = "";
    55			char protocol[BUF_SIZE] = "";
    56			int len;
    57	
    58			int filefd;
    59			char file[BUF_SIZE] = "";

いよいよメインループです。
クライアントからの処理を無限ループで処理します。
変数の説明は割愛します。

    61			// clientからの接続を受け付ける
    62			memset(&client, 0, sizeof(client));
    63			len = sizeof(client);
    64			if ((newfd = accept(sockfd, (struct sockaddr *) &client, &len)) < 0) {
    65				perror("accept");
    66				exit(EXIT_FAILURE);
    67			}

clientからの接続を受け付けます。
以降の処理はacceptが返したディスクリプタnewfdに対して行います。

    69			// request line読み込み
    70			if (recv(newfd, buf, sizeof(buf), 0) < 0) {
    71				perror("recv");
    72				exit(EXIT_FAILURE);
    73			}
    74			sscanf(buf, "%s %s %s", method, url, protocol);
    75	
    76			// request headerの終わりまで読み飛ばし
    77			// bodyは無視		
    78			do {
    79				if (strstr(buf, "\r\n\r\n")) {
    80					break;
    81				}
    82				if (strlen(buf) >= sizeof(buf)) {
    83					memset(&buf, 0, sizeof(buf));
    84				}
    85			} while (recv(newfd, buf+strlen(buf), sizeof(buf) - strlen(buf), 0) > 0);

clientからのリクエストを読み込みます。
request lineは「GET / HTTP/1.0」になってるはずなので、ssanfします。

すぐ応答を返してもおそらく処理してくれますが、
一応ヘッダの終わり(\r\n\r\n)まで読み飛ばします。
※ 1024バイトの倍数付近に\r\n\r\nが来るとダメです。
 あと、bodyがあることは想定してません。

    87			// ファイルパス生成(脆弱性有り)		
    88			sprintf(file, DOCUMENT_ROOT);
    89			strcat(file, url);		
    90
    91			// index.html補完
    92			if( file[strlen(file)-1] == '/' ) {
    93				strcat(file, "index.html" );
    94			}

「ドキュメントルート+リクエストファイル名」でファイルパスを作成してます。
末尾が「/」ならindex.htmlを追加します。

    96			// debug
    97			#ifdef DEBUG
    98				sleep(1);
    99			#endif

デバッグ用に1秒sleepさせます。
すぐ応答されると、同時接続数が増えても違いがわかりづらいので…

   101			// (最低限の)header送信
   102			char *header = "HTTP/1.0 200 OK\n"
   103									"Content-type: text/html\n"
   104									"\n";
   105			send(newfd, header, strlen(header), 0);
   106	
   107			//	body送信
   108			if ((filefd = open(file, O_RDONLY)) < 0) {
   109				perror("open");
   110				fprintf(stderr, "file: %s\n", file);
   111			}
   112			else {
   113				while ((len = read(filefd, buf, sizeof(buf))) > 0) {
   114					if (send(newfd, buf, len, 0) < 0) {
   115						perror("send2");
   116					}
   117				}
   118			}
   119	
   120			close(newfd);

最後にヘッダとファイルの中身を送信して、 ディスクリプタをcloseします。

これで1クライアントの処理が終わり、
ループ先頭に戻って次のクライアントを処理します。

まとめ

超適当な説明でしたが、今回のソースはこんな感じです。

C言語は大学でやった程度なので、
たったこれだけのプログラムを作るのにもかなり苦労しました^^

問題点だらけだと思うので、突っ込みいただけると幸いです。
ここどういう意味?とかも大歓迎です。

あ、脆弱性だらけなので、くれぐれも公開サーバ上で起動しないで下さい。
/etc/passwdとか見られちゃいますよ…


というところで、今回は終了です。
今後は、

  • マルチクライアント
  • CGI実行
  • html以外のファイル応答

あたりの機能を追加していきます。
乞うご期待・・・!?

PR

Home > hello httpd

Search
Loading
Feeds
Links
スポンサードリンク

Page Top