第3章:HTTPプロトコル

はじめに

HTTP(HyperText Transfer Protocol)はWebの基盤となるプロトコルです。本章では、HTTP/1.1リクエストとレスポンスのパースを学びます。

---

1. HTTPリクエストのパース

1.1 リクエストの構造

POST /upload HTTP/1.1\r\n
Host: localhost:8080\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 27\r\n
\r\n
name=John&message=Hello+World

+------------------+
| Request Line     |  メソッド SP URI SP バージョン CRLF
+------------------+
| Headers          |  名前: 値 CRLF (複数行)
+------------------+
| Empty Line       |  CRLF
+------------------+
| Body (optional)  |  リクエストボディ
+------------------+

1.2 リクエストクラス

class Request {
public:
    enum Method {
        GET,
        POST,
        DELETE,
        UNKNOWN
    };

    enum ParseState {
        REQUEST_LINE,
        HEADERS,
        BODY,
        COMPLETE,
        ERROR
    };

private:
    Method      _method;
    std::string _uri;
    std::string _version;
    std::map<std::string, std::string> _headers;
    std::string _body;
    ParseState  _state;
    size_t      _contentLength;
    bool        _chunked;
    std::string _buffer;

public:
    Request();
    void parse(const std::string& data);
    bool isComplete() const { return _state == COMPLETE; }
    bool hasError() const { return _state == ERROR; }

    Method getMethod() const { return _method; }
    const std::string& getUri() const { return _uri; }
    const std::string& getHeader(const std::string& name) const;
    const std::string& getBody() const { return _body; }
};

1.3 リクエストラインのパース

bool Request::parseRequestLine(const std::string& line) {
    std::istringstream iss(line);
    std::string method, uri, version;

    if (!(iss >> method >> uri >> version)) {
        return false;
    }

    // メソッドの判定
    if (method == "GET") {
        _method = GET;
    } else if (method == "POST") {
        _method = POST;
    } else if (method == "DELETE") {
        _method = DELETE;
    } else {
        _method = UNKNOWN;
    }

    // URIのデコード
    _uri = urlDecode(uri);

    // バージョンの確認
    if (version != "HTTP/1.1" && version != "HTTP/1.0") {
        return false;
    }
    _version = version;

    return true;
}

1.4 ヘッダーのパース

bool Request::parseHeader(const std::string& line) {
    size_t colonPos = line.find(':');
    if (colonPos == std::string::npos) {
        return false;
    }

    std::string name = line.substr(0, colonPos);
    std::string value = line.substr(colonPos + 1);

    // 前後の空白を削除
    value = trim(value);

    // ヘッダー名を小文字に正規化
    std::transform(name.begin(), name.end(), name.begin(), ::tolower);

    _headers[name] = value;

    // 特別なヘッダーの処理
    if (name == "content-length") {
        _contentLength = std::stoul(value);
    } else if (name == "transfer-encoding" && value == "chunked") {
        _chunked = true;
    }

    return true;
}

1.5 状態マシンによるパース

void Request::parse(const std::string& data) {
    _buffer += data;

    while (_state != COMPLETE && _state != ERROR) {
        if (_state == REQUEST_LINE) {
            size_t pos = _buffer.find("\r\n");
            if (pos == std::string::npos) break;

            std::string line = _buffer.substr(0, pos);
            _buffer = _buffer.substr(pos + 2);

            if (!parseRequestLine(line)) {
                _state = ERROR;
                break;
            }
            _state = HEADERS;
        }
        else if (_state == HEADERS) {
            size_t pos = _buffer.find("\r\n");
            if (pos == std::string::npos) break;

            std::string line = _buffer.substr(0, pos);
            _buffer = _buffer.substr(pos + 2);

            if (line.empty()) {
                // 空行 = ヘッダー終了
                if (_method == GET || _contentLength == 0) {
                    _state = COMPLETE;
                } else {
                    _state = BODY;
                }
            } else {
                if (!parseHeader(line)) {
                    _state = ERROR;
                    break;
                }
            }
        }
        else if (_state == BODY) {
            if (_chunked) {
                parseChunkedBody();
            } else {
                if (_buffer.size() >= _contentLength) {
                    _body = _buffer.substr(0, _contentLength);
                    _buffer = _buffer.substr(_contentLength);
                    _state = COMPLETE;
                } else {
                    break;  // データ待ち
                }
            }
        }
    }
}

---

2. チャンク転送エンコーディング

2.1 形式

POST /upload HTTP/1.1
Transfer-Encoding: chunked

7\r\n
Hello, \r\n
6\r\n
World!\r\n
0\r\n
\r\n

チャンクサイズ(16進数)\r\n
チャンクデータ\r\n
...
0\r\n      ← 最終チャンク
\r\n       ← トレーラー終了

2.2 パース実装

void Request::parseChunkedBody() {
    while (true) {
        // チャンクサイズを探す
        size_t pos = _buffer.find("\r\n");
        if (pos == std::string::npos) break;

        std::string sizeStr = _buffer.substr(0, pos);
        size_t chunkSize = std::stoul(sizeStr, nullptr, 16);

        // 最終チャンク
        if (chunkSize == 0) {
            _buffer = _buffer.substr(pos + 2);
            // トレーラーをスキップ
            pos = _buffer.find("\r\n");
            if (pos != std::string::npos) {
                _buffer = _buffer.substr(pos + 2);
                _state = COMPLETE;
            }
            break;
        }

        // チャンクデータが揃っているか確認
        if (_buffer.size() < pos + 2 + chunkSize + 2) break;

        // チャンクデータを追加
        _body += _buffer.substr(pos + 2, chunkSize);
        _buffer = _buffer.substr(pos + 2 + chunkSize + 2);
    }
}

---

3. URLデコード

3.1 パーセントエンコーディング

エンコード例:
スペース → %20 または +
日本語   → %E6%97%A5%E6%9C%AC%E8%AA%9E
/        → %2F(パス区切り以外)

3.2 デコード実装

std::string urlDecode(const std::string& str) {
    std::string result;

    for (size_t i = 0; i < str.size(); ++i) {
        if (str[i] == '%' && i + 2 < str.size()) {
            // %XX → 文字に変換
            int value;
            std::istringstream iss(str.substr(i + 1, 2));
            if (iss >> std::hex >> value) {
                result += static_cast<char>(value);
                i += 2;
            } else {
                result += str[i];
            }
        } else if (str[i] == '+') {
            result += ' ';
        } else {
            result += str[i];
        }
    }

    return result;
}

---

4. クエリパラメータ

4.1 形式

GET /search?q=hello&lang=ja HTTP/1.1

URI = パス + "?" + クエリ文字列
クエリ = key1=value1&key2=value2...

4.2 パース実装

std::map<std::string, std::string> parseQueryString(const std::string& query) {
    std::map<std::string, std::string> params;

    std::istringstream iss(query);
    std::string pair;

    while (std::getline(iss, pair, '&')) {
        size_t pos = pair.find('=');
        if (pos != std::string::npos) {
            std::string key = urlDecode(pair.substr(0, pos));
            std::string value = urlDecode(pair.substr(pos + 1));
            params[key] = value;
        }
    }

    return params;
}

---

5. HTTPレスポンスの生成

5.1 レスポンスクラス

class Response {
private:
    int         _statusCode;
    std::string _statusMessage;
    std::map<std::string, std::string> _headers;
    std::string _body;

public:
    Response(int code = 200);

    void setStatus(int code, const std::string& message);
    void setHeader(const std::string& name, const std::string& value);
    void setBody(const std::string& body);
    void setBody(const std::vector<char>& body);

    std::string toString() const;
};

Response::Response(int code) : _statusCode(code) {
    _statusMessage = getReasonPhrase(code);
    setHeader("Server", "webserv/1.0");
    setHeader("Date", getCurrentDate());
}

std::string Response::toString() const {
    std::ostringstream oss;

    // ステータスライン
    oss << "HTTP/1.1 " << _statusCode << " " << _statusMessage << "\r\n";

    // ヘッダー
    for (const auto& header : _headers) {
        oss << header.first << ": " << header.second << "\r\n";
    }

    // Content-Length(自動設定)
    if (_headers.find("Content-Length") == _headers.end()) {
        oss << "Content-Length: " << _body.size() << "\r\n";
    }

    // 空行
    oss << "\r\n";

    // ボディ
    oss << _body;

    return oss.str();
}

5.2 日付のフォーマット

std::string getCurrentDate() {
    time_t now = time(NULL);
    struct tm* gmt = gmtime(&now);

    char buffer[100];
    strftime(buffer, sizeof(buffer),
             "%a, %d %b %Y %H:%M:%S GMT", gmt);

    return std::string(buffer);
}

// 出力例: "Mon, 01 Jan 2024 00:00:00 GMT"

---

6. MIMEタイプ

6.1 Content-Typeの設定

std::string getMimeType(const std::string& path) {
    size_t pos = path.rfind('.');
    if (pos == std::string::npos) {
        return "application/octet-stream";
    }

    std::string ext = path.substr(pos);

    static std::map<std::string, std::string> mimeTypes = {
        {".html", "text/html"},
        {".htm",  "text/html"},
        {".css",  "text/css"},
        {".js",   "application/javascript"},
        {".json", "application/json"},
        {".png",  "image/png"},
        {".jpg",  "image/jpeg"},
        {".jpeg", "image/jpeg"},
        {".gif",  "image/gif"},
        {".svg",  "image/svg+xml"},
        {".ico",  "image/x-icon"},
        {".txt",  "text/plain"},
        {".pdf",  "application/pdf"},
        {".xml",  "application/xml"}
    };

    auto it = mimeTypes.find(ext);
    if (it != mimeTypes.end()) {
        return it->second;
    }

    return "application/octet-stream";
}

---

7. Keep-Alive

7.1 HTTP/1.1の持続的接続

HTTP/1.0: デフォルトで接続を閉じる
          Connection: keep-alive で維持

HTTP/1.1: デフォルトで接続を維持
          Connection: close で閉じる

7.2 実装の考慮点

bool shouldKeepAlive(const Request& req) {
    std::string connection = req.getHeader("connection");

    // 小文字に変換
    std::transform(connection.begin(), connection.end(),
                   connection.begin(), ::tolower);

    if (req.getVersion() == "HTTP/1.1") {
        // HTTP/1.1はデフォルトでkeep-alive
        return connection != "close";
    } else {
        // HTTP/1.0はデフォルトで閉じる
        return connection == "keep-alive";
    }
}

---

まとめ

本章で学んだこと:

  • リクエスト構造: リクエストライン、ヘッダー、ボディ
  • 状態マシン: インクリメンタルなパース
  • チャンク転送: Transfer-Encoding: chunked
  • URLデコード: パーセントエンコーディング
  • レスポンス生成: ステータスライン、ヘッダー
  • MIMEタイプ: Content-Typeの設定
  • Keep-Alive: 持続的接続

次章では、I/O多重化を学びます。