본문 바로가기
컴퓨터 활용(한글, 오피스 등)/기타

C언어로 HTTP 서버 구현

by 3604 2024. 4. 5.
728x90

출처: https://fascination-euna.tistory.com/entry/P4C-W4-W5-C%EC%96%B8%EC%96%B4%EB%A1%9C-HTTP-%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84

# 1. 구현 내용

- 서버 프로그램이 존재하는 디렉터리를 기준으로 파일을 접근할 수 있는 서버
- 개발 순서

  1. socket(), bind(), listen() 등을 활용하여 TCP 소켓을 만듬
  2. accept() 후에 HTTP 프로토콜로 처리하는 함수를 만듬
  3. 처리 중에 에러가 발생하면 404, 500 상태 코드로 응답

- 과제 제출 예시 영상

 

# 2. TCP 소켓 생성하기

1) TCP란?

  • Transmission Control Protocol
  • 서버와 클라이언트 간에 데이터를 신뢰성 있게 전달하기 위해 만들어진 프로토콜
  • 데이터를 전송하기 전에 데이터 전송을 위한 연결을 만드는 연결지향 프로토콜
  • 데이터는 네트워크선로를 통해 전달되는 과정에서 손실되거나 순서가 뒤바뀌어서 전달될 수 있는데, TCP는 손실을 검색해내서, 이를 교정하고 순서를 재조립할 수 있도록 해줌

2) TCP 특징

  • 신뢰성
  • 흐름 제어
  • 다중화
  • 연결형 서비스
  • TCP 연결은 데이터를 양방향으로 운반할 수 있음
  • TCP 연결은 3way handshake 절차를 사용하여 열림

3) Socket 이란?

  • 소켓은 프로세스가 드넓은 네트워크 세계로 데이터를 내보내거나 혹은 그 세계로부터 데이터를 받기 위한 실제적인 창구 역할을 함
  • 프로세스가 데이터를 보내거나 받기 위해서는 소켓을 열어서 소켓에 데이터를 써보내거나 소켓으로부터 데이터를 읽어들여야 함
  • 소켓은 프로토콜, IP 주소, 포트 넘버로 정의됨
  • 소켓은 떨어져 있는 두 호스트를 연결해주는 도구로써 인터페이스의 역할을 하는데, 데이터를 주고 받을 수 있는 구조체로 소켓을 통해 데이터 통로가 만들어짐
  • 소켓은 역할에 따라 서버 소켓, 클라이언트 소켓으로 구분됨

4) 소켓통신의 흐름

- 서버(Server): 클라이언트 소켓의 연결 요청을 대기하고, 연결 요청이 오면 클라이언트 소켓을 생성하여 통신이 가능하게 함

  • socket() 함수를 이용하여 소켓을 생성
  • bind() 함수로 ip와 port 번호를 설정하게 됨
  • listen() 함수로 클라이언트와 접근 요청에 수신 대기열을 만들어 몇 개의 클라이언트를 대기 시킬지 결정
  • accept() 함수를 사용하여 클라이언트와의 연결을 기다림

- 클라이언트(Client): 실제로 데이터 송수신이 일어나는 것은 클라이언트 소켓임

  • socket() 함수로 가장먼저 소켓을 엶
  • connect() 함수를 이용하여 통신 할 서버의 설정된 ip와 port 번호에 통신을 시도
  • 통신을 시도 시, 서버가 accept() 함수를 이용하여 클라이언트의 socket descriptor를 반환
  • 이를 통해 클라이언트와 서버가 서로 read(), write()를 하며 통신 (이 과정이 반복됨)

5) TCP 소켓 준비

- bind() 함수

/*

    생성된 소켓 lsock(sd)에 주소 할당

    return bind() 값

*/

int bind_lsock(int lsock, int port) {

    struct sockaddr_in sin;

    sin.sin_family = AF_INET;

    sin.sin_addr.s_addr = htonl(INADDR_ANY);

    sin.sin_port = htons(port);

    return bind(lsock, (struct sockaddr *)&sin, sizeof(sin));

}

생성된 소켓 lsock(sd)에 주소 할당 및 bind된 결과를 리턴

- main() 함수

int main(int argc, char **argv) {

    int port, pid;

    int lsock, asock;

    struct sockaddr_in remote_sin;

    socklen_t remote_sin_len;

    if (argc < 2) {

        printf("Usage: %s {port}\n",argv[0]);

        exit(0);

    }

    port = atoi(argv[1]);

    printf("[INFO] The server will listen to port: %d.\n", port);

    

    lsock = socket(AF_INET, SOCK_STREAM, 0);

    if (lsock < 0) {

        perror("[ERR] failed to create lsock.\n");

        exit(1);

    }

    if (bind_lsock(lsock, port) < 0) {

        perror("[ERR] failed to bind lsock.\n");

        exit(1);

    }

  

    printf("bind() success\n"); // 바인드 성공

    if (listen(lsock, 10) < 0) {

        perror("[ERR] failed to listen lsock.\n");

        exit(1);

    }

    printf("socket() success\n"); // 소켓 성공

    signal(SIGCHLD, SIG_IGN);

    while (1) {

        asock = accept(lsock, (struct sockaddr *)&remote_sin, &remote_sin_len);

        if (asock < 0) {

            perror("[ERR] failed to accept.\n");

            continue;

        }

        pid = fork(); // 멀티프로세스 생성 -> fork() 사용

        if (pid == 0) {

            close(lsock);

            http_handler(asock);

            close(asock);

            exit(0);

        }

        if (pid != 0)   { close(asock); }

        if (pid < 0)    { perror("[ERR] failed to fork.\n"); }

    }

}

TCP 소켓 생성
이때 멀티 스레드 방식과 멀티 프로세스 방식을 사용할 수 있는데 해당 코드는 멀티 프로세스를 사용함

 

# 3. HTTP 구현

1) HTTP 프로토콜

- HTTP 프로토콜: TCP 소켓을 바탕으로 특정한 형식, 포맷으로 데이터를 주고 받는 것
- 브라우저가 보내는 요청:

GET / HTTP/1.1
Host: developer.mozilla.org Accept-Language: fr

- 서버가 보내는 응답

HTTP/1.1 200 OK
Date: Sat, 09 Oct 2010 14:28:02 GMT
Server: Apache
Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
ETag: "51142bc1-7449-479b075b2891b"
Accept-Ranges: bytes
Content-Length: 29769
Content-Type: text/html
<!DOCTYPE html... (here comes the 29769 bytes of the requested web page)

- 요청은 브라우저에서 보내주므로, 구현할 서버는 이 요청을 읽고 적절한 응답을 보내는 것

  • 요청으로부터 Path("/")를 읽어 적절한 리소스를 반환해야 함
  • 응답에 적절한 상태 코드와 헤더를 적어주어야 함
  • Content-Length: 브라우저가 헤더 다음 몇 바이트만큼 읽어야 하는지 알려줌
  • Content-Type: body가 어떤 타입인지, 브라우저에서 어떻게 보여주어야 하는지 알려줌

2) HTTP 구현 함수

- fill_header(): 상태 코드, 헤더 내용 등을 주어진 포인터에 채움

/*

    주어진 매개 변수를 기준으로 HTTP 헤더 형식 지정

*/

void fill_header(char *header, int status, long len, char *type) {

    char status_text[40];

    switch (status) {

        case 200:

            strcpy(status_text, "OK"); break;

        case 404:

            strcpy(status_text, "Not Found"); break;

        case 500:

        default:

            strcpy(status_text, "Internal Server Error"); break;

    }

    sprintf(header, HEADER_FMT, status, status_text, len, type);

}

- find_mime(): 파일의 확장자를 참조하여 적절한 Content Type 값을 주어진 포인터에 채움

/*

    uri로부터 content type 찾기

*/

void find_mime(char *ct_type, char *uri) {

    char *ext = strrchr(uri, '.');

    if (!strcmp(ext, ".html"))

        strcpy(ct_type, "text/html");

    else if (!strcmp(ext, ".jpg") || !strcmp(ext, ".jpeg"))

        strcpy(ct_type, "image/jpeg");

    else if (!strcmp(ext, ".png"))

        strcpy(ct_type, "image/png");

    else if (!strcmp(ext, ".css"))

        strcpy(ct_type, "text/css");

    else if (!strcmp(ext, ".js"))

        strcpy(ct_type, "text/javascript");

    else strcpy(ct_type, "text/plain");

}

- handle_404(), handle_500(): 상태 코드 400, 500로 응답할 때 사용

/*

    handler for not found(404)

*/

void handle_404(int asock) {

    char header[BUF_SIZE];

    fill_header(header, 404, sizeof(NOT_FOUND_CONTENT), "text/html");

    write(asock, header, strlen(header));

}

/*

    handler for internal server error(500)

*/

void handle_500(int asock) {

    char header[BUF_SIZE];

    fill_header(header, 500, sizeof(SERVER_ERROR_CONTENT), "text/html");

    write(asock, header, strlen(header));

}

- http_handler(): main() 함수에서 호출되는 대표 handler로, 요청된 파일을 읽으려고하며, 파일 접근에 성공하면 상태 코드 200으로 파일의 내용을 정상적으로 보냄. 도중에 실패하면 위의 handle_404() 혹은 handle_500()을 호출

/*

    main http handler

    요청된 리소스를 열고 전송

    failure에 대한 에러 호출

*/

void http_handler(int asock) {

    char header[BUF_SIZE];

    char buf[BUF_SIZE];

    char safe_uri[BUF_SIZE];

    char *local_uri;

    struct stat st;

    if (read(asock, buf, BUF_SIZE) < 0) {

        perror("[ERR] Failed to read request.\n");

        handle_500(asock); return;

    }

    printf("%s",buf); // 버퍼에 읽어들인 내용 모두 출력

    char *method = strtok(buf, " ");

    char *uri = strtok(NULL, " ");

    strcpy(safe_uri, uri);

    if (!strcmp(safe_uri, "/")) strcpy(safe_uri, "/index.html"); // '/'라면 자동으로 index.html을 match

    

    local_uri = safe_uri + 1;

    if (stat(local_uri, &st) < 0) {

        handle_404(asock); return;

    }

    int fd = open(local_uri, O_RDONLY);

    if (fd < 0) {

        handle_500(asock); return;

    }

    int ct_len = st.st_size;

    char ct_type[40];

    find_mime(ct_type, local_uri);

    fill_header(header, 200, ct_len, ct_type);

    write(asock, header, strlen(header));

    int cnt;

    while ((cnt = read(fd, buf, BUF_SIZE)) > 0)

        write(asock, buf, cnt);

}

 

# 4.산출물

- 컴파일: gcc -o http_server http_server.c (MacOS/Linux)
- 사용 파일: index.html, index.css, index.js
- index.html

<!doctype html>

<html>

    <head>

        <title>Testing</title>

        <link rel="stylesheet" href="index.css">

    </head>

    <body>

        <p>

            <strong>Hello, nonsong!</strong>

        </p>

        <img src="img/nonsong.png">

        <script src="index.js"></script>

    </body>

</html>

- index.css

* {

    background-color: skyblue;

    color: gray;

}

img {

    border-radius: 10px;

}

- index.js

console.log("Hello!");

console.log("Thank you.");

- 결과 영상

* 실행 후 터미널에서 해당 port에 대한 프로세스를 kill 해주어야 다음번에도 실행이 가능함 (다른 포트를 사용한다면 안해줘도 됨)

sudo lsof -i :8080

kill [위의 결과로 나온 프로세스 번호]
728x90