[WEEK 07] C - TINY 웹 서버

신호정 벨로그·2021년 9월 19일
2

Today I Learned

목록 보기
32/89

11.6 종합 설계: 소형 웹 서버

TINY라고 부르는 프로세서 제어, Unix I/O, 소켓 인터페이스, HTTP 같은 개념들을 결합한 소형 웹 서버를 개발

TINY 웹 서버는 정적 및 동적 컨텐츠를 모두 제공할 수 있다.

main 루틴

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

int main(int argc, char **argv) {
    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(0);
    }
    
    listenfd = open_listenfd(argv[1]);

    while (1) {
        clientlen = sizeof(clientaddr);
        connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
        getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);

        printf("Accepted connection from (%s, %s) \n", hostname, port);
        doit(connfd);
        close(connfd);
    }
}
  1. open_listenfd 함수를 호출해서 듣기 소켓을 오픈한다.

2-1. 무한 서버 루프를 실행한다.

2-2. 반복적으로 연결 요청을 접수한다.

2-3. 트랜잭션을 수행한다.

  1. 자신 쪽의 연결 끝을 닫는다.

RIO: Robust I/O

RIO 패키지는 짧은 카운트가 발생할 수 있는 네트워크 프로그램 같은 응용에서 편리하고, 안정적이고 효율적인 I/O를 제공한다.

  • 읽기 버퍼의 포맷을 초기화하는 함수 rio_readinitb 함수는 한 개의 빈 버퍼를 설정하고, 이 버퍼와 한 개의 오픈한 파일 식별자를 연결한다.

  • rio_readn 함수는 식별자 fd의 현재 파일 위치에서 메모리 위치 usrbuf로 최대 n바이트를 전송한다.

  • rio_writen 함수는 usrbuf에서 식별자 fd로 n바이트로 전송한다.

/* RIO(Robust I/O) 패키지 */
#include "csapp.h"

void rio_readinitb(rio_t *rp, int fd);

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
  1. void rio_readinitb(rio_t, *rp, int fd) 함수는 식별자 fd를 주소 rp에 위치한 rio_t 타입의 읽기 버퍼와 연결한다.

  2. ssize_t rio_readlineb(rio_t rp, void usrbuf, size_t maxlen) 함수는 다음 텍스트 줄을 파일 rp(종료 세 줄 문자를 포함해서)에서 읽고, 이것을 메모리 위치 usrbuf로 복사하고, 텍스트 라인을 NULL 문자로 종료시킨다.

  3. ssize_t rio_readnb 함수는 최대 n바이트를 파일 rp로부터 메모리 위치 usrbuf로 읽는다.

10.6 파일 메타데이터 읽기

  • S_ISREG(m): Is this a regular file?

  • S_ISRID(m): Is this a directory file?

  • S_ISSOCK(m). Is this a network socket?

doit 함수

doit 함수는 한 개의 HTTP 트랜잭션을 처리한다.

void doit(int fd) {
    int is_static;
    struct stat sbuf;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char filename[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 requests */
    is_static = parse_uri(uri, filename, cgiargs);
    
    if (stat(filename, &sbuf) < 0) {
        clienterror(fd, filename, "404", "Not found", "TINY couldn't find this file");
        return;
    }

    /* Serve static content */
    if (is_static) {
        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);
    }

    /* Serve dynamic content */
    else {
        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);
    }
}
  1. rio_readlineb 함수를 사용해서 요청 라인을 읽고 분석한다.

2-1. 클라이언트가 다른 종류의 메소드를 요청하면 에러 메시지를 보내고 main 루틴으로 돌아온 후, 연결을 닫고 다음 연결 요청을 기다린다.

2-2. 클라이언트가 GET 메소드를 요청하면 요청을 받아들이고 다른 요청 헤더들을 무시한다.

3-1. URI를 CGI 인자 스트링으로 분석하고, 요청이 정적 또는 동적 컨텐츠를 위한 것인지 나타내는 플래그를 설정한다.

3-2. 요청이 정적 컨텐츠를 위한 것이라면 읽기 권한을 가지고 있는지 검증하고 정적 컨텐츠를 클라이언트에게 제공한다.

3-3. 요청이 동적 컨텐츠에 대한 것이라면 파일이 실행 가능한지 검증하고 동적 컨텐츠를 클라이언트에게 제공한다.

client_error 함수

client_error 함수HTTP 응답을 응답 라인에 적절한 상태 코드와 상태 메시지와 함께 클라이언트에 보내며, 브라우저 사용자에게 에러를 설명하는 응답 본체에 HTML 파일도 함께 보낸다.

HTML 응답은 본체에서 컨텐츠의 크기와 타입을 나타내야 한다.

void client_error(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>");
    sprintf(body, "%s<body bgcolor=""ffffff""\r\n", body);
    sprintf(body, "%s%s: s\r\n", body, longmsg, cause);
    sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
    sprintf(body, "%s<hr><em>The TINY Web Server</em>\r\n", body);

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

read_requesthdrs 함수

TINY는 요청 헤더 내의 어떤 정보도 사용하지 않으며 read_requesthdrs 함수를 호출해서 이들을 읽고 무시한다.

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;
}

parse_uri 함수

TINY는 정적 컨텐츠를 위한 홈 디렉토리가 자신의 현재 디렉토리이고, 실행 파일의 홈 디렉토리는 /cgi-bin이라고 가정한다.

스트링 cgi-bin을 포함하는 모든 URI는 동적 컨텐츠를 요청하는 것을 나타낸다고 가정한다.

parse_uri 함수는 URI를 파일 이름과 옵션으로 CGI 인자 스트링을 분석한다.

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

    /* Static content */
    if (!strstr(uri, "cgi-bin")) {
        strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);

        if (uri[strlen(uri) - 1] == '/')
            strcat(filename, "home.html");

        return 1;
    }

    /* Dynamic content */
    else {
        ptr = index(uri, '?');

        if (ptr) {
            strcpy(cgiargs, ptr + 1);
            *ptr = '\0';
        }

        else
            strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        
        return 0;
    }
}

만일 요청이 정적 컨텐츠를 위한 것이라면 CGI 인자 스트링을 지우고 URI를 상대 리눅스 경로 이름(./index.html)으로 변환한다.

만일 요청이 동적 컨텐츠를 위한 것이라면 모든 CGI 인자들을 추출하고 나머지 URI 부분을 상대 리눅스 파일 이름으로 변환한다.

serve_static 함수

TINY는 HTML 파일, 무형식 텍스트 파일, GIF, PNG, JPEG으로 인코딩된 영상의 다섯 개의 서로 다른 정적 컨텐츠 타입을 지원한다.

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

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\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);
}
  1. 파일 이름의 접미어 부분을 검사해서 파일 타입을 결정하고 클라이언트에 응답 줄과 응답 헤더를 보낸다.

  2. 요청한 파일의 내용을 연결 식별자 fd로 복사해서 응답 본체를 보낸다.

  3. 파일을 메모리로 매핑한 후 식별자는 필요 없기 때문에 파일을 닫고 클라이언트에게 전송한다.

  4. rio_writen 함수는 주소 srcp에서 시작하는 filesize 바이트를 클라이언트의 연결 식별자로 복사한다.

  5. 매핑된 가상 메모리 주소를 반환한다.

serve_dynamic 함수

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

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, strlen(buf));
    sprintf(buf, "Server: TINY Web Server\r\n");
    rio_writen(fd, buf, strlen(buf));

    /* Child */
    if (fork() == 0) {
        /* Real server would set all CGI vars here */
        setenv("QUERY_STRING", cgiargs, 1);
        /* Redirect stdout to client */
        dup2(fd, STDOUT_FILENO);
        /* Run CGI program */
        execve(filename, emptylist, environ);
    }
    /* Parent waits for and reaps child */
    wait(NULL);
}
  1. serve_dynamic 함수는 성공을 알려주는 응답 라인을 클라이언트에게 보낸다.

  2. 응답의 첫 번째 부분을 보낸 후에 새로운 자식 프로세스를 fork한다.

  3. 자식은 QUERY_STRING 환경 변수를 요청 URI의 CGI 인자들로 초기화한다.

  4. 자식은 자식의 표준 출력을 연결 파일 식별자로 재지정하고 CGI 프로그램을 로드하고 실행한다.

  5. 부모는 자식이 종료되어 정리되는 것을 기다리기 위해 wait 함수에서 블록된다.

0개의 댓글