컴퓨터 시스템 : CSAPP 11장 정리 - 11.6 종합설계 :소형 웹 서버 Part.2

지난 포스팅에서 11.6장 Tiny Web Server의 main()과 doit() 함수에 대해 알아봤다.

2025.05.03 - [크래프톤 정글 (컴퓨터 시스템: CSAPP)/11장 네트워크 프로그래밍] - 컴퓨터 시스템 : CSAPP 11장 정리 - 11.6 종합설계 :소형 웹 서버 Part.1

 

컴퓨터 시스템 : CSAPP 11장 정리 - 11.6 종합설계 :소형 웹 서버 Part.1

11.6절 Putting It Together: The Tiny Web Server이 절에서는 지금까지 배운 내용을 종합하여 작동 가능한 소형 웹 서버 Tiny를 구현한다. 이 서버는 다음을 처리할 수 있다:정적 콘텐츠 (HTML, 이미지 등)동적

www.gowoong.com

이제 다음 함수들에 대해 계속 알아보도록 하겠다.


1. clienterror() 함수 

함수는 Tiny 웹 서버에서 클라이언트의 잘못된 요청이나 서버 오류 상황이 발생했을 때, 클라이언트에게 HTTP 형식에 맞춘 에러 메시지를 전송하는 유틸리티 함수이다.

함수 정의 (그림 11.31 기준)

void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)
{
    char buf[MAXLINE], body[MAXBUF];

    /* 1. HTML 에러 본문 생성 */
    sprintf(body, "<html><title>Tiny Error</title>");
    sprintf(body, "%s<body bgcolor=\"ffffff\">\r\n", body);
    sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
    sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
    sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);

    /* 2. HTTP 응답 헤더 생성 및 전송 */
    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    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));

    /* 3. HTTP 응답 본문 전송 */
    Rio_writen(fd, body, strlen(body));
}

파라미터 설명

인자 의미
fd 클라이언트와 연결된 소켓 디스크립터
cause 오류를 유발한 원인 (파일명, 메서드 등)
errnum HTTP 상태 코드 문자열 ("404", "501" 등)
shortmsg 간단한 상태 메시지 ("Not Found", "Not Implemented" 등)
longmsg 보다 자세한 설명 메시지

구성 요소 요약

  • HTML 형식의 에러 본문 생성
    • 사용자에게 웹 브라우저 상에서 보기 쉽게 설명
  • HTTP 응답 헤더 생성
    • Content-type, Content-length 포함
  • 본문 출력
    • 앞서 생성한 HTML 문자열을 rio_writen()으로 클라이언트에게 전송

💡 만약 sprintf와 Rio_writen 등의 사용법이 궁금하다면 이전 포스팅에 있는 부록을 확인하라


2. read_requesthdrs() 함수 

이 함수는 클라이언트가 보낸 HTTP 요청에서 요청 헤더 부분을 한 줄씩 robust 하게 읽어 들인다. HTTP 요청 메시지는 보통 다음과 같은 구조를 가진다:

GET /index.html HTTP/1.1        ← 요청 라인
Host: localhost:8000            ← 헤더 1
User-Agent: ...                 ← 헤더 2
...
\r\n                            ← 빈 줄 (헤더 끝 표시)

read_requesthdrs()는 요청 라인을 제외한 나머지 요청 헤더를 모두 읽고 무시하며, 디버깅을 위해 출력한다.

세부 동작

rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
  • 첫 번째 헤더 라인을 읽고 출력
  • rp는 robust I/O 구조체 포인터 (이전에 Rio_readinitb()로 초기화됨)
  • 이 readline은 줄 단위로 데이터를 읽어준다 (\n까지 포함)
while(strcmp(buf, "\r\n")) {
    rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
}
  • buf의 내용이 "\r\n"일 때 반복 종료
  • 즉, 빈 줄이 나올 때까지 계속해서 헤더를 읽고 출력한다
  • HTTP 프로토콜에서 빈 줄은 헤더와 본문을 구분하는 경계 표시

지금은 이 코드가 왜 필요한지 잘 모르겠지만 프록시 서버를 구현할 때 이 함수는 대격변 수준의 변경이 있을 수 있다.


3. parse_uri() 함수

전체 코드 (그림 11.33)

int parse_uri(char *uri, char *filename, char *cgiargs)
{
    char *ptr;

    if (!strstr(uri, "cgi-bin")) { /* Static content */
        strcpy(cgiargs, "");                  // CGI 인자 비움
        strcpy(filename, ".");                // 현재 디렉토리를 기준으로
        strcat(filename, uri);                // 상대 경로 생성
        if (uri[strlen(uri)-1] == '/')        // 끝이 '/'이면 홈페이지로 간주
            strcat(filename, "home.html");    // 기본 파일로 설정
        return 1;
    }
    else { /* Dynamic content */
        ptr = index(uri, '?');                // '?' 기준으로 인자 분리
        if (ptr) {
            strcpy(cgiargs, ptr+1);           // '?' 뒤의 문자열은 인자
            *ptr = '\0';                      // '?'를 '\0'로 바꿔 URI 분리
        }
        else {
            strcpy(cgiargs, "");              // 인자 없음
        }
        strcpy(filename, ".");                // 현재 디렉토리 기준
        strcat(filename, uri);                // 실행할 CGI 프로그램 경로
        return 0;
    }
}

함수 역할 요약

역할 설명
URI 분석 요청된 자원이 정적(static)인지 동적(dynamic)인지 판별
경로 추출 정적 콘텐츠: 단순 파일 경로 / 동적 콘텐츠: CGI 인자와 실행 파일 분리
반환값 1이면 정적 콘텐츠, 0이면 동적 콘텐츠
정적 콘텐츠 처리 (cgi-bin 미포함)
if (!strstr(uri, "cgi-bin")) {
  • cgi-bin 문자열이 없으면 정적 콘텐츠로 간주
  • URI를 파일 경로로 변환
    • 예: /index.html → ./index.html
  • URI가 /로 끝나면 → ./home.html로 기본 파일 설정
항목
uri /index.html
filename ./index.html
cgiargs "" (빈 문자열)
리턴값 1 (정적)

동적 콘텐츠 처리 (cgi-bin 포함)

ptr = index(uri, '?');
  • URI에? 가 있으면 → 그 뒤는 인자
  • ? 앞은 실행할 CGI 프로그램 경로
    • 예: /cgi-bin/adder?15000&213
      • filename → ./cgi-bin/adder
      • cgiargs → 15000&213
항목
uri /cgi-bin/adder?15000&213
filename ./cgi-bin/adder
cgiargs 15000&213
리턴값 0 (동적)

4. serve_static() 함수

전체 코드 요약

void serve_static(int fd, char *filename, int filesize, int is_head){
  int srcfd; // 파일을 열 때 사용할 디스크립터
  /*
    srcp: 메모리에 매핑된 파일의 포인터
    filetype: MIME 타입 저장용 (예: text/html)
    buf: HTTP 응답 헤더 작성용 버퍼
  */
  char *srcp, filetype[MAXLINE], buf[MAXLINE];

  /* Send response headers to client */
  get_filetype(filename , filetype); //파일확장자를 보고 filetype 설정
  /*
    응답 상태줄: HTTP/1.0 200 OK
    서버 이름, 연결 상태, 콘텐츠 길이, 콘텐츠 타입
    마지막 \r\n\r\n으로 헤더 종료
  */
  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); // 서버 측 로그 출력용
  if (is_head == 0){
    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0); // 파일 열기
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); //Mmap으로 파일을 메모리에 매핑 (읽기 전용, private)
    Close(srcfd); // 파일 디스크립터는 닫아도 매핑은 유지된다.
    Rio_writen(fd, srcp, filesize); // 매핑된 메모리 내용을 클라이언트에게 전송
    Munmap(srcp, filesize); // 매핑 해제하여 메모리 정리
  }
}

1. HTTP 응답 헤더 생성

get_filetype(filename, filetype);

파일 확장자에 따라 MIME 타입 결정 (.html → text/html, .jpg → image/jpeg 등)

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));
  • 표준 HTTP 응답 헤더 작성
  • Content-length, Content-type 포함
  • \r\n\r\n으로 헤더 종료

2. 파일 내용을 응답 본문으로 전송

srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);
  • 요청한 파일을 읽기 전용으로 열고
  • mmap()으로 파일을 메모리에 매핑
  • 매핑된 메모리를 클라이언트로 전송
  • 메모리 매핑 해제

mmap을 사용하는 이유는 성능 때문이다. 별도의 버퍼 없이 파일 내용을 직접 전송할 수 있다.

💡 나는 숙제 문제를 구현했기 때문에 is_head라는 인자와 if 조건문이 포함된 것이다.


5. get_filetype() 함수

/*
  filename: 클라이언트가 요청한 파일 이름 예) ./index/.html
  filetype: MIME 타입을 저장할 문자열 버퍼
*/
void get_filetype(char *filename, char *filetype){  
  if (strstr(filename, ".html")){ //strstr은 전체 문자열에서 찾을 부분 문자열이 처음 등장하는 위치를 반환한다.
    strcpy(filetype, "text/html");
  }
  else if (strstr(filename, ".gif")){
    strcpy(filetype, "image/gif");
  }
  else if (strstr(filename, ".png")){
    strcpy(filetype, "image/png");
  }
  else if (strstr(filename, ".jpg")){
    strcpy(filetype, "image/jpeg");
  }
  else if (strstr(filename, ".mpg")){
    strcpy(filetype, "video/mpeg");
  }
  else{
    strcpy(filetype, "text/plain");
  }
}
  • 확장자 검사 후 적절한 MIME 타입 설정
  • 실제 브라우저가 파일 내용을 어떻게 해석할지를 결정한다

6. serve_dynamic() 함수

전체 코드 (그림 11.35)

/*
  fd: 클라이언트 소켓 디스크립터
  filename: 실행할 CGI 프로그램 파일명
  cgiargs: 클라이언트가 보낸 CGI 인자 (예: ?a=1&b=2)
*/
void serve_dynamic(int fd, char *filename, char *cgiargs){
  /*
    buf: 응답 헤더용 임시 버퍼
    emptylist: 프로그램 인자 없음 (argv 전달용)
  */
  char buf[MAXLINE], *emptylist[] = { NULL };

  /* Return first part of HTTP response */
  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));

  if (Fork() == 0) { // 자식 프로세스 생성 CGI 프로그램은 자식 프로세스에서 실행
    /* Real server would set all CGI vars here */
    setenv("QUERY_STRING", cgiargs, 1); // cgiargs를 환경 변후 "QUERY_STRING"에 저장 
                                        // CGI 프로그램은 이 값을 통해 인자를 읽는다.
    Dup2(fd, STDOUT_FILENO); // stdout을 클라이언트 소켓으로 바꿈 CGI 프로그램의 출력이 클라이언트로 바로 전송됨
    /*
      CGI 프로그램 실행
      filename: 실행할 경로
      emptylist: 인자 없음
      environ: 환경 변수 전달
    */
    Execve(filename, emptylist, environ);
  }
  Wait(NULL); // 부모는 자식이 종료할 때까지 기다린다. CGI 실행 완료 후 정리
}

1. HTTP 헤더 전송

/* 1. 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));
  • 응답 시작 줄(200 OK)과 서버 정보 헤더 전송
  • 이후 응답 본문은 CGI 프로그램이 직접 출력

2. 자식 프로세스 생성

if (Fork() == 0)
  • fork()를 통해 자식 프로세스 생성
  • 자식만 CGI 프로그램을 실행하고, 부모는 대기

3. 환경 변수 설정 + 출력 리디렉션

setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO);
  • CGI 인자 문자열을 환경 변수로 설정
  • 표준 출력을 소켓으로 바꾸어 CGI 출력이 클라이언트로 전달되게 함

4. CGI 실행

Execve(filename, emptylist, environ);
  • CGI 프로그램을 실행 (예: adder)
  • CGI는 printf() 등을 사용해 직접 클라이언트에 응답 HTML을 출력함

5. 부모는 자식 종료 대기

Wait(NULL);
  • 부모 프로세스는 자식 종료를 기다림
  • 자식 프로세스가 응답을 완료하면 자원 회수

예시 흐름

GET /cgi-bin/adder?15000&213 HTTP/1.0
→ serve_dynamic 호출
→ 환경변수 QUERY_STRING=15000&213 설정
→ ./cgi-bin/adder 실행
→ adder는 HTML 출력: "The answer is: 15000 + 213 = 15213"
→ 출력은 그대로 브라우저로 전송됨