第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多重化を学びます。