[WebProxy] Tiny 서버 구현 코드 해석 및 응답 확인하기

2025. 5. 3. 22:50·크래프톤 정글/Code 정글(C언어)

Tiny 서버 구현 코드 해석 및 응답 확인하기

echo 서버 구현이 끝났다면, 이제 실제 클라이언트가 요청한 콘텐츠를 처리할 수 있는 서버를 만들어 볼 차례인데요. 바로 Tiny 웹 서버입니다. 이제 우리가 요청한 웹 페이지나 이미지를 요청하면 이를 서버가 제공해줄 수 있고, 심지어 두 개의 숫자를 인자로 넘겨주면 그 합을 받아볼 수도 있습니다.

 

기존의 echo 서버가 단순히 클라이언트의 메시지를 다시 반환하는 수준에 머물렀다면, 이제는 Tiny 웹 서버를 통해 클라이언트가 HTTP 프로토콜을 이용해 요청한 정적 콘텐츠와 동적 콘텐츠를 실제로 처리하여 응답해주는 과정을 경험하게 됩니다.

 

이 글에서는 Tiny 서버의 핵심 함수들을 라인별로 하나씩 뜯어보며, 어떤 방식으로 요청을 처리하고 응답을 생성하는지 살펴보고자 합니다. 마지막에는 우리가 만든 서버가 웹 브라우저에서 콘텐츠를 잘 처리하는지 실제로 테스트해보는 방법까지 소개합니다.

 

 

구현 코드 해석하기

main()

클라이언트의 요청을 반복적으로 수락하고 처리하는 웹 서버의 메인 루프 함수입니다.

int main(int argc, char **argv)
{
  int listenfd, connfd;  // listenfd: 클라이언트의 연결 요청을 기다리는 소켓
                         // connfd: 실제 통신에 사용되는 소켓 (accept 후 사용됨)

  char hostname[MAXLINE], port[MAXLINE];  // 클라이언트의 주소 정보(IP, 포트)를 저장할 버퍼

  socklen_t clientlen;  // 클라이언트 주소 구조체의 크기를 저장할 변수

  struct sockaddr_storage clientaddr;  // 클라이언트 주소 정보를 담을 구조체
                                       // IPv4, IPv6 모두 호환 가능한 형태

  // 입력 인자 개수가 2개가 아닌 경우 (즉, 포트 번호가 제공되지 않으면) 에러 출력
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);  // 사용법 안내
    exit(1);  // 에러로 인한 강제 종료
  }

  // 지정된 포트 번호로 듣기 소켓 생성 및 바인딩
  listenfd = Open_listenfd(argv[1]);

  // 서버는 무한 루프를 돌며 클라이언트 요청을 기다림
  while (1)
  {
    clientlen = sizeof(clientaddr);  // 클라이언트 주소 구조체 크기 초기화

    // 클라이언트로부터 연결 요청 수락
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);

    // 연결된 클라이언트의 호스트 이름과 포트 번호를 얻어옴
    Getnameinfo((SA *)&clientaddr, clientlen,
                hostname, MAXLINE,
                port, MAXLINE,
                0);

    // 클라이언트 정보 출력
    printf("Accepted connection from (%s, %s)\n", hostname, port);

    // 요청 처리 함수 호출 (정적/동적 컨텐츠 처리)
    doit(connfd);

    // 통신 종료 후 연결 닫기
    Close(connfd);
  }
}

 

doit()

클라이언트 요청을 분석해 정적 또는 동적 콘텐츠를 제공하는 Tiny 웹 서버의 핵심 처리 함수입니다.

void doit(int fd) {
  int is_static;  // 요청한 URI가 정적 콘텐츠인지 동적 콘텐츠인지 구분
  struct stat sbuf; // 요청한 파일의 상태(존재 여부, 권한 등)를 저장하는 구조체

  // 클라이언트 요청 줄과 메서드, URI, HTTP 버전 등을 저장할 버퍼
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];

  // 정적 콘텐츠일 경우 파일 이름, 동적 콘텐츠일 경우 CGI 인자들이 저장될 버퍼
  char filename[MAXLINE], cgiargs[MAXLINE];

  rio_t rio;  // robust I/O를 위한 구조체

  // 요청 줄 읽기
  Rio_readinitb(&rio, fd);                 // rio 구조체를 fd로 초기화
  Rio_readlineb(&rio, buf, MAXLINE);       // 요청 줄 한 줄 읽음
  printf("Request headers:\n");
  printf("%s", buf);                       // 요청 줄 출력 (예: GET /index.html HTTP/1.0)

  // 요청 줄을 파싱하여 메서드, URI, 버전 추출
  sscanf(buf, "%s %s %s", method, uri, version);

  // Tiny는 GET 메서드만 지원하므로, 다른 메서드는 에러 응답
  if (strcasecmp(method, "GET")) {
    clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
    return;
  }

  // 요청 헤더는 무시하고 읽기 처리만 진행 (다음 요청 처리를 위한 준비)
  read_requesthdrs(&rio);

  /* URI를 분석해 파일 이름과 CGI 인자 추출 */
  is_static = parse_uri(uri, filename, cgiargs);  // 정적이면 1, 동적이면 0 반환

  // 요청한 파일이 존재하는지 확인. 실패하면 404 응답
  if (stat(filename, &sbuf) < 0) {
    clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
    return;
  }

  /* 파일 존재 시, 정적/동적 콘텐츠에 따라 분기 처리 */
  if (is_static) {
    // 정적 콘텐츠일 경우: 일반 파일이며 읽기 권한이 있어야 함
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
      clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't read the file");
      return;
    }
    // 정적 콘텐츠 제공
    serve_static(fd, filename, sbuf.st_size);
  } else {
    // 동적 콘텐츠일 경우: 일반 파일이며 실행 권한이 있어야 함
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
      clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
      return;
    }
    // 동적 콘텐츠 제공
    serve_dynamic(fd, filename, cgiargs);
  }
}

 

clienterror()

클라이언트 요청 처리 중 오류가 발생했을 때, HTML 형식의 에러 메시지를 클라이언트에게 전송합니다.

void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
  char buf[MAXLINE], body[MAXBUF];  // 응답 헤더와 바디를 저장할 버퍼

  /* HTML 형식의 에러 메시지 바디 생성 */
  sprintf(body, "<html><title>Tiny Error</title>");  // HTML 타이틀
  sprintf(body, "%s<body bgcolor=\"ffffff\">\r\n", body);  // 배경색 흰색으로 설정
  sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);   // ex)"404: Not found"
  sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);  // ex)"Tiny couldn't find this file: /bad/path"
  sprintf(body, "%s<hr><em>The Tiny Web Server</em>\r\n", body);  // 서버 서명

  /* HTTP 응답 헤더 생성 및 전송 */
  // 상태 줄 전송: 예) HTTP/1.0 404 Not found
  sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
  Rio_writen(fd, buf, strlen(buf));

  // 콘텐츠 타입: HTML
  sprintf(buf, "Content-type: text/html\r\n");
  Rio_writen(fd, buf, strlen(buf));

  // 콘텐츠 길이 명시
  sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
  Rio_writen(fd, buf, strlen(buf));

  /* HTML 본문 전송 */
  Rio_writen(fd, body, strlen(body));  // 위에서 만든 에러 HTML 바디 전송
}

 

read_requesthdrs()

Tiny 서버는 요청 헤더를 별도로 처리하지 않기 때문에, 단순히 모든 헤더 줄을 읽고 서버 터미널에 출력한 뒤 무시합니다. 이는 주로 디버깅을 위한 용도로 쓰이지요. (doit 하위 함수)

void read_requesthdrs(rio_t *rp) {
  char buf[MAXLINE];  // 요청 헤더 한 줄씩 저장할 버퍼

  // 첫 번째 요청 헤더 줄 읽기
  Rio_readlineb(rp, buf, MAXLINE);

  // 빈 줄(\r\n)이 나올 때까지 헤더를 계속 읽음
  while (strcmp(buf, "\r\n")) {
    Rio_readlineb(rp, buf, MAXLINE);  // 다음 줄 읽기
    printf("%s", buf);  // 헤더 내용 출력 (디버깅 목적)
  }

  return;  // 헤더는 따로 처리하지 않고 무시
}

 

parse_uri()

URI를 분석해 요청이 정적 콘텐츠인지 동적 콘텐츠인지 구분하고, 파일 경로와 CGI 인자들을 추출합니다. (doit 하위 함수)

int parse_uri(char *uri, char *filename, char *cgiargs) {
  char *ptr;	// 동적 콘텐츠 인자 분리에 쓰이는 포인터

  /* 정적 콘텐츠: URI에 "cgi-bin"이 포함되지 않은 경우 */
  if (!strstr(uri, "cgi-bin")) {
    strcpy(cgiargs, "");         // CGI 인자는 없음 (빈 문자열로 설정)
    strcpy(filename, ".");       // 상대 경로 시작 (서버 루트 기준)
    strcat(filename, uri);       // URI를 이어 붙여 실제 파일 경로 생성

    // URI가 '/'로 끝나면 기본 파일인 "home.html"로 설정
    if (uri[strlen(uri) - 1] == '/') {
      strcat(filename, "home.html");
    }

    return 1;  // 정적 콘텐츠임을 반환
  }

  /* 동적 콘텐츠: URI에 "cgi-bin"이 포함된 경우 */
  else {
    ptr = index(uri, '?');       // '?' 문자를 기준으로 CGI 인자 분리 시도

    if (ptr) {
      // '?' 뒤의 문자열을 cgiargs에 저장
      strcpy(cgiargs, ptr + 1);
      *ptr = '\0';               // '?' 자리에 널 문자 삽입 → URI 문자열 자르기
    } else {
      strcpy(cgiargs, "");       // 인자가 없는 경우
    }

    strcpy(filename, ".");       // 상대 경로 시작
    strcat(filename, uri);       // 나머지 URI로 파일 이름 구성 (스크립트 경로)

    return 0;  // 동적 콘텐츠임을 반환
  }
}

예시로 이해하기

  • 요청: GET /index.html HTTP/1.0
    • filename = ./index.html, cgiargs = "", 정적 콘텐츠 → return 1
  • 요청: GET /cgi-bin/add?1&2 HTTP/1.0
    • filename = ./cgi-bin/add, cgiargs = 1&2, 동적 콘텐츠 → return 0

 

serve_static()

정적 콘텐츠(예: HTML, 이미지 등)를 클라이언트에게 전송하는 함수입니다.

void serve_static(int fd, char *filename, int filesize) {
  int srcfd;                           // 디스크에서 파일을 열 때 사용할 파일 디스크립터
  char *srcp;                          // 파일을 메모리로 매핑할 포인터
  char filetype[MAXLINE], buf[MAXBUF];  // MIME 타입, 응답 헤더를 저장할 버퍼

  /* 응답 헤더 생성 및 전송 */
  get_filetype(filename, filetype);   // 파일 확장자 기반으로 MIME 타입 결정

  // 상태 줄, 헤더 정보 구성
  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
  sprintf(buf, "%sConnection: close\r\n", buf);
  sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
  sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

  // 헤더 전송
  Rio_writen(fd, buf, strlen(buf));
  printf("Response headers:\n");
  printf("%s", buf);  // 터미널에도 출력 (디버깅용)

  /* 응답 바디 전송 (파일 내용 전송) */
  srcfd = Open(filename, O_RDONLY, 0);  // 파일 열기 (읽기 전용)
  
  // 파일을 가상 메모리 주소에 매핑 (읽기 전용, private)
  srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);

  Close(srcfd);                         // 매핑 완료 후 파일 디스크립터는 닫음

  Rio_writen(fd, srcp, filesize);       // 매핑된 파일 내용을 클라이언트로 전송
  Munmap(srcp, filesize);               // 매핑된 메모리 해제
}

 

get_filetype()

요청된 파일의 확장자를 보고 MIME 타입을 결정하여 filetype에 저장합니다. (serve_static 하위 함수)

void get_filetype(char *filename, char *filetype) {
  // 확장자에 따라 MIME 타입을 결정하여 filetype에 저장
  if (strstr(filename, ".html")) {
    strcpy(filetype, "text/html");      // HTML 문서
  } else if (strstr(filename, ".gif")) {
    strcpy(filetype, "image/gif");      // GIF 이미지
  } else if (strstr(filename, ".png")) {
    strcpy(filetype, "image/png");      // PNG 이미지
  } else if (strstr(filename, ".jpg")) {
    strcpy(filetype, "image/jpeg");     // JPG 이미지
  } else {
    strcpy(filetype, "text/plain");     // 기본값: 일반 텍스트
  }
}

 

serve_dynamic()

CGI 프로그램을 실행해 동적 콘텐츠를 생성하고, 결과를 클라이언트에게 전송합니다.

void serve_dynamic(int fd, char *filename, char *cgiargs) {
  char buf[MAXLINE];               // 응답 헤더를 담을 버퍼
  char *emptylist[] = {NULL};      // CGI 프로그램에 넘길 인자 리스트 (빈 리스트)

  /* 클라이언트에게 기본적인 HTTP 응답 헤더 전송 */
  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  Rio_writen(fd, buf, strlen(buf));  // 상태 줄 전송

  sprintf(buf, "Server: Tiny Web Server\r\n");
  Rio_writen(fd, buf, strlen(buf));  // 서버 이름 헤더 전송

  /* 자식 프로세스를 생성하여 CGI 프로그램 실행 */
  if (Fork() == 0) {  // 자식 프로세스
    // CGI 환경 변수 설정 (쿼리 문자열 전달)
    setenv("QUERY_STRING", cgiargs, 1);  // 예: QUERY_STRING="1&2"

    // 표준 출력을 클라이언트 소켓으로 변경 (CGI 출력이 클라이언트로 직접 전송됨)
    Dup2(fd, STDOUT_FILENO);  // STDOUT_FILENO == 1

    // CGI 프로그램 실행 (인자 없음, 환경 변수는 부모와 동일)
    Execve(filename, emptylist, environ);  // 실패 시 return하지 않고 종료됨
  }

  /* 부모 프로세스는 자식이 끝날 때까지 기다림 */
  Wait(NULL);  // 좀비 프로세스 방지
}

 

CGI: adder.c

웹 브라우저에서 전달받은 두 수를 더해 결과를 HTML 형식으로 출력하는 CGI 프로그램입니다. (serve_dynamic에서 실행)

#include "csapp.h"

int main(void)
{
  char *buf, *p;                       // buf: 전체 쿼리 문자열, p: '&'를 찾기 위한 포인터
  char arg1[MAXLINE], arg2[MAXLINE];  // 첫 번째/두 번째 인자를 담을 문자열 버퍼
  char content[MAXLINE];              // 최종 출력할 HTML 콘텐츠
  int n1 = 0, n2 = 0;                  // 파싱한 두 정수

  /* 쿼리 문자열에서 두 인자를 추출 */
  if ((buf = getenv("QUERY_STRING")) != NULL)
  {
    // QUERY_STRING 예: "num1=3&num2=5"
    p = strchr(buf, '&');     // '&' 위치 찾기 → "num2=5" 시작 위치
    *p = '\0';                // '&'를 널 문자로 바꿔 "num1=3"과 "num2=5"를 분리

    strcpy(arg1, buf);        // arg1 = "num1=3"
    strcpy(arg2, p + 1);      // arg2 = "num2=5"

    // '=' 다음 문자열을 정수로 변환
    n1 = atoi(strchr(arg1, '=') + 1);  // n1 = 3
    n2 = atoi(strchr(arg2, '=') + 1);  // n2 = 5
  }

  /* 결과 HTML 콘텐츠 구성 */
  sprintf(content, "QUERY_STRING=%s\r\n<p>", buf);  // 쿼리 문자열 출력
  sprintf(content + strlen(content), "Welcome to add.com: ");
  sprintf(content + strlen(content), "THE Internet addition portal.\r\n<p>");
  sprintf(content + strlen(content), "The answer is: %d + %d = %d\r\n<p>",
          n1, n2, n1 + n2);                         // 덧셈 결과 출력
  sprintf(content + strlen(content), "Thanks for visiting!\r\n");

  /* HTTP 응답 헤더 및 바디 출력 (stdout → 클라이언트로 전달됨) */
  printf("Content-type: text/html\r\n");           // MIME 타입
  printf("Content-length: %d\r\n", (int)strlen(content));  // 길이 헤더
  printf("\r\n");                                   // 헤더와 바디 구분
  printf("%s", content);                            // HTML 본문 출력
  fflush(stdout);                                   // 출력 즉시 전송

  exit(0);  // 정상 종료
}

 

브라우저에서 Tiny 서버의 응답 확인하기

Tiny 서버의 구현이 끝났다면, 이제 잘 동작하는지 확인을 해봐야겠죠. 브라우저에서 URL을 입력해 직접 정적 콘텐츠와 동적 콘텐츠를 받아보면서, 하나의 서버를 완성해 냈다는 보람을 느껴 보셨으면 좋겠습니다.

 

1. make로 tiny 및 adder 컴파일

각각 tiny 디렉터리와 cgi-bin 디렉터리에서 make 명령어를 통해 Tiny 서버 프로그램과 CGI 프로그램(adder)을 만들어 줍니다.

 

2. Tiny 서버 실행

터미널에서 아래의 명령어를 실행해 tiny 서버를 실행합니다.

./tiny 8000

 

3. 정적 콘텐츠 응답 확인하기

브라우저에서 아래의 URL을 입력한 뒤, 서버의 home.html가 실제로 전송되어 브라우저에 표시되는지 확인해 봅니다.

http://localhost:8000/home.html

정적 콘텐츠의 브라우저 반환 결과

 

4. 동적 콘텐츠 응답 확인하기

브라우저에서 아래 URL을 입력 후, 서버가 adder를 통해 계산된 두 수의 합이 전송되어 브라우저에 잘 표시되는지 확인해 봅니다.

http://localhost:8000/cgi-bin/adder?num1=3&num2=4

 

동적 콘텐츠의 브라우저 반환 결과

 

저작자표시 비영리 변경금지 (새창열림)

'크래프톤 정글 > Code 정글(C언어)' 카테고리의 다른 글

[WebProxy] 프록시 서버 제대로 알고 시작하기  (0) 2025.05.05
[WebProxy] Tiny 서버 숙제 문제 분석 및 해결하기  (0) 2025.05.03
[WebProxy] echo 서버 구현 실습 및 코드 해석하기  (0) 2025.05.02
[WebProxy] VSCode에서 빨간 줄이 뜨는 현상 해결하기  (0) 2025.05.02
[Malloc] (참고용) Segregated Free List 기반 동적 할당기 구현하기  (0) 2025.04.28
'크래프톤 정글/Code 정글(C언어)' 카테고리의 다른 글
  • [WebProxy] 프록시 서버 제대로 알고 시작하기
  • [WebProxy] Tiny 서버 숙제 문제 분석 및 해결하기
  • [WebProxy] echo 서버 구현 실습 및 코드 해석하기
  • [WebProxy] VSCode에서 빨간 줄이 뜨는 현상 해결하기
그냥사람_
그냥사람_
IT 관련 포스팅을 합니다. 크래프톤 정글 8기 정경호
  • 그냥사람_
    그냥코딩
    그냥사람_
  • 전체
    오늘
    어제
    • 글 전체보기 N
      • 크래프톤 정글 N
        • 로드 투 정글(입학시험)
        • CS기초(키워드, 개념정리) N
        • 컴퓨터구조(CSAPP)
        • Code 정글(C언어)
        • 마이 정글(WIL, 에세이) N
      • 자료구조&알고리즘
        • 자료구조
        • 알고리즘
      • 일상
  • 블로그 메뉴

    • 홈
  • 링크

    • Github
  • hELLO· Designed By정상우.v4.10.3
그냥사람_
[WebProxy] Tiny 서버 구현 코드 해석 및 응답 확인하기
상단으로

티스토리툴바