본문 바로가기
카테고리 없음

C언어로 HTTP 서버구현하기 (소켓 통신, 요청, 응답)

by info95686 2025. 10. 20.

C언어로 만든 HTTP 서버
C언어로 만든 HTTP 서버

웹 개발에서 HTTP는 기본 중의 기본이지만, 이를 가장 낮은 수준인 소켓(Socket)으로 직접 구현해 보는 경험은 네트워크 프로그래밍의 본질을 이해하는 데 매우 유익합니다. 특히 C언어로 HTTP 서버를 직접 구현해 보면, 브라우저와 서버가 실제로 어떤 식으로 요청(Request)과 응답(Response)을 주고받는지, TCP 연결은 어떻게 맺어지는지를 명확히 체감할 수 있습니다. 이 글에서는 운영체제의 소켓 API를 사용해 간단한 HTTP 서버를 만드는 과정을 단계별로 살펴보고, TCP 통신 구조와 HTTP 프로토콜의 핵심을 함께 이해해봅니다.

소켓 통신의 기본 개념 이해

HTTP 서버를 직접 만든다는 것은 결국 TCP 기반 소켓 통신을 구현한다는 의미입니다. C언어의 소켓 프로그래밍은 리눅스 시스템 콜을 기반으로 작동하며, 다음 다섯 단계로 구성됩니다.

  1. socket() – 통신을 위한 소켓 생성
  2. bind() – IP와 포트를 소켓에 할당
  3. listen() – 클라이언트의 접속 대기
  4. accept() – 연결 요청 수락
  5. recv() / send() – 데이터 송수신

예를 들어, 브라우저가 http://127.0.0.1:8080 으로 접속하면 TCP 연결을 시도하고, 서버는 8080 포트에서 대기 중인 소켓을 통해 연결을 수락합니다. 이후 서버는 요청을 읽고 HTTP 응답을 반환합니다. 소켓은 전화기에 비유할 수 있습니다. socket()은 전화기를 설치하는 과정, bind()는 전화번호(IP+PORT)를 등록하는 과정, listen()은 전화벨이 울리길 기다리는 과정, accept()는 전화를 받는 과정, 그리고 send()/recv()는 실제 대화입니다. 이 흐름을 명확히 이해하면 이후 구현이 훨씬 쉬워집니다.

C언어로 간단한 HTTP 서버 구현하기

아래 예제는 “Hello, World!”를 반환하는 간단한 HTTP 서버 코드입니다. TCP 소켓을 이용해 클라이언트의 요청을 수신하고, HTTP 프로토콜 형식으로 응답을 돌려줍니다.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[1024] = {0};

    // 1. 소켓 생성
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket failed");
        exit(1);
    }

    // 2. 서버 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);

    // 3. 바인드
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(1);
    }

    // 4. 연결 대기
    listen(server_fd, 5);
    printf("HTTP 서버 실행 중... 포트 8080\n");

    // 5. 클라이언트 수락
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
    if (client_fd < 0) {
        perror("accept failed");
        close(server_fd);
        exit(1);
    }

    // 6. 요청 수신
    read(client_fd, buffer, sizeof(buffer) - 1);
    printf("요청:\n%s\n", buffer);

    // 7. HTTP 응답 작성 및 송신
    const char *response =
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html\r\n"
        "Connection: close\r\n\r\n"
        "<html><body><h1>Hello, World!</h1></body></html>";

    write(client_fd, response, strlen(response));

    // 8. 종료
    close(client_fd);
    close(server_fd);
    return 0;
}

위 코드를 리눅스에서 gcc server.c -o server 로 컴파일 후 실행하고, 브라우저에서 http://localhost:8080 으로 접속하면 “Hello, World!” 페이지가 표시됩니다. HTTP 프로토콜은 텍스트 기반으로, 요청과 응답 모두 문자열로 교환됩니다. 헤더와 본문은 반드시 \r\n\r\n으로 구분되어야 합니다.

확장과 개선: 실무형 서버로 발전시키기

현재 예제는 하나의 연결만 처리하지만, 실제 서버는 다중 연결을 처리해야 합니다. 다음과 같은 개선 방식을 적용할 수 있습니다.

  • 멀티프로세스: fork()를 사용해 각 클라이언트마다 자식 프로세스를 생성합니다. 구현이 간단하지만 프로세스 생성 비용이 큽니다.
  • 멀티스레드: pthread를 이용해 각 연결을 스레드로 처리합니다. 메모리를 공유할 수 있어 효율적이지만 동기화 관리가 필요합니다.
  • 논블로킹 I/O: select(), poll(), epoll()을 사용하면 하나의 프로세스로 수백 개의 연결을 동시에 처리할 수 있습니다.
  • HTTP 파서 추가: GET/POST 요청 구분, 헤더 파싱, MIME 타입 대응, Keep-Alive 지원 등을 통해 표준 서버로 확장합니다.
  • 보안(HTTPS): OpenSSL을 연동해 TLS 통신을 추가하면 보안 채널을 구축할 수 있습니다.

또한 실무 환경에서는 소켓 옵션(SO_REUSEADDR 등)을 설정하여 포트 재사용 문제를 방지하고, 서버의 부하 분산을 위해 로드밸런서와 함께 사용하는 경우가 많습니다. 이러한 확장은 C언어 네트워크 프로그래밍의 핵심 응용이라 할 수 있습니다.

결론: 직접 구현하며 네트워크의 본질을 이해하자

C언어로 HTTP 서버를 직접 구현하는 경험은 TCP/IP 통신의 근본 원리를 이해하는 최고의 방법입니다. 소켓 생성과 연결, 요청·응답 구조, 스트림 기반 통신 방식을 체득하게 되며, 이는 Java나 Python 등 다른 언어에서도 그대로 통합니다. “웹 서버는 결국 소켓 통신 위에 HTTP라는 텍스트 프로토콜을 얹은 것이다.” 이 단순한 원리를 코드로 구현해 보면, 우리가 매일 사용하는 웹이 얼마나 논리적이고 단순한 구조 위에 세워져 있는지를 깨닫게 됩니다. 직접 구현해 본 개발자는 네트워크 프로그래밍의 본질을 두려워하지 않게 되고, 성능과 안정성을 고려한 설계 감각을 자연스럽게 익히게 됩니다.