第6章:設定とテスト
はじめに
Webサーバーは、設定ファイルによって動作を柔軟に変更できる必要があります。本章では、Nginx風の設定ファイルのパースと、サーバーのテスト方法を学びます。
---
1. 設定ファイルの構造
1.1 Nginx風の設定
# webserv.conf
server {
listen 8080;
server_name localhost;
root /var/www/html;
index index.html index.htm;
client_max_body_size 10m;
error_page 404 /error/404.html;
error_page 500 502 503 504 /error/50x.html;
location / {
autoindex on;
allow_methods GET POST;
}
location /upload {
allow_methods POST;
upload_store /var/www/uploads;
}
location /api {
allow_methods GET POST DELETE;
return 301 /api/v2;
}
location /cgi-bin {
cgi_pass /usr/bin/python3;
cgi_extension .py;
}
}
server {
listen 8081;
server_name example.com www.example.com;
root /var/www/example;
}
1.2 設定項目一覧
サーバーレベル:
├── listen # ポート番号(必須)
├── server_name # サーバー名(ホスト名マッチング)
├── root # ドキュメントルート
├── index # デフォルトファイル
├── client_max_body_size # 最大ボディサイズ
└── error_page # エラーページ
ロケーションレベル:
├── root / alias # パス設定
├── autoindex # ディレクトリリスト
├── allow_methods # 許可するメソッド
├── return # リダイレクト
├── cgi_pass # CGIインタープリタ
├── cgi_extension # CGI拡張子
└── upload_store # アップロードディレクトリ
---
2. 設定のデータ構造
2.1 設定クラス
struct LocationConfig {
std::string path;
std::string root;
std::string alias;
std::vector<std::string> index;
bool autoindex;
std::vector<std::string> allowMethods;
std::string returnCode;
std::string returnUrl;
std::string cgiPass;
std::string cgiExtension;
std::string uploadStore;
LocationConfig()
: autoindex(false) {
allowMethods = {"GET"}; // デフォルト
}
};
struct ServerConfig {
int port;
std::vector<std::string> serverNames;
std::string root;
std::vector<std::string> index;
size_t clientMaxBodySize;
std::map<int, std::string> errorPages;
std::vector<LocationConfig> locations;
ServerConfig()
: port(80),
clientMaxBodySize(1024 * 1024) { // 1MB
index = {"index.html"};
}
};
class Config {
private:
std::vector<ServerConfig> _servers;
public:
void parse(const std::string& filename);
const std::vector<ServerConfig>& getServers() const { return _servers; }
const ServerConfig* findServer(int port, const std::string& host) const;
const LocationConfig* findLocation(const ServerConfig& server,
const std::string& uri) const;
};
---
3. 設定ファイルのパース
3.1 トークナイザー
enum TokenType {
TOKEN_STRING,
TOKEN_OPEN_BRACE, // {
TOKEN_CLOSE_BRACE, // }
TOKEN_SEMICOLON, // ;
TOKEN_EOF
};
struct Token {
TokenType type;
std::string value;
int line;
int column;
};
class Tokenizer {
private:
std::string _content;
size_t _pos;
int _line;
int _column;
public:
Tokenizer(const std::string& content)
: _content(content), _pos(0), _line(1), _column(1) {}
Token nextToken() {
skipWhitespaceAndComments();
if (_pos >= _content.size()) {
return {TOKEN_EOF, "", _line, _column};
}
char c = _content[_pos];
if (c == '{') {
_pos++; _column++;
return {TOKEN_OPEN_BRACE, "{", _line, _column - 1};
}
if (c == '}') {
_pos++; _column++;
return {TOKEN_CLOSE_BRACE, "}", _line, _column - 1};
}
if (c == ';') {
_pos++; _column++;
return {TOKEN_SEMICOLON, ";", _line, _column - 1};
}
return readString();
}
private:
void skipWhitespaceAndComments() {
while (_pos < _content.size()) {
char c = _content[_pos];
if (std::isspace(c)) {
if (c == '\n') {
_line++;
_column = 1;
} else {
_column++;
}
_pos++;
} else if (c == '#') {
// コメント行末まで
while (_pos < _content.size() && _content[_pos] != '\n') {
_pos++;
}
} else {
break;
}
}
}
Token readString() {
int startColumn = _column;
std::string value;
// クォートされた文字列
if (_content[_pos] == '"' || _content[_pos] == '\'') {
char quote = _content[_pos++];
_column++;
while (_pos < _content.size() && _content[_pos] != quote) {
if (_content[_pos] == '\\' && _pos + 1 < _content.size()) {
_pos++;
_column++;
}
value += _content[_pos++];
_column++;
}
if (_pos < _content.size()) {
_pos++; // 閉じクォート
_column++;
}
} else {
// 通常の文字列
while (_pos < _content.size()) {
char c = _content[_pos];
if (std::isspace(c) || c == '{' || c == '}' || c == ';') {
break;
}
value += c;
_pos++;
_column++;
}
}
return {TOKEN_STRING, value, _line, startColumn};
}
};
3.2 パーサー
class ConfigParser {
private:
Tokenizer _tokenizer;
Token _currentToken;
public:
ConfigParser(const std::string& content)
: _tokenizer(content) {
_currentToken = _tokenizer.nextToken();
}
std::vector<ServerConfig> parse() {
std::vector<ServerConfig> servers;
while (_currentToken.type != TOKEN_EOF) {
if (_currentToken.value == "server") {
servers.push_back(parseServer());
} else {
throw ConfigError("Expected 'server', got '" +
_currentToken.value + "'",
_currentToken.line);
}
}
return servers;
}
private:
void advance() {
_currentToken = _tokenizer.nextToken();
}
void expect(TokenType type) {
if (_currentToken.type != type) {
throw ConfigError("Unexpected token", _currentToken.line);
}
advance();
}
void expectString(const std::string& value) {
if (_currentToken.type != TOKEN_STRING ||
_currentToken.value != value) {
throw ConfigError("Expected '" + value + "'", _currentToken.line);
}
advance();
}
ServerConfig parseServer() {
ServerConfig server;
expectString("server");
expect(TOKEN_OPEN_BRACE);
while (_currentToken.type != TOKEN_CLOSE_BRACE) {
parseServerDirective(server);
}
expect(TOKEN_CLOSE_BRACE);
return server;
}
void parseServerDirective(ServerConfig& server) {
std::string directive = _currentToken.value;
advance();
if (directive == "listen") {
server.port = std::stoi(_currentToken.value);
advance();
expect(TOKEN_SEMICOLON);
}
else if (directive == "server_name") {
while (_currentToken.type == TOKEN_STRING) {
server.serverNames.push_back(_currentToken.value);
advance();
}
expect(TOKEN_SEMICOLON);
}
else if (directive == "root") {
server.root = _currentToken.value;
advance();
expect(TOKEN_SEMICOLON);
}
else if (directive == "index") {
server.index.clear();
while (_currentToken.type == TOKEN_STRING) {
server.index.push_back(_currentToken.value);
advance();
}
expect(TOKEN_SEMICOLON);
}
else if (directive == "client_max_body_size") {
server.clientMaxBodySize = parseSize(_currentToken.value);
advance();
expect(TOKEN_SEMICOLON);
}
else if (directive == "error_page") {
std::vector<int> codes;
while (_currentToken.type == TOKEN_STRING &&
std::isdigit(_currentToken.value[0])) {
codes.push_back(std::stoi(_currentToken.value));
advance();
}
std::string page = _currentToken.value;
advance();
expect(TOKEN_SEMICOLON);
for (int code : codes) {
server.errorPages[code] = page;
}
}
else if (directive == "location") {
server.locations.push_back(parseLocation());
}
else {
throw ConfigError("Unknown directive: " + directive,
_currentToken.line);
}
}
LocationConfig parseLocation() {
LocationConfig location;
location.path = _currentToken.value;
advance();
expect(TOKEN_OPEN_BRACE);
while (_currentToken.type != TOKEN_CLOSE_BRACE) {
parseLocationDirective(location);
}
expect(TOKEN_CLOSE_BRACE);
return location;
}
void parseLocationDirective(LocationConfig& location) {
std::string directive = _currentToken.value;
advance();
if (directive == "root") {
location.root = _currentToken.value;
advance();
expect(TOKEN_SEMICOLON);
}
else if (directive == "alias") {
location.alias = _currentToken.value;
advance();
expect(TOKEN_SEMICOLON);
}
else if (directive == "autoindex") {
location.autoindex = (_currentToken.value == "on");
advance();
expect(TOKEN_SEMICOLON);
}
else if (directive == "allow_methods") {
location.allowMethods.clear();
while (_currentToken.type == TOKEN_STRING) {
location.allowMethods.push_back(_currentToken.value);
advance();
}
expect(TOKEN_SEMICOLON);
}
else if (directive == "return") {
location.returnCode = _currentToken.value;
advance();
if (_currentToken.type == TOKEN_STRING) {
location.returnUrl = _currentToken.value;
advance();
}
expect(TOKEN_SEMICOLON);
}
else if (directive == "cgi_pass") {
location.cgiPass = _currentToken.value;
advance();
expect(TOKEN_SEMICOLON);
}
else if (directive == "cgi_extension") {
location.cgiExtension = _currentToken.value;
advance();
expect(TOKEN_SEMICOLON);
}
else if (directive == "upload_store") {
location.uploadStore = _currentToken.value;
advance();
expect(TOKEN_SEMICOLON);
}
else {
throw ConfigError("Unknown location directive: " + directive,
_currentToken.line);
}
}
size_t parseSize(const std::string& str) {
size_t value = 0;
size_t i = 0;
while (i < str.size() && std::isdigit(str[i])) {
value = value * 10 + (str[i] - '0');
i++;
}
if (i < str.size()) {
char unit = std::tolower(str[i]);
if (unit == 'k') {
value *= 1024;
} else if (unit == 'm') {
value *= 1024 * 1024;
} else if (unit == 'g') {
value *= 1024 * 1024 * 1024;
}
}
return value;
}
};
---
4. サーバー/ロケーションの選択
4.1 サーバー選択
const ServerConfig* Config::findServer(int port, const std::string& host) const {
const ServerConfig* defaultServer = nullptr;
for (const auto& server : _servers) {
if (server.port != port) continue;
// デフォルトサーバー(最初のマッチ)
if (!defaultServer) {
defaultServer = &server;
}
// server_nameでマッチング
for (const auto& name : server.serverNames) {
if (name == host) {
return &server;
}
// ワイルドカードマッチング
if (name[0] == '*') {
std::string suffix = name.substr(1);
if (host.size() >= suffix.size() &&
host.substr(host.size() - suffix.size()) == suffix) {
return &server;
}
}
}
}
return defaultServer;
}
4.2 ロケーション選択
const LocationConfig* Config::findLocation(const ServerConfig& server,
const std::string& uri) const {
const LocationConfig* bestMatch = nullptr;
size_t bestMatchLength = 0;
for (const auto& location : server.locations) {
const std::string& path = location.path;
// プレフィックスマッチング
if (uri.find(path) == 0) {
if (path.size() > bestMatchLength) {
bestMatchLength = path.size();
bestMatch = &location;
}
}
}
return bestMatch;
}
---
5. 設定のバリデーション
5.1 必須項目のチェック
void Config::validate() const {
for (size_t i = 0; i < _servers.size(); ++i) {
const ServerConfig& server = _servers[i];
// ポートのチェック
if (server.port <= 0 || server.port > 65535) {
throw ConfigError("Invalid port: " + std::to_string(server.port));
}
// rootのチェック
if (server.root.empty()) {
throw ConfigError("Server " + std::to_string(i) + ": root is required");
}
// rootディレクトリの存在確認
struct stat st;
if (stat(server.root.c_str(), &st) < 0 || !S_ISDIR(st.st_mode)) {
throw ConfigError("Root directory does not exist: " + server.root);
}
// ポートの重複チェック
for (size_t j = 0; j < i; ++j) {
if (_servers[j].port == server.port) {
// 同じポートの場合、server_nameが必要
if (server.serverNames.empty()) {
throw ConfigError("Duplicate port " +
std::to_string(server.port) +
" requires server_name");
}
}
}
// ロケーションの検証
for (const auto& location : server.locations) {
validateLocation(location);
}
}
}
void Config::validateLocation(const LocationConfig& location) const {
// メソッドの検証
for (const auto& method : location.allowMethods) {
if (method != "GET" && method != "POST" && method != "DELETE") {
throw ConfigError("Unknown method: " + method);
}
}
// CGI設定の検証
if (!location.cgiPass.empty()) {
if (location.cgiExtension.empty()) {
throw ConfigError("cgi_pass requires cgi_extension");
}
struct stat st;
if (stat(location.cgiPass.c_str(), &st) < 0) {
throw ConfigError("CGI interpreter not found: " + location.cgiPass);
}
}
// アップロードディレクトリの検証
if (!location.uploadStore.empty()) {
struct stat st;
if (stat(location.uploadStore.c_str(), &st) < 0 ||
!S_ISDIR(st.st_mode)) {
throw ConfigError("Upload directory does not exist: " +
location.uploadStore);
}
}
}
---
6. テスト手法
6.1 curlによるテスト
# 基本的なGETリクエスト
curl -v http://localhost:8080/
# ヘッダーのみ取得
curl -I http://localhost:8080/
# POSTリクエスト
curl -X POST -d "name=John&email=john@example.com" http://localhost:8080/form
# ファイルアップロード
curl -X POST -F "file=@test.txt" http://localhost:8080/upload
# JSONデータ送信
curl -X POST -H "Content-Type: application/json" \
-d '{"key":"value"}' http://localhost:8080/api
# DELETEリクエスト
curl -X DELETE http://localhost:8080/file.txt
# カスタムヘッダー
curl -H "Host: example.com" http://localhost:8080/
# タイムアウト設定
curl --connect-timeout 5 --max-time 10 http://localhost:8080/
# 詳細出力
curl -v --trace-time http://localhost:8080/
6.2 telnetによる生リクエスト
# 接続
telnet localhost 8080
# リクエストを手動入力
GET / HTTP/1.1
Host: localhost
Connection: close
# 空行を入力してリクエスト送信
6.3 netcatによるテスト
# リクエストを送信
echo -e "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" | nc localhost 8080
# ファイルからリクエストを送信
nc localhost 8080 < request.txt
# 接続維持
nc -k localhost 8080
---
7. 負荷テスト
7.1 siege
# インストール
brew install siege # macOS
apt install siege # Ubuntu
# 基本的な負荷テスト
siege -c 100 -t 30s http://localhost:8080/
# オプション
# -c: 同時接続数
# -t: テスト時間
# -r: リクエスト回数
# -b: ベンチマークモード(遅延なし)
# 複数URLのテスト
siege -c 50 -t 30s -f urls.txt
# 結果の例
# Transactions: 10000 hits
# Availability: 100.00 %
# Elapsed time: 29.95 secs
# Data transferred: 12.34 MB
# Response time: 0.15 secs
# Transaction rate: 334.00 trans/sec
# Throughput: 0.41 MB/sec
# Concurrency: 49.50
# Successful transactions: 10000
# Failed transactions: 0
7.2 ab(Apache Bench)
# 基本的なテスト
ab -n 10000 -c 100 http://localhost:8080/
# オプション
# -n: 総リクエスト数
# -c: 同時接続数
# -k: Keep-Alive有効
# -t: テスト時間
# POST テスト
ab -n 1000 -c 50 -p data.txt -T "application/x-www-form-urlencoded" \
http://localhost:8080/form
7.3 wrk
# インストール
brew install wrk # macOS
# 基本的なテスト
wrk -t 4 -c 100 -d 30s http://localhost:8080/
# オプション
# -t: スレッド数
# -c: 接続数
# -d: テスト時間
# Luaスクリプトでカスタムリクエスト
wrk -t 4 -c 100 -d 30s -s custom.lua http://localhost:8080/
---
8. デバッグ手法
8.1 ログ出力
enum LogLevel {
LOG_DEBUG,
LOG_INFO,
LOG_WARN,
LOG_ERROR
};
class Logger {
private:
LogLevel _level;
std::ostream& _out;
public:
Logger(LogLevel level = LOG_INFO, std::ostream& out = std::cerr)
: _level(level), _out(out) {}
void debug(const std::string& msg) { log(LOG_DEBUG, msg); }
void info(const std::string& msg) { log(LOG_INFO, msg); }
void warn(const std::string& msg) { log(LOG_WARN, msg); }
void error(const std::string& msg) { log(LOG_ERROR, msg); }
private:
void log(LogLevel level, const std::string& msg) {
if (level < _level) return;
static const char* levelNames[] = {"DEBUG", "INFO", "WARN", "ERROR"};
time_t now = time(NULL);
struct tm* tm = localtime(&now);
char timeStr[64];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", tm);
_out << "[" << timeStr << "] "
<< "[" << levelNames[level] << "] "
<< msg << std::endl;
}
};
// グローバルロガー
Logger g_logger(LOG_DEBUG);
// 使用例
g_logger.info("Server started on port 8080");
g_logger.debug("Request: GET /index.html");
g_logger.error("Failed to open file: " + path);
8.2 リクエスト/レスポンスのダンプ
void dumpRequest(const Request& req) {
std::cerr << "=== REQUEST ===" << std::endl;
std::cerr << methodToString(req.getMethod()) << " "
<< req.getUri() << " "
<< req.getVersion() << std::endl;
for (const auto& header : req.getHeaders()) {
std::cerr << header.first << ": " << header.second << std::endl;
}
if (!req.getBody().empty()) {
std::cerr << std::endl;
std::cerr << "[Body: " << req.getBody().size() << " bytes]" << std::endl;
}
std::cerr << "===============" << std::endl;
}
void dumpResponse(const Response& resp) {
std::cerr << "=== RESPONSE ===" << std::endl;
std::cerr << "HTTP/1.1 " << resp.getStatusCode() << " "
<< resp.getStatusMessage() << std::endl;
for (const auto& header : resp.getHeaders()) {
std::cerr << header.first << ": " << header.second << std::endl;
}
std::cerr << std::endl;
std::cerr << "[Body: " << resp.getBody().size() << " bytes]" << std::endl;
std::cerr << "================" << std::endl;
}
8.3 Valgrindによるメモリチェック
# メモリリーク検出
valgrind --leak-check=full --show-leak-kinds=all ./webserv config.conf
# 詳細な追跡
valgrind --track-origins=yes ./webserv config.conf
# ファイルディスクリプタリーク
valgrind --track-fds=yes ./webserv config.conf
8.4 straceによるシステムコール追跡
# すべてのシステムコール
strace ./webserv config.conf
# ネットワーク関連のみ
strace -e trace=network ./webserv config.conf
# ファイル関連のみ
strace -e trace=file ./webserv config.conf
# 時間計測
strace -T ./webserv config.conf
---
9. 自動テストスクリプト
9.1 Pythonテストスクリプト
#!/usr/bin/env python3
# test_webserv.py
import requests
import subprocess
import time
import sys
BASE_URL = "http://localhost:8080"
def test_get_index():
"""GETリクエストのテスト"""
r = requests.get(f"{BASE_URL}/")
assert r.status_code == 200
assert "Content-Type" in r.headers
print("✓ GET / passed")
def test_404():
"""404エラーのテスト"""
r = requests.get(f"{BASE_URL}/nonexistent")
assert r.status_code == 404
print("✓ 404 test passed")
def test_post():
"""POSTリクエストのテスト"""
data = {"name": "test", "value": "123"}
r = requests.post(f"{BASE_URL}/form", data=data)
assert r.status_code in [200, 201]
print("✓ POST test passed")
def test_upload():
"""ファイルアップロードのテスト"""
files = {"file": ("test.txt", "Hello, World!", "text/plain")}
r = requests.post(f"{BASE_URL}/upload", files=files)
assert r.status_code in [200, 201]
print("✓ Upload test passed")
def test_delete():
"""DELETEリクエストのテスト"""
# まずファイルを作成
files = {"file": ("delete_me.txt", "test", "text/plain")}
r = requests.post(f"{BASE_URL}/upload", files=files)
assert r.status_code in [200, 201]
# 削除
r = requests.delete(f"{BASE_URL}/uploads/delete_me.txt")
assert r.status_code in [200, 204]
print("✓ DELETE test passed")
def test_large_request():
"""大きなリクエストのテスト"""
large_data = "x" * (1024 * 1024) # 1MB
r = requests.post(f"{BASE_URL}/upload", data=large_data)
# client_max_body_sizeに応じてステータスが変わる
print(f"✓ Large request test: status={r.status_code}")
def test_concurrent():
"""同時接続のテスト"""
import concurrent.futures
def make_request(i):
r = requests.get(f"{BASE_URL}/")
return r.status_code
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
futures = [executor.submit(make_request, i) for i in range(100)]
results = [f.result() for f in futures]
success = sum(1 for r in results if r == 200)
print(f"✓ Concurrent test: {success}/100 succeeded")
def run_all_tests():
tests = [
test_get_index,
test_404,
test_post,
test_upload,
test_delete,
test_large_request,
test_concurrent,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except Exception as e:
print(f"✗ {test.__name__} failed: {e}")
failed += 1
print(f"\n{passed} passed, {failed} failed")
return failed == 0
if __name__ == "__main__":
sys.exit(0 if run_all_tests() else 1)
9.2 シェルスクリプトテスト
#!/bin/bash
# test_webserv.sh
PORT=8080
PASS=0
FAIL=0
# テスト関数
test_request() {
local name="$1"
local expected_status="$2"
local curl_opts="$3"
status=$(curl -s -o /dev/null -w "%{http_code}" $curl_opts http://localhost:$PORT/)
if [ "$status" = "$expected_status" ]; then
echo "✓ $name (status: $status)"
((PASS++))
else
echo "✗ $name (expected: $expected_status, got: $status)"
((FAIL++))
fi
}
# GETテスト
test_request "GET /" "200" ""
test_request "GET /nonexistent" "404" "-X GET http://localhost:$PORT/nonexistent"
# POSTテスト
test_request "POST /form" "200" "-X POST -d 'test=data' http://localhost:$PORT/form"
# 結果
echo ""
echo "Passed: $PASS"
echo "Failed: $FAIL"
exit $FAIL
---
まとめ
本章で学んだこと:
- 設定ファイル構造: Nginx風の階層構造
- パーサー実装: トークナイザーとパーサー
- サーバー/ロケーション選択: マッチングアルゴリズム
- バリデーション: 設定の検証
- curlテスト: 基本的なHTTPテスト
- 負荷テスト: siege, ab, wrk
- デバッグ: ログ、Valgrind、strace
- 自動テスト: Python/シェルスクリプト
これでwebservの基本実装は完成です。実際のプロジェクトでは、エッジケースの処理やエラーハンドリングをさらに強化してください。