[LINUX] TINY webserver 만들기 (4) - TINY webserver 설계

piopiop·2021년 1월 27일
3

Linux

목록 보기
5/6

지금까지 공부한 내용으로 아주 작지만 기본 기능을 갖고 있는 웹 서버를 만들어보자.
웹에 기초적인 내용이나 웹에서 컨텐츠를 제공하는 방법에 대해서는 코드를 살펴보며 간단하게 설명하도록 하겠다.

1. main

이전 포스팅에서 소개한 open_listenfd 함수를 사용해 듣기 소켓을 생성하고 Accept 함수를 통해 연결을 요청한 clientfd와 연결한다.
accept함수에서 clientaddr에 저장한 클라이언트의 주소getnameinfo 함수의 인자로 넣어 서버의 ip주소와 포트 번호를 얻고 이를 프린트한다.
다음으로 client에서 받은 요청을 처리하는
doit**함수로 들어간다.

int main(int argc, char **argv)
{   
    int listenfd, connfd; //듣기소켓, 연결소켓 초기화
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }

    //인자로 받은 port에 listenfd 생성
    listenfd = Open_listenfd(argv[1]);
    while(1){
        clientlen = sizeof(clientaddr);
        //lisetenfd에 연결을 요청한 client의 주소를 sockaddr_stoage에 저장함
        //client의 주소, 크기를 받아서 저장할 곳의 포인터를 인자로 받음 
        //accept의 세번째 인자는 일단 addr의 크기를 설정하고(input) 접속이 완료되면 실제로 addr에 설정된 접속한 client의 주소 정보의 크기를 저장함 
        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);
    }
}

2. doit

doit함수는 한 개의 HTTP 트랜잭션을 처리한다. 즉 한 개의 클라이언트의 요청을 처리해 클라이언트에게 컨텐츠를 제공한다.
클라이언트가 서버에게 HTTP 요청을 보낼 때는 아래와 같이 요청 라인과 추가적인 요청 헤더를 보낸다. 요청 헤더는 요청에 대한 정보들을 나타낸다.

GET / HTTP/1.1  (요청 라인 - method, uri, 요청이 준수하는 http 버전)
(= 서버에게 HTML 파일 index.html(기본 홈페이지)을 가져와 리턴할 것을 요구)
Host: www.kaist.ac.kr (요청 헤더)
(= 서버의 도메인)
Connection: keep-alive  (요청 헤더) 
Accept: ~~  (요청 헤더)
(= 돌려줄 타입에 대해 서버에 알림)

tiny서버는 최소한의 기능만 충족하도록 GET 메소드만 지원하고, 요청 헤더는 무시하도록 하겠다.

doit함수는 아래와 같은 순서로 실행된다.
1. 클라이언트의 HTTP 요청에서 요청 라인만 읽음
(이때 rio_readinitb와 rio_readlineb는 linux에서 파일을 읽을 때 사용되는 함수이다. 각 함수의 설명을 생략하고 여기서는 rio_readlineb를 통해 요청 텍스트의 제일 위 한줄(요청 라인)을 읽었다고 생각하고 넘어가도록 하자.)
2. GET메소드 인지 확인한다.
3. 요청헤더들은 사용하지 않을 것이기 때문에 읽고 무시한다.
4. uri를 잘라 uri, filename, cigiargs로 나누고 클라이언트가 정적 컨텐츠를 원하는지, 동적 컨텐츠를 원하는지 확인한다.
(컨텐츠에 관련해서는 아래에서 설명하겠다.)
5. 실행을 원하는 파일의 stat 구조체의 st_mode를 이용해 파일이 읽기 권한과 실행 권한이 있는지 확인한다.

//stat 구조체 (파일의 정보 저장)
 struct stat { 
     dev_t      st_dev; /* ID of device containing file */ 
     ino_t      st_ino; /* inode number */ 
     mode_t     st_mode; /* 파일의 종류 및 접근권한 */ 
     nlink_t    st_nlink; /* hardlink 된 횟수 */ 
     uid_t      st_uid; /* 파일의 owner */ 
     gid_t      st_gid; /* group ID of owner */ 
     dev_t      st_rdev; /* device ID (if special file) */ 
     off_t      st_size; /* 파일의 크기(bytes) */ 
     blksize_t  st_blksize; /* blocksize for file system I/O */ 
     blkcnt_t   st_blocks; /* number of 512B blocks allocated */ 
     time_t     st_atime; /* time of last access */ 
     time_t     st_mtime; /* time of last modification */ 
     time_t     st_ctime; /* time of last status change */ 
     };

stat구조체는 파일의 정보를 저장하는 구조체로 stat(파일 이름, 정보를 저장할 주소) 함수를 실행해 얻을 수 있다.

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;

    //rio와 fd 연결
    Rio_readinitb(&rio, fd);
    Rio_readlineb(&rio, buf, MAXLINE); //request line, header 읽기 
    printf("Request headers:\n");
    printf("%s", buf);
    //buf에서 공백문자로 구분된 문자열 3개 읽어 각자 method, uri, version에 저장해라
    sscanf(buf, "%s %s %s", method, uri, version);

    //GET요청인지 아닌지 확인
    if (strcasecmp(method, "GET")){
        clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
        return;
    }

    //요청 헤더는 무시함 
    read_requesthdrs(&rio);

    //uri를 잘라 uri, filename, cgiargs로 나눈다
    is_static = parse_uri(uri, filename, cgiargs);
    //stat(파일 명 or 파일 상대/절대 경로, 파일의 상태 및 정보를 저장할 buf 구조체)
    if (stat(filename, &sbuf) < 0) {
        clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
        return;
    }
	
    //정적 컨텐츠
    if (is_static) {
        //실행 가능한지 확인하는 조건문 -> 보통파일인지, 읽기 권한을 갖고 있는지 확인
        //S_ISREG -> isregular - 일반파일인지 체크하는 macro
        //st_mode는 파일의 유형값으로 bit& 연산으로 파일의 유형을 확인 가능함 
        //S_IXUSR -> 실행 권한 있는지 / S_IRUSR -> 읽기 권한 있는지
        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_static(fd, filename, sbuf.st_size);
    }
    //동적 컨텐츠
    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);
    }
    

}

3. clienterror

일부 명백한 에러에 대해서 client에게 HTTP응답을 보낸다.

void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)
{
    char buf[MAXLINE], body[MAXBUF];

    //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, 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);

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

4. read_requesthdrs

우리가 만드는 웹서버는 요청 헤더 내의 아무런 정보도 사용하지 않는다.
따라서 요청 헤더를 종료하는 빈 텍스트줄("\r\n")이 나올 때까지 요청 헤더를 모두 읽어들인다.

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

피드백 환영합니다.
-끝-

profile
piopiop1178@gmail.com

0개의 댓글