지난 포스팅에서 11.6장 Tiny Web Server의 main()과 doit() 함수에 대해 알아봤다.
컴퓨터 시스템 : 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이면 동적 콘텐츠 |
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
- 예: /cgi-bin/adder?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"
→ 출력은 그대로 브라우저로 전송됨
'크래프톤 정글 (컴퓨터 시스템: CSAPP) > 11장 네트워크 프로그래밍' 카테고리의 다른 글
컴퓨터 시스템 : CSAPP 11장 정리 - 11.6 종합설계 :소형 웹 서버 Part.1 (0) | 2025.05.03 |
---|---|
컴퓨터 시스템 : CSAPP 11장 정리 - 11.5 웹 서버 이론편 (0) | 2025.05.03 |
컴퓨터 시스템 : CSAPP 11장 정리 - 11.4 소켓 인터페이스 (0) | 2025.05.02 |
컴퓨터 시스템 : CSAPP 11장 정리 - 11.3 글로벌 IP 인터넷 (0) | 2025.05.02 |
컴퓨터 시스템 : CSAPP 11장 정리 - 11.1 ~ 11.2 (1) | 2025.05.02 |