【C言語】簡単な「HTTPサーバー」を作る

HTTPサーバーの作り方の解説ページアイキャッチ

このページにはプロモーションが含まれています

今回は下記ページで解説したソケット通信を用いて、簡単な「HTTP サーバープログラム」を作っていきたいと思います。

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

使用する関数はほぼ上記のページで紹介したもののみです。

作成する HTTP サーバーはあくまでも簡易版です。かなり機能を制限しています。

ですが、この作成した HTTP サーバーを使用することで、Google Chrome などのウェブブラウザと実際に通信したり、HTTP サーバーから送信したデータをブラウザ上に表示することもでき、作ってみると割と達成感があると思います!

MEMO

最後に紹介するプログラムは MacOSX で作成・動作確認したものになりますが、おそらく Linux でも動作するのではないかと思います

Windows の場合はインクルードするファイルが違ったりするので注意が必要ですが、各関数の名前や実行するタイミングなどはほぼ同じになると思います

HTTP とは

まずは HTTP について説明しておきます。

Wikipedia では下記のように HTTP のことを説明されています。

Hypertext Transfer Protocol(ハイパーテキスト・トランスファー・プロトコル、略称 HTTP)とは、WebブラウザがWebサーバと通信する際に主として使用する通信プロトコルであり、インターネット・プロトコル・スイートのメンバである。HTMLなどのテキストによって記述されたWebページ等のコンテンツの送受信に用いられる。

引用元:Wikipedia

要は、ウェブページを表示するときに使用される「ウェブブラウザとウェブサーバー間の通信プロトコル」です。

HTTPの説明図

HTML だけでなく、ウェブページを構成するデータの送受信にも利用されます。例えばウェブページ上の画像もこの HTTP により送受信されます。

HTTPで送受信されるデータ

リクエストとレスポンス

HTTP においては、サーバーとクライアント間でやり取りするデータは「HTTP メッセージ」になります。

この「HTTP メッセージ」は次の2つに分類されます。

  • リクエストメッセージ
  • レスポンスメッセージ

リクエストはまさに「要求」で、クライアントから HTTP サーバーに何かの処理をお願いすることを言います。

そして、このリクエストをする時にクライアントから HTTP サーバーに送るデータが「リクエストメッセージ」です。

HTTPにおけるリクエストを表す図

HTTP でのデータのやりとりの発端はこのクライアントからのリクエストになります。

なので、サーバーはクライアントと接続後はリクエストが来ること(リクエストメッセージが送信されてくること)を待つことになります。

そして、クライアントからリクエストがあった時に、サーバーがクライアントに対して行うのがレスポンスです。

レスポンスはまさに「応答」で、クライアントからリクエストに対するサーバーの処理結果を返すことを言います。

そして、このレスポンスをする時に HTTP サーバーからクライアントに送るデータが「レスポンスメッセージ」です。

HTTPにおけるレスポンスを表す図

HTTP は、このリクエストとレスポンスを下記のように繰り返すプロトコルになります。

  • クライアントがサーバーに「リクエストメッセージ」を送る
  • サーバーがそのリクエストメッセージに対して「レスポンスメッセージ」を送る

HTTPでのリクエストとレスポンスの順番

例えばウェブページを表示するためには、HTML だけでなく画像やスタイルシート、スクリプトなどのデータも必要になります。

なので、ウェブページを1つ表示するためだけにも、表示に必要なデータを取得するためにクライアントは上記のリクエストを繰り返し実行し、サーバーもそのリクエストに対してレスポンスを返す必要があります。

HTTP のバージョン等によっては、リクエストからレスポンスの流れを一回行うたびに接続からやり直したりする場合や、一回の接続で上記を必要回数繰り返すような場合もあります。

スポンサーリンク

リクエストメッセージ

続いては、このリクエストメッセージが具体的にどのようなデータなのかを見ていきたいと思います。

リクエストメッセージは下記の3つから構成されます。

  • リクエスト
  • ヘッダーフィールド
  • ボディ

リクエストメッセージの1行目はリクエストそのもので、2行目以降はヘッダーフィールドとなります。

リクエストの種類によってはヘッダーフィールドに続いて「空行」を挟んでボディを含めたデータとなる場合があります。

リクエストメッセージのデータ構成

リクエスト

リクエストは「サーバーに対して要求する」ことを示す文字列になります。

例えば下記はリクエストの一例になります。

GET /index.html HTTP/1.1

このリクエストでは、サーバーに対して「/index.html」を「取得する(GET)」ことを要求しています。

さらにこのデータのやりとりを「HTTP の 1.1 バージョン」のプロトコルに則って行うことを要求しています。

メソッド

このリクエストにおいて GET は「メソッド」と呼ばれます。

メソッドは要求の種類を表します。要はサーバーに対して「どんな要求をしたいか」を表す情報になります。

例えば GET は、サーバに対してデータの取得を要求するメソッドになります。

GET はウェブページを表示するためのデータを取得する際に使用されるメソッドで、HTTP において一番利用されるメソッドになります。

他の分かりやすいメソッドには POST があります。

これは GET とは逆に、サーバーに対してデータの送信を要求するメソッドで、例えば掲示板やウェブページにコメントを書き込む時などに使用されるメソッドです。

他にどんなメソッドが存在するかは下記の Wikipedia などで参照することができます。

https://ja.wikipedia.org/wiki/Hypertext_Transfer_Protocol#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

リクエストターゲット

上記のリクエスト例における /index.html はリクエストターゲットと呼ばれます。

リクエストはサーバーに送るものですが、そのサーバー上には様々なウェブページ、さらには様々なデータが存在します。

そういった様々なデータから、具体的にどのデータに対して要求をするのかを指定するのがこのリクエストターゲットです。

より具体的には URL のパス部分と考えて良いです。

http://daeudaeu.com/index.html

メソッドが GET の場合は、このリクエストターゲットに対応するデータの取得を要求することになります。

これにより、クライアントはウェブページを表示するための HTML やページに表示する画像などを取得することが出来ます。

バージョン

上記のリクエスト例における HTTP/1.1 は HTTP のバージョンを示しています。この例では HTTP/1.1 のバージョンで HTTP 通信を行うことを指定していることになります。

HTTP プロトコル自体もどんどんバージョンアップされて改良されており、さまざまなバージョンの HTTP が存在します。

このバージョンによって、データのやりとりの仕方などが異なるので、どのバージョンの HTTP で通信するかをリクエスト時に設定します。

スポンサーリンク

ヘッダーフィールド

ヘッダーフィールドはリクエストに対して「追加情報」を指定するデータになります。

ヘッダーフィールドは下記のような形式の文字列になります。

フィールド名: 内容

どのようなフィールド名が存在するかは下記の Wikipedia 等で参照できます。

https://ja.wikipedia.org/wiki/Hypertext_Transfer_Protocol#HTTP%E3%83%98%E3%83%83%E3%83%80%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89

例えば下記のヘッダーフィールドの行はフィールド名を Accept-Language で内容を ja とした場合の例になります。

Accept-Language: ja

この場合、サーバーに「受け入れ可能な言語は日本語」だという追加の情報をリクエスト時に送信することになります。

これを受け取ったサーバーは、リクエストされたページに複数の言語用のデータが存在するのであれば、その中から日本語のものをレスポンスとしてクライアントに送信します。

こんな感じでヘッダーフィールドを付加することでリクエストの内容をより詳細に指定するようなことが可能になります。

スポンサーリンク

ボディ

ボディはサーバーに送信するデータ本体のことです。

例えばメソッドが POST の場合は、サーバーにデータ本体を送信する要求になりますので、リクエストと一緒にデータ本体を送信する必要があります。

このデータ本体は、例えばコメントの書き込み時であれば「コメント本文」、例えば画像のアップロード時であれば「画像データ」になると思います。

レスポンスメッセージ

続いてレスポンスメッセージについて解説していきます。

レスポンスメッセージはクライアントから送信されたリクエストメッセージに対する応答であり、下記のようなデータから構成されます。

  • ステータス
  • ヘッダー
  • ボディ

レスポンスメッセージのデータ構成

ステータス

レスポンスメッセージの1行目は「ステータス」になります。

下記はステータス行の一例になります。

HTTP/1.1 200 OK

バージョン

このステータス行における HTTP/1.1 の部分は HTTP のバージョンになります。

ステータスコード

また、200 の部分はステータスコードになります。ステータスコードとは、要はクライアントからのリクエストの処理結果を数字で表したものです。

200 は OK を表し、リクエストに対して正常に処理できたことを表すステータスコードになります。

例えば下記のリクエストであれば、

GET /index.html HTTP/1.1

ステータスコード 200/index.html のデータをクライアントに送信できることを表すコードになります。

ただし、リクエストに対して正常に処理できないような場合があります。このような場合には、その理由などをステータスコードに設定してレスポンスが行われます。

例えば要求されたリクエストターゲットが存在しない場合は、ステータスコードを 404 (Not Found) にしてレスポンスしたり、クライアントに要求されたリクエストターゲットへのアクセス権が存在しない場合は 403 (Forbidden) にしてレスポンスしたりします。

どんなステータスコードが存在するかは Wikipedia 等から参照することが出来ます。

https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89

メッセージ

OK の部分はメッセージになります。

ステータスコードを文字列として表したものと考えて良いと思います。

例えばステータスコードが 200 の場合はメッセージが OK になりますが、404 の場合はメッセージが Not Found403 の場合はメッセージが Forbidden になります。

スポンサーリンク

ヘッダーフィールド

これはリクエストとほぼ同じ意味合いなので説明は省略します。

要はレスポンスに対して追加情報を付加するデータになります。

スポンサーリンク

ボディ

ボディもリクエストと同じです。

ただ HTTP で一番利用されるリクエストのメソッドは GET ですので、多くのケースでレスポンスメッセージにボディが付加された状態でレスポンスが行われることになります。

HTTP サーバーの作り方

では HTTP サーバーはどのように作成すれば良いのかについて解説していきたいと思います。

ざっくりいうと、下記のような処理を行うプログラムを作成すれば良いです。

  1. クライアントと接続
  2. リクエストメッセージを受信
  3. リクエストメッセージを解析
  4. 解析結果に基づいて処理
  5. レスポンスメッセージを作成
  6. レスポンスメッセージを送信
  7. 2. 〜 8. を繰り返して一通りのデータのやり取りが終わったら接続を閉じて 1. に戻る

スポンサーリンク

クライアントと接続

まずはリクエストメッセージを送信してくるクライアントと接続を行う必要があります。

HTTP通信のためにサーバーとクライアントで接続する様子

これはクライアントサーバー間で単純に接続すれば良いだけで、下記で解説している手順で接続を行えば良いです。

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

リクエストメッセージを受信

クライアントとの接続後はクライアントからのリクエストメッセージを受信します。

クライアントからリクエストメッセージを受信する様子

要は、ソケット通信の recv 関数を実行してクライントから送信されてくるデータを受信します。

リクエストメッセージを解析

続いて受信したリクエストメッセージから、どのようなリクエストであるかを解析します。

リクエストメッセージを解析する様子

リクエストメッセージで解説したように、クライアントからは「どのようなリクエストであるか」をリクエストメッセージのメソッドやリクエストターゲット、ヘッダーフィールドなどによって指定されます。

ただし、サーバーが受信するのは単なる文字列ですので、その文字列からリクエストのメソッドやリクエストターゲット、ヘッダーフィールドの内容がどのようなものであるかを解析する必要があります。

スポンサーリンク

解析結果に基づいて処理

そして、解析した結果(メソッドやリクエストターゲットなど)に応じてサーバーが処理を行います。

この処理結果が「ステータスコード」になります。

例えばリクエストのメソッドが GET なのであれば、リクエストに応じるためにはリクエストターゲットに対応するデータを用意します。

リクエストに応じた処理を実行する様子

ただし、クライアントからのリクエストに応じた処理が実行できないような場合もあります。

例えば GET の場合は、そのリクエストターゲットに対応するデータが存在しない場合はそのデータは用意できないですよね。より具体的にいうと、存在しないページを表示するためのデータをリクエストされたような場合です。

このように処理ができないような場合には、その処理できない理由に応じてステータスコードを設定します。

また、しっかりした HTTP サーバーを作成するのであれば、メソッドやヘッダーフィールドの設定それぞれに応じて実行する処理を切り替える必要があります。

ただし、全てのメソッドやフィールドに対応するのは大変なので、このページで紹介するサンプルではメソッドは GET のみを受け付け、フィールドは無視するようにしています。

レスポンスメッセージを作成

続いてクライアントにレスポンスするためにレスポンスメッセージを作成します。

作成するレスポンスメッセージの中身の詳細はレスポンスメッセージで紹介した通りです。

レスポンスメッセージを作成する様子

このレスポンスメッセージは解析結果に基づいて処理の結果に基づいて作成します。

例えば、レスポンスのステータスコードは処理結果に基づいて、処理が正常に終了したら 200 を、それ以外は 200 以外の値を設定します。

またボディには処理を実行して作成・用意したデータをコピーします。

ヘッダーフィールドの Content-Length にはそのデータのサイズを設定します。

こんな感じで、リクエストに対する処理結果に応じてレスポンスメッセージを作成します。

レスポンスメッセージを送信

続いて作成したレスポンスメッセージを send 関数でクライアントに送信します。

レスポンスメッセージをクライアントに送信する様子

これで1つのリクエストに対する処理の一連の流れが実行されることになります。

送信したデータやクライアントによっては、続けて次のリクエストを送信してくる場合がありますので、HTTP サーバーはこのリクエストに対してここまで解説したきた処理を同様にして行います。

スポンサーリンク

HTTP サーバーのサンプル

ではここまで解説してきた内容を踏まえて作成した HTTP サーバーのサンプルプログラムを紹介していきたいと思います。

今回作成するのはあくまでも「簡単な HTTP サーバー」であり、主に下記のような制限を持った HTTP サーバーになります。

  • 受け付けるリクエストのメソッドは GET のみ
    • GET 以外はステータスコード 404 でレスポンスする
    • リクエストターゲットに対応するファイルが存在しない場合もステータスコード 404 でレスポンス
  • リクエストのヘッダーフィールドは全て無視
  • レスポンスのヘッダーフィールドは Contet-Length のみ
  • リクエストメッセージとレスポンスメッセージのサイズは 5 KB 以内とする
  • HTTP のバージョンは 1.1

ソースコード

HTTP サーバーのサンプルプログラムのソースコードは下記のようになります。

server.c
#include <sys/socket.h>
#include <netinet/in.h>	
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 8080
#define SIZE (5*1024)

int httpServer(int);
int recvRequestMessage(int, char*, unsigned int);
int parseRequestMessage(char*, char*, char*);
int getProcessing(char*, char*);
int createResponseMessage(char*, int, char*, char*, unsigned int);
int sendResponseMessage(int, char*, unsigned int);
unsigned int getFileSize(const char*);

/* ファイルサイズを取得する */
unsigned int getFileSize(const char *path) {
    int size, read_size;
    char read_buf[SIZE];
    FILE *f;

    f = fopen(path, "rb");
    if (f == NULL) {
        return 0;
    }

    size = 0;
    do {
        read_size = fread(read_buf, 1, SIZE, f);
        size += read_size;
    } while(read_size != 0);

    fclose(f);

    return size;
}

/*
 * リクエストメッセージを受信する
 * sock:接続済のソケット
 * request_message:リクエストメッセージを格納するバッファへのアドレス
 * buf_size:そのバッファのサイズ
 * 戻り値:受信したデータサイズ(バイト長)
 */
int recvRequestMessage(int sock, char *request_message, unsigned int buf_size) {
    int recv_size;
    
    recv_size = recv(sock, request_message, buf_size, 0);

    return recv_size;
}

/*
 * リクエストメッセージを解析する(今回はリクエスト行のみ)
 * method:メソッドを格納するバッファへのアドレス
 * target:リクエストターゲットを格納するバッファへのアドレス
 * request_message:解析するリクエストメッセージが格納されたバッファへのアドレス
 * 戻り値:成功時は0、失敗時は-1
 */
int parseRequestMessage(char *method, char *target, char *request_message) {

    char *line;
    char *tmp_method;
    char *tmp_target;
    
    /* リクエストメッセージの1行目のみ取得 */
    line = strtok(request_message, "\n");

    /* " "までの文字列を取得しmethodにコピー */
    tmp_method = strtok(line, " ");
    if (tmp_method == NULL) {
        printf("get method error\n");
        return -1;
    }
    strcpy(method, tmp_method);

    /* 次の" "までの文字列を取得しtargetにコピー */
    tmp_target = strtok(NULL, " ");
    if (tmp_target == NULL) {
        printf("get target error\n");
        return -1;
    }
    strcpy(target, tmp_target);

    return 0;
}

/*
 * リクエストに対する処理を行う(今回はGETのみ)
 * body:ボディを格納するバッファへのアドレス
 * file_path:リクエストターゲットに対応するファイルへのパス
 * 戻り値:ステータスコード(ファイルがない場合は404)
 */
int getProcessing(char *body, char *file_path) {

    FILE *f;
    int file_size;

    /* ファイルサイズを取得 */
    file_size = getFileSize(file_path);
    if (file_size == 0) {
        /* ファイルサイズが0やファイルが存在しない場合は404を返す */
        printf("getFileSize error\n");
        return 404;
    }

    /* ファイルを読み込んでボディとする */
    f = fopen(file_path, "r");
    fread(body, 1, file_size, f);
    fclose(f);

    return 200;
}

/*
 * レスポンスメッセージを作成する
 * response_message:レスポンスメッセージを格納するバッファへのアドレス
 * status:ステータスコード
 * header:ヘッダーフィールドを格納したバッファへのアドレス
 * body:ボディを格納したバッファへのアドレス
 * body_size:ボディのサイズ
 * 戻り値:レスポンスメッセージのデータサイズ(バイト長)
 */
int createResponseMessage(char *response_message, int status, char *header, char *body, unsigned int body_size) {

    unsigned int no_body_len;
    unsigned int body_len;

    response_message[0] = '\0';

    if (status == 200) {
        /* レスポンス行とヘッダーフィールドの文字列を作成 */
        sprintf(response_message, "HTTP/1.1 200 OK\r\n%s\r\n", header);

        no_body_len = strlen(response_message);
        body_len = body_size;

        /* ヘッダーフィールドの後ろにボディをコピー */
        memcpy(&response_message[no_body_len], body, body_len);
    } else if (status == 404) {
        /* レスポンス行とヘッダーフィールドの文字列を作成 */
        sprintf(response_message, "HTTP/1.1 404 Not Found\r\n%s\r\n", header);

        no_body_len = strlen(response_message);
        body_len = 0;
    } else {
        /* statusが200・404以外はこのプログラムで非サポート */
        printf("Not support status(%d)\n", status);
        return -1;
    }

    return no_body_len + body_len;
}

/*
 * レスポンスメッセージを送信する
 * sock:接続済のソケット
 * response_message:送信するレスポンスメッセージへのアドレス
 * message_size:送信するメッセージのサイズ
 * 戻り値:送信したデータサイズ(バイト長)
 */
int sendResponseMessage(int sock, char *response_message, unsigned int message_size) {

    int send_size;
    
    send_size = send(sock, response_message, message_size, 0);

    return send_size;
}

void showMessage(char *message, unsigned int size) {
/* コメントを外せばメッセージが表示される
    unsigned int i;

    printf("Show Message\n\n");

    for (i = 0; i < size; i++) {
        putchar(message[i]);
    }
    printf("\n\n");
*/
}

/*
 * HTTPサーバーの処理を行う関数
 * sock:接続済のソケット
 * 戻り値:0
 */
int httpServer(int sock) {

    int request_size, response_size;
    char request_message[SIZE];
    char response_message[SIZE];
    char method[SIZE];
    char target[SIZE];
    char header_field[SIZE];
    char body[SIZE];
    int status;
    unsigned int file_size;
    

    while (1) {

        /* リクエストメッセージを受信 */
        request_size = recvRequestMessage(sock, request_message, SIZE);
        if (request_size == -1) {
            printf("recvRequestMessage error\n");
            break;
        }

        if (request_size == 0) {
            /* 受信サイズが0の場合は相手が接続閉じていると判断 */
            printf("connection ended\n");
            break;
        }

        /* 受信した文字列を表示 */
        showMessage(request_message, request_size);

        /* 受信した文字列を解析してメソッドやリクエストターゲットを取得 */
        if (parseRequestMessage(method, target, request_message) == -1) {
            printf("parseRequestMessage error\n");
            break;
        }

        /* メソッドがGET以外はステータスコードを404にする */
        if (strcmp(method, "GET") == 0) {
            if (strcmp(target, "/") == 0) {
                /* /が指定された時は/index.htmlに置き換える */
                strcpy(target, "/index.html");
            }

            /* GETの応答をするために必要な処理を行う */
            status = getProcessing(body, &target[1]);
        } else {
            status = 404;
        }

        /* ヘッダーフィールド作成(今回はContent-Lengthのみ) */
        file_size = getFileSize(&target[1]);
        sprintf(header_field, "Content-Length: %u\r\n", file_size);

        /* レスポンスメッセージを作成 */
        response_size = createResponseMessage(response_message, status, header_field, body, file_size);
        if (response_size == -1) {
            printf("createResponseMessage error\n");
            break;
        }

        /* 送信するメッセージを表示 */
        showMessage(response_message, response_size);

        /* レスポンスメッセージを送信する */
        sendResponseMessage(sock, response_message, response_size);
        
    }

    return 0;
}

int main(void) {
    int w_addr, c_sock;
    struct sockaddr_in a_addr;

    /* ソケットを作成 */
    w_addr = socket(AF_INET, SOCK_STREAM, 0);
    if (w_addr == -1) {
        printf("socket error\n");
        return -1;
    }

    /* 構造体を全て0にセット */
    memset(&a_addr, 0, sizeof(struct sockaddr_in));

    /* サーバーのIPアドレスとポートの情報を設定 */
    a_addr.sin_family = AF_INET;
    a_addr.sin_port = htons((unsigned short)SERVER_PORT);
    a_addr.sin_addr.s_addr = inet_addr(SERVER_ADDR);

    /* ソケットに情報を設定 */
    if (bind(w_addr, (const struct sockaddr *)&a_addr, sizeof(a_addr)) == -1) {
        printf("bind error\n");
        close(w_addr);
        return -1;
    }

    /* ソケットを接続待ちに設定 */
    if (listen(w_addr, 3) == -1) {
        printf("listen error\n");
        close(w_addr);
        return -1;
    }

    while (1) {
        /* 接続要求の受け付け(接続要求くるまで待ち) */
        printf("Waiting connect...\n");
        c_sock = accept(w_addr, NULL, NULL);
        if (c_sock == -1) {
            printf("accept error\n");
            close(w_addr);
            return -1;
        }
        printf("Connected!!\n");

        /* 接続済のソケットでデータのやり取り */
        httpServer(c_sock);

        /* ソケット通信をクローズ */
        close(c_sock);

        /* 次の接続要求の受け付けに移る */
    }

    /* 接続待ちソケットをクローズ */
    close(w_addr);

    return 0;
}

ソースコードの解説

ソースコードは大きく分けて下記の2つから構成されます。

  • クライアントとの接続(接続を閉じる処理も)
  • クライアントと HTTP でデータのやり取り

前者に関しては main 関数で行っており、下記のソケット通信の解説ページ紹介したサーバープログラムとほぼ同じものになります(コールする関数が異なるのみ)。

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

後者は httpServer 関数で行っています。

この httpServer では HTTP サーバーの作り方で紹介した下記の処理を行なっています。

  • リクエストメッセージを受信
  • リクエストメッセージを解析
  • 解析結果に基づいて処理
  • レスポンスメッセージを作成
  • レスポンスメッセージを送信

リクエストメッセージを受信

「リクエストメッセージを受信」する処理は recvRequestMessage で行っています。

見ての通り recv 関数でクライアントからデータを受信しているだけです。

また、この recvRequestMessage 関数の呼び出し側では、戻り値が 0 の場合はソケットが接続を閉じられていると判断して HTTP によるデータのやり取りを終了するようにしています。

リクエストメッセージを解析

「リクエストメッセージを解析」する処理は parseRequestMessage 関数で行っています。

今回作成する HTTP サーバーでは、リクエストメッセージのヘッダーフィールドは無視し、さらに受け付けるメソッドは GET のみに制限していますのでボディも無いものとして扱います。さらに HTTP のバージョンも 1.1 固定です。

ですので、本当に必要になるのはリクエストメッセージにおける「リクエスト」の、特に「リクエストターゲット」のみになります。

ただし、メソッドが GET 以外の場合はステータスコード 404 で応答するようにしたいので、メソッドが GET かどうかを判断できるようにメソッドも取得するようにしています。

この「メソッド」と「リクエストターゲット」を取得するために、strtok 関数を利用して文字列を分割しています。

まず strtok 関数でリクエストメッセージを '\n' で分割し、リクエストメッセージの一行目のみ、つまり「リクエスト」を取得しています。

さらに strtok 関数で「リクエスト」を ' '(スペース)で分割し、最初の ' ' までを「メソッド」、そこから次の ' ' までを「リクエストターゲット」として取得しています。

解析結果に基づいて処理

さらに解析結果に基づいて処理を行います。

ただし、今回作成する HTTP サーバーでは、受け付けるメソッドは GET のみなので、GET 用の処理しか用意していません。

で、この処理を行うのが getProcessing 関数になります。

この getProcessing 関数ではリクエストターゲットに対応するデータ(HTML など)を用意する処理を行います。

このデータの用意は単純にリクエストターゲットに対応するファイルを読み込むことで行います。

どのようなファイルを読み込むかは後述の実行方法で解説します。

また、getProcessing 関数の戻り値はレスポンスメッセージのステータスコードになります。

リクエストターゲットに対応するファイルがない場合やファイルサイズが 0 の場合は、そんなデータは存在しないことをクライアントに伝えるためにステータスコードを 404(Not Found) を返却し、それ以外の場合は 200(OK)を返却するようにしています。

レスポンスメッセージを作成

続いて上記の処理結果に基づいてレスポンスメッセージを作成します。

この「レスポンスメッセージを作成」する処理は createResponseMessage 関数で行っています。

createResponseMessage 関数では引数(status)で与えられたステータスコードに応じてレスポンスメッセージを作成しています。

また、ヘッダーフィールドも引数(header)で与えられるようにしています(ヘッダーフィールドは httpServer 関数で createResponseMessage 関数実行前に作成)。

ポイントは status200(OK)の場合のみボディをレスポンスメッセージに付加するようにしているところです。このボディは getProcessing 関数で用意したデータになります。

レスポンスメッセージを送信

リクエストに対する一連の処理の最後に、クライアントに対してレスポンスメッセージを送信します。

この「レスポンスメッセージを送信」する処理は sendResponseMessage 関数で行っています。

見ていただければわかるとおり、単に send 関数で作成したレスポンスメッセージを作成しているだけです。

スポンサーリンク

実行方法

プログラムを実行するためには前準備が必要ですので、まずはこの準備について説明し、続いて実行方法の解説をしていきたいと思います。

送信するデータの準備

まずはクライアントへのレスポンス時に送信するためのデータ(ファイル)を準備しておきます。

必須は HTML ファイル(index.html)です。これは自身で作成していただいて問題ないのですが、一応例として私が使用していたものを紹介しておきます。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>INDEX.HTML</title>
  </head>
  <body>
    <h1>ウェブページ1</h1>
    <p>このページは自作HTTPサーバーから受信したページです</p>
    <p><img src="./image.jpg"></p>
  </body>
</html>

この index.html を使用する場合、この index.htmlimage.jpg を読み込むようにしていますので、一緒に image.jpg という名前のファイルも用意する必要があります。

この image.jpg もご自身で用意していただいても問題ありません。ですが、メッセージのサイズの上限は 5 KB なので、5 KB よりもファイルは小さい必要があるので注意してください。

私は下の画像ファイルを使用して動作確認しましたので、よろしければご利用ください(いらすとやさんの画像をグレースケールにしたものです)。

image.jpg

これらのファイルは「プログラムを実行するフォルダと同じフォルダに設置する」ようにしてください。

これは、今回作成したプログラムではリクエストターゲットの / を、プログラムを実行するフォルダとみなすようにしているためです。

例えばリクエストターゲットが /index.html であれば、同じフォルダの index.html を読み込んで、その読み込んだデータをレスポンスするようにしています。

クライアントの準備

プログラムは「サーバー」ですので、通信を行うためにはクライアントが必要になります。

ただし、今回作成したのは HTTP サーバーですので、いつも使用しているウェブブラウザをクライアントとして利用することが出来ます。

私は Google Chrome と Safari で実際に動作確認をしました。

サーバーの起動

実行時には、まず今回紹介したソースコードをコンパイルし、生成した実行可能ファイルを実行します。

これにより HTTP サーバーがクライアントからの接続待ち状態になります。

gcc を利用する場合は、例えば下記のコマンドでコンパイルを行うことが出来ます(ソースコード名は server.c と仮定しており、生成される実行可能ファイルの名前は server.exe となります)。

gcc server.c -o server.exe

ウェブブラウザの起動

続いてクライアントとなるウェブブラウザを立ち上げます。

ウェブブラウザでのページ表示

さらにウェブブラウザのアドレスバーに下記を入力してエンターキーを押すと、ウェブブラウザが HTTP サーバーと接続し、続いてリクエストが送信されます。

http://127.0.0.1:8080/

HTTP サーバーはそのリクエストに対してレスポンスを行うことで、ウェブブラウザ上にページが描画されます。

このページで紹介した index.htmlimage.jpg を使用した場合は下の画像のようなページが表示されると思います。

HTTPサーバーから受け取ったHTMLがウェブブラウザに描画される様子

プログラムの動作

最後にウェブブラウザで下記 URL を指定した時に紹介したプログラムがどのような動作をするのかについて解説しておきます。

http://127.0.0.1:8080/

127.0.0.1 は HTTP サーバーの IP アドレスであり、この IP アドレスはソースコードの SERVER_ADDR で定義しています。

8080 は HTTP サーバーのポート番号で、これはソースコードの SERVER_PORT で定義しています。

なので、上記 URL をウェブブラウザに指定することで、作成した HTTP サーバーにウェブブラウザが接続し、続いてリクエストメッセージを送信してきます。

このリクエストメッセージのメソッドは GET であり、リクエストターゲットは / になります。

httpServer 関数ではリクエストターゲットが / の時にはボディに index.html のデータをコピーしてレスポンスするようにしています(同じフォルダの index.html を読み込んでそのデータをボディにコピー)ので、ウェブブラウザが index.html 受け取ることになります。

さらにウェブブラウザはこの index.html をページとして描画する時に必要になる(index.html から参照されている) image.jpg をリクエストターゲットしてまた新たにリクエストメッセージを HTTP サーバーに送信してきます。

で、またこのリクエストメッセージをサーバーが処理してレスポンスメッセージを送ります。

私が用意した index.html では image.jpg しか参照していないので index.html の後に送られてくるリクエストメッセージは一つだけですが、もっと多くのデータが参照されている場合は、それに応じた回数リクエストメッセージが送られてくることになります。

MEMO

ブラウザからは index.htmlimage.jpg だけでなく favicon.ico というファイル(アドレスバーに表示するアイコン)をリクエストターゲットとしてリクエストしてくることがあります

ちなみに http://127.0.0.1:8080/ の後ろ側に、HTTP サーバーのプログラムを実行しているフォルダに存在するファイルの名前を指定すれば、そのファイルをリクエストすることもできます。

逆に http://127.0.0.1:8080/ の後ろ側に、HTTP サーバーのプログラムを実行しているフォルダに “存在しない” ファイル名を指定した場合は HTTP サーバーはステータスコード 404 でレスポンスします。

おそらくこの場合はウェブブラウザに「ページが見つかりません」などのページが表示されると思います。Google Chrome の場合は下の画像のようなページが表示されます。

ステータスコード404を受信した時にウェブブラウザに表示される画面

まとめ

このページではまず HTTP について解説し、その解説内容を踏まえて作成した HTTP サーバーのプログラムの紹介を行いました!

実際にウェブブラウザと通信できるプログラムが作成できるので、出来上がった時には結構満足感があると思います!

今回作成した HTTP サーバーではかなり機能を制限していますので、もっといろんなメソッドやステータスコードに対応させてみると良い勉強になると思います!

単にプログラミング力を付けるだけでなく、ウェブページが表示される時のソフトウェアの動きも理解することもできますので是非挑戦してみてください!

同じカテゴリのページ一覧を表示