TINY Web Server

CSAPP 11 네트워크 프로그래밍(3)

11.6 종합 설계: 소형 웹 서버

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
 *	tiny.c - A simple, iterative HTTP/1.0 Web server that uses the
 *	GET method to serve static and dynamic content
 */
#include "csapp.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_sattic(int fd, char *filename, char *cgiargs);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum,
                char *shortmsg, char *longmsg);

int main(int argc, char **argv)
{
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    /* Check command-line args */
    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,
                   prot, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);
        doit(connfd);
        Close(connfd);
    }
}

TINY main

반복실행 서버로 명령줄에서 넘겨받은 포트로의 연결 요청 듣는다.

  • open_listenfd 함수를 호출해서 듣기 소켓을 연다.
  • TINY는 전형적인 무한 서버 루프 실행
  • 반복적으로 연결 요청을 접수(line 32), 트랜잭션 수행(line 36), 자신 쪽의 연결 끝을 닫음(line 37)

doit 함수 : 한개의 HTTP 트랜잭션을 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void doit(int fd)
{
    int is_static;
    struct stat sbuf;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char filenmae[MAXLINE], cgiargs[MAXLINE];
    rio_t rio;

    /* Read request line and headers */
    Rio_readinitb(&rio, fd);
    Rio_readlineb(&rio, buf, MAXLINE);
    printf("Request headers:\n");
    printf("%s", buf);
    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;
           }
           read_requesthdrs(&rio);

           /* Parse URI from GET request */
           is_static = parse_uri(uri, filename, cgiargs);
           if (stat(filename, &sbuf) < 0) {
               clienterror(fd, filenname, "404", "Not found",
                          "Tiny coudn't find this file");
               return;
           }

           if (is_static) { /* Serve static content */
           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 { /* Serve dynamic content */
           		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);
           }
}
  • 한 개의 HTTP 트랜잭션 처리.
  • 요청 라인을 읽고 분석(line 11~ 14) <- 위 경우 rio_readlineb함수를 사용해서 읽음
  • TINY는 GET 메소드만 지원
    • 따라서 클라이언트가 다른 메소드(ex. POST)를 요청하면 에러 메시지를 보내고 main 루틴으로 돌아옴(line 15~19)
    • 그 후에 연결을 닫고 다음 연결 요청을 기다림
  • 아닐때는 다른 요청 헤더들을 무시(line 20)
  • 그 후 URI를 파일 이름과 비어 있을 수도 있는 CGI 인자 스트링으로 분석하고, 요청이 정적인지 동적인지 플래그 설정(line 23)
  • 이 파일이 디스크 상에 있지 않으면, 에러 메시지를 클라이언트에 보내고 리턴
    • 요청이 정적 컨텐츠라면, 이 파일이 보통 파일인지, 읽기 권한을 가지고 있는지 검증(line 31)
    • 맞다면 클라이언트에게 정적 컨텐츠 제공(line 36)
    • 요청이 동적 컨텐츠라면, 이 파일이 실행 가능한지 검증(line 39)
    • 맞다면 동적 컨텐츠 제공(line 44)

Clienterror함수 : 에러 메시지를 클라이언트에게 보낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void clienterror(*int fd, char *cause, char *errnum,
                 char *shortmsg, char *longmsg)
{
    char buf [MAXLINE], body[MAXBUF];

    /* Build the HTTP response body */
    sprintf(body, "<html><title>Tiny Error</title>");
    sprinf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
    sprinf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
    sprinf(body, "%s<p>%s : %s\r\n", body, longmsg, cause);
    sprinf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);

    /* Print the HTTP response */
    sprinf(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));
    Rio_writen(fd, body, strlen(body));
}

HTTP 응답을 응답 라인에 적절한 상태 코드와 상태 메시지와 함꼐 클라이언트에 보냄

브라우저 사용자에게 에러를 설명하는 응답 본체에 HTML 파일도 함께 보냄

  • HTML 응답은 본체에서 컨텐츠의 크기와 타입을 나타낸다.
  • HTML 컨텐츠를 한 개의 스트링으로 만들었고, 그 크기를 쉽게 결정할 수 있음.

read_requesthdrs함수 : 요청 헤더를 읽고 무시한다.

1
2
3
4
5
6
7
8
9
10
11
void read_requesthdrs(rio_t *rp)
{
    char buf[MAXLINE];

    Rio_readlineb(rp, buf, MAXLINE);
    while(strcmp(buf, "\r\n")) {
        Rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf);
    }
    return;
}

TINY는 요청 헤더 내의 어떤 정보도 사용하지 않음

요청 헤더를 종료하는 빈 텍스트 줄이 line 6에서 체크하고 있는 carriage return과 line feed 쌍으로 구성되어 있음.


TINY parse_uri : HTTP URI를 분석한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int parse_uri(char *uri, char *filename, char *cgiargs)
{
    char *ptr;

    if (!strstr(uri, "cgi-bin")) { /* Static content */
    strcpy(cgiargs, "");
    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';
        }
        else
            strcpy(cgiargs, "");
            strcpy(filename, ".");
            strcat(filename, uri);
            return 0;
        }
}
  • TINY : static content를 위한 홈 디렉토리가 자신의 현재 디렉토리고, 실행파일의 홈 디렉토리는 /cgi-bin 이라고 가정.

  • 스트링 cgi-bin을 포함하는 모든 URI는 dynamic content를 요청하는 것이라고 가정. 기본 파일 이름은 ./home.html
  • URI를 파일 이름과 옵션으로 CGI 인자 스트링을 분석
  • 요청이 정적 컨텐츠를 위한것이면(line 5)
    • CGI 인자 스트링을 지운다(line 6)
    • URI를 ./index.html 같은 상대 리눅스 경로이름(line 7~8)으로 변환
    • 만일 URI가 ‘/’문자로 끝난다면(line 9), 기본 파일 이름을 추가한다(line 10).
  • 요청이 동적 컨텐츠를 위한것이면(line 13)
    • 모든 CGI 인자들을 추출(line 14~20)
    • 나머지 URI 부분을 상대 리눅스 파일 이름으로 변환(line 21~22)

serve_static함수 : 정적 컨텐츠를 클라이언트에게 서비스한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void serve_static(int fd, char *filename, int filesize)
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */
    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));
    printf("Response headers:\n");
    printf("%s", buf);

    /* Send response body to client */
    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);
}

/*
 * get_filetype - Derive file type from filename
 */
void get_filetype(char *filename, char *filetype)
{
    if (strstr(filename, ".html")
       strcpy(filetype, "text/html");
    else if (strstr(filename, ".gif"))
       strcpy(filetype, "image/gif");
    else if (strstr(filename, ".webp"))
       strcpy(filetype, "image/png");
    else if (strstr(filename, ".webp"))
       strcpy(filetype, "image/jpeg");
    else
       strcpy(filetype, "text/plain");
}

TINY가 지원하는 5개의 정적 컨텐츠 : HTML 파일, 무형식 텍스트 파일, GIF, PNG, JPEG으로 인코딩된 영상

serve_static함수는 지역 파일의 내용을 포함하고 있는 본체를 갖는 HTTP 응답을 보낸다.

  • 파일 이름의 접미어(suffix)부분을 검사해 파일 타입을 결정한다.(line 7)
  • 클라이언트에 응답 줄과 응답 헤더를 보낸다(line 8~13) <= 빈 줄 1개가 헤더를 종료하고 있다.
  • 요청한 파일의 내용을 연결 식별자 fd로 복사해서 응답 본체를 보낸다.
    • 읽기 위해서 filename을 오픈하고 식별자를 얻어오는 과정(line 18)
    • 리눅스 mmap함수가 요청한 파일을 가상메모리 영역으로 mapping(line 19)
    • 파일을 메모리로 mapping한 후 이 식별자는 필요 없으니 파일을 닫음(line 20) => 없으면 메모리 누수 발생
    • 실제로 파일을 클라이언트에게 전송(line 21)
    • rio_writen함수는 주소 srcp에서 시작하는 filesize 바이트(요청한 파일에 mapping 되어있음)를 클라이언트의 연결 식별자로 복사한다.
    • 매핑된 가상메모리 주소를 반환(line 22)

serve_dynamic함수 : 동적 컨텐츠를 클라이언트에게 서비스한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void serve_dynamic(int fd, char *filename, char *cgiargs)
{
    char buf[MAXLINE], *emptylist[] = { NULL };

    /* Return first part of HTTP response */
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    Rio_writen(fd, buf, strle(buf));
    sprintf(buf, "Server: Tiny Web Server\r\n");
    Rio_writen(fd, buf, strlen(buf));

    if (Fork() == 0) { /* Child */
    /* Real server would set all CGI vars here */
    setenv("QUERY_STRING", cgiargs, 1);
    Dup2(fd, STDOUT_FILENO);	/* Redirect stdout to client */
    Execve(filename, emptylist, environ); /* Run CGI program */
    }
    Wait(NULL); /* Parent waits for and reaps child */
}

TINY는 자식 프로세스를 fork하고 그 후에 CGI 프로그램을 자식의 컨텍스트에서 실행하며 모든 종류의 동적 컨텐츠를 제공한다.

serve_dynamic함수

  • 클라이언트에 성공을 알려주는 응답 라인을 보내는 것으로 시작
  • CGI 프로그램은 응답의 나머지 부분을 보내야 함
    • CGI 프로그램이 에러를 만날 수 있다는 가능성을 배제하였기에 우리 기대만큼 견고하지 않음
  • 응답의 첫 번째 부분을 보낸 후, 새로운 자식 프로세스를 fork(line 11)
  • 자식은 QUERY_STRING 환경변수를 요청 URI의 CGI 인자들로 초기화(line 13)
    • 실제 서버는 여기서 다른 CGI 환경변수들도 설정하지만 여기선 생략됨
  • 자식은 자식의 표준 출력을 연결 파일 식별자로 재지정(line 14)
  • CGI 프로그램을 로드하고 실행(line 15)
    • CGI 프로그램이 자식 컨텍스트에서 실행되기 때문에 execve함수를 호출하기 전에 존재하던 열린 파일들과 환경변수들에도 동일하게 접근 가능
    • 따라서 CGI 프로그램이 표준 출력에 쓰는 모든 것은 직접 클라이언트 프로세스로 부모 프로세스의 어떤 간섭도 없이 전달
  • 부모는 자식이 종료되어 정리되는 것을 기다리기 위해 wait함수에 블록됌(line 17)

11.7 요약

  • 모든 network application 은 client-server model에 기초하고 있다.
  • 이 모델을 사용하면 application은 1 개의 server와 1개 이상의 clients 로 구성된다.
  • server는 resouce를 관리하고, client에게 service를 제공한다.
  • 클라이언트-서버 모델에서의 기본 연산은 client-server transaction이다.
  • 이 트랜잭션은 클라이언트로부터의 request와 이에 대한 서버의 response로 이루어진다.
  • 클라이언트와 서버는 Internet이라고 하는 글로벌 네트워크를 통해서 통신한다.
  • 프로그래머의 관점에서 인터넷은 전 세계적인 규모의 호스트 집단인데 다음 특성을 지녔다.
    • 각 Internet host는 IP adress라고 하는 고유한 32비트 이름을 가진다.
    • IP 주소 집합은 Internet domain 이름 집합과 대응된다.
    • 서로 다른 인터넷 호스트에서의 process들은 connection을 통해서 서로 통신한다.
  • 클라이언트와 서버는 socket interface를 사용해서 연결을 수립한다.
  • 소켓은 연결의 end point이며 application에게는 file descriptor(식별자)의 형태로 제공된다.
  • 소켓 인터페이스는 소켓 식별자를 열고 닫기 위한 함수들을 제공한다.
  • 이 식별자들을 서로 읽고 써서 클라리언트와 서버는 통신한다.
  • 웹 서버와 이들의 클라이언트들(like 브라우저)은 HTTP protocol을 사용해서 서로 통신한다.
  • 브라우저는 서버로부터 static or dynamic contents를 요청한다.
  • 정적 컨텐츠를 위한 요청은 서버의 디스크에서 파일을 가져와서 이것을 클라이언트에 돌려주는 방식으로 처리한다.
  • 동적 컨텐츠에 대한 요청은 서버에서 child process의 컨텍스트에서 프로그램을 돌리고, 그 출력을 클라이언트로 리턴해서 처리한다.