[2026] C++ HTTP 기초 완벽 가이드 | 요청/응답 파싱·헤더·청크 인코딩·Beast 실전 [#30-1]
이 글의 핵심
C++ HTTP 기초 : 요청/응답 파싱·헤더·청크 인코딩·Beast 실전 [#30-1]. 실무에서 겪은 문제·HTTP 프로토콜 구조.
들어가며: “HTTP 요청 파싱이 버그 투성이예요”
문제 상황 1: 수동 파싱의 함정
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 문제: 직접 파싱하면 엣지 케이스에서 크래시
std::string parse_path(const std::string& raw) {
auto pos = raw.find(" ");
auto pos2 = raw.find(" ", pos + 1);
return raw.substr(pos + 1, pos2 - pos - 1); // 💥 공백 2개면? 빈 문자열?
}
// 문제점:
// - HTTP/1.0 vs HTTP/1.1 차이
// - 연속 공백, 탭, CRLF vs LF 혼용
// - 멀티바이트 문자 (Content-Length vs 실제 바이트)
// - 청크 인코딩: Transfer-Encoding: chunked 처리 누락
왜 이런 일이 발생할까요?
HTTP 프로토콜은 단순해 보이지만 엣지 케이스가 많습니다. \r\n vs \n, 연속 공백, 퍼센트 인코딩, 청크 인코딩, Keep-Alive 등 직접 파싱하면 버그가 쌓입니다.
추가 문제 시나리오
시나리오 2: Content-Length와 본문 불일치
클라이언트가 Content-Length: 100을 보냈는데 실제 본문이 50바이트만 오면, async_read가 영원히 대기합니다. 타임아웃 없으면 서버 스레드가 블로킹됩니다.
시나리오 3: 청크 인코딩 파싱 실패
스트리밍 응답에서 Transfer-Encoding: chunked를 처리하지 않으면, 본문을 끝까지 읽을 수 없어 응답이 잘립니다. 대용량 파일 다운로드·실시간 스트리밍에서 필수입니다.
시나리오 4: 헤더 대소문자·중복
Content-Type vs content-type, Set-Cookie가 여러 개 오는 경우를 처리하지 않으면 파싱 오류나 보안 취약점이 발생합니다.
시나리오 5: 요청이 여러 TCP 패킷에 분할
한 요청이 여러 async_read_some 호출에 걸쳐 도착합니다. “요청 완료” 시점을 정확히 판단하지 못하면 잘못된 데이터를 다음 요청으로 넘깁니다.
해결책:
- Boost.Beast: RFC 준수 파서, 에러 처리 내장
- flat_buffer: 파싱 중 데이터 보존
- http::read: 요청/응답 완료 시점 자동 판단
- chunked 인코딩: Beast가 자동 처리 목표:
- HTTP 요청/응답 구조 완전 이해
- 헤더 파싱 (대소문자, 중복, 인코딩)
- 청크 인코딩 (Transfer-Encoding: chunked)
- Beast 파서 사용법
- 일반적인 에러와 해결법
- 베스트 프랙티스와 프로덕션 패턴 요구 환경: Boost.Beast 1.70+, Boost.Asio 1.70+ 이 글을 읽으면:
- HTTP 프로토콜의 정확한 구조를 이해할 수 있습니다.
- Beast로 안전한 요청/응답 파싱을 구현할 수 있습니다.
- 프로덕션 수준의 HTTP 서버/클라이언트 기초를 다질 수 있습니다.
개념을 잡는 비유
소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- HTTP 프로토콜 구조
- 요청 파싱 (Request)
- 응답 파싱 (Response)
- 헤더 처리
- 청크 인코딩 (Chunked Transfer)
- Beast 기반 완전한 파서
- 일반적인 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
1. HTTP 프로토콜 구조
요청/응답 흐름
아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
sequenceDiagram
participant C as 클라이언트
participant S as 서버
C->>S: Request Line + Headers + CRLF + Body
Note over S: 파싱 → 라우팅 → 처리
S->>C: Status Line + Headers + CRLF + Body
HTTP 요청 구조
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
GET /api/users?id=1 HTTP/1.1\r\n
Host: example.com\r\n
Content-Type: application/json\r\n
Content-Length: 0\r\n
\r\n
구성 요소:
- Request Line:
METHOD SP Request-URI SP HTTP-Version CRLF - Headers:
Field-Name: Field-Value CRLF(반복) - 빈 줄:
CRLF(헤더와 본문 구분) - Body:
Content-Length또는Transfer-Encoding: chunked로 길이 결정
HTTP 응답 구조
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
HTTP/1.1 200 OK\r\n
Content-Type: application/json\r\n
Content-Length: 27\r\n
\r\n
{"message":"Hello World"}
구성 요소:
- Status Line:
HTTP-Version SP Status-Code SP Reason-Phrase CRLF - Headers: 요청과 동일 형식
- 빈 줄: 헤더와 본문 구분
- Body: 응답 본문
HTTP 메시지 파싱 시각화
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph Request[HTTP 요청]
RL[Request Line\nGET /path HTTP/1.1]
H1[Headers\nHost: example.com\nContent-Type: ...]
BL[빈 줄 CRLF]
BD[Body\n본문 데이터]
end
RL --> H1 --> BL --> BD
style RL fill:#4caf50
style BL fill:#ff9800
2. 요청 파싱 (Request)
Request Line 파싱
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string>
#include <sstream>
#include <stdexcept>
struct ParsedRequestLine {
std::string method; // GET, POST, ...
std::string path; // /api/users
std::string query; // id=1 (쿼리 스트링)
std::string version; // HTTP/1.1
};
ParsedRequestLine parse_request_line(const std::string& line) {
std::istringstream iss(line);
ParsedRequestLine result;
// METHOD SP Request-URI SP HTTP-Version
if (!(iss >> result.method >> result.path >> result.version)) {
throw std::runtime_error("Invalid request line");
}
// 쿼리 스트링 분리: /api/users?id=1 → path=/api/users, query=id=1
auto qpos = result.path.find('?');
if (qpos != std::string::npos) {
result.query = result.path.substr(qpos + 1);
result.path = result.path.substr(0, qpos);
}
return result;
}
// 사용 예
int main() {
auto parsed = parse_request_line("GET /api/users?id=1 HTTP/1.1");
// parsed.method == "GET"
// parsed.path == "/api/users"
// parsed.query == "id=1"
// parsed.version == "HTTP/1.1"
}
주의점: 실제 프로덕션에서는 퍼센트 인코딩(%20 → 공백) 디코딩, 경로 순회 공격(/../../../etc/passwd) 방지가 필요합니다. Beast는 이를 내장합니다.
헤더와 본문 구분
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// CRLF 두 번 연속 = 헤더 끝
std::pair<std::string, std::string> split_headers_and_body(
const std::string& raw)
{
// \r\n\r\n 또는 \n\n 찾기 (일부 클라이언트는 LF만 사용)
const std::string crlfcrlf = "\r\n\r\n";
const std::string lflf = "\n\n";
auto pos = raw.find(crlfcrlf);
if (pos == std::string::npos) {
pos = raw.find(lflf);
}
if (pos == std::string::npos) {
return {"", ""}; // 아직 헤더 수신 중
}
size_t header_end = (raw.find(crlfcrlf) != std::string::npos)
? pos + crlfcrlf.size()
: pos + lflf.size();
return {
raw.substr(0, pos),
raw.substr(header_end)
};
}
Content-Length 기반 본문 읽기
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <cstdlib>
#include <optional>
std::optional<size_t> get_content_length(const std::string& headers) {
// Content-Length: 123 형태에서 123 추출
const std::string key = "Content-Length:";
auto pos = headers.find(key);
if (pos == std::string::npos) {
return std::nullopt; // 본문 없음 또는 chunked
}
pos += key.size();
while (pos < headers.size() && headers[pos] == ' ') ++pos;
char* end;
long value = std::strtol(headers.c_str() + pos, &end, 10);
if (value < 0 || end == headers.c_str() + pos) {
return std::nullopt; // 잘못된 형식
}
return static_cast<size_t>(value);
}
3. 응답 파싱 (Response)
Status Line 파싱
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct ParsedStatusLine {
std::string version; // HTTP/1.1
int status_code; // 200, 404, ...
std::string reason; // OK, Not Found, ...
};
ParsedStatusLine parse_status_line(const std::string& line) {
std::istringstream iss(line);
ParsedStatusLine result;
if (!(iss >> result.version >> result.status_code)) {
throw std::runtime_error("Invalid status line");
}
std::getline(iss, result.reason); // 나머지: " OK\r" 또는 " OK"
// 앞뒤 공백 제거
result.reason.erase(0, result.reason.find_first_not_of(" \t\r\n"));
result.reason.erase(result.reason.find_last_not_of(" \t\r\n") + 1);
return result;
}
// 사용 예
// parse_status_line("HTTP/1.1 200 OK") → 200, "OK"
// parse_status_line("HTTP/1.1 404 Not Found") → 404, "Not Found"
응답 본문 읽기 전략
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 본문 읽기 전략 결정
enum class BodyReadStrategy {
NoBody, // HEAD, 204, 304 등
ContentLength, // Content-Length 있음
Chunked, // Transfer-Encoding: chunked
UntilClose // HTTP/1.0, 본문 끝까지 (연결 종료 시)
};
BodyReadStrategy determine_strategy(
int status_code,
const std::string& method,
const std::map<std::string, std::string>& headers)
{
if (method == "HEAD" || status_code == 204 || status_code == 304) {
return BodyReadStrategy::NoBody;
}
auto it = headers.find("transfer-encoding");
if (it != headers.end() &&
it->second.find("chunked") != std::string::npos) {
return BodyReadStrategy::Chunked;
}
if (headers.count("content-length")) {
return BodyReadStrategy::ContentLength;
}
return BodyReadStrategy::UntilClose; // HTTP/1.0 폴백
}
4. 헤더 처리
헤더 파싱 (대소문자 무시)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <map>
#include <algorithm>
#include <cctype>
std::map<std::string, std::string> parse_headers(const std::string& header_block) {
std::map<std::string, std::string> headers;
std::istringstream iss(header_block);
std::string line;
while (std::getline(iss, line) && !line.empty() &&
(line.back() == '\r' ? (line.pop_back(), true) : true)) {
auto colon = line.find(':');
if (colon == std::string::npos) continue;
std::string name = line.substr(0, colon);
std::string value = line.substr(colon + 1);
// 앞뒤 공백 제거
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t\r\n") + 1);
// 헤더 이름 소문자로 정규화 (HTTP 헤더는 대소문자 무시)
std::transform(name.begin(), name.end(), name.begin(),
{ return std::tolower(c); });
// 동일 헤더 여러 개: Set-Cookie 등은 별도 처리 필요
if (headers.count(name)) {
headers[name] += ", " + value; // 간단한 병합
} else {
headers[name] = value;
}
}
return headers;
}
주요 헤더 설명
| 헤더 | 용도 | 예시 |
|---|---|---|
Content-Type | 본문 MIME 타입 | application/json, text/html |
Content-Length | 본문 바이트 수 | 1024 |
Transfer-Encoding | 전송 인코딩 | chunked |
Host | 요청 대상 호스트 | example.com:8080 |
Connection | 연결 유지 | keep-alive, close |
Accept-Encoding | 압축 지원 | gzip, deflate, br |
Content-Type 파싱 (MIME + charset)
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct ParsedContentType {
std::string media_type; // application/json
std::string charset; // utf-8 (있으면)
};
ParsedContentType parse_content_type(const std::string& value) {
ParsedContentType result;
auto semicolon = value.find(';');
result.media_type = value.substr(0, semicolon);
result.media_type.erase(0, result.media_type.find_first_not_of(" \t"));
result.media_type.erase(result.media_type.find_last_not_of(" \t") + 1);
if (semicolon != std::string::npos) {
std::string rest = value.substr(semicolon + 1);
auto eq = rest.find('=');
if (eq != std::string::npos) {
std::string key = rest.substr(0, eq);
std::string val = rest.substr(eq + 1);
// 공백 제거, 따옴표 제거
val.erase(0, val.find_first_not_of(" \t\""));
val.erase(val.find_last_not_of(" \t\"") + 1);
if (key.find("charset") != std::string::npos) {
result.charset = val;
}
}
}
return result;
}
5. 청크 인코딩 (Chunked Transfer)
청크 형식
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
5\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
청크 디코딩 구현
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <vector>
#include <cctype>
std::pair<std::vector<char>, size_t> decode_chunk(
const char* data, size_t size, size_t& consumed)
{
std::vector<char> body;
consumed = 0;
const char* p = data;
const char* end = data + size;
while (p < end) {
// 청크 크기 읽기 (16진수)
if (p + 2 > end) break; // 최소 "0\r\n" 필요
char* hex_end;
unsigned long chunk_size = std::strtoul(p, &hex_end, 16);
p = hex_end;
// \r\n 건너뛰기
if (p + 2 > end) break;
if (p[0] != '\r' || p[1] != '\n') {
throw std::runtime_error("Invalid chunk: expected CRLF");
}
p += 2;
consumed = p - data;
if (chunk_size == 0) {
// 마지막 청크, 뒤에 \r\n 있을 수 있음
if (p + 2 <= end && p[0] == '\r' && p[1] == '\n') {
consumed += 2;
}
break;
}
// 청크 데이터
if (p + chunk_size + 2 > end) {
break; // 아직 데이터 부족
}
body.insert(body.end(), p, p + chunk_size);
p += chunk_size;
consumed = p - data;
if (p[0] != '\r' || p[1] != '\n') {
throw std::runtime_error("Invalid chunk: expected CRLF after data");
}
p += 2;
consumed = p - data;
}
return {body, consumed};
}
청크 인코딩 시각화
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart LR
subgraph Chunked[청크 인코딩]
C1[5\r\nHello\r\n]
C2["6\r\n World\r\n"]
C3[0\r\n\r\n]
end
C1 --> C2 --> C3
subgraph Decoded[디코딩 결과]
D["Hello World"]
end
Chunked -->|decode_chunk| Decoded
6. Beast 기반 완전한 파서
Beast로 요청 읽기
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <boost/beast.hpp>
#include <boost/asio.hpp>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = net::ip::tcp;
void read_http_request(tcp::socket& socket) {
beast::flat_buffer buffer;
http::request<http::string_body> req;
beast::error_code ec;
http::read(socket, buffer, req, ec);
if (ec) {
if (ec == http::error::end_of_stream) {
// 연결 종료 (정상)
return;
}
std::cerr << "Read error: " << ec.message() << "\n";
return;
}
// 파싱 완료된 요청 사용
std::cout << "Method: " << req.method_string() << "\n";
std::cout << "Path: " << req.target() << "\n";
std::cout << "Version: " << req.version() << "\n";
for (const auto& field : req) {
std::cout << field.name() << ": " << field.value() << "\n";
}
std::cout << "Body: " << req.body() << "\n";
}
Beast로 응답 읽기 (청크 자동 처리)
다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void read_http_response(beast::tcp_stream& stream) {
beast::flat_buffer buffer;
http::response_parser<http::string_body> parser;
parser.body_limit(std::numeric_limits<std::uint64_t>::max()); // 본문 제한
beast::error_code ec;
http::read(stream, buffer, parser, ec);
if (ec) {
std::cerr << "Read error: " << ec.message() << "\n";
return;
}
auto res = parser.get();
std::cout << "Status: " << res.result_int() << "\n";
std::cout << "Body: " << res.body() << "\n";
// Beast가 Transfer-Encoding: chunked를 자동 디코딩함
}
비동기 요청 읽기
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void do_read_async(beast::tcp_stream& stream,
std::function<void(http::request<http::string_body>)> on_request)
{
auto buffer = std::make_shared<beast::flat_buffer>();
auto req = std::make_shared<http::request<http::string_body>>();
http::async_read(stream, *buffer, *req,
[&stream, buffer, req, on_request](beast::error_code ec, std::size_t) {
if (ec) {
if (ec != http::error::end_of_stream) {
std::cerr << "Read error: " << ec.message() << "\n";
}
return;
}
on_request(std::move(*req));
});
}
HTTP 응답 생성 및 전송
다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void send_json_response(beast::tcp_stream& stream,
unsigned status, const std::string& json_body)
{
http::response<http::string_body> res{http::status::ok, 11};
res.set(http::field::server, "MyServer/1.0");
res.set(http::field::content_type, "application/json");
res.body() = json_body;
res.prepare_payload(); // Content-Length 자동 설정
if (status != 200) {
res.result(static_cast<http::status>(status));
}
beast::error_code ec;
http::write(stream, res, ec);
if (ec) {
std::cerr << "Write error: " << ec.message() << "\n";
}
}
완전한 HTTP 서버 예시 (Beast)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <iostream>
#include <memory>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = net::ip::tcp;
class HttpSession : public std::enable_shared_from_this<HttpSession> {
beast::tcp_stream stream_;
beast::flat_buffer buffer_;
http::request<http::string_body> req_;
public:
explicit HttpSession(tcp::socket socket)
: stream_(std::move(socket)) {}
void start() { do_read(); }
private:
void do_read() {
req_ = {};
buffer_.consume(buffer_.size());
auto self = shared_from_this();
http::async_read(stream_, buffer_, req_,
[self, this](beast::error_code ec, std::size_t) {
if (ec) {
if (ec != http::error::end_of_stream)
std::cerr << "read: " << ec.message() << "\n";
return;
}
handle_request();
});
}
void handle_request() {
http::response<http::string_body> res{http::status::ok, req_.version()};
res.set(http::field::server, "Beast-HTTP-Server");
res.set(http::field::content_type, "text/plain");
if (req_.method() == http::verb::get && req_.target() == "/") {
res.body() = "Hello, World!";
} else if (req_.method() == http::verb::get &&
req_.target().starts_with("/api/")) {
res.set(http::field::content_type, "application/json");
res.body() = "{\"message\":\"API response\"}";
} else {
res.result(http::status::not_found);
res.body() = "Not Found";
}
res.prepare_payload();
auto self = shared_from_this();
http::async_write(stream_, res,
[self, this](beast::error_code ec, std::size_t) {
if (!ec) {
if (req_.keep_alive()) {
do_read(); // Keep-Alive: 다음 요청
}
}
});
}
};
int main() {
net::io_context ioc;
tcp::acceptor acceptor(ioc, {tcp::v4(), 8080});
auto do_accept = [&]() {
acceptor.async_accept(ioc,
[&](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<HttpSession>(std::move(socket))->start();
}
do_accept();
});
};
do_accept();
std::cout << "HTTP server on :8080\n";
ioc.run();
}
7. 일반적인 에러와 해결법
문제 1: “end_of_stream” 또는 “connection reset”
원인: 클라이언트가 요청 도중 연결을 끊음 (브라우저 새로고침, 타임아웃 등). 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
http::async_read(stream_, buffer_, req_,
[self, this](beast::error_code ec, std::size_t) {
if (ec) {
if (ec == http::error::end_of_stream ||
ec == net::error::connection_reset) {
// 정상적인 연결 종료로 처리
return;
}
std::cerr << "read error: " << ec.message() << "\n";
return;
}
handle_request();
});
문제 2: “body limit exceeded”
원인: 요청 본문이 body_limit을 초과함 (DoS 방지용 기본 제한).
해결법:
http::request_parser<http::string_body> parser;
parser.body_limit(10 * 1024 * 1024); // 10MB
http::async_read(stream_, buffer_, parser, ...);
문제 3: “partial message” 또는 읽기 대기 무한 루프
원인: Content-Length와 실제 본문 크기 불일치, 또는 청크 인코딩 파싱 오류.
해결법:
- Beast 사용 시 자동 처리됨. 수동 파싱 시
Content-Length검증 필수. - 타임아웃 설정으로 무한 대기 방지:
stream_.expires_after(std::chrono::seconds(30));
http::async_read(stream_, buffer_, req_, handler);
문제 4: Keep-Alive에서 다음 요청 파싱 실패
원인: 한 연결에 여러 요청이 올 때, 이전 요청의 버퍼를 비우지 않음. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void do_read() {
req_ = {}; // 요청 초기화
buffer_.consume(buffer_.size()); // 버퍼 비우기
http::async_read(stream_, buffer_, req_, ...);
}
문제 5: 헤더 인젝션 (CRLF Injection)
원인: 사용자 입력을 헤더에 그대로 넣으면 \r\n으로 새 헤더 주입 가능.
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 위험
res.set("X-Custom", user_input);
// ✅ 안전: CRLF 제거
std::string safe_value = user_input;
safe_value.erase(
std::remove(safe_value.begin(), safe_value.end(), '\r'),
safe_value.end());
safe_value.erase(
std::remove(safe_value.begin(), safe_value.end(), '\n'),
safe_value.end());
res.set("X-Custom", safe_value);
문제 6: 대용량 본문 메모리 폭발
원인: string_body로 1GB 파일 업로드 시 메모리 1GB 사용.
해결법: dynamic_body 또는 file_body 사용:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
http::request<http::dynamic_body> req;
// 또는
http::request_parser<http::file_body> parser;
parser.body_limit(100 * 1024 * 1024); // 100MB
boost::beast::file_mode mode = boost::beast::file_mode::write;
parser.get().body().open("/tmp/upload.dat", mode);
8. 베스트 프랙티스
1. 항상 Beast 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 수동 파싱: 엣지 케이스 버그
std::string path = extract_path(raw_request);
// ✅ Beast: RFC 준수, 검증됨
http::request<http::string_body> req;
http::read(socket, buffer, req);
std::string path = std::string(req.target());
2. body_limit 설정
http::request_parser<http::string_body> parser;
parser.body_limit(1024 * 1024); // 1MB 제한 (업로드 크기 제한)
3. 타임아웃 설정
stream_.expires_after(std::chrono::seconds(30));
4. prepare_payload() 호출
res.body() = "Hello";
res.prepare_payload(); // Content-Length 자동 설정
5. Keep-Alive 처리
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
if (req.keep_alive()) {
res.keep_alive(true);
do_read(); // 다음 요청 대기
} else {
res.keep_alive(false);
stream_.socket().shutdown(tcp::socket::shutdown_send);
}
6. 에러 응답 일관성
다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::string escape_json(const std::string& s) {
std::string out;
for (char c : s) {
if (c == '"') out += "\\\"";
else if (c == '\\') out += "\\\\";
else if (c == '\n') out += "\\n";
else if (c == '\r') out += "\\r";
else out += c;
}
return out;
}
void send_error(beast::tcp_stream& stream, unsigned status,
const std::string& message)
{
http::response<http::string_body> res{
static_cast<http::status>(status), 11};
res.set(http::field::content_type, "application/json");
res.body() = "{\"error\":\"" + escape_json(message) + "\"}";
res.prepare_payload();
http::write(stream, res);
}
9. 프로덕션 패턴
패턴 1: 요청 로깅 미들웨어
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void log_request(const http::request<http::string_body>& req) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::cerr << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
<< " " << req.method_string() << " " << req.target()
<< " " << req.version() << "\n";
}
패턴 2: 요청 크기 제한 (Rate Limiting)
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
constexpr size_t MAX_HEADER_SIZE = 8 * 1024; // 8KB
constexpr size_t MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
http::request_parser<http::string_body> parser;
parser.header_limit(MAX_HEADER_SIZE);
parser.body_limit(MAX_BODY_SIZE);
패턴 3: Graceful Shutdown
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::atomic<bool> shutdown_requested{false};
void do_accept() {
if (shutdown_requested) return;
acceptor_.async_accept(
[this](beast::error_code ec, tcp::socket socket) {
if (shutdown_requested) return;
if (!ec) {
std::make_shared<HttpSession>(std::move(socket))->start();
}
do_accept();
});
}
// SIGINT 핸들러
void on_signal() {
shutdown_requested = true;
acceptor_.close();
}
패턴 4: 연결 풀 (클라이언트)
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class HttpClientPool {
net::io_context& ioc_;
std::queue<std::unique_ptr<beast::tcp_stream>> pool_;
std::mutex mtx_;
tcp::resolver::results_type endpoints_;
public:
void get_connection(std::function<void(beast::tcp_stream&)> callback) {
std::unique_lock lock(mtx_);
if (!pool_.empty()) {
auto stream = std::move(pool_.front());
pool_.pop();
lock.unlock();
callback(*stream);
return;
}
lock.unlock();
auto stream = std::make_unique<beast::tcp_stream>(ioc_);
stream->async_connect(endpoints_,
[this, cb = std::move(callback), s = stream.get()]
(beast::error_code ec) {
if (!ec) cb(*s);
});
}
void release_connection(std::unique_ptr<beast::tcp_stream> stream) {
std::lock_guard lock(mtx_);
pool_.push(std::move(stream));
}
};
패턴 5: 헬스 체크 엔드포인트
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
if (req.target() == "/health") {
res.result(http::status::ok);
res.set(http::field::content_type, "application/json");
res.body() = "{\"status\":\"ok\"}";
res.prepare_payload();
// DB/캐시 체크 생략, 빠른 응답
return;
}
패턴 6: CORS 헤더
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method() == http::verb::options) {
res.result(http::status::ok);
res.body() = "";
res.prepare_payload();
return; // Preflight 응답
}
구현 체크리스트
- Beast
http::read/http::write사용 (수동 파싱 지양) -
body_limit설정 (DoS 방지) -
expires_after타임아웃 설정 -
prepare_payload()호출 - Keep-Alive 처리 (
buffer_.consume,req_ = {}) - CRLF 인젝션 방지 (헤더 값 검증)
- 에러 응답 일관성 (JSON 형식)
- 로깅 미들웨어
- Graceful shutdown
참고 자료
- RFC 7230 - HTTP/1.1: Message Syntax and Routing
- RFC 7231 - HTTP/1.1: Semantics and Content
- Boost.Beast Documentation
이전 글: C++ 실전 가이드 #29-3: 멀티스레드 네트워크 서버
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. HTTP 프로토콜 파싱이 헷갈리는 문제를 해결합니다. 요청/응답 구조, 헤더 파싱, 청크 인코딩, Beast 기반 파서, 일반적인 에러, 베스트 프랙티스, 프로덕션 패턴까지 실전 코드로 완벽 정리. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다. 다음 글: C++ 실전 가이드 #30-2: SSL/TLS 보안 통신
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
- C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]
- C++ REST API 서버 완벽 가이드 | Beast 라우팅·JSON·미들웨어 [#31-2]