【C言語】簡単な「HTTPクライアント」を作る

HTTPクライアントの作り方の解説ページアイキャッチ

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

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

「HTTP クライアント」の代表例はウェブブラウザですね!

ウェブブラウザを作ろうと思うとかなり大変なので、今回作成する HTTP クライアントはあくまでも簡易版になります。かなり機能を制限しています。

ですが、この作成した HTTP クライアントを使用することで、実際にウェブサーバーと通信してウェブページを表示するためのデータを取得することが出来ます!

HTTP や HTTP 通信時にやり取りされるデータ(リクエストメッセージ・レスポンスメッセージ)については下記の HTTP サーバーの解説ページで説明しています。

HTTPサーバーの作り方の解説ページアイキャッチ【C言語】簡単な「HTTPサーバー」を作る

ですので、これらの解説に関しては上記ページを参照していただければと思います。

このページでは、早速 HTTP クライアントの作り方について解説していきます!

MEMO

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

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

HTTP クライアントの作り方

では早速、HTTP クライアントの作り方について解説していきたいと思います。

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

  1. サーバーの IP アドレスを取得
  2. サーバーと接続
  3. リクエストメッセージを作成
  4. リクエストメッセージを送信
  5. レスポンスメッセージを受信
  6. レスポンスメッセージに応じた処理
  7. サーバーとの接続をクローズ

各処理がどのようなものであるかについて解説していきたいと思います。

サーバーの IP アドレスを取得

今回作成する HTTP クライアントでも、基本的に行うことは下記ページで解説しているクライアントと同じです。

要は「サーバーへの接続」と「サーバーとのデータのやりとり」を行います。

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

サーバーへの接続を行う際に connect 関数を利用していますが、この connect 関数には「サーバーの IP アドレス」を指定する必要がありました(正確には IP アドレスを設定した struct sockaddr 構造体を指定する)。

IPアドレスを指定してconnectする様子

通常のウェブブラウザにおける IP アドレス取得

ただし、基本的にウェブページを表示するときにウェブブラウザに指定するのは IP アドレスではなく URL です(今回作成する HTTP クライアントも URL が指定できるものを作っていきます)。

ですので、connect を使ってサーバーに接続するためには、まず URL からサーバーの IP アドレスを取得する必要があります。

実はこれは通常のウェブブラウザでも同様です。

ウェブブラウザではウェブサーバーに接続する前に、まず DNS サーバーに問い合わせして IP アドレスを取得するようになっています。そして、IP アドレスを取得後にウェブサーバーに接続します。

DNSサーバーにIPアドレスを問い合わせる様子

寄り道になりますが、この辺りについては下記ページで詳しく解説していますので興味のある方は読んでみてください。

Webページ表示の仕組みと表示までの流れ

C言語プログラムにおける IP アドレスの取得

これは自作する HTTP クライアントでも同様で、IP アドレスが分からない場合は、ウェブサーバー(HTTP サーバー)に接続する前に、DNS サーバーに問い合わせして IP アドレスを取得する必要があります。

これを行うための関数として、C言語では下記の gethostbyname が用意されています。

gethostbyname
#include <netdb.h>

struct hostent *gethostbyname(const char *name);

引数 name は「ホスト名」になります。URL そのものではないので注意です。

この gethostbyname 関数を実行すると、関数の中で DNS サーバーへの問い合わせが行われ、引数で指定した「ホスト名」に対する「ホストの情報の構造体(struct hostent)」が戻り値として返却されます。

gethostbynameがDNSサーバーにIPアドレスを問い合わせる様子

で、「ホストの情報の構造体」のメンバに IP アドレスもセットされています。

gethostbyname 関数の戻り値が、引数で指定したホスト名のホストの情報の構造体になります。そして、この構造体のメンバ(h_addr_list)に DNS サーバーから取得した IP アドレスもセットされています。

ですので、URL からホスト名を取得し、さらにそのホスト名から gethostbyname で IP アドレスを取得してやれば、connect 関数で IP アドレスを指定してサーバーに接続することができます。

ホスト名とパス

そのホスト名は、基本的には URL の http:// の後ろから最初の / の直前までの部分になります(ウェブに詳しい人にとっては FQDN という方が分かりやすいかもしれません)。

URLにおけるホスト名とパス

ちなみにホスト名の後ろ側はパスと呼ばれます。

ですので、URL からホスト名の部分のみを取得し、それを gethostbyname の引数に指定して実行することで IP アドレスを取得することができます。

ただし、URL は最初の / の前に : でポート番号が指定されることもあるので注意してください(下記はポート番号 8080 を指定する例です)。

http://daeudaeu.com:8080/test/

このページで最後に紹介するプログラムでは、簡単のため URL はホスト名とパスのみから構成されると仮定してプログラミングしています。

スポンサーリンク

サーバーと接続

gethostbyname を実行して IP アドレスが取得できたら、次はその IP アドレスを用いて connect 関数でサーバーに接続します。

クライアントとサーバーが接続する様子

リクエストメッセージを作成

接続が確立できたら、次はサーバーに送信するためのリクエストメッセージを作成します。

リクエストメッセージがどのようなものであるかは下記ページのリクエストメッセージで解説していますので是非こちらをご参照ください。

HTTPサーバーの作り方の解説ページアイキャッチ【C言語】簡単な「HTTPサーバー」を作る

要はリクエストとヘッダーフィールド(とボディ)から構成されるデータを作成します。

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

リクエスト

リクエストは下のように「メソッド」「リクエストターゲット」「HTTP のバージョン」の3つのデータから構成されます。

リクエストのデータ構成

リクエストの「メソッド」はユーザーの操作に応じて設定します。

例えばウェブページを表示する場合はメソッドを GET に、ウェブページにコメントを残す場合はメソッドを POST に設定します。

ただし、今回作成する HTTP クライアントは簡易版ですので、メソッドは GET 固定にしています。

また、「リクエストターゲット」で「どこに」対してメソッドに対応した処理をサーバーにリクエストするかを指定することが出来ます。

メソッドが GET の場合は、URL のパスやクエリを指定したりします。

今回作成する HTTP クライアントでは、ホスト名の後ろ側全てをパスとしてみなして処理を行いますので、特にパスやクエリの区別は行いません。

また、使用する「HTTP のバージョン」を指定することが出来ます。

今回作成する HTTP クライアントでは「HTTP/1.1」を固定で指定するようにしたいと思います。

ヘッダーフィールド

ヘッダーフィールドを指定することで、リクエストの内容をより詳細にサーバーに伝えることが出来ます。

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

Accept-Language: ja

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

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

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

今回は簡易版の HTTP クライアントということで、Connection: close とホスト名のみをフィールド名 Host で指定するようにしたいと思います。

MEMO

Connection: close を設定することで1回のリクエスト – レスポンスを行うたびに接続をクローズするように指定することができます

ボディ

リクエストメッセージにおけるボディはサーバーに送信するデータの実体になります。

例えばコメントを書き込む際のリクエストメッセージでは「コメント本文」などがボディに設定されます。

今回作成する HTTP クライアントで対応するのは GET のみなので、特にサーバーにデータを送信せず、リクエストメッセージのボディは空のままになります。

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

リクストメッセージが作成できたら、次はこのリクエストメッセージを接続中のサーバーに送信します。

クライアントがサーバーにリクエストメッセージを送信する様子

要は send 関数でリクエストメッセージを送信すれば良いです。

スポンサーリンク

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

リクストメッセージを送信したら、次はそのリクエストに対応したレスポンスメッセージがサーバーから送信されてくるのを待ち、送信されてきたらそのレスポンスメッセージを受信します。

クライアントがサーバーからレスポンスメッセージを受信する様子

要は recv 関数でリクエストメッセージを受信すれば良いです(recv 関数を実行すればデータを受信するまで recv 関数の中で自動的に待ちが行われます)。

ただし、サーバーがレスポンスメッセージを1度で送信せずに、複数回に分割して送信してくることがある点に注意が必要です。

なので、サーバーからのレスポンスメッセージ全体を受信するまで recv 関数を繰り返し実行する必要があります。

レスポンスメッセージに応じた処理

レスポンスメッセージを受信したら、続いてレスポンスメッセージに応じた処理を行います。

レスポンスメッセージがどのようなものであるかは下記ページのレスポンスメッセージで解説していますので、詳しく知りたい方はこちらを参照してください。

HTTPサーバーの作り方の解説ページアイキャッチ【C言語】簡単な「HTTPサーバー」を作る

例えば代表的な HTTP クライアントであるウェブブラウザだと、ウェブページを表示する際はレスポンスメッセージの「ボディ」部分の HTML を解析して、ブラウザ上にページを描画するような処理が行われます。

受信したレスポンスメッセージからページを描画する様子

また、HTML では他のファイルを参照している場合があり(例えば画像やスタイルシートなどなど)、その HTML から参照されているファイルを取得するために、新たにリクエストメッセージの送信が行われます。

さらに、レスポンスメッセージの「レスポンス」には「ステータスコード」が含まれており、このステータスコードに応じた処理を行う場合もあります。

例えばステータスコード 301 は「リクエストターゲットが他の URL に移動した」ことを示します。この移動先の URL はヘッダーフィールドのフィールド名 Location に記述されていますので、改めてその URL に対してリクエストを実行するような処理が行われます。

リダイレクト先に再度リクエストする様子

こんな感じで、より高機能な HTTP クライアントを作成する場合は、レスポンスメッセージに応じてさまざまな処理を行う必要があります。

ただし、全部対応しようと思うとかなり大変です…。

ですので、今回作成する HTTP クライアントでは、単純に受信したレスポンスメッセージの「ボディ部のファイル保存」のみを行うものとしたい思います。

サーバーとの接続をクローズ

リクエストとレスポンスが行われた後は、サーバーとの接続をクローズします。

HTTP のバージョンやサーバーによっては、1回の接続でリクエストとレスポンスを繰り返し行うことも可能です。

例えば HTTP/1.1 では1回の接続で複数回のリクエストとレスポンスを繰り返し行うことが出来ます。

また、ヘッダーフィールドの Connection により、リクエストとレスポンスを1度行ったら毎回接続をクローズするか、1度の接続でリクエストとレスポンスを複数回行えるかを指定することもできます。

  • 毎回接続をクローズする場合:
    • Connection: close
  • 接続を維持したまま複数回のデータのやりとりをする場合:
    • Connection: keep-alive

スポンサーリンク

HTTP クライアントのサンプル

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

今回作成するのはあくまでも「簡単な HTTP クライアント」であり、主に下記のような制限を持った HTTP クライアントになります。

  • IP アドレスは IPv4 のみに対応
  • ポート番号は 80 固定
  • 発行するリクエストのメソッドは GET のみ
  • リクエストのヘッダーフィールドは Connection と Host のみ設定
  • レスポンスのステータスコードとヘッダーフィールドは全て無視
  • レスポンスメッセージに対する処理はボディ部のファイル保存(test.html で保存)のみ
  • リクエストメッセージのサイズは 1 KB 以内とする
  • レスポンスメッセージのサイズは 5 MB 以内とする
  • HTTP のバージョンは 1.1

ソースコード

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

client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>

#define CONNECT_PORT 80
#define MAX_RESPONSE_SIZE (1024*1024*5)
#define MAX_REQUEST_SIZE (1024)
#define MAX_HOSTNAME_SIZE 1024
#define MAX_PATH_SIZE 1024
#define MAX_SIZE 128
#define MAX_URL 256


/*
 * ホスト名からIPアドレスを取得する
 * hostname:ホスト名
 * 戻り値:IPアドレスが格納されたバッファへのアドレス(失敗時はNULL)
 */
char *getIpAddress(char *hostname) {
    struct hostent *host;

    /* ホスト名からホスト情報を取得 */
    host = gethostbyname(hostname);
    if (host == NULL) {
        printf("gethostbyname error\n");
        return NULL;
    }

    /* IPv4以外はエラー */
    if (host->h_length != 4) {
        printf("Not ipv4\n");
        return NULL;
    }

    /* ホスト情報のアドレス群の1つ目をIPアドレスとする */
    return host->h_addr_list[0];
}

/*
 * URLからホスト名とパスを取得する
 * hostname:ホスト名を格納するバッファへのアドレス
 * path:パスを格納するバッファへのアドレス
 * url:URLが格納されたバッファへのアドレス
 * 戻り値:0
 */
int getHostnameAndPath(char *hostname, char *path, char *url) {
    unsigned int i;
    char hostname_path[MAX_HOSTNAME_SIZE + MAX_PATH_SIZE];

    /* URLの最初が"http://"の場合は"http://"を取り除く */
    if (strncmp(url, "http://", strlen("http://")) == 0) {
        sscanf(url, "http://%s", hostname_path);
    } else {
        strcpy(hostname_path, url);
    }
    
    /* 最初の'/'までの文字数をカウント */
    for (i = 0; i < strlen(hostname_path); i++) {
        if (hostname_path[i] == '/') {
            break;
        }
    }

    if (i == strlen(hostname_path)) {
        /* '/'がhostname_pathに含まれていなかった場合 */

        /* hostname_path全体をhostnameとする */
        strcpy(hostname, hostname_path);

        /* pathは'/'とする */
        strcpy(path, "/");
    } else {
        /* '/'がhostname_pathに含まれていなかった場合 */

        /* '/'の直前までをhostnameとする */
        strncpy(hostname, hostname_path, i);

        /* '/'以降をpathとする */
        strcpy(path, &hostname_path[i]);
    }

    return 0;
}

/*
 * リクエストメッセージを作成する
 * request_message:リクエストメッセージを格納するバッファへのアドレス
 * target:リクエストターゲット(リクストするファイル)
 * host:リクエスト先のホスト名
 * 戻り値:メッセージのサイズ
 */
int createRequestMessage(char *request_message, char *path, char *hostname) {

    char request[MAX_SIZE];
    char header[MAX_SIZE];

    sprintf(request, "GET %s HTTP/1.1", path);
    sprintf(header, "Host: %s\r\nConnection: close", hostname);
    sprintf(request_message, "%s\r\n%s\r\n\r\n", request, header);
    
    return strlen(request_message);
}

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

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

    return send_size;
}

/*
 * レスポンスメッセージを受信する
 * sock:接続済のソケット
 * response_message:レスポンスメッセージを格納するバッファへのアドレス
 * buffer_size:↑のバッファのサイズ
 * 戻り値:受信したデータサイズ(バイト長)
 */
int recvResponseMessage(int sock, char *request_message, unsigned int buffer_size) {

    int total_recv_size = 0;
    int i;

    while (1) {
        int recv_size = recv(sock, &request_message[total_recv_size], buffer_size, 0);
        if (recv_size == -1) {
            printf("recv error\n");
            return -1;
        }
        if (recv_size <= 0) {
            printf("connection ended\n");
            break;
        }

        for (i = 0; i < recv_size; i++) {
            putc(request_message[total_recv_size + i], stdout);
        }
        total_recv_size += recv_size;
    }
    
    return total_recv_size;
}

/*
 * ボディをファイル保存する
 * file_path:保存先のファイルパス
 * response_message:レスポンスメッセージを格納するバッファへのアドレス
 * response_size:レスポンスメッセージのサイズ
 * 戻り値:成功時 0、失敗時 -1
 */
int saveBody(const char *file_path, char *response_message, unsigned int response_size) {

    FILE *fo;
    char *tmp;
    unsigned int skip_size = 0;

    /* レスポンスメッセージを空行まで読み飛ばし */
    tmp = strtok(response_message, "\n");
    while (tmp != NULL && (strcmp(tmp, "\r") != 0 && strcmp(tmp, "") != 0)) {
        skip_size += strlen(tmp) + strlen("\n");

        tmp = strtok(NULL, "\n");
        printf("%s\n", tmp);
    }

    if (tmp == NULL) {
        printf("body is not found\n");
        return -1;
    }

    /* 空行の次の行からをボディと見做してファイル保存 */
    skip_size += strlen(tmp) + strlen("\n");

    fo = fopen(file_path, "wb");
    if (fo == NULL) {
        printf("Open error (%s)\n", file_path);
        return -1;
    }

    fwrite(&response_message[skip_size], 1, response_size - skip_size, fo);

    fclose(fo);
    
    return 0;
}

/*
 * HTTP通信でサーバーとデータのやりとりを行う
 * sock:サーバーと接続済のソケット
 * hostname:ホスト名が格納されたバッファへのアドレス
 * path:パスが格納されたバッファへのアドレス
 * 戻り値:成功時 0、失敗時 -1
 */
int httpClient(int sock, char *hostname, char *path) {

    int request_size, response_size;
    char request_message[MAX_REQUEST_SIZE];
    char response_message[MAX_RESPONSE_SIZE];

    /* リクエストメッセージを作成 */
    request_size = createRequestMessage(request_message, path, hostname);
    if (request_size == -1) {
        printf("createRequestMessage error\n");
        return -1;
    }

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

    /* レスポンスメッセージを受信 */
    response_size = recvResponseMessage(sock, response_message, MAX_RESPONSE_SIZE);
    if (response_size == -1) {
        printf("recvResponseMessage error\n");
        return -1;
    }

    /* レスポンスメッセージに応じた処理(ボディのファイル保存のみ) */
    if (saveBody("test.html", response_message, response_size) == -1) {
        printf("saveBody error\n");
        return -1;
    }

    return 0;
}



int main(int argc, char *argv[]) {
    int sock = -1;
    struct sockaddr_in addr;
    unsigned short port = CONNECT_PORT;
    char url[MAX_URL];
    char *ip_address;

    char hostname[MAX_HOSTNAME_SIZE];
    char path[MAX_PATH_SIZE];
    
    if (argc == 2) {
        strcpy(url, argv[1]);
    } else {
        printf("set url!!\n");
        return -1;
    } 
    
    /* ホスト名とパスを取得 */
    getHostnameAndPath(hostname, path, url);

    /* IPアドレスを取得 */
    ip_address = getIpAddress(hostname);
    if (ip_address == NULL) {
        printf("getIPAddress error\n");
        return -1;
    }

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

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

    /* 接続先の情報を設定 */
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    
    /* ip_addressは数値の配列なのでそのままコピー(inet_addrは不要) */
    memcpy(&(addr.sin_addr.s_addr), ip_address, 4);
    
    /* サーバーに接続 */
    printf("Connect to %u.%u.%u.%u\n",
        (unsigned char)ip_address[0],
        (unsigned char)ip_address[1],
        (unsigned char)ip_address[2],
        (unsigned char)ip_address[3]
    );

    if (connect(sock, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) == -1) {
        printf("connect error\n");
        close(sock);
        return -1;
    }

    /* HTTP通信でデータのやりとり */
    httpClient(sock, hostname, path);

    /* ソケット通信をクローズ */
    if (close(sock) == -1) {
        printf("close error\n");
        return -1;
    }

    return 0;
}

ソースコードの解説

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

  • ホスト名・パス・IP アドレスの取得
  • サーバーとの接続(接続を閉じる処理も)
  • サーバーと HTTP でデータのやり取り

「サーバーとの接続」に関しては main 関数で行っており、ソケット通信の解説ページ紹介したクライアントプログラムとほぼ同じものになります。

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

ただし、今回作成する HTTP クライアントでは、ユーザーから URL を受け付けるようにしていますので、事前に下記の処理を行なっています。

  • ホスト名・パス・IP アドレスの取得

また、「サーバーと HTTP でデータのやり取り」は httpClient 関数で行っています。

この httpClient では HTTP クライアントの作り方で紹介した下記の処理を行なっています。

  • リクエストメッセージを作成
  • リクエストメッセージを送信
  • レスポンスメッセージを受信
  • レスポンスメッセージに応じた処理

ここからは下記の5つの処理について、どのような処理をどの関数で行っているかを説明していきたいと思います。

  • ホスト名・パス・IP アドレスの取得
  • リクエストメッセージを作成
  • リクエストメッセージを送信
  • レスポンスメッセージを受信
  • レスポンスメッセージに応じた処理

ホスト名・パス・IP アドレスの取得

URL からのホスト名とパスの取得は getHostnameAndPath 関数で行っています。

getHostnameAndPath では、まず sscanf 関数を利用して http:// の部分を取り除いた文字列を取得しています。

さらに、その取得した文字列の「最初の / の直前まで」と「/ 以降」の2つの文字列に分離し、それぞれをホスト名とパスとして取得しています。

さらにホスト名から IP アドレスを取得する処理を getIpAddress 関数で行っています。

getIpAddress では gethostbyname 関数を実行してホスト名の「ホストの情報」の構造体(struct hostent)を取得し、この構造体の「IP アドレス」を表すメンバ(h_addr_list[0])から IP アドレスを取得しています。

IPv4 のみに対応するので、IP アドレスを表す数値の数(h_length)が 4 以外の場合はエラーにしています。

リクエストメッセージを作成

リクエストメッセージの作成は createRequestMessage 関数で行っています。

今回作成する HTTP クライアントでは下記の制限を設けていますので、要はリクエストメッセージは「リクエストターゲット」と「Host の内容」のみを URL に応じて設定してやれば良いことになります。

  • 発行するリクエストのメソッドは GET のみ
  • リクエストのヘッダーフィールドは Connection と Host のみ設定
  • HTTP のバージョンは 1.1

ですので、createRequestMessage では「リクエストターゲット」となるパス(path)と「Host の内容」となるホスト名(hostname)を引数として受け取り、これをリクエストメッセージのフォーマットに埋め込むために sprintf 関数を実行しています(Connectionclose 固定で設定)。

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

リクエストメッセージの送信は sendRequestMessage 関数で行っています。

見ていただければわかる通り、作成したリクエストメッセージを引数で受け取り、それを send 関数で接続先のサーバーに送信しているだけです。

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

レスポンスメッセージの受信は recvResponseMessage 関数で行っています。

前述の通り、レスポンスメッセージはサーバーから複数回に分割して送信されてくる可能性があります。

ですので、recv 関数は1度だけでなく、recv 関数の戻り値が 0 になるまで繰り返し行うようにしています。

MEMO

レスポンスメッセージの Content-Length の値のサイズ分だけ受信するようにしても良いです

レスポンスメッセージに応じた処理

最後に受信したレスポンスメッセージに応じた処理を saveBody 関数で行っています。

前述の通り、今回作成する HTTP クライアントではレスポンスメッセージに応じた処理はボディ部のファイル保存のみです。

ですので、この saveBody では、ボディ部のファイル保存のみを行っています。

ただし、レスポンスメッセージはボディ部だけでなく、レスポンスやヘッダーフィールドも含まれていますので、ボディ部までデータを読み飛ばし、ボディ部のみをファイル保存するようにしています。

具体的には、レスポンスメッセージを1行ずつ取得して「空行」を探し出し、「空行」を見つけたらその次の行からのデータをボディ部とみなしてファイル保存しています。

保存するファイルのファイル名は引数 file_path で指定できるようにしており、これは httpClient"test.html" で指定するようにしています。

スポンサーリンク

実行方法

HTTP サーバーの時は色々と前準備が必要でしたが、HTTP クライアントの場合は準備はコンパイルのみになります。

コンパイル

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

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

gcc client.c -o client.exe

URL を指定して実行

続いて生成した実行可能ファイルを実行します。

実行時には、表示したいウェブページの URL を引数で指定してください。

例えば http://daeudaeu.work を表示したいのであれば下記のように実行します(実行可能ファイルの名前を client.exe としています)。

client.exe http://daeudaeu.work

実行すると、プログラムを実行したフォルダに test.html という名前のファイルが生成されます。これが今回作成した HTTP クライアントが HTTP サーバーからレスポンスメッセージで受け取ったデータ(ボディ)になります。

この test.html をウェブブラウザの画面にドラッグ&ドロップすれば、test.html の内容をウェブブラウザが描画してくれると思います。

MEMO

画像なども表示されるかもしれませんが、この画像は今回作成した HTTP クライアントが GET で取得したデータではありません

これらは test.html をページとして描画するときに必要になったデータを “ウェブブラウザ” が GET で取得してくれたデータです

もし自作の HTTP クライアントで HTML から参照している画像などのデータを取得したいのであれば、HTML から参照しているデータを別途 GET で取得するようにプログラムを変更する必要があります

注意点は、https には対応していないという点です。URL は必ず http で始まるものにしてください。

また、リダイレクトが設定されているサイトの場合はステータスコード 301 をレスポンスとして受け取ることになりますが、今回作成した HTTP クライアントでは移動先の URL に接続するようなことはしませんので、この点も注意してください。

レスポンスメッセージを表示するようにしていますので、上手く動作しない場合はそのレスポンスメッセージを確認して理由を確認していただければと思います。

まとめ

このページでは HTTP クライアントの作り方と、そのサンプルプログラムの紹介を行いました!

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

HTTP クライアント作成時のポイントの1つは IP アドレスを DNS サーバーから取得するところだと思います。

こんな感じで、HTTP クライアントプログラムを作成してみることで、単にプログラミング力を付けるだけでなく、ウェブページを表示する際の動作の知識もつけることが出来ます。

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

今回は HTTP クライアントの作り方の解説を行いましたが、下記ページでは HTTP サーバーの作り方の解説も行っています!

もっとソケット通信の具体的な使い方・サーバー・HTTP 等について知りたい方は是非読んでみてください!

HTTPサーバーの作り方の解説ページアイキャッチ【C言語】簡単な「HTTPサーバー」を作る

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です