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

C 언어는 성능과 제어력이 뛰어난 언어로, 네트워크 프로그래밍에서 중요한 역할을 합니다. 본 기사에서는 C 언어를 사용해 간단한 HTTP 서버를 구현하는 과정을 다룹니다. 이를 통해 네트워크 소켓의 작동 원리를 이해하고, HTTP 요청 및 응답 처리 방법을 학습할 수 있습니다. 간단한 예제를 통해 서버의 기본 구성부터 동시 요청 처리까지 단계별로 설명합니다. 이 기사를 통해 네트워크 프로그래밍의 기본 개념을 배우고 실제로 동작하는 HTTP 서버를 구현할 수 있을 것입니다.

HTTP 서버의 기본 개념


HTTP 서버는 클라이언트와의 통신을 통해 요청을 수신하고, 그에 대한 적절한 응답을 반환하는 소프트웨어입니다. HTTP(Hypertext Transfer Protocol)는 웹 통신의 기본 프로토콜로, 클라이언트와 서버 간의 데이터 교환을 표준화합니다.

HTTP 프로토콜의 동작 원리


HTTP는 요청(request)과 응답(response)으로 구성된 클라이언트-서버 모델입니다. 클라이언트는 특정 리소스를 요청하는 메시지를 전송하고, 서버는 요청을 처리한 뒤 결과를 반환합니다.
예:

  1. 클라이언트 요청: “GET /index.html HTTP/1.1”
  2. 서버 응답: “HTTP/1.1 200 OK\n\n…”

HTTP 서버의 주요 기능

  1. 요청 수신: TCP 소켓을 통해 클라이언트의 HTTP 요청을 수신합니다.
  2. 요청 처리: 요청 헤더를 분석하고, 해당 리소스를 검색하거나 동적으로 생성합니다.
  3. 응답 반환: 적절한 HTTP 상태 코드와 함께 데이터를 클라이언트에 반환합니다.

HTTP 상태 코드


서버는 응답에 상태 코드를 포함하여 요청의 처리 결과를 나타냅니다.

  • 200 OK: 요청 성공
  • 404 Not Found: 요청한 리소스가 없음
  • 500 Internal Server Error: 서버 내부 오류

HTTP 서버를 구축하려면 위 개념을 이해하고 요청과 응답의 흐름을 효과적으로 처리하는 구조를 설계해야 합니다.

네트워크 소켓의 이해


네트워크 소켓은 서버와 클라이언트 간의 데이터 통신을 가능하게 하는 소프트웨어 인터페이스입니다. C 언어에서 소켓은 주로 BSD 소켓 API를 통해 구현됩니다.

소켓의 개념


소켓은 네트워크 프로토콜 계층에서 데이터 교환의 끝점을 나타냅니다.

  • 소켓 주소: IP 주소와 포트 번호로 구성됩니다.
  • 프로토콜: 소켓은 TCP(신뢰성 있는 연결 기반 프로토콜)나 UDP(비연결형 프로토콜)를 사용할 수 있습니다.

소켓의 주요 동작 단계

  1. 소켓 생성: socket() 함수를 사용해 소켓을 생성합니다.
   int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • AF_INET: IPv4 주소 체계 사용
  • SOCK_STREAM: TCP 프로토콜 사용
  1. 주소 바인딩: bind()를 사용해 소켓에 IP 주소와 포트 번호를 할당합니다.
  2. 연결 대기: listen()으로 연결 요청을 대기합니다.
  3. 연결 수락: accept()를 통해 클라이언트의 연결을 수락합니다.
  4. 데이터 송수신: send()recv()로 데이터를 교환합니다.

소켓의 간단한 구조


아래는 간단한 TCP 소켓 서버의 구조입니다.

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

int main() {
    int sockfd, new_sock;
    struct sockaddr_in server_addr, client_addr;
    char buffer[1024];

    // 1. 소켓 생성
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);

    // 2. 주소 바인딩
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

    // 3. 연결 대기
    listen(sockfd, 5);

    // 4. 연결 수락
    int addr_len = sizeof(client_addr);
    new_sock = accept(sockfd, (struct sockaddr*)&client_addr, (socklen_t*)&addr_len);

    // 5. 데이터 송수신
    recv(new_sock, buffer, 1024, 0);
    printf("Received: %s\n", buffer);
    send(new_sock, "Hello, Client!", strlen("Hello, Client!"), 0);

    close(new_sock);
    close(sockfd);
    return 0;
}

소켓 통신의 장점

  • 다양한 네트워크 프로토콜과 호환
  • TCP를 통한 안정적인 데이터 전송
  • 다양한 플랫폼에서의 호환성

소켓은 HTTP 서버 구현의 핵심이므로 기본적인 동작 원리를 명확히 이해해야 합니다.

서버 초기화와 소켓 생성


HTTP 서버를 구현하려면 먼저 서버를 초기화하고 소켓을 생성해야 합니다. 이는 클라이언트 요청을 처리할 준비 단계에 해당합니다.

1. 소켓 생성


소켓은 네트워크 연결의 끝점이며, 서버와 클라이언트 간의 데이터 교환을 가능하게 합니다.
C 언어에서는 socket() 함수를 사용하여 소켓을 생성합니다.

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("Socket creation failed");
    exit(EXIT_FAILURE);
}
  • AF_INET: IPv4 주소 체계 사용
  • SOCK_STREAM: TCP 프로토콜 사용
  • 0: 기본 프로토콜 선택

2. 주소 구조체 초기화


서버의 IP 주소와 포트 번호를 설정하는 구조체를 초기화합니다.

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;  // 모든 네트워크 인터페이스에서 연결 허용
server_addr.sin_port = htons(8080);       // 포트 번호 설정 (8080)
  • sin_family: 주소 체계 (IPv4)
  • sin_addr.s_addr: 서버 IP 주소 (INADDR_ANY는 모든 주소를 허용)
  • sin_port: 포트 번호 (네트워크 바이트 순서로 변환 필요)

3. 소켓 주소 바인딩


생성된 소켓에 IP 주소와 포트 번호를 바인딩합니다.

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

4. 연결 대기 상태로 설정


소켓을 연결 요청을 대기할 상태로 설정합니다.

if (listen(sockfd, 5) < 0) {
    perror("Listen failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
  • 5: 대기열의 최대 클라이언트 요청 수

5. 전체 초기화 과정 코드


아래는 서버 초기화와 소켓 생성 과정을 포함한 코드입니다.

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

int main() {
    int sockfd;
    struct sockaddr_in server_addr;

    // 1. 소켓 생성
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 주소 구조체 초기화
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);

    // 3. 주소 바인딩
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 4. 연결 대기
    if (listen(sockfd, 5) < 0) {
        perror("Listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port 8080\n");

    close(sockfd);
    return 0;
}

결과


위 과정을 통해 서버는 클라이언트 요청을 수락할 준비 상태가 됩니다. 다음 단계에서는 클라이언트 연결을 처리하는 방법을 설명합니다.

클라이언트 연결 처리


HTTP 서버에서 클라이언트와의 연결을 처리하는 단계는 클라이언트 요청을 수락하고 이를 처리하기 위한 핵심 단계입니다.

1. 연결 요청 수락


클라이언트가 서버에 연결 요청을 보내면 서버는 accept() 함수를 사용해 이를 처리합니다.

int client_sock;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_sock < 0) {
    perror("Accept failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
printf("Client connected\n");
  • accept(): 대기 중인 클라이언트 요청을 수락하고 새로운 소켓을 생성합니다.
  • client_sock: 클라이언트와의 통신에 사용되는 소켓 디스크립터입니다.

2. 클라이언트 요청 처리


클라이언트로부터 데이터를 수신하고, 이에 대한 응답을 생성합니다.

char buffer[1024] = {0};
int bytes_received = recv(client_sock, buffer, sizeof(buffer), 0);
if (bytes_received < 0) {
    perror("Receive failed");
    close(client_sock);
    return -1;
}
printf("Received request: %s\n", buffer);
  • recv(): 클라이언트로부터 데이터를 수신합니다.

3. 간단한 응답 전송


HTTP 요청을 처리한 후 클라이언트로 응답을 전송합니다.

const char* response = 
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/plain\r\n"
    "Content-Length: 13\r\n"
    "\r\n"
    "Hello, World!";
send(client_sock, response, strlen(response), 0);
  • HTTP 응답 구성: 상태 코드, 헤더, 본문으로 구성된 응답 메시지를 전송합니다.

4. 연결 종료


응답 전송이 완료되면 클라이언트와의 연결을 종료합니다.

close(client_sock);

5. 전체 코드


아래는 클라이언트 연결을 처리하는 전체 코드입니다.

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

int main() {
    int sockfd, client_sock;
    struct sockaddr_in server_addr, client_addr;
    char buffer[1024];
    socklen_t addr_len = sizeof(client_addr);

    // 서버 초기화
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(sockfd, 5);

    printf("Server is listening on port 8080\n");

    // 클라이언트 연결 처리
    client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_sock < 0) {
        perror("Accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("Client connected\n");

    // 요청 수신
    recv(client_sock, buffer, sizeof(buffer), 0);
    printf("Received request: %s\n", buffer);

    // 응답 전송
    const char* response = 
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/plain\r\n"
        "Content-Length: 13\r\n"
        "\r\n"
        "Hello, World!";
    send(client_sock, response, strlen(response), 0);

    // 연결 종료
    close(client_sock);
    close(sockfd);

    return 0;
}

결과


이 코드는 클라이언트의 요청을 수락하고 간단한 “Hello, World!” 응답을 반환합니다. 이후 단계에서는 HTTP 요청 및 응답을 더 세부적으로 구성하는 방법을 설명합니다.

HTTP 요청과 응답 구성


HTTP 서버는 클라이언트 요청을 파싱하고 적절한 HTTP 응답을 생성해야 합니다. 이 과정은 웹 서버의 핵심적인 동작이며, 요청의 처리와 응답의 구성이 정확해야 합니다.

1. HTTP 요청 파싱


클라이언트가 보낸 HTTP 요청은 텍스트 형식으로 전달됩니다. 주요 정보를 파싱해야 합니다.

예제 요청:

GET /index.html HTTP/1.1
Host: localhost:8080

파싱할 주요 요소:

  1. HTTP 메서드: GET, POST
  2. 경로: 요청 리소스 경로 (/index.html)
  3. 프로토콜: HTTP 버전 (HTTP/1.1)

파싱 예제 코드:

char method[16], path[256], protocol[16];
sscanf(buffer, "%s %s %s", method, path, protocol);
printf("Method: %s, Path: %s, Protocol: %s\n", method, path, protocol);

2. HTTP 응답 구성


HTTP 응답은 상태 코드, 헤더, 본문으로 구성됩니다.

예제 응답:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 19

<h1>Hello World</h1>
  • 상태 코드: 요청 처리 결과 (200 OK, 404 Not Found, 등)
  • 헤더: 응답의 메타데이터 (Content-Type, Content-Length 등)
  • 본문: 클라이언트에 전송할 실제 데이터

응답 구성 코드:

const char* response = 
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/html\r\n"
    "Content-Length: 19\r\n"
    "\r\n"
    "<h1>Hello World</h1>";
send(client_sock, response, strlen(response), 0);

3. 상태 코드 처리


요청 경로와 처리 상태에 따라 적절한 응답을 반환합니다.

if (strcmp(path, "/") == 0 || strcmp(path, "/index.html") == 0) {
    const char* response = 
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html\r\n"
        "Content-Length: 19\r\n"
        "\r\n"
        "<h1>Hello World</h1>";
    send(client_sock, response, strlen(response), 0);
} else {
    const char* not_found = 
        "HTTP/1.1 404 Not Found\r\n"
        "Content-Type: text/html\r\n"
        "Content-Length: 13\r\n"
        "\r\n"
        "404 Not Found";
    send(client_sock, not_found, strlen(not_found), 0);
}

4. 전체 요청 및 응답 처리 코드

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

int main() {
    int sockfd, client_sock;
    struct sockaddr_in server_addr, client_addr;
    char buffer[1024];
    socklen_t addr_len = sizeof(client_addr);

    // 서버 초기화
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(sockfd, 5);

    printf("Server is listening on port 8080\n");

    // 클라이언트 연결 처리
    client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
    recv(client_sock, buffer, sizeof(buffer), 0);

    // HTTP 요청 파싱
    char method[16], path[256], protocol[16];
    sscanf(buffer, "%s %s %s", method, path, protocol);

    // 응답 구성
    if (strcmp(path, "/") == 0 || strcmp(path, "/index.html") == 0) {
        const char* response = 
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: 19\r\n"
            "\r\n"
            "<h1>Hello World</h1>";
        send(client_sock, response, strlen(response), 0);
    } else {
        const char* not_found = 
            "HTTP/1.1 404 Not Found\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: 13\r\n"
            "\r\n"
            "404 Not Found";
        send(client_sock, not_found, strlen(not_found), 0);
    }

    // 연결 종료
    close(client_sock);
    close(sockfd);

    return 0;
}

결과


이 코드는 HTTP 요청을 파싱하고 요청 경로에 따라 적절한 응답을 반환합니다. 다음 단계에서는 정적 콘텐츠 제공 방법을 다룹니다.

정적 콘텐츠 제공


HTTP 서버는 클라이언트의 요청에 따라 HTML, CSS, 이미지와 같은 정적 콘텐츠를 제공할 수 있습니다. 이를 구현하기 위해 요청 경로를 기반으로 파일을 읽고 그 내용을 HTTP 응답으로 반환하는 방식을 사용합니다.

1. 요청 경로와 파일 매핑


클라이언트의 요청 경로를 기반으로 제공할 파일을 결정합니다.

char filepath[256] = "./www";
if (strcmp(path, "/") == 0) {
    strcat(filepath, "/index.html");
} else {
    strcat(filepath, path);
}
  • ./www: 정적 파일이 저장된 기본 디렉토리
  • /index.html: 기본 경로에 대한 파일

2. 파일 읽기


요청된 파일을 열고 내용을 읽습니다.

FILE* file = fopen(filepath, "r");
if (file == NULL) {
    const char* not_found = 
        "HTTP/1.1 404 Not Found\r\n"
        "Content-Type: text/html\r\n"
        "Content-Length: 13\r\n"
        "\r\n"
        "404 Not Found";
    send(client_sock, not_found, strlen(not_found), 0);
    return;
}

// 파일 내용 읽기
char file_buffer[1024];
size_t bytes_read = fread(file_buffer, 1, sizeof(file_buffer), file);
fclose(file);

3. HTTP 응답 생성


파일 내용을 HTTP 응답 본문으로 반환합니다.

char response[2048];
snprintf(response, sizeof(response),
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/html\r\n"
    "Content-Length: %ld\r\n"
    "\r\n",
    bytes_read);
send(client_sock, response, strlen(response), 0);
send(client_sock, file_buffer, bytes_read, 0);
  • Content-Length: 파일 크기
  • 파일 데이터 전송: 파일 내용을 본문으로 클라이언트에 전송

4. 전체 코드


아래는 정적 콘텐츠 제공을 포함한 전체 코드입니다.

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

int main() {
    int sockfd, client_sock;
    struct sockaddr_in server_addr, client_addr;
    char buffer[1024];
    socklen_t addr_len = sizeof(client_addr);

    // 서버 초기화
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(sockfd, 5);

    printf("Server is listening on port 8080\n");

    // 클라이언트 연결 처리
    client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
    recv(client_sock, buffer, sizeof(buffer), 0);

    // HTTP 요청 파싱
    char method[16], path[256], protocol[16];
    sscanf(buffer, "%s %s %s", method, path, protocol);

    // 요청 경로를 파일 경로로 매핑
    char filepath[256] = "./www";
    if (strcmp(path, "/") == 0) {
        strcat(filepath, "/index.html");
    } else {
        strcat(filepath, path);
    }

    // 파일 읽기
    FILE* file = fopen(filepath, "r");
    if (file == NULL) {
        const char* not_found = 
            "HTTP/1.1 404 Not Found\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: 13\r\n"
            "\r\n"
            "404 Not Found";
        send(client_sock, not_found, strlen(not_found), 0);
        close(client_sock);
        close(sockfd);
        return 0;
    }

    char file_buffer[1024];
    size_t bytes_read = fread(file_buffer, 1, sizeof(file_buffer), file);
    fclose(file);

    // HTTP 응답 생성 및 전송
    char response[2048];
    snprintf(response, sizeof(response),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html\r\n"
        "Content-Length: %ld\r\n"
        "\r\n",
        bytes_read);
    send(client_sock, response, strlen(response), 0);
    send(client_sock, file_buffer, bytes_read, 0);

    // 연결 종료
    close(client_sock);
    close(sockfd);

    return 0;
}

결과


이 코드는 클라이언트가 요청한 정적 파일을 제공하며, 파일이 없을 경우 404 Not Found 응답을 반환합니다. 이후 단계에서는 멀티스레딩을 통한 동시 요청 처리 방법을 다룹니다.

멀티스레딩 서버 설계


HTTP 서버가 여러 클라이언트의 요청을 동시에 처리하려면 멀티스레딩을 구현해야 합니다. 멀티스레딩은 각각의 클라이언트 요청을 독립적인 스레드로 처리함으로써 병렬 처리를 가능하게 합니다.

1. 멀티스레딩의 개념


멀티스레딩 서버는 클라이언트 요청마다 별도의 스레드를 생성하여 처리합니다. 이를 통해 하나의 클라이언트 요청이 처리 중일 때도 다른 요청이 대기하지 않고 병렬로 처리됩니다.

2. POSIX 스레드(Pthread) 사용


C 언어에서 멀티스레딩은 주로 POSIX 스레드 라이브러리(Pthread)를 사용하여 구현됩니다.

  • pthread_create: 새로운 스레드를 생성합니다.
  • pthread_exit: 스레드 종료를 처리합니다.

3. 스레드 핸들러 함수


스레드가 수행할 작업을 함수로 정의합니다. 이 함수는 클라이언트 요청을 처리합니다.

void* handle_client(void* arg) {
    int client_sock = *(int*)arg;
    free(arg);

    char buffer[1024] = {0};
    recv(client_sock, buffer, sizeof(buffer), 0);

    char method[16], path[256], protocol[16];
    sscanf(buffer, "%s %s %s", method, path, protocol);

    char filepath[256] = "./www";
    if (strcmp(path, "/") == 0) {
        strcat(filepath, "/index.html");
    } else {
        strcat(filepath, path);
    }

    FILE* file = fopen(filepath, "r");
    if (file == NULL) {
        const char* not_found = 
            "HTTP/1.1 404 Not Found\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: 13\r\n"
            "\r\n"
            "404 Not Found";
        send(client_sock, not_found, strlen(not_found), 0);
    } else {
        char file_buffer[1024];
        size_t bytes_read = fread(file_buffer, 1, sizeof(file_buffer), file);
        fclose(file);

        char response[2048];
        snprintf(response, sizeof(response),
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: %ld\r\n"
            "\r\n",
            bytes_read);
        send(client_sock, response, strlen(response), 0);
        send(client_sock, file_buffer, bytes_read, 0);
    }

    close(client_sock);
    pthread_exit(NULL);
}

4. 클라이언트 연결과 스레드 생성


각 클라이언트 연결마다 새로운 스레드를 생성하여 처리합니다.

while (1) {
    int* client_sock = malloc(sizeof(int));
    *client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
    if (*client_sock < 0) {
        perror("Accept failed");
        free(client_sock);
        continue;
    }

    pthread_t thread_id;
    pthread_create(&thread_id, NULL, handle_client, client_sock);
    pthread_detach(thread_id);  // 스레드 종료 후 자동 리소스 정리
}

5. 전체 코드

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

void* handle_client(void* arg) {
    int client_sock = *(int*)arg;
    free(arg);

    char buffer[1024] = {0};
    recv(client_sock, buffer, sizeof(buffer), 0);

    char method[16], path[256], protocol[16];
    sscanf(buffer, "%s %s %s", method, path, protocol);

    char filepath[256] = "./www";
    if (strcmp(path, "/") == 0) {
        strcat(filepath, "/index.html");
    } else {
        strcat(filepath, path);
    }

    FILE* file = fopen(filepath, "r");
    if (file == NULL) {
        const char* not_found = 
            "HTTP/1.1 404 Not Found\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: 13\r\n"
            "\r\n"
            "404 Not Found";
        send(client_sock, not_found, strlen(not_found), 0);
    } else {
        char file_buffer[1024];
        size_t bytes_read = fread(file_buffer, 1, sizeof(file_buffer), file);
        fclose(file);

        char response[2048];
        snprintf(response, sizeof(response),
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: %ld\r\n"
            "\r\n",
            bytes_read);
        send(client_sock, response, strlen(response), 0);
        send(client_sock, file_buffer, bytes_read, 0);
    }

    close(client_sock);
    pthread_exit(NULL);
}

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(sockfd, 5);

    printf("Server is listening on port 8080\n");

    while (1) {
        int* client_sock = malloc(sizeof(int));
        *client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
        if (*client_sock < 0) {
            perror("Accept failed");
            free(client_sock);
            continue;
        }

        pthread_t thread_id;
        pthread_create(&thread_id, NULL, handle_client, client_sock);
        pthread_detach(thread_id);
    }

    close(sockfd);
    return 0;
}

결과


이 코드는 여러 클라이언트 요청을 동시 처리하며, 요청마다 새로운 스레드를 생성해 독립적으로 처리합니다. 이후 단계에서는 오류 처리 및 디버깅 방법을 설명합니다.

오류 처리 및 디버깅


HTTP 서버는 실행 중 다양한 오류 상황에 직면할 수 있습니다. 적절한 오류 처리와 디버깅 기법을 활용하면 서버의 안정성과 유지보수성을 높일 수 있습니다.

1. 일반적인 오류와 해결 방법

1.1 소켓 생성 오류

  • 원인: 소켓을 생성하지 못한 경우.
  • 해결 방법: 오류 코드를 확인하고 perror()로 상세 메시지를 출력합니다.
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("Socket creation failed");
    exit(EXIT_FAILURE);
}

1.2 포트 바인딩 실패

  • 원인: 포트가 이미 사용 중인 경우.
  • 해결 방법: 다른 포트를 사용하거나 이전 프로세스를 종료합니다.
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("Bind failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

1.3 클라이언트 연결 수락 실패

  • 원인: 클라이언트 연결이 올바르지 않거나 시스템 리소스가 부족한 경우.
  • 해결 방법: 연결 실패를 기록하고 서버를 계속 실행합니다.
int client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_sock < 0) {
    perror("Accept failed");
    continue;
}

2. 로그 파일 활용


서버 로그를 작성하면 문제를 추적하고 디버깅하기 용이합니다.

FILE* log_file = fopen("server.log", "a");
fprintf(log_file, "Client connected: %s\n", inet_ntoa(client_addr.sin_addr));
fclose(log_file);
  • 로그 파일: 주요 이벤트와 오류를 기록합니다.
  • IP 주소 기록: inet_ntoa()를 사용해 클라이언트 IP를 기록합니다.

3. 디버깅 기법

3.1 디버깅 출력

  • 특정 변수 값을 출력해 서버 동작을 확인합니다.
printf("Received request: %s\n", buffer);
printf("Serving file: %s\n", filepath);

3.2 gdb 디버거 사용

  • gdb를 사용해 코드의 중단점 설정, 변수 값 확인 등 심층 디버깅을 수행합니다.
gdb ./server
  • 중단점 설정: 특정 라인에서 서버 실행을 멈추고 상태를 확인합니다.
break main

4. 메모리 누수 검사


HTTP 서버에서 메모리 할당 및 해제가 제대로 이루어지지 않으면 메모리 누수가 발생할 수 있습니다.

  • valgrind를 사용하여 메모리 누수를 검사합니다.
valgrind --leak-check=full ./server
  • 결과 분석: 누수된 메모리의 위치와 크기를 확인하고 수정합니다.

5. 에러 응답 개선


오류 상황에서 적절한 HTTP 응답을 반환하여 클라이언트가 문제를 이해할 수 있도록 합니다.

const char* error_response = 
    "HTTP/1.1 500 Internal Server Error\r\n"
    "Content-Type: text/html\r\n"
    "Content-Length: 23\r\n"
    "\r\n"
    "<h1>Server Error</h1>";
send(client_sock, error_response, strlen(error_response), 0);

6. 전체 오류 처리 코드

int client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_sock < 0) {
    perror("Accept failed");
    FILE* log_file = fopen("server.log", "a");
    fprintf(log_file, "Error accepting connection\n");
    fclose(log_file);
    continue;
}

결과


위의 기법을 활용하면 서버의 오류를 효율적으로 처리하고 안정성을 높일 수 있습니다. 다음 단계에서는 기사 내용을 요약합니다.

요약


C 언어로 간단한 HTTP 서버를 구현하는 과정을 소개했습니다. HTTP 서버의 기본 개념과 네트워크 소켓의 이해부터 서버 초기화, 클라이언트 요청 처리, 정적 콘텐츠 제공, 멀티스레딩을 통한 동시 요청 처리 방법을 다뤘습니다. 또한, 서버 실행 중 발생할 수 있는 오류 처리와 디버깅 방법까지 설명했습니다. 이를 통해 C 언어를 활용한 네트워크 프로그래밍의 핵심 개념을 학습하고, 실질적으로 동작하는 HTTP 서버를 구축할 수 있습니다.