프로젝트 1: 간단한 웹 서버
이번 프로젝트에서는 간단한 웹 서버를 구축해보겠습니다. 이 웹 서버는 HTTP 요청을 처리하고, 정적 파일을 제공하는 기능을 구현할 것입니다. 이를 통해 네트워크 프로그래밍, 스레드, 파일 입출력 등의 개념을 실습할 수 있습니다.
프로젝트 목표
- HTTP 프로토콜 이해: HTTP 요청과 응답 구조를 이해하고 구현합니다.
- 소켓 프로그래밍: TCP 소켓을 사용하여 클라이언트와 서버 간의 통신을 구현합니다.
- 멀티스레딩: 여러 클라이언트 요청을 동시에 처리하기 위해 멀티스레딩을 구현합니다.
- 정적 파일 제공: 서버가 정적 파일 (HTML, CSS, JS)을 제공할 수 있도록 합니다.
프로젝트 설계
1. HTTP 요청과 응답
HTTP 프로토콜은 클라이언트와 서버 간의 요청/응답 프로토콜입니다. HTTP 요청은 메서드, 경로, 버전, 헤더, 바디로 구성됩니다. HTTP 응답은 상태 코드, 헤더, 바디로 구성됩니다.
HTTP 요청 예시:
GET /index.html HTTP/1.1
Host: localhost:8080
HTTP 응답 예시:
HTTP/1.1 200 OK
Content-Type: text/html
<html>
<head>
<title>Test</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
2. 소켓 프로그래밍
TCP 소켓을 사용하여 클라이언트와 서버 간의 통신을 구현합니다. 서버는 클라이언트의 요청을 받아들이고, 적절한 응답을 보냅니다.
소켓 프로그래밍 기본 구조:
- 서버 소켓 생성
- 소켓 바인딩
- 소켓 리스닝
- 클라이언트 연결 수락
- 데이터 송수신
3. 멀티스레딩
멀티스레딩을 사용하여 여러 클라이언트 요청을 동시에 처리합니다. 각 클라이언트 연결은 별도의 스레드에서 처리됩니다.
4. 정적 파일 제공
서버는 클라이언트의 요청에 따라 정적 파일 (HTML, CSS, JS)을 제공합니다. 파일 시스템에서 요청된 파일을 읽어 클라이언트에 전송합니다.
프로젝트 구현
프로젝트 구조
/webserver
/src
main.cpp
server.h
server.cpp
request.h
request.cpp
response.h
response.cpp
/www
index.html
styles.css
1. 서버 설정
먼저, 서버 설정을 위한 클래스를 정의합니다.
server.h
#ifndef SERVER_H
#define SERVER_H
#include <string>
#include <thread>
#include <vector>
#include <atomic>
#include <netinet/in.h>
class Server {
public:
Server(const std::string& address, int port);
~Server();
void start();
void stop();
private:
void handleClient(int clientSocket);
std::string address_;
int port_;
int serverSocket_;
std::vector<std::thread> threads_;
std::atomic<bool> running_;
struct sockaddr_in serverAddr_;
};
#endif // SERVER_H
server.cpp
#include "server.h"
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
Server::Server(const std::string& address, int port) : address_(address), port_(port), running_(false) {
serverSocket_ = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket_ == -1) {
throw std::runtime_error("Failed to create socket");
}
serverAddr_.sin_family = AF_INET;
serverAddr_.sin_addr.s_addr = inet_addr(address.c_str());
serverAddr_.sin_port = htons(port);
if (bind(serverSocket_, (struct sockaddr*)&serverAddr_, sizeof(serverAddr_)) == -1) {
throw std::runtime_error("Failed to bind socket");
}
if (listen(serverSocket_, 10) == -1) {
throw std::runtime_error("Failed to listen on socket");
}
}
Server::~Server() {
stop();
close(serverSocket_);
}
void Server::start() {
running_ = true;
while (running_) {
int clientSocket = accept(serverSocket_, nullptr, nullptr);
if (clientSocket == -1) {
std::cerr << "Failed to accept client" << std::endl;
continue;
}
threads_.emplace_back(&Server::handleClient, this, clientSocket);
}
}
void Server::stop() {
running_ = false;
for (auto& thread : threads_) {
if (thread.joinable()) {
thread.join();
}
}
}
void Server::handleClient(int clientSocket) {
// 클라이언트 요청 처리
close(clientSocket);
}
2. HTTP 요청과 응답 처리
HTTP 요청과 응답을 처리하는 클래스를 정의합니다.
request.h
#ifndef REQUEST_H
#define REQUEST_H
#include <string>
#include <unordered_map>
class Request {
public:
Request(const std::string& rawRequest);
std::string getMethod() const;
std::string getPath() const;
std::string getVersion() const;
std::string getHeader(const std::string& name) const;
private:
void parseRequest(const std::string& rawRequest);
std::string method_;
std::string path_;
std::string version_;
std::unordered_map<std::string, std::string> headers_;
};
#endif // REQUEST_H
request.cpp
#include "request.h"
#include <sstream>
Request::Request(const std::string& rawRequest) {
parseRequest(rawRequest);
}
void Request::parseRequest(const std::string& rawRequest) {
std::istringstream stream(rawRequest);
std::string line;
std::getline(stream, line);
std::istringstream requestLine(line);
requestLine >> method_ >> path_ >> version_;
while (std::getline(stream, line) && line != "\r") {
size_t colon = line.find(':');
if (colon != std::string::npos) {
std::string name = line.substr(0, colon);
std::string value = line.substr(colon + 2, line.size() - colon - 3);
headers_[name] = value;
}
}
}
std::string Request::getMethod() const {
return method_;
}
std::string Request::getPath() const {
return path_;
}
std::string Request::getVersion() const {
return version_;
}
std::string Request::getHeader(const std::string& name) const {
auto it = headers_.find(name);
if (it != headers_.end()) {
return it->second;
}
return "";
}
response.h
#ifndef RESPONSE_H
#define RESPONSE_H
#include <string>
#include <unordered_map>
class Response {
public:
Response(int statusCode);
void setHeader(const std::string& name, const std::string& value);
void setBody(const std::string& body);
std::string toString() const;
private:
int statusCode_;
std::unordered_map<std::string, std::string> headers_;
std::string body_;
};
#endif // RESPONSE_H
response.cpp
#include "response.h"
#include <sstream>
Response::Response(int statusCode) : statusCode_(statusCode) {}
void Response::setHeader(const std::string& name, const std::string& value) {
headers_[name] = value;
}
void Response::setBody(const std::string& body) {
body_ = body;
}
std::string Response::toString() const {
std::ostringstream response;
response << "HTTP/1.1 " << statusCode_ << "\r\n";
for (const auto& header : headers_) {
response << header.first << ": " << header.second << "\r\n";
}
response << "\r\n" << body_;
return response.str();
}
3. 서버 실행
이제 서버를 실행하는 코드를 작성합니다.
main.cpp
#include "server.h"
int main() {
try {
Server server("127.0.0.1", 8080);
server.start();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
이제 프로젝트의 기본 설계와 일부 구현을 마쳤습니다. 앞으로 며칠 동안 이 프로젝트를 확장하여 HTTP 프로토콜 구현, 멀티스레딩, 정적 파일 제공 기능 등을 추가하겠습니다.
질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "HTTP 프로토콜 이해 및 구현"에 대해 학습하고 구현하겠습니다.