C++로 웹 서버 만들기: HTTP 서버 구현 프로젝트 🖥️🌐
웹 개발의 세계는 끊임없이 진화하고 있습니다. 그 중심에서 C++은 여전히 강력한 도구로 자리 잡고 있죠. 오늘은 C++을 사용하여 HTTP 서버를 구현하는 흥미진진한 여정을 떠나보려고 합니다. 이 프로젝트는 단순히 코드를 작성하는 것을 넘어서, 웹의 근간을 이루는 기술을 직접 만들어보는 소중한 경험이 될 것입니다.
우리의 목표는 간단하면서도 효율적인 HTTP 서버를 만드는 것입니다. 이 과정에서 네트워크 프로그래밍의 기초부터 고급 기술까지 다양한 개념을 다루게 될 것입니다. 재능넷과 같은 플랫폼에서 활동하는 개발자들에게 이런 프로젝트는 실력 향상의 좋은 기회가 될 수 있습니다.
자, 그럼 이제 본격적으로 시작해볼까요? 🚀
1. 프로젝트 개요 및 준비 📋
1.1 프로젝트 목표
우리의 주요 목표는 다음과 같습니다:
- 기본적인 HTTP 요청을 처리할 수 있는 서버 구현
- 멀티스레딩을 통한 동시 연결 처리
- 정적 파일 서빙 기능 구현
- 간단한 라우팅 시스템 개발
- 로깅 및 에러 처리 구현
이 프로젝트를 통해 우리는 네트워크 프로그래밍, 동시성 처리, 파일 I/O, 문자열 처리 등 C++의 다양한 측면을 깊이 있게 다루게 될 것입니다.
1.2 개발 환경 설정
먼저, 우리의 개발 환경을 설정해봅시다. 이 프로젝트를 위해 다음과 같은 도구들이 필요합니다:
- C++ 컴파일러: GCC나 Clang을 추천합니다.
- IDE 또는 텍스트 에디터: Visual Studio Code, CLion, 또는 Sublime Text 등을 사용할 수 있습니다.
- 버전 관리 도구: Git을 사용하여 프로젝트를 관리합니다.
- 빌드 도구: CMake를 사용하여 프로젝트를 빌드합니다.
개발 환경이 준비되었다면, 이제 프로젝트 구조를 설계해볼까요?
1.3 프로젝트 구조
우리의 프로젝트는 다음과 같은 구조를 가질 것입니다:
이 구조는 코드의 모듈성과 재사용성을 높여줄 것입니다. src/
디렉토리에는 실제 구현 코드를, include/
디렉토리에는 헤더 파일을 저장합니다.
1.4 필요한 라이브러리
C++ 표준 라이브러리 외에도, 다음과 같은 추가 라이브러리들을 사용할 예정입니다:
- Boost.Asio: 비동기 I/O 작업을 위한 라이브러리
- nlohmann/json: JSON 파싱을 위한 라이브러리
- spdlog: 로깅을 위한 라이브러리
이 라이브러리들은 우리의 서버 구현을 더욱 강력하고 효율적으로 만들어줄 것입니다.
이제 우리의 프로젝트 기반이 준비되었습니다. 다음 섹션에서는 실제 서버 구현을 시작해보겠습니다. 흥미진진한 코딩 여정이 우리를 기다리고 있습니다! 🚀
2. 기본 서버 구조 구현 🏗️
2.1 서버 클래스 설계
우리의 HTTP 서버의 핵심은 Server
클래스가 될 것입니다. 이 클래스는 클라이언트의 연결을 수신하고, 요청을 처리하며, 응답을 전송하는 역할을 담당합니다.
먼저 include/server.h
파일에 Server
클래스의 선언을 작성해봅시다:
#ifndef HTTP_SERVER_H
#define HTTP_SERVER_H
#include <boost/asio.hpp>
#include <string>
#include <memory>
class Server {
public:
Server(const std::string& address, unsigned short port);
void run();
private:
void do_accept();
void handle_request(boost::asio::ip::tcp::socket socket);
boost::asio::io_context io_context_;
boost::asio::ip::tcp::acceptor acceptor_;
};
#endif // HTTP_SERVER_H
이 클래스는 서버의 주소와 포트를 생성자 매개변수로 받아 초기화합니다. run()
메서드는 서버를 시작하고, do_accept()
는 새로운 연결을 수락하며, handle_request()
는 클라이언트의 요청을 처리합니다.
2.2 서버 클래스 구현
이제 src/server.cpp
파일에 Server
클래스의 구현을 작성해봅시다:
#include "server.h"
#include <iostream>
Server::Server(const std::string& address, unsigned short port)
: acceptor_(io_context_, boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address(address), port))
{
do_accept();
}
void Server::run()
{
std::cout << "Server running on http://" << acceptor_.local_endpoint().address().to_string()
<< ":" << acceptor_.local_endpoint().port() << std::endl;
io_context_.run();
}
void Server::do_accept()
{
acceptor_.async_accept(
[this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket)
{
if (!ec)
{
handle_request(std::move(socket));
}
do_accept();
});
}
void Server::handle_request(boost::asio::ip::tcp::socket socket)
{
// 여기에 요청 처리 로직을 구현할 예정입니다.
// 지금은 간단한 "Hello, World!" 메시지만 보내봅시다.
std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!";
boost::asio::write(socket, boost::asio::buffer(response));
}
이 구현에서 주목할 점은 다음과 같습니다:
- 서버는 비동기 I/O를 사용하여 효율적으로 여러 연결을 처리할 수 있습니다.
do_accept()
메서드는 재귀적으로 호출되어 계속해서 새로운 연결을 수락합니다.handle_request()
메서드는 현재 매우 간단하지만, 이후에 더 복잡한 로직을 추가할 것입니다.
2.3 메인 함수 구현
마지막으로, src/main.cpp
파일에 메인 함수를 구현하여 서버를 실행해봅시다:
#include "server.h"
#include <iostream>
int main()
{
try
{
Server server("0.0.0.0", 8080);
server.run();
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
이 메인 함수는 서버 객체를 생성하고 실행합니다. 서버는 모든 인터페이스(0.0.0.0)에서 8080 포트로 리스닝하게 됩니다.
2.4 빌드 및 테스트
이제 우리의 기본적인 서버 구조가 완성되었습니다. CMake를 사용하여 프로젝트를 빌드해봅시다. 루트 디렉토리에 CMakeLists.txt
파일을 다음과 같이 작성합니다:
cmake_minimum_required(VERSION 3.10)
project(http_server)
set(CMAKE_CXX_STANDARD 17)
find_package(Boost REQUIRED COMPONENTS system)
include_directories(${Boost_INCLUDE_DIRS} include)
add_executable(http_server src/main.cpp src/server.cpp)
target_link_libraries(http_server ${Boost_LIBRARIES})
이제 다음 명령어로 프로젝트를 빌드할 수 있습니다:
mkdir build
cd build
cmake ..
make
빌드가 완료되면 ./http_server
명령어로 서버를 실행할 수 있습니다. 웹 브라우저에서 http://localhost:8080
에 접속하면 "Hello, World!" 메시지를 볼 수 있을 것입니다.
다음 섹션에서는 이 기본 구조를 확장하여 더 복잡한 HTTP 요청을 처리하고, 정적 파일을 서빙하며, 라우팅 시스템을 구현해볼 것입니다. 계속해서 흥미진진한 여정을 이어가봅시다! 🚀
3. HTTP 요청 처리 구현 🔍
3.1 HTTP 요청 파싱
이제 우리의 서버가 실제 HTTP 요청을 이해하고 처리할 수 있도록 만들어봅시다. 먼저 HTTP 요청을 파싱하는 클래스를 만들어보겠습니다.
include/http_request.h
파일을 생성하고 다음과 같이 작성합니다:
#ifndef HTTP_REQUEST_H
#define HTTP_REQUEST_H
#include <string>
#include <map>
class HttpRequest {
public:
HttpRequest() = default;
void parse(const std::string& raw_request);
std::string method;
std::string uri;
std::string http_version;
std::map<std::string, std::string> headers;
std::string body;
};
#endif // HTTP_REQUEST_H
이제 src/http_request.cpp
파일을 생성하고 parse
메서드를 구현합니다:
#include "http_request.h"
#include <sstream>
void HttpRequest::parse(const std::string& raw_request) {
std::istringstream request_stream(raw_request);
std::string line;
// 요청 라인 파싱
std::getline(request_stream, line);
std::istringstream request_line(line);
request_line >> method >> uri >> http_version;
// 헤더 파싱
while (std::getline(request_stream, line) && line != "\r") {
auto colon_pos = line.find(':');
if (colon_pos != std::string::npos) {
auto key = line.substr(0, colon_pos);
auto value = line.substr(colon_pos + 1);
// 앞뒤 공백 제거
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t") + 1);
headers[key] = value;
}
}
// 바디 파싱
std::string body_str((std::istreambuf_iterator<char>(request_stream)),
std::istreambuf_iterator<char>());
body = body_str;
}
3.2 HTTP 응답 생성
다음으로, HTTP 응답을 생성하는 클래스를 만들어봅시다. include/http_response.h
파일을 생성합니다:
#ifndef HTTP_RESPONSE_H
#define HTTP_RESPONSE_H
#include <string>
#include <map>
class HttpResponse {
public:
HttpResponse() = default;
void set_status(int status_code, const std::string& status_message);
void set_header(const std::string& key, const std::string& value);
void set_body(const std::string& body);
std::string to_string() const;
private:
int status_code_ = 200;
std::string status_message_ = "OK";
std::map<std::string, std::string> headers_;
std::string body_;
};
#endif // HTTP_RESPONSE_H
src/http_response.cpp
파일을 생성하고 메서드들을 구현합니다:
#include "http_response.h"
#include <sstream>
void HttpResponse::set_status(int status_code, const std::string& status_message) {
status_code_ = status_code;
status_message_ = status_message;
}
void HttpResponse::set_header(const std::string& key, const std::string& value) {
headers_[key] = value;
}
void HttpResponse::set_body(const std::string& body) {
body_ = body;
headers_["Content-Length"] = std::to_string(body_.length());
}
std::string HttpResponse::to_string() const {
std::ostringstream response;
response << "HTTP/1.1 " << status_code_ << " " << status_message_ << "\r\n";
for (const auto& header : headers_) {
response << header.first << ": " << header.second << "\r\n";
}
response << "\r\n" << body_;
return response.str();
}
3.3 요청 핸들러 개선
이제 우리의 Server
클래스의 handle_request
메서드를 개선하여 실제 HTTP 요청을 처리하고 응답을 생성하도록 만들어봅시다. src/server.cpp
파일을 다음과 같이 수정합니다:
#include "server.h"
#include "http_request.h"
#include "http_response.h"
#include <iostream>
#include <vector>
// ... (이전 코드는 그대로 유지)
void Server::handle_request(boost::asio::ip::tcp::socket socket)
{
auto self(shared_from_this());
socket.async_read_some(boost::asio::buffer(data_, max_length),
[this, self, &socket](boost::system::error_code ec, std::size_t length)
{
if (!ec)
{
HttpRequest request;
request.parse(std::string(data_, length));
HttpResponse response;
// 간단한 라우팅 로직
if (request.uri == "/") {
response.set_status(200, "OK");
response.set_body("<html><body><h1>Welcome to our C++ HTTP Server!</h1></body></html>");
} else if (request.uri == "/about") {
response.set_status(200, "OK");
response.set_body("<html><body><h1>About Us</h1><p>We are passionate C++ developers.</p></body></html>");
} else {
response.set_status(404, "Not Found");
response.set_body("<html><body><h1>404 Not Found</h1></body></html>");
}
response.set_header("Content-Type", "text/html");
std::string response_str = response.to_string();
boost::asio::async_write(socket, boost::asio::buffer(response_str),
[this, self, &socket](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
handle_request(std::move(socket));
}
});
}
});
}
이 개선된 버전에서는 다음과 같은 변경사항이 있습니다:
- HTTP 요청을 파싱합니다.
- 간단한 라우팅 로직을 구현하여 다른 URI에 대해 다른 응답을 생성합니다.
- HTTP 응답을 생성하고 클라이언트에게 전송합니다.
3.4 테스트 및 결과 확인
이제 우리의 서버를 다시 빌드하고 실행해봅시다. 웹 브라우저에서 다음 URL들을 테스트해볼 수 있습니다:
http://localhost:8080/
- 환영 메시지를 표시합니다.http://localhost:8080/about
- "About Us" 페이지를 표시합니다.http://localhost:8080/nonexistent
- 404 Not Found 오류를 표시합니다.
다음 섹션에서는 정적 파일 서빙, 더 복잡한 라우팅 시스템, 그리고 보안 기능 등을 추가하여 우리의 서버를 더욱 강력하게 만들어볼 것입니다. C++로 웹 서버를 구현하는 이 여정이 얼마나 흥미진진한지 느껴지시나요? 계속해서 더 깊이 파고들어봅시다! 🚀