[CSAPP] 11장 네트워크 프로그래밍 (4/4)
출처 Randal E.Byrant, David R. O'Hallaron, 컴퓨터 시스템 Computer Systems A Programmer's Perspective, 김형신 옮김 (퍼스트북), Pearson
6. Tiny 웹 서버
TINY라고 불리는, 작지만 실용적인 웹 서버를 개발하는 것으로 이 챕터를 마치겠다. TINY는 우리가 배운 많은 아이디어들, 프로세스 컨트롤, Unix I/O, 소켓 인터페이스, HTTP 등을 250 줄 안에 합친다. 실제 서버의 기능, robustness, 보안은 부족하지만, TINY는 실제 웹 브라우저에게 정적, 동적 콘텐츠 모두를 서비스하기에는 충분히 강력하다.
TINY 메인 루틴
다음은 TINY의 메인 루틴을 보여준다.
*코드 맨 상단에 csapp.h가 include되어 있고 전체 함수들에 대한 프로토타입들이 선언되어 있다.
TINY는 커맨드 라인으로부터 전달된 포트 번호에서 들어오는 연결 요청을 listen하는 iterative 서버이다. open_listenfd 함수를 호출함으로써 listening socket을 연 후에, TINY는 무한 서버 루프를 실행시키면서 반복적으로 연결 요청을 수락하고, 트랜잭션을 수행하고, 소켓을 종료시킨다.
doit 함수
doit 함수는 하나의 HTTP 트랜잭션을 처리한다.
먼저 HTTP 요청 라인을 읽고 파싱한다.
TINY는 GET 메서드만을 지원한다. 만약 클라이언트가 POST와 같은 다른 메서드를 요청한다면, 에러 메시지를 보내고 메인 루틴으로 돌아갈 것이다.
다음에, 우리는 URI를 파일 이름과 아마 비어 있는 CGI 인자 문자열로 파싱하고, 요청이 정적 콘텐츠인지 동적 콘텐츠인지를 가리키는 플래그를 설정할 것이다. 만약 파일이 디스크에 존재하지 않는다면, 우리는 즉시 클라이언트에게 메시지를 전송하고 리턴할 것이다.
요청이 정적 콘텐츠에 대한 것이라면, 파일이 regular file이고 읽기 권한이 있는지를 확인할 것이다. 입증이 됐다면 클라이언트에게 정적 콘텐츠를 서비스할 것이다.
요청이 동적 콘텐츠에 대한 것이라면, 실행 권한이 있는지 확인한 다음에 입증이 되면 동적 콘텐츠를 서비스할 것이다.
clienterror 함수
TINY는 실제 서버에게 있는 에러 핸들링 함수들을 많이 가지고 있지는 않다. 하지만, 명백한 에러는 체크를 하며 에러를 클라이언트에게 보고한다. clienterror 함수는 응답 라인에 적절한 상태 코드와 상태 메시지를 담아 클라이언트에게 보낸다. response body에는 브라우저의 사용자에게 에러를 설명하는 HTML 파일을 담는다.
HTML 응답은 body에 있는 콘텐츠의 사이즈와 타입을 표시해야 한다. 이런 점에서 HTML 콘텐츠는 단일 문자열로 만들어지는 것이 선호된다. 사이즈를 쉽게 확인할 수 있기 때문이다.
read_requesthdrs 함수
TINY는 read_requesthdrs 함수를 호출하여 요청 헤더를 읽는다.
요청 헤더를 종결시키는 빈 줄은 carriage return과 line feed의 쌍으로 이루어져 있다.
parse_uri 함수
TINY는 정적 콘텐츠의 홈 디렉토리가 현재 디렉토리라고 가정하며, 실행 가능한 파일의 홈 디렉토리가 ./cgi-bin임을 가정한다. cgi_bin 문자열을 포함하고 있는 모든 URI는 동적 콘텐츠에 대한 요청을 나타내는 것으로 간주된다. 기본 파일 이름은 ./home.html이다.
parse_uri 함수는 URI를 파일 이름과 옵션인 CGI 인자 문자열로 파싱한다. 만약 요청이 정적 콘텐츠를 위한 것이라면 CGI 인자 문자열을 clear하고, URI를 상대 리눅스 경로로 변환한다. 예를 들면 ./index.html이다. 반면에, 요청이 동적 콘텐츠를 위한 것이라면 CGI 인자를 추출하고 남아 있는 URI의 부분을 리눅스 파일 이름으로 변환한다.
serve_static 함수
TINY는 정적 콘텐츠의 다섯 가지 common type을 서비스한다. HTML 파일, unformatted text files, GIF, PNG, JPEG 포맷으로 인코딩된 이미지이다.
serve_static 함수는 body에 local file의 콘텐츠를 담고 있는 HTTP 응답을 전송한다.
먼저 파일 이름에 있는 suffix를 조사함으로써 파일 타입을 알아내고, 응답 라인과 응답 헤더를 클라이언트에게 전송한다. 빈 줄은 헤더를 종료시킨다.
다음으로, 요청받은 파일의 콘텐츠를 연결된 descriptor fd로 복사함으로써 response body를 전송한다. 읽기를 위해 파일 이름을 열고 descriptor를 얻어온다. 리눅스 mmap 함수는 요청받은 파일을 가상 메모리 영역으로 매핑한다. 파일을 메모리로 매핑했다면 더 이상 descriptor가 필요하지 않다. 따라서 파일을 닫는다. 이것에 실패한다면 잠재적으로 치명적인 메모리 누수가 유발될 수 있다. 파일에서 클라이언트로의 실제 전달을 수행한다. rio_written 함수는 srcp 위치에서 시작하는 filesize 바이트를 클라이언트와 연결된 descriptor로 복사한다. 마지막으로 매핑된 가상 메모리 영역을 free한다. 이는 잠재적인 치명적인 메모리 누수를 피하기 위해 중요한 부분이다.
serve_dynamic 함수
TINY는 자식 프로세스를 fork하고 자식의 맥락에서 CGI 프로그램을 실행함으로서 모든 유형의 동적 콘텐츠를 서비스한다.
serve_dynamic 함수는 클라이언트에게 성공을 나타내는 응답 라인과 헤더를 전송함으로써 시작된다. CGI 프로그램은 응답의 나머지를 전송할 책임이 있다. 이것은 우리가 소망하는 것처럼 robust하지는 않다. 왜냐하면 이는 CGI 프로그램이 어떤 에러를 마주할 수도 있다는 가능성을 허용하지 않기 때문이다.
응답의 첫 번째 부분을 전송한 후에 새로운 자식 프로세스를 fork한다. 자식은 QUERY_STRING 환경 변수를 요청 URI에 있는 CGI 인자로 초기화한다. 실제 서버는 다른 CGO 환경 변수들도 설정한다는 것을 기억하라. 간단하게 하기 위해서 우리는 이 스텝을 생략했다.
다음에 자식은 자식의 표준 출력을 연결된 파일 descriptor로 리다이렉트하고, CGI 프로그램을 로드하고 실행한다. CGI 프로그램이 자식의 맥락에서 실행되기 때문에 CGI 프로그램은 execve 함수를 호출하기 전에 존재했던 open file과 환경 변수에 대한 접근을 가지고 있다. 따라서 CGI 프로그램이 표준 출력에 작성하는 모든 것은 클라이언트 프로세스에게 직접적으로 간다. 그런 와중에 부모는 wait에 대한 호출에서 block된다. child가 종료됐을 때 child를 거둘 수 있게 기다린다.
일찍 종료된 connection 다루기
웹 서버의 기본적인 기능들은 간단하기는 하지만 실제 웹 서버를 만드는 일은 쉽지 않다. 문제 없이 긴 시간 동안 운영될 수 있는 튼튼한 웹 서버를 만드려면 리눅스 시스템 프로그래밍에 대한 더 깊은 이해가 필요하다. 예를 들어, 클라이언트에 의해 이미 종료된 연결에 서버가 무언가를 write하려고 한다면 첫 번째 write는 정상적으로 리턴하더라도 두 번째 write는 프로세스를 종료시키는 SIGPIPE 시그널을 야기한다. SIGPIPE 시그널이 잡히거나 무시되면 두 번째 write 작업은 errno를 EPIPE로 설정하고 -1을 리턴한다. strerr나 perror는 EPIPE 에러를 "Broken pipe"로 보고한다. "Broken pipe"는 직관적인 메시지가 아니라서 많은 학생들을 혼란스럽게 한다. 튼튼한 서버라면 SIGPIPE 시그널을 잡아서 EPIPE 에러를 유발시키는 write 함수 호출을 검사해야만 한다.