Tiny web

qwbsy·2021년 12월 9일

tiny web server의 코드를 작성하려면 네트워크 프로그래밍과 시스템 수준의 입출력에 대해서 이해하고 있어야 한다. 우선 책에 있는 코드를 기준으로 순차적으로 관련 내용들을 살펴보자.

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);  // line:netp:tiny:accept
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE,
                0);
    printf("Accepted connection from (%s, %s)\n", hostname, port);
    doit(connfd);   // line:netp:tiny:doit
    Close(connfd);  // line:netp:tiny:close
  }
}

tiny.c 의 메인 코드이다. 우선 실행 명령어의 인자(argc)가 2개가 아니면 사용법을 출력해서 제대로 사용하라고 꾸짖는다. 제대로 된 명령어가 들어왔다면, listenfd를 생성한다. 여기서 Open_listenfd 함수를 살펴보자.

int Open_clientfd(char *hostname, char *port) {
    int rc;

    if ((rc = open_clientfd(hostname, port)) < 0) 
	unix_error("Open_clientfd error");
    return rc;
}
int open_listenfd(char *port) 
{
    struct addrinfo hints, *listp, *p;
    int listenfd, rc, optval=1;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;             /* Accept connections */
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
    hints.ai_flags |= AI_NUMERICSERV;            /* ... using port number */
    if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
        return -2;
    }

    /* Walk the list for one that we can bind to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) 
            continue;  /* Socket failed, try the next */

        /* Eliminates "Address already in use" error from bind */
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,    //line:netp:csapp:setsockopt
                   (const void *)&optval , sizeof(int));

        /* Bind the descriptor to the address */
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break; /* Success */
        if (close(listenfd) < 0) { /* Bind failed, try the next */
            fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
            return -1;
        }
    }


    /* Clean up */
    freeaddrinfo(listp);
    if (!p) /* No address worked */
        return -1;

    /* Make it a listening socket ready to accept connection requests */
    if (listen(listenfd, LISTENQ) < 0) {
        close(listenfd);
	return -1;
    }
    return listenfd;
}

함수의 첫 번째 글자가 대문자인 것은 에러핸들링을 해서 함수로 한 번 감쌌다고 생각하면 된다. 실제 실행되는 소문자 함수를 보면 우선 이거저거 설정해주고 getaddrinfo함수를 실행한다. 이후 listp부터 시작해서 다음 주소가 있을 동안 socket함수와 bind함수가 성공하는지 판단한다. 이후 유효한 listenfd가 생성됐다면 그 값을 리턴하고 아니면 에러코드를 리턴한다.
이후 while문에서 연결요청에 대해 Accept함수로 connfd를 생성하고 연결됐다는 문구와 함께 doit함수를 실행한 다음 완료되면 close한다.

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_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);

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

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

doit함수에 Robust I/O 관련 함수(Rio_...)가 나오는데 이 내용은 CSAPP 10장을 참고하고 여기서는 read_requesthdrs함수를 실행하기 위한 작업 정도로 생각하자. 이 코드에서는 GET 이외의 요청은 처리하지 못 한다는 에러를 설정한다.
이후에는 parse_uri함수로 uri를 분석하고, 파일을 찾을 수 없으면 404 error를 반환하고 해당 파일이 정적 파일인지 동적 파일인지에 따라 작업이 이루어진다. 권한이 있는지 판단한 후 각각 serve_static함수, serve_dynamic함수를 실행한다.

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

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

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

clineterror; 에러 화면을 컨트롤한다.

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

read_requesthdrs; rio_t 구조체에 담았던 내용을 읽는 작업을 한다.

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

  if(!strstr(uri, "cgi-bin")){
    strcpy(cgiargs, "");
    strcpy(filename, ".");
    strcat(filename, uri);
    if(uri[strlen(uri)-1] == '/')
      strcat(filename, "home.html");
    return 1;
  }else{
    ptr = index(uri, '?');
    if(ptr){
      strcpy(cgiargs, ptr+1);
      *ptr = '\0';
    }else{
      strcpy(cgiargs, "");
    }
    strcpy(filename, ".");
    strcpy(filename, uri);
    return 0;
  }
}

parse_uri함수에서 인자로 받은 uri에 "cgi-bin"이 포함되었는지 여부로 정적 파일과 동적 파일을 구분한다. 각 파일 형식에 맞는 filename으로 변경해준 뒤 정적 파일은 1, 동적 파일은 0 값을 리턴한다.

void serve_static(int fd, char *filename, int filesize){
  int srcfd;
  char *srcp, filetype[MAXLINE], buf[MAXBUF];

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

  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);
}
//filename의 확장자로 filetype 가져오는 함수
void get_filetype(char *filename, char *filetype){
  if(strstr(filename, ".html"))
    strcpy(filetype, "text/html");
  else if(strtstr(filename, ".gif"))
    strcpy(filetype, "image/gif");
  else if(strtstr(filename, ".png"))
    strcpy(filetype, "image/png");
  else if(strtstr(filename, ".jpg"))
    strcpy(filetype, "image/jpeg");
  else
    strcpy(filetype, "text/plain");
}

Response headers를 출력하고, 해당 파일을 읽기전용으로 열어서 srcfd에 식별자를 할당한다. srcp에 메모리매핑을 한 뒤에 열었던 파일을 다시 닫고 메모리에 매핑된 것으로 쓰기작업을 한 뒤 메모리도 다시 반환해준다.

void serve_dynamic(int fd, char *filename, char *cgiargs){
  char buf[MAXLINE], *emptylist[] = { NULL };

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

동적 파일인 경우에는 프로세스를 fork하고 표준출력 식별자를 fd 식별자값으로 재지정해주고 자식 프로세스에서 해당 파일을 실행한다.

컴퓨터시스템 숙제문제 참고
proxy구현 참고

0개의 댓글