CSAPP 11장 공부 기록 3편
HTTP, CGI, Tiny Web Server: 요청이 코드가 되는 과정
1편에서는 데이터가 네트워크 바깥세상에서 어떻게 포장되어 이동하는지 봤고, 2편에서는 소켓 fd가 커널과 NIC까지 이어지는 내부 경로를 봤다. 3편에서는 그 위에 HTTP를 올린다. 브라우저의 요청은 Tiny Web Server 코드 안에서 어떻게 읽히고, 파일 응답이나 CGI 실행 결과로 바뀌는가?
이번 글에서 다루는 것
- HTTP가 TCP 연결 위에서 텍스트 요청/응답을 주고받는 방식
- FTP와 HTTP가 왜 다르게 설계되었는지
- MIME type, status code, Telnet 테스트의 의미
- 정적 콘텐츠와 동적 콘텐츠의 차이
- CGI에서
QUERY_STRING,fork,dup2,execve가 하는 일 - Tiny Web Server의
main,doit,parse_uri,serve_static,serve_dynamic흐름 curl로 정적/동적 요청을 보내 Tiny 코드 흐름을 확인하는 법- Proxy Lab으로 넘어가면 Tiny가 어떻게 확장되는지
1. HTTP는 브라우저와 서버가 주고받는 약속이다
2편까지는 소켓과 TCP 연결을 다뤘다. 하지만 TCP 연결만으로는 브라우저와 웹 서버가 무엇을 요청하고 무엇을 응답해야 하는지 알 수 없다. TCP는 바이트 스트림을 안전하게 옮겨 줄 뿐이고, 그 바이트의 의미는 응용 계층 프로토콜이 정한다.
HTTP(Hypertext Transfer Protocol)는 웹 클라이언트와 웹 서버가 주고받는 텍스트 기반 약속이다. 브라우저는 요청 라인과 헤더를 보내고, 서버는 상태 라인, 응답 헤더, 응답 본문을 보낸다.
HTTP 트랜잭션 한 번
Client -> Server
GET /home.html HTTP/1.0\r\n
Host: example.com\r\n
\r\n
Server -> Client
HTTP/1.0 200 OK\r\n
Content-type: text/html\r\n
Content-length: 125\r\n
\r\n
<html>...</html>
여기서 빈 줄이 중요하다. HTTP header는 빈 줄로 끝난다. Tiny의 read_requesthdrs가 \r\n이 나올 때까지 줄을 읽는 이유가 이것이다.
2. FTP와 HTTP: 파일 전송과 웹 문서의 차이
공부 중 FTP가 갑자기 등장해서 헷갈렸다. 핵심은 FTP를 깊게 배우라는 뜻이 아니라, 웹이 왜 특별한지 비교하기 위한 것이다.
| 구분 | FTP | HTTP |
|---|---|---|
| 주 목적 | 파일 업로드/다운로드 | 웹 콘텐츠 요청/응답 |
| 연결 구조 | 제어 연결과 데이터 연결을 분리 | 요청과 응답을 하나의 흐름으로 처리 |
| 상태 | 로그인, 현재 디렉터리 같은 상태를 기억 | 기본적으로 요청마다 독립적인 stateless 설계 |
| 강점 | 파일 전송 자체에 집중 | HTML, 이미지, API 응답 등 다양한 콘텐츠 표현 |
HTML의 강점은 파일을 내려받는 데서 끝나지 않고, 브라우저에게 텍스트와 이미지 배치, 링크 연결을 설명할 수 있다는 점이다. 즉 FTP가 "파일 창고"에 가깝다면, HTTP와 HTML은 "문서와 연결된 화면 경험"에 가깝다.
3. MIME type과 status code: 브라우저가 응답을 해석하게 만드는 힌트
서버가 응답 본문으로 바이트를 보내면 브라우저는 그것이 HTML인지, 이미지인지, plain text인지 알아야 한다. 이때 Content-type 헤더에 MIME type을 넣는다.
| MIME type | 의미 | Tiny의 예 |
|---|---|---|
text/html |
HTML 문서 | .html |
image/png |
PNG 이미지 | .png |
image/jpeg |
JPEG 이미지 | .jpg |
상태 코드는 요청 처리 결과를 숫자로 알려준다. Tiny의 clienterror는 이런 상태 코드를 응답 첫 줄에 넣어 브라우저에게 에러 상황을 설명한다.
| 코드 | 메시지 | 의미 |
|---|---|---|
| 200 | OK | 요청 성공 |
| 301 | Moved permanently | 콘텐츠 위치가 영구 이동 |
| 400 | Bad request | 요청 문법을 이해할 수 없음 |
| 403 | Forbidden | 권한 부족 |
| 404 | Not found | 파일 없음 |
| 501 | Not implemented | 서버가 method를 지원하지 않음 |
| 505 | HTTP version not supported | HTTP 버전 미지원 |
4. Telnet으로 HTTP를 손으로 쳐볼 수 있는 이유
HTTP가 텍스트 기반이라는 점은 큰 장점이다. 브라우저가 없어도 TCP 연결을 열고 직접 요청 문자열을 치면 서버 응답을 볼 수 있다. 예전에는 telnet으로 이 흐름을 직접 확인할 수 있었다.
$ telnet example.com 80
GET / HTTP/1.0
HTTP/1.0 200 OK
Content-type: text/html
...
이 예시는 HTTP가 TCP 위의 텍스트 약속이라는 사실을 직접 보여준다. TCP는 바이트를 나르고, HTTP는 그 바이트를 어떻게 해석할지 정한다.
5. HTTP/1.0과 HTTP/1.1: 연결을 끊느냐 유지하느냐
Tiny는 HTTP/1.0 서버다. HTTP/1.0에서는 요청 하나를 처리하고 연결을 닫는 단순한 모델이 자연스럽다. 그래서 Tiny의 응답 헤더에도 Connection: close가 들어간다.
HTTP/1.1은 persistent connection이 기본이 되어, 하나의 TCP 연결에서 여러 요청/응답을 처리할 수 있다. 웹페이지 하나가 HTML, CSS, JS, 이미지 여러 개로 구성된다는 점을 생각하면 연결을 매번 새로 맺는 비용을 줄이는 것이 중요하다.
Tiny를 읽을 때의 주의점
Tiny는 교육용 HTTP/1.0 서버다. 현대 웹 서버의 모든 기능을 구현하는 코드가 아니다. 이 코드는 소켓, HTTP, Unix I/O, 프로세스 제어가 어떻게 연결되는지를 보여주는 종합 예제다.
6. 정적 콘텐츠와 동적 콘텐츠
웹 서버가 제공하는 콘텐츠는 크게 정적 콘텐츠와 동적 콘텐츠로 나뉜다. 이 차이를 잡아야 Tiny의 serve_static과 serve_dynamic이 왜 나뉘는지 보인다.
| 구분 | 정적 콘텐츠 | 동적 콘텐츠 |
|---|---|---|
| 정의 | 디스크에 이미 있는 파일을 그대로 보냄 | 요청 시 프로그램을 실행해 결과를 만듦 |
| 예시 | home.html, 로고 이미지, CSS 파일 |
검색 결과, 계산 결과, 로그인 후 개인화 페이지 |
| Tiny 함수 | serve_static |
serve_dynamic |
| 비유 | 창고에 있는 완제품을 꺼내 줌 | 주문을 받고 주방에서 즉석 조리 |
7. CGI: 웹 서버가 외부 프로그램에게 일을 맡기는 약속
CGI(Common Gateway Interface)는 웹 서버가 동적 콘텐츠 생성을 외부 프로그램에게 맡기는 규약이다. Tiny 서버는 요청을 받으면 자식 프로세스를 만들고, 그 자식이 CGI 프로그램으로 변신해 결과를 만든다.
공부하면서 가장 중요했던 장면은 fork, setenv, dup2, execve가 한 줄로 이어지는 흐름이었다.
GET 요청의 CGI 인자 전달
브라우저 요청:
GET /cgi-bin/adder?n1=15000&n2=213 HTTP/1.0
Tiny:
filename = "./cgi-bin/adder"
cgiargs = "n1=15000&n2=213"
setenv("QUERY_STRING", cgiargs, 1)
CGI 프로그램:
getenv("QUERY_STRING") // "n1=15000&n2=213" 읽기
GET 요청에서는 URI의 ? 뒤쪽 문자열이 CGI 인자가 되고, Tiny는 이를 QUERY_STRING 환경변수로 넘긴다. POST 요청이라면 본문 데이터가 표준 입력으로 들어가는 방식이 더 중요해진다.
dup2가 핵심인 이유
CGI 프로그램은 네트워크 소켓을 몰라도 된다. 자식 프로세스가 dup2(fd, STDOUT_FILENO)로 표준 출력을 클라이언트 소켓으로 꺾어 두면, CGI 프로그램의 printf 출력이 모니터가 아니라 브라우저로 흘러간다.
dup2 전후의 fd 테이블 변화
[fork 직후 자식 프로세스]
fd 0 -> stdin
fd 1 -> terminal stdout
fd 2 -> stderr
fd 4 -> client socket(connfd)
Dup2(fd=4, STDOUT_FILENO=1)
[dup2 이후 자식 프로세스]
fd 0 -> stdin
fd 1 -> client socket(connfd)
fd 2 -> stderr
fd 4 -> client socket(connfd)
Execve("./cgi-bin/adder", ...)
CGI program:
printf("Content-type: text/html\r\n\r\n");
printf("answer = 15213\n");
실제 흐름:
printf -> fd 1 -> client socket -> browser
이 장면 때문에 CGI 프로그램은 자기 출력이 네트워크로 나간다는 사실을 몰라도 된다. 프로그램 입장에서는 평소처럼 표준 출력에 쓰는 것이고, Tiny가 미리 fd 테이블의 물길을 바꿔 둔 것이다.
8. Tiny 전체 구조: main은 문지기, doit은 트랜잭션 처리자
Tiny는 반복 서버(iterative server)다. 연결 하나를 받고, 그 연결에서 HTTP 요청 하나를 처리하고, 닫고, 다시 다음 연결을 기다린다.
main()
listenfd = Open_listenfd(port)
while (1) {
connfd = Accept(listenfd, ...)
doit(connfd)
Close(connfd)
}
doit(connfd)
HTTP request line 읽기
method가 GET인지 확인
header 읽고 버림
URI 분석
정적 파일이면 serve_static
CGI 요청이면 serve_dynamic
Echo server에서 메인 루틴과 echo 함수가 나뉘었던 것처럼, Tiny도 main과 doit이 나뉜다. main은 연결 관리, doit은 HTTP 트랜잭션 처리를 맡는다.
9. doit: 요청 라인을 읽고 정적/동적으로 분기한다
doit는 Tiny에서 가장 중요한 함수다. 연결 fd 하나를 받아 HTTP 요청 한 번을 처리한다. 먼저 Robust I/O 버퍼를 초기화하고, 요청 라인을 한 줄 읽는다.
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE);
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) {
clienterror(fd, method, "501", "Not implemented",
"Tiny does not implement this method");
return;
}
Tiny는 GET만 지원한다. 그래서 POST 같은 method가 오면 501 Not implemented를 보낸다. 이후 요청 헤더를 읽어 버리고, URI를 분석해 파일 이름과 CGI 인자를 분리한다.
URI 분석 결과
/home.html
-> static
-> filename = "./home.html"
-> cgiargs = ""
/cgi-bin/adder?n1=15000&n2=213
-> dynamic
-> filename = "./cgi-bin/adder"
-> cgiargs = "n1=15000&n2=213"
그다음 stat으로 파일 존재와 메타데이터를 확인한다. 정적 콘텐츠라면 읽기 권한이 필요하고, 동적 콘텐츠라면 실행 권한이 필요하다.
10. serve_static: 디스크 파일을 HTTP 응답으로 보낸다
정적 콘텐츠는 디스크에 이미 있는 파일을 보내는 것이다. Tiny는 먼저 파일 확장자로 MIME type을 결정하고, HTTP 응답 헤더를 쓴다.
get_filetype(filename, filetype);
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));
그 뒤 파일을 열고 mmap으로 메모리에 매핑한 뒤, 그 메모리 영역을 그대로 연결 fd에 쓴다.
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);
여기서 연결되는 앞선 공부
파일은 fd로 열고, 메모리는 VM으로 매핑하고, 응답은 socket fd로 쓴다. 그래서 Tiny의 정적 콘텐츠 처리는 9장 VM, 10장 Unix I/O, 11장 소켓이 한 줄로 만나는 장면이다.
11. serve_dynamic: 자식을 만들고 CGI 프로그램으로 변신시킨다
동적 콘텐츠에서는 서버가 파일을 그대로 보내지 않는다. 요청 인자를 환경변수로 준비하고, 자식 프로세스를 만든 뒤, 그 자식이 CGI 프로그램을 실행한다.
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) {
setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO);
Execve(filename, emptylist, environ);
}
Wait(NULL);
동적 콘텐츠 실행 시나리오
QUERY_STRING으로 넣는다.이 구조를 이해하면 dup2가 단순한 보조 함수가 아니라는 것이 보인다. fd 테이블의 1번 칸이 가리키는 대상을 바꿔, 프로그램의 출력 물줄기를 모니터에서 소켓으로 꺾는 함수다.
12. 요청 하나를 Tiny 코드에 실제로 대입해 보기
브라우저가 GET /home.html HTTP/1.0을 보냈다고 하자. Tiny는 아래 순서로 움직인다.
| 단계 | Tiny 코드 | 의미 |
|---|---|---|
| 연결 수락 | Accept |
브라우저와 대화할 connfd를 얻는다. |
| 요청 라인 읽기 | Rio_readlineb |
TCP byte stream에서 HTTP 요청 한 줄을 꺼낸다. |
| URI 분석 | parse_uri |
./home.html이라는 파일명으로 바꾼다. |
| 파일 확인 | stat |
파일 존재, 권한, 크기를 확인한다. |
| 응답 전송 | serve_static |
헤더를 쓰고, 파일 본문을 connfd로 쓴다. |
| 연결 종료 | Close |
HTTP/1.0 방식으로 요청 하나 처리 후 닫는다. |
동적 요청이라면 serve_static 대신 serve_dynamic으로 가고, 파일 내용을 보내는 대신 CGI 프로그램의 실행 결과를 보낸다.
정적 요청과 동적 요청을 나란히 대입해 보기
[정적 콘텐츠 요청]
Request:
GET /home.html HTTP/1.0
parse_uri 결과:
is_static = 1
filename = "./home.html"
cgiargs = ""
doit:
stat("./home.html")로 존재/권한/크기 확인
serve_static(connfd, "./home.html", filesize)
serve_static:
Content-type 결정
HTTP response header 작성
mmap으로 파일을 메모리에 매핑
Rio_writen(connfd, srcp, filesize)
[동적 콘텐츠 요청]
Request:
GET /cgi-bin/adder?n1=15000&n2=213 HTTP/1.0
parse_uri 결과:
is_static = 0
filename = "./cgi-bin/adder"
cgiargs = "n1=15000&n2=213"
doit:
stat("./cgi-bin/adder")로 존재/실행 권한 확인
serve_dynamic(connfd, "./cgi-bin/adder", "n1=15000&n2=213")
serve_dynamic:
HTTP response 첫 줄과 Server header 작성
fork로 자식 생성
setenv("QUERY_STRING", "n1=15000&n2=213", 1)
dup2(connfd, STDOUT_FILENO)
execve("./cgi-bin/adder", ...)
CGI:
getenv("QUERY_STRING")로 인자 읽기
printf 결과가 connfd를 통해 브라우저로 전송
이 두 흐름을 나란히 보면 Tiny의 핵심 분기가 더 선명해진다. 정적 요청은 이미 있는 파일을 찾아서 보내는 경로이고, 동적 요청은 프로그램을 실행해 그 출력이 응답이 되게 만드는 경로다.
13. curl 실습: 브라우저 없이 Tiny를 직접 확인하기
Tiny를 눈으로만 읽으면 main, doit, serve_static, serve_dynamic이 따로 노는 함수처럼 보일 수 있다. 그래서 가장 좋은 확인 방법은 브라우저 대신 curl로 HTTP 요청을 직접 보내고, 그 요청이 코드의 어느 경로를 지나가는지 대입해 보는 것이다.
실습 전제
- CSAPP에서 제공하는 Tiny 디렉터리 안에
tiny.c,csapp.c,csapp.h,cgi-bin/adder가 준비되어 있다고 가정한다. - 수업 skeleton에
Makefile이 있으면make를 쓰고, 없다면 제공된 빌드 명령에 맞춘다. - 아래 출력은 최종 레포
https://github.com/choihyunjin1/week8의 Tiny를 Docker 개발 환경에서 빌드한 뒤 확인한 결과다. - 이 레포의
adder.c는n1=15000&n2=213처럼 이름이 붙은 query string을 기대한다. 교재 기본 예시처럼?15000&213만 보내면 CGI 프로그램 쪽에서 정상 본문을 만들지 못한다.
# Tiny 빌드
make
# 8000번 포트에서 Tiny 실행
./tiny 8000
13-1. 정적 콘텐츠 요청 확인
정적 콘텐츠는 이미 디스크에 있는 파일을 읽어 보내는 경로다. /home.html을 요청하면 Tiny는 parse_uri에서 파일명을 ./home.html로 바꾸고, serve_static에서 파일을 mmap한 뒤 Rio_writen으로 클라이언트 소켓에 쓴다.
curl --http1.0 -i -sS http://localhost:8000/home.html
실제 curl 출력
HTTP/1.0 200 OK
Server: Tiny Web Server
Connection: close
Content-length: 127
Content-type: text/html
<html>
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
</body>
</html>
Tiny 코드에 대입한 흐름
Accept
-> doit(connfd)
-> Rio_readlineb
-> method == "GET"
-> read_requesthdrs
-> parse_uri("/home.html")
-> stat("./home.html")
-> serve_static
-> mmap
-> Rio_writen(connfd, file)
13-2. 동적 콘텐츠 요청 확인
동적 콘텐츠는 파일을 그대로 보내는 것이 아니라, 실행 파일을 돌린 결과가 응답이 되는 경로다. 아래 요청에서 ? 뒤의 n1=15000&n2=213은 parse_uri에서 cgiargs로 분리되고, serve_dynamic에서 QUERY_STRING 환경변수로 넘어간다.
curl --http1.0 -i -sS "http://localhost:8000/cgi-bin/adder?n1=15000&n2=213"
실제 curl 출력
HTTP/1.0 200 OK
Server: Tiny Web Server
Content-type: text/html
Content-length: 141
QUERY_STRING=n1=15000
<p>Welcome to add.com: THE Internet addition portal.
<p>The answer is: 15000 + 213 = 15213
<p>Thanks for visiting!
여기서 QUERY_STRING 줄이 n1=15000까지만 보이는 이유는 이 레포의 adder.c가 & 위치를 '\0'로 바꿔 두 인자를 나누기 때문이다. 계산에는 이미 n2=213까지 사용되었으므로 결과는 정상이다.
Tiny 코드에 대입한 흐름
parse_uri
-> filename = "./cgi-bin/adder"
-> cgiargs = "n1=15000&n2=213"
serve_dynamic
-> HTTP/1.0 200 OK 먼저 전송
-> fork
-> setenv("QUERY_STRING", cgiargs, 1)
-> dup2(connfd, STDOUT_FILENO)
-> execve("./cgi-bin/adder", ...)
adder
-> getenv("QUERY_STRING")
-> printf(...)
-> 출력이 connfd로 이동
이 실습의 핵심은 curl 명령 자체가 아니라, 출력 줄을 보고 코드 경로를 역추적하는 것이다. 200 OK와 Content-type이 보이면 HTTP 응답 헤더가 만들어진 것이고, 동적 요청에서 adder의 HTML이 보이면 dup2로 표준 출력이 소켓으로 꺾였다는 뜻이다.
14. Tiny에서 Proxy Lab으로 넘어가면 무엇이 달라지는가
Tiny는 최종 서버다. 브라우저가 요청하면 Tiny가 자기 디스크의 파일을 읽거나 CGI 프로그램을 실행해 응답한다. Proxy Lab의 proxy는 중간 전달자다. 브라우저와 실제 서버 사이에 서서, 클라이언트에게는 서버처럼 보이고 서버에게는 클라이언트처럼 보인다.
Tiny:
Browser -> Tiny Server -> local file or CGI -> Browser
Proxy:
Browser -> Proxy -> Origin Server
Browser <- Proxy <- Origin Server
| 단계 | Tiny에서 배운 것 | Proxy에서 확장되는 것 |
|---|---|---|
| 요청 읽기 | 클라이언트의 HTTP 요청 라인과 헤더를 읽음 | 요청을 분석해 origin server 주소와 path를 분리 |
| 응답 쓰기 | 로컬 파일 또는 CGI 결과를 클라이언트에게 씀 | origin server의 응답을 읽어 클라이언트에게 전달 |
| 소켓 역할 | 서버 소켓 하나가 중심 | 클라이언트 쪽 connfd와 서버 쪽 clientfd를 동시에 사용 |
| 성능 확장 | 반복 서버 | 동시성, thread pool, cache, lock 문제로 확장 |
15. 동시성은 왜 갑자기 어려워지는가
Tiny는 한 번에 한 클라이언트만 처리한다. 이 구조는 이해하기 쉽지만, 어떤 클라이언트가 느린 네트워크나 큰 파일 때문에 오래 걸리면 다른 클라이언트는 기다려야 한다.
Proxy Lab에서는 여러 요청을 동시에 처리해야 하므로 thread, thread pool, 동기화, cache가 등장한다. 여기서 중요한 것은 동시성을 "빠르게 만들기 위한 옵션" 정도로만 보는 것이 아니라, 서버가 여러 클라이언트를 현실적으로 감당하기 위한 구조 변화로 보는 것이다.
동시성에서 새로 생기는 위험
- 여러 thread가 같은 cache entry를 동시에 수정하면 race condition이 생길 수 있다.
- lock을 잘못 잡으면 deadlock이나 성능 병목이 생길 수 있다.
- 클라이언트 fd와 서버 fd를 헷갈리면 proxy가 잘못된 방향으로 데이터를 쓸 수 있다.
- HTTP header를 그대로 넘길지, proxy용으로 재작성할지 판단해야 한다.
그래서 Proxy Lab은 Tiny의 단순 확장처럼 보이지만, 실제로는 11장 네트워크와 12장 동시성의 연결 지점이다.
16. 이 코드에서 조심해서 봐야 할 교육용 단순화
Tiny 코드는 학습용이다. 그래서 실제 서비스 서버 기준으로는 빠져 있거나 단순화된 부분이 많다. 이 점을 알고 읽어야 코드를 과대평가하지 않는다.
| 부분 | Tiny의 단순화 | 학습 목적 |
|---|---|---|
| HTTP method | GET만 지원 | 요청 라인 parsing과 응답 흐름에 집중 |
| HTTP version | HTTP/1.0 중심, 연결 닫기 | 하나의 트랜잭션을 단순하게 관찰 |
| 요청 헤더 | 읽고 대부분 무시 | HTTP header 종료 조건 이해 |
| 동시성 | 반복 서버 | 소켓, HTTP, CGI의 기본 흐름을 먼저 고정 |
| 보안 | 입력 검증과 path traversal 방어가 제한적 | 교육용 코드라는 경계를 인식 |
이번 글에서 기억할 것
- HTTP는 TCP byte stream 위에서 동작하는 텍스트 기반 요청/응답 약속이다.
- MIME type은 브라우저가 응답 본문을 어떻게 해석할지 알려주는 힌트다.
- 정적 콘텐츠는 이미 있는 파일을 보내고, 동적 콘텐츠는 요청 시 프로그램을 실행해 만든다.
- CGI는 웹 서버가 외부 실행 파일에게 동적 콘텐츠 생성을 맡기는 규약이다.
dup2(fd, STDOUT_FILENO)는 CGI 프로그램의 표준 출력을 클라이언트 소켓으로 꺾는다.- Tiny는 소켓, RIO, HTTP parsing, mmap, fork, execve, dup2가 한 코드 안에서 만나는 종합 예제다.
- Proxy Lab은 Tiny의 흐름을 중간 전달자로 확장하고, 동시성/캐시/동기화 문제를 추가한다.
스스로 점검
- HTTP request line과 response line은 각각 어떤 정보를 담는가?
- Tiny가
read_requesthdrs에서 빈 줄까지 읽는 이유는 무엇인가? - 정적 콘텐츠와 동적 콘텐츠를 Tiny 함수 이름으로 연결해 설명할 수 있는가?
- CGI 프로그램이
printf만 했는데 브라우저가 결과를 받는 이유는 무엇인가? - Proxy는 왜 Tiny와 달리 클라이언트 역할과 서버 역할을 동시에 해야 하는가?
힌트: 4번은 fork로 fd를 상속받고, dup2로 표준 출력의 목적지를 바꾸는 흐름을 떠올리면 된다.
다음에 이어서 볼 것
이 3편까지가 CSAPP 11장의 표준 mainline이다. 여기서 더 깊게 가려면 Proxy Lab 구현을 실제 코드로 따라가며 요청 header 재작성, sequential proxy, concurrent proxy, cache, reader-writer lock을 별도 글로 분리하는 편이 좋다.
한 줄 정리
Tiny Web Server는 브라우저의 HTTP 요청을 소켓 fd로 읽고, URI를 정적 파일 또는 CGI 실행으로 분기한 뒤, 같은 fd로 HTTP 응답을 써서 돌려주는 CSAPP 11장의 종합 실습 코드다.