Fascination
article thumbnail

W4-W5: C언어로 HTTP 서버 구현

참고 자료: C, TCP 기반으로 간단한 HTTP 서버 작성하기


# 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 [위의 결과로 나온 프로세스 번호]

 

profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!