[Webserver]miniWebserver – (1)

👉🏻 웹서버의 구조를 이해하기 위해서 아주 간단한 웹서버를 만들어보려고 합니다.
To understand the structure of a web server, I am going to try building a very simple web server.

👉🏻 같이 재미있게 만들어 봅시다.
Let’s have fun making this together.

👉🏻 아래는 브라우저에 간단한 메시지를 출력합니다.
The following outputs a simple message to the browser.

👉🏻 설명은 코드주석을 참고하세요
Please refer to the code comments for the explanation.

✔️ 전체코드 / full code

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <thread>
#include <vector>

#define PORT 5080
#define BACKLOG 10 // 대기 큐 크기 / wait queue size

// 클라이언트와의 통신을 담당하는 함수 (각 사용자를 위한 별도의 쓰레드)
// Function responsible for communication with clients (separate thread for each user)
void handle_client(int client_socket) {
    char buffer[1024];
    std::cout << "[Connection] New client connected." << std::endl;

    // 1. 데이터 수신 / receive data (Read)
    ssize_t bytes_read = read(client_socket, buffer, sizeof(buffer) - 1);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0'; // C-string
        std::string request = buffer;
        std::cout << "[Received] " << request.substr(0, 30) << "..." << std::endl;

        // 2. 응답 전송 / send response(Write)
        const char* response = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n<h1>👋Hi~ Hi~ Hi~~~!</h1><p>Welcome to the miniWebserver!</p>";
        write(client_socket, response, strlen(response));
    }

    // 3. 연결 종료 / close connection
    close(client_socket);
    std::cout << "[Connection] Client disconnected." << std::endl;
}

int main() {
    // --- 1. 소켓 생성 (Socket Creation) --- //
    // AF_INET(Address Family Internet):
    // IPv4 사용, SOCK_STREAM: TCP 연결 방식 사용, 0: 프로토콜 기본 사용(기본값 사용)
    // Use IPv4, SOCK_STREAM: Use TCP connection method, 0: Use default protocol (use default)
    //
    // AF_INET6: IPv6 사용, SOCK_DGRAM : UDP 연결 방식 사용 / Use UDP connection method
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        // perror() : 어떤 시스템 함수가 실패할 경우 내가 정의한 오류 메세지와 운영체제가 정의한 오류 메세지를 함께 출력해줌.
        perror("socket failed");
        /*
         exit(1)의 동작 결과는 exit(EXIT_FAILURE)가 의도하는 동작 결과와 동일합니다. 즉, 프로그램이 '실패' 상태로 종료된다는 의미가 같습니다.
         exit(0)의 동작 결과는 exit(EXIT_SUCCESS)가 의도하는 동작 결과와 동일합니다. 즉, 프로그램이 '성공' 상태로 종료된다는 의미가 같습니다.
         The result of exit(1) is the same as the intended result of exit(EXIT_FAILURE). In other words, it means the same thing: the program terminates in a 'failed' state.
         The result of exit(0) is the same as the intended result of exit(EXIT_SUCCESS). In other words, it means the same thing: the program terminates in a 'successful' state.
         */
        exit(EXIT_FAILURE);
    }

    // 포트 재사용 옵션 설정 (같은 포트를 재사용 가능하게 함)
    /* setsockopt() : 소켓에설정적용하기 / Applying settings to the socket
     * setsockopt(int sockfd,           // 1. 소켓 식별자 / socket identifier
                int level,              //.2. 프로토콜 레벨 (어느 계층의 설정인지) / Protocol level (which layer's setting)
                int optionname,         // 3. 설정할 옵션의 이름 (어떤 설정을 할지) / Name of the option to set (which setting to make)
                const void *optionval,  // 4. 옵션의 실제 값 (값을 1로 설정할지) / The actual value of the option (whether to set the value to 1)
                socklen_t optionlen);   // 5. 옵션 값의 길이 / Length of the option value

       SOL_SOCKET :
        소켓 통로 자체의 전반적인 옵션 및 속성(소켓통신을 위한 기본 값)
        Overall options and properties of the socket channel itself (default values ​​for socket communication)

       SO_REUSEADDR :
        지금 이 포트는 임시적으로 점유된 상태일지라도, 이 주소를 다시 사용하도록 요청하는 것
        Requesting to use this address again, even if this port is currently temporarily occupied

       opt = 1 : 포트 재사용 허용 / Allow port reuse

    */
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    // --- 2. 바인딩 (Binding) --- //
    // struct sockaddr_in :
    //   IPv4 주소만 다루기 위한 구체적인 구조체입니다.
    //   This is a specific structure for handling only IPv4 addresses.
    //
    // struct sockaddr_in6 :
    //   IPv6 주소만 다루기 위한 구체적인 구조체입니다.
    //   This is a specific structure designed to handle only IPv6 addresses.
    //
    // struct sockaddr :
    //   이 구조체는 특정 IP 버전(IPv4인지 IPv6인지)에 관계없이 사용할 수 있도록 설계되었습니다.
    //   This structure is designed to be usable regardless of the specific IP version (whether IPv4 or IPv6).

    struct sockaddr_in address;
    address.sin_family = AF_INET;         // IPv4
    address.sin_addr.s_addr = INADDR_ANY; // 모든 인터페이스 IP 사용 / Use all interface IPs
    address.sin_port = htons(PORT);       // 포트를 네트워크 바이트 순서로 변환 / Convert ports to network byte order

    // 바로 위에서 선언한 구조체 address에 소켓을 바인딩함.
    // Bind the socket to the structure address declared just above.
    //
    // (struct sockaddr *)&address : &address변수의 값을 (struct sockaddr *) 타입으로 타입 캐스팅함.
    // (struct sockaddr *)&address : Type casts the value of the &address variable to the type (struct sockaddr *).

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // --- 3. 리스닝 (Listening) --- //
    // 최대 대기할 연결 요청 수를 지정 (BACKLOG)
    // Specify the maximum number of connection requests to wait for (BACKLOG)
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    std::cout << "Server listening on port " << PORT << "..." << std::endl;

    // --- 4. 무한 루프: 연결 수락 및 처리 / Accept and handle incoming connections indefinitely --- //
    while (true) {
        // IPv4구조체
        struct sockaddr_in client_address;
        // 주소 구조체가 메모리에서 차지하는 바이트 크기를 저장하는 변수의 타입
        socklen_t addrlen = sizeof(client_address);

        // accept() 함수가 블로킹(Blocking) 상태에서 클라이언트 연결을 기다림
        int client_socket = accept(server_fd, (struct sockaddr *)&client_address, &addrlen);
        if (client_socket < 0) {
            perror("accept failed");
            continue; // 실패했으면 다음 루프를 돌며 재시도 / Retry in the next loop if it fails
        }

        // 5. 동시성 처리: 새로운 연결이 오면 새로운 쓰레드를 생성하여 처리
        // Concurrency Handling: When a new connection arrives, create a new thread to handle it.
        std::thread client_thread(handle_client, client_socket);
        // 쓰레드를 분리하고 메인 루프를 계속 실행하게 함
        // Detach the thread so that the main loop continues running.
        client_thread.detach();
    }

    // 이 부분은 무한 루프가 도달하지 않습니다. / This part is not reached by an infinite loop.
    close(server_fd);
    return 0;
}

✔️ 컴파일 / Compiling

g++ miniserver.cpp -o miniserver

✔️ 실행 / Run

./miniserver

✔️브라우저접속 / Access Browser

— 브라우저에 아래의 주소로 접속합니다. / Access the following address in your browser.

http://localhost:5080

— 터미널과 브라우저에 메세지가 출력됩니다. / Messages are displayed in the terminal and browser.

Leave a Reply