Tiny web

qwbsy·2021년 12월 9일
0

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개의 댓글