HTTPサーバプログラムの作成

HTTPサーバはTCP/IP通信のサーバの一つです。HTTPサーバはホームページの用 サーバとなるだけでなく、インターネットにおける汎用的な情報発信サーバと して利用されつつあります。実際、HTTPサーバを組み込んだ情報家電製品など が登場してきていてます。インターネット上のすべてのコンピュータにHTTPサー バが組み込まれる時代も遠い未来ではないかもしれません。

ここでは、HTTPサーバプログラムの作成方法について解説します。取り上げる 例題は最も簡単なHTTPサーバであり、HTTPリクエストのメソッドのうち「GET」 にしか対応していません。また、重要なエラー処理以外は省略されています。 しかし、簡単な情報発信であれば十分に使えるものです。他のメソッドや例外 処理は読者の皆様にお任せすることにします。



#include <sys/fcntl.h>

#include <sys/socket.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <netdb.h>

#include <stdio.h>



#define HTTP_TCP_PORT 80

#define CR 13

#define LF 10



void httpd(int sockfd);

int send_msg(int fd, char *msg);



main() 

{

  int sockfd, new_sockfd;

  int writer_len;

  struct sockaddr_in reader_addr, writer_addr;

  bzero((char *) &reader_addr, sizeof(reader_addr));

  reader_addr.sin_family = AF_INET;

  reader_addr.sin_addr.s_addr = htonl(INADDR_ANY);

  reader_addr.sin_port = htons(HTTP_TCP_PORT);



  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {

    fprintf(stderr, "error: socket()\n");

    exit(1);

  }



  if (bind(sockfd, (struct sockaddr *)&reader_addr, sizeof(reader_addr)) < 0) {

    fprintf(stderr, "error: bind()\n");

    close(sockfd);

    exit(1);

  }



  if (listen(sockfd, 5) < 0) {

    fprintf(stderr, "error: listen()\n");

    close(sockfd);

    exit(1);

  }



  while(1) {

    if ((new_sockfd

	 = accept(sockfd,(struct sockaddr *)&writer_addr, &writer_len)) < 0) {

      fprintf(stderr, "error: accepting a socket.\n");

      break;

    }

    else {

      http(new_sockfd);

      close(new_sockfd);

    }

  }

  close(sockfd);

}



void http(int sockfd) 

{

  int len;

  int read_fd;

  char buf[1024];

  char meth_name[16];

  char uri_addr[256];

  char http_ver[64];

  char *uri_file;



  if (read(sockfd, buf, 1024) <= 0 ) {

    fprintf(stderr, "error: reading a request.\n");

  }

  else {

    sscanf(buf, "%s %s %s", meth_name, uri_addr, http_ver);

    if (strcmp(meth_name, "GET") != 0) {

      send_msg(sockfd, "501 Not Implemented");

    }

    else {

      uri_file = uri_addr+1;

      if ((read_fd = open(uri_file, O_RDONLY, 0666)) == -1) {

	send_msg(sockfd, "404 Not Found");

      }

      else {

	send_msg(sockfd, "HTTP/1.0 200 OK\r\n");

	send_msg(sockfd, "text/html\r\n");

	send_msg(sockfd, "\r\n");

	while((len = read(read_fd, buf, 1024)) > 0) {

	  if (write(sockfd, buf, len) != len) {

	    fprintf(stderr, "error: writing a response.\n");

	    break;

	  }

	}

	close(read_fd);

      }

    }

  }	

}



int send_msg(int fd, char *msg) {

  int len;

  len = strlen(msg);

  if ( write(fd, msg, len) != len ){

    fprintf(stderr, "error: writing.");

  }

  return len;

}


HTTPサーバプログラムの解説

一般のTCP通信サーバプログラムと大きな違いはありません。違いはTCP通信の ネットワーク接続された後は、ホームページアクセスのための通信プロトコル であるHTTPに従って、WebクライアントとHTTPサーバプログラムが通信をするこ とだけです。TCP通信プログラムについて知識のある人は、後述する関数 http()の解説だけを読めば十分です。

関数http()がHTTPの処理を行っている部分です。他の部分はTCPの通信接続を 処理する部部であり、これまでに見てきたTCP通信のサーバ側プログラムとソ ケット生成、通信接続と同じです。注意すべきことは一つだけで、それはHTTP サーバは通常80番を使うということです。このため、Webブラウザは特に指定 しない限りは、相手のHTTPサーバプログラムは80番の通信ポートで接続を待っ ていると仮定して、接続要求を送ります。80番以外の通信ポートを使っている HTTPサーバのホームページをアクセスするときはURLを次にように与える必 要があります。

http://www.some.where.co.jp:8080/index.html

これはwww.some.where.co.jpというアドレスのコンピュータ上で動いている HTTPサーバに通信ポート8080番を通してネットワーク接続し、ファイル index.htmlの内容を返送してもらうことになります。

次にHTTPサーバプログラムを詳細に見ていきます。

コラム:IPv6

【作成中】現行のIPアドレスが不足していること。新しいIPアドレス方式とし てIPv6が提案されていること。普及には時間がかかることを書く。

HTTP処理

accept()システムコールによりネットワーク接続が完了するといよいよWebブ ラウザとHTTPサーバの間で通信ができるようになり、関数http()が呼び出され ます。関数http()はソケットからGETメソッドからなるHTTPリクエストとして を受け取ると、ファイルを読み出し、その中身とともにHTTPレスポンスを作り、 そのソケットを通じて送り返します。関数http()はwhileによる無限ループと して実行されており、このHTTPサーバプログラムはこの動作を永遠に繰り返す ことになります。


Webブラウザへのソケット

HTTPサーバプログラムで、HTTPの処理を行っているのは関数http()の部分です。 関数の引数sockfdはWebクライアントとの間の仮想通信路のソケット番号です。 sockfdにread()やwrite()で読み出しや書き込みを行えば、Webクライアントか らの受信や送信ができることになります。


  char buf[1024];

  read(sockfd, buf, 1024);

例えば、sockfdをread()システムコールで読み出せばWebクライアントが送信 したデータを受信することができます。この例では、Webクライアントが送信 したデータを最大1024バイト分だけ読み出してをbufに格納します。


  int len;

  char *buf = "HTTP/1.0 200 Document follows\r\n";

  len = strlen(buf);

  write(sockfd, buf, len);

また、sockfdにwrite()システムコールでデータを書き出せば Webクライアントにそのデータを送信できます。この例では、 Webクライアントにbufに入っている文字列"HTTP/1.0 200 Document follows\r\n"を 送ります。

HTTPリクエストの受信と解釈

HTTPの通信手順では、HTTPサーバは常にWebブラウザからのHTTPリクエストを受 け取ると、処理をしてその返答をHTTPレスポンスとして返答します。そこで、 HTTPサーバプログラムはまずはWebブラウザからのHTTPリクエストをソケット sockfdに対してread()システムコールを実行してHTTPリクエストを受信します。



  if (read(sockfd, buf, 1024) <= 0 ) {

    fprintf(stderr, "error: reading a request.\n");

  }


read()は失敗した場合は-1が返値となり、成功した場合は受信したデータサイ ズが返値になります。例えば、HTTPリクエストが300バイトならread()の返値 は300となります。そこで、if文を使ってread()の返値を調べてread()の返値 が0以下ならばHTTPリクエストが空か失敗のどちらかと判定し、ソケットを閉 じることにします。ただし、このプログラムは簡単化のためHTTPリクエストの 1024バイト分しか受信しません。しかし、HTTPが1024バイト以上になる可能性 は少ないので、実験的なHTTPサーバならこれでもよいでしょう。

HTTPリクエストの受信に成功したらHTTPリクエストを解釈します。HTTPリクエ ストはWebブラウザからHTTPサーバに送られる要求のことです。その中身は1行 目のリクエスト行と2行目以降の付加情報に分かれています。リクエスト行は 次のように与えられます。

メソッド名 オブジェクトのURI HTTPのバージョン番号

「メソッド名」、「URI」、 「HTTPバージョン番号」の間はスペース(空白)で 区切られます。メソッドがGETならばオブジェクト(情報)の返送要求になり ます。URIはその返送要求されたオブジェクトの名前、HTTPバージョン番号は Webブラウザが従うHTTPのバージョン番号でHTTP/1.0やHTTP/1.1などが入りま す。例えばリクエスト行が

GET /index.html HTTP/1.0

であれば、このHTTPサーバのオブジェクト(情報)の格納用 ディレクトリにあるindex.htmlというファイルの中身を 返送する要求となります。



  else {

    sscanf(buf, "%s %s %s", meth_name, uri_addr, http_ver);

    if (strcmp(meth_name, "GET") != 0) {

      send_msg(sockfd, "501 Not Implemented");

    }


read()で読み出したHTTPリクエストはbufに格納されています。 HTTPの規則より、リクエスト行の各項目(メソッド名、URI、 HTTPバージョン番号)はスペースで区切られています。 そこで、sscanf()を使って項目を取り出します。sscanf()はscanf()が キーボードなどの標準入力からデータを取り出すのにたいし、 sscanf()は文字列からデータを取り出します。 bufにはリクエスト行が入ることから、 リクエスト行が「GET /index.html HTTP/1.0」となるとき、 sscanf(buf, "%s %s %s", meth_name, uri_addr, http_ver)の meth_name、uri_addr、http_verは以下のようになります。

変数名内容(文字列)
meth_name"GET"
uri_addr"/index.html"
http_ver"HTTP/1.0"

リクエスト行の項目が分けられたら、まずメソッド名が"GET"であるか を調べます。



    if (strcmp(meth_name, "GET") != 0) {

      send_msg(sockfd, "501 Not Implemented");

    }


GETメソッドとなっているか、つまりmeth_nameが"GET"となっているかの判定 には、文字列の比較関数strcmp(char *s1, char *s2)を用います。なお、この HTTPサーバはHTTPリクエストのメソッドのうち「GET」にしか対応していません。 それ以外のメソッドの場合は、未実装のメソッドである旨をHTTPレスポンスと してWebクライアントに送り、ソケットを閉じます。HTTPレスポンスの送信は、 ここ以外にも数カ所必要になります。そこで、HTTPレスポンス用の送信関数で あるsend_msg()を作ることにします。



  if (read(sockfd, buf, 1024) <= 0 ) {

    fprintf(stderr, "error: reading a request.\n");

  }

  else {

    if (strcmp(meth_name, "GET") != 0) {

      send_msg(sockfd, "501 Not Implemented");

    }


send_msg()については後述します。

ファイルの読み込み

HTTPリクエストがGETならば返送要求のあったオブジェクト、つまりファイル を送り返す必要がありますが、それにはまず、ファイルを読み出さなければい けません。



      uri_file = uri_addr+1;

      if ((read_fd = open(uri_file, O_RDONLY, 0666)) == -1) {

	send_msg(sockfd, "404 Not Found");

      }


読み出すべきファイルの名前はオブジェクトのURIを格納したuri_addrに入っ ています。例えば、「http://www.some.where.ac.jp/index.html」というホー ムページアクセスならば、Webブラウザが送ったHTTPリクエストは「GET /index.html HTTP/1.0」となり、uri_addrには"/index.html"が入ることにな ります。また、「http://www.some.where.ac.jp/image/button.gif」ならば HTTPリクエストは「GET /image/button.gif HTTP/1.0」となり、uri_addrには "/image/button.gif"が入ります。

ここで一つ問題があります。"/index.html"や"/image/button.gif"などのファ イルはどこにあるのかということです。多くのOSでは"/"から始まるファイ ルやディレクトリはルートディレクトリの下にファイルやディレクトリととい うことになります。しかし、これらのindex.htmlやimage/button.gifは本来、 ホームページデータの格納してあるディレクトリの下にあるファイルです。こ のため、その名前を変更する必要があり、このHTTPサーバプログラムでは、こ のサーバプログラムを実行したときのディレクトリにあるようにします。そこ で、先頭の"/"を取ることにします。

返送すべきファイルの名前が決まったら、そのファイルを読み込み専用のファ イルとして開きます。これにはopen()システムコールを利用します。



      uri_file = uri_addr+1;

      if ((read_fd = open(uri_file, O_RDONLY, 0666)) == -1) {

	send_msg(sockfd, "404 Not Found");

      }


開くモード、つまり、読み込み専用、書き込み専用、読み書き両用などをして します。この場合、読み込み専用で開けばいいので、"O_RDONLY"と指定します。 第三引数はアクセス権です。これはUNIXのファイルアクセス権番号と同じもの ですが、ここでは0666と指定しておけばよいでしょう。 open()の返値は開い たファイルのファイル記述子となります。また、ファイルを開らくのに失敗し た場合は-1を返します。失敗した場合はWebブラウザに失敗した旨を send_msg()を使って伝えます。このときのメッセージにはHTTPレスポンスのス テータスコードを先頭に入れておきます。

HTTPレスポンスの送信

send_msg()はHTTPレスポンスを行う関数です。 send_msg()の第一引数はint型 整数ですが、ここにはファイル記述子またはソケット番号が入ります。そして 第二引数には文字列へのポインタを与えます。もし、ファイル記述子であれば そのファイルへメッセージを書き込みますし、ソケット番号であればその通信 ソケットの先へのメッセージを送信します。



int send_msg(int fd, char *msg)

{

  int len;

  len = strlen(msg);

  if ( write(fd, msg, len) != len ){

    fprintf(stderr, "error: writing.");

  }

  return len;

}


write()システムコールの第二引数には書き込むまたは送信するデータへのポ インタを与えますが、どれだけのデータを書き込むまたは送信するかを教えて やる必要があります。そこで、第三引数でそのデータのサイズを与えます。こ こではmsgが文字列であるのでstrlen()関数により文字列の長さを調べ、その 長さ分をデータサイズとします。

HTTPレスポンスの作成



	send_msg(sockfd, "HTTP/1.0 200 OK\r\n");

	send_msg(sockfd, "text/html\r\n");

	send_msg(sockfd, "\r\n");


HTTPレスポンスの中身を与える部分です。送信は、send_msg()をWWWクライア ントへのソケット番号とそのレスポンス内容を引数として与えて呼び出すこと によって実現します。なお、"HTTP/1.0 200 OK\r\n"のHTTP/1.0はHTTPレスポ ンスがHTTPバージョン1.0をもとにして作成されたことを、200はHTTPリクエス トに従いオブジェクト(ファイル)の返送できること、OKは成功したことをし めす説明です。OKの部分はHTTPサーバは無視します。また、"text/html\r\n"は 返送されるファイルがテキストファイルでデータの格納形式がHTMLであること を示します。ところで、"\r\n"という文字列がどのメッセージにも着いていま す。これはHTTPレスポンスにおける改行を示すものです。ただし、HTTPレスポ ンスと返送するオブジェクト(ファイル)の間には"\r\n"を入れる必要があり ます。これはHTTPレスポンスとオブジェクト(ファイル)の区切りが空行つま り、改行だけを送ります。

オブジェクトの返送

HTTPレスポンスのあとに改行("\r\n")を送ったら、返送するオブジェクト(ファ イル)を送ります。すでに返送するファイルは開いていますから、そのファイ ルへのファイル記述子をread()に与えてデータを読み出し、読み出したデータ をWebブラウザへのソケットにwrite()で書き出します。これにより、ファイル の内容がWebブラウザに送られます。



	while((len = read(read_fd, buf, 1024)) > 0) {

	  if (write(sockfd, buf, len) != len) {

	    fprintf(stderr, "error: writing a response.\n");

	    break;

	  }

	}

ファイル内容をすべてWebブラウザに送信したら、ソケットsockfdによる 通信接続を閉じます。