【C++】C++でHTTPサーバを実装してみた

はじめに

サーバの理解を深めるためになんとしてもC++でサーバを実装してみたい!と思い、実装してみました。

今回のゴール

ブラウザに"Hello World!"と出力する。

エンドポイントの作成

各説明は以下より引用

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

#include <sys/types.h>
#include <sys/socket.h>

....

int socket(int domain, int type, int protocol);  

socketは通信のためのエンドポイントを作成し、ディスクリプタを返します。

domain

domainはどのプロトコルファミリを通信に使用するか指定するためのドメインを指定します。

以下のようなものがあります。

名前 説明
AF_UNIX, AF_LOCAL ローカル通信
AF_INET IPv4 インターネットプロトコルを使用しての通信
AF_INET6 IPv6 インターネットプロトコルを使用しての通信
AF_PACKET 低レベルのパケットインターフェース
AF_ALG カーネルの暗号APIへのインターフェース

type

typeは通信方式を指定します。

(ほとんどのサンプルがSOCK_STREAMを使ってた)

名前 説明
SOCK_STREAM 順序性と信頼性があり、双方向の、接続された バイトストリーム (byte stream) を提供する
SOCK_DGRAM データグラム (コネクションレス、信頼性無し、固定最大長メッセージ) をサポートする。
SOCK_SEQPACKET 固定最大長のデータグラム転送パスに基づいた順序性、信頼性のある 双方向の接続に基づいた通信を提供する。受け取り側ではそれぞれの入力 システムコールでパケット全体を読み取ることが要求される。
SOCK_RAW 生のネットワークプロトコルへのアクセスを提供する。
SOCK_RDM 信頼性はあるが、順序は保証しないデータグラム層を提供する。
SOCK_NONBLOCK 新しく生成されるオープンファイル記述 (open file description) の O_NONBLOCK ファイルステータスフラグをセットする。
SOCK_CLOEXEC 新しいファイルディスクリプターに対して close-on-exec (FD_CLOEXEC) フラグをセットする。

sockaddr_in構造体

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

sin_familyにはAF_INETをセットする。

sin_portはポート番号

sin_addrはIPホストアドレス 詳しくは以下を参照

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/ip.7.html

bind

ソケットに名前をつける

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

bindはsockfd(ファイルディスクリプタ)参照されるソケットにaddrで指定されたアドレスを割り当てます。

詳しくは以下参照

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/ip.7.html

listen

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

listenはsockfdが参照するソケットを接続待ちソケットしてマークする。

sockfd についての保留中の接続のキューの最大長を指定する。

詳しくは以下参照

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

accept

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

ソケットへの接続を受けます。

引数のaddrはsockaddr構造体へのポインタ。接続相手のソケットのアドレス。

addrlenは接続相手のアドレスの大きさが入る。

詳しくは以下参照

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

recv

接続されたソケットまたはバインドされた非接続ソケットからデータを受診します。

int recv(int sockfd,  char* buf, int len, int flags
);

bufは受診データを格納するバッファへのポインタ

lenはbufのバッファの長さ

詳しくは以下参照

recv

send

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ソケットへメッセージを送信します。

送信するメッセージをbufに格納し、その長さをlenで指定します。

lflagsに指定するフラグは以下のフラグの論理和をとったものを指定します。

名前 説明
MSG_CONFIRM 転送処理に進展があった、つまり相手側から成功の応答を受けたことをリンク層に知らせる
MSG_DONTROUTE パケットを送り出すのにゲートウェイを使用せず、 直接接続されているネットワーク上のホストだけに送る。
MSG_DONTWAIT 非停止 (nonblocking) 操作を有効にする。
MSG_EOR レコードの終了を指示する (SOCK_SEQPACKET のようにこの概念に対応しているソケット種別のときに有効)。
MSG_MORE 呼び出し元にさらに送るデータがあることを示す。 このフラグは TCP ソケットとともに使用され、 TCP_CORK ソケットオプションと同じ効果が得られる
MSG_NOSIGNAL ストリーム指向のソケットで相手側が接続を切断した時に、エラーとして SIGPIPE を送信しないように要求する。
MSG_OOB 帯域外 (out-of-band) データをサポートするソケット (例えば SOCK_STREAM) で 帯域外 データを送る。

詳しくは以下参照

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

最終的なコード

今回書いたコードは以下のようになりました。

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <vector>
#include <string>
#include <sstream>
#include <iostream>


int main()
{
    auto sock = socket(AF_INET, SOCK_STREAM, 0);

    if (!sock) {
        std::cout << "Failed to initialize a socket." << std::endl;
        return 1;
    }

    struct sockaddr_in server_addr;

    memset(&server_addr, 0, sizeof(server_addr));

    int port = 5000;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);

    int optval = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
        std::cout << "Failed to setsocket" << std::endl;
        close(sock);
        return 1;
    }

    if (bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {
        std::cout << "Failed to bind. error:" << strerror(errno) << std::endl;
        close(sock);
        return 1;
    }

    if (listen(sock, 5) != 0) {
        std::cout << "Failed to listen. error:" << errno << std::endl;
        close(sock);
        return 1;
    }

    auto body = std::string("Hello World!");
    auto response = std::string("");
    std::ostringstream oss;

    oss << "Content-Length: " << 20 << "\r\n";

    response.append("HTTP/1.1 200 OK\r\n");
    response.append("Content-Type: text/html; charset=UTF-8\r\n");
    response.append(oss.str());
    response.append("Connection: Keep-Alive\r\n");
    response.append("\r\n");
    response.append(body);

    std::cout << "response:" << response << std::endl;

    char inbuf[2048];

    while (true) {
        auto connfd = accept(sock, nullptr, nullptr);
        if (connfd < 0) {
            std::cout << "Failed to accept." << std::endl;
            break;
        }

        memset(inbuf, 0, sizeof(inbuf));
        recv(connfd, inbuf, sizeof(inbuf), 0);

        if (send(connfd, response.c_str(), response.length(), 0) == -1) {
            std::cout << "Failed to send." << std::endl;
        }
    }

    close(sock);

    return 0;
}

実行結果

portを5000に指定したので

http://127.0.0.1:5000/にアクセスしてみます

無事Hello Worldと出力されました!

f:id:dasuko:20200731202908p:plain

まとめ

今回はIPv4TCPのサーバを実装してみましたが、

IPv6 サーバの実装はこの辺りを参考にするといいかと思います。

IPv6 server & client in C · GitHub

参考

HTTPサーバの作成(TCPサーバサンプル):Geekなぺーじ

Webサーバの動作を理解するための事前知識とC++によるフルスクラッチで実装して理解を深めよう - Qiita

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

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/ip.7.html

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

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

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

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

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

recv