5/19 Tiny Web Server 구현

JK·2023년 5월 19일
0

지금까지 공부하면서 항상 이론적인 부분을 먼저 공부하고 코드를 보거나 짜보거나 했었는데 처음에 이론을 공부해도 나중에 코드를 짜볼 때 다시 공부했던 걸 찾아보고 시간을 2배로 사용하는 거 같아서 이번 주는 순서를 바꿔서 코드를 먼저 보고 분석해보며 이론을 공부해보기로 했습니다

Tiny Web Server

이번 주는 웹서버를 구현하여 여러 기능을 만들어보고 과제를 해결하는 걸 목표로 잡고 공부를 시작했습니다
그래서 오늘은 Tiny Web Server를 구현하는 코드를 책에서 찾아보며 clone coding을 하여 코드를 분석해봤습니다

#include "csapp.h"

// 함수 원형 선언
void doit(int fd); //클라이언트 요청을 처리하는 함수
void read_requesthdrs(rio_t *rp); //요청 헤더를 읽는 함수
int parse_uri(char *uri, char *filename, char *cgiargs); //URI를 구문 분석하여 파일명과 CGI 인수를 추출하는 함수
void serve_static(int fd, char *filename, int filesize); // 정적 파일을 제공하는 함수
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);

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

// 명령행 인자의 개수가 2가 아니면 사용법 출력 후 종료
  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);
    // 클라이언트의 IP 주소와 포트 번호를 얻어옴
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
    printf("Accepted connection from (%s, %s)\n", hostname, port);
    // 클라이언트 요청을 처리하는 함수 호출
    doit(connfd);
    // 클라이언트와의 연결을 종료
    Close(connfd);
  }
}

//doit: 클라이언트 요청을 처리하는 함수
void doit(int fd) {
  int is_static; // 정적 파일 여부를 나타내는 변수
  struct stat sbuf; // 파일 정보를 저장하는 구조체
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE]; // 버퍼 및 문자열 변수
  char filename[MAXLINE], cgiargs[MAXLINE]; // 파일명과 CGI 인수를 저장하는 변수
  rio_t rio; // Rio 입출력 구조체

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

// 요청 메서드가 GET이 아닌 경우, 501 오류를 반환하고 함수 종료
  if (strcasecmp(method, "GET")) {
    clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
    return;
  }

// 요청 헤더를 읽음
  read_requesthdrs(&rio);

// URI를 파싱하여 파일명과 CGI 인수를 추출하고, 정적 파일 여부를 결정함
  is_static = parse_uri(uri, filename, cgiargs);

// 파일 정보를 확인하여 파일이 존재하지 않는 경우, 404 오류를 반환하고 함수 종료
  if (stat(filename, &sbuf) < 0) {
    clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
    return;
  }

// 정적 파일인 경우
  if (is_static) {
// 파일이 일반 파일이 아니거나 읽기 권한이 없는 경우, 403 오류를 반환하고 함수 종료
    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 {
// 파일이 일반 파일이 아니거나 실행 권한이 없는 경우, 403 오류를 반환하고 함수 종료
    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);
  }
}

//clienterror: 클라이언트에 오류 응답을 전송하는 함수
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
  char buf[MAXLINE], body[MAXBUF];

  // HTML 오류 페이지의 내용을 생성함
  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 응답 헤더를 생성하여 클라이언트에 전송함
  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));

  // HTML 오류 페이지의 내용을 클라이언트에 전송함
  Rio_writen(fd, body, strlen(body));
}

//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: URI를 구문 분석하여 파일명과 CGI 인수를 추출하는 함수
int parse_uri(char *uri, char *filename, char *cgiargs) {
  char *ptr;

  // URI에 "cgi-bin"이 포함되어 있지 않은 경우
  if (!strstr(uri, "cgi-bin")) {
    strcpy(cgiargs, "");
    strcpy(filename, ".");
    strcat(filename, uri);
    if (uri[strlen(uri)-1] == '/')
      strcat(filename, "home.html");
    return 1;
  }

  // URI에 "cgi-bin"이 포함된 경우
  else {
  // URI에서 "?" 문자를 찾습니다.
    ptr = index(uri, '?');
    if (ptr) {
      // "?" 이후의 문자열을 CGI 인수로 복사하고, "?"을 널 문자로 대체합니다.
      strcpy(cgiargs, ptr+1);
      *ptr = '\0';
    }

    // ptr 변수가 NULL인 경우
    else
      strcpy(cgiargs, "");
    strcpy(filename, ".");
    strcat(filename, uri);
    return 0;
  }
}

//serve_static: 정적 파일을 제공하는 함수
void serve_static(int fd, char *filename, int filesize) {
  int srcfd;
  char *srcp, filetype[MAXLINE], buf[MAXBUF];

  // 정적 콘텐츠를 서비스하는 함수입니다.

  // 파일의 MIME 타입을 가져옵니다.
  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);
}

//get_filetype: 파일 확장자에 따라 파일 유형을 결정하는 함수
void get_filetype(char *filename, char *filetype) {

  // 주어진 파일 이름에 ".html"이 포함되어 있다면, 파일의 타입은 "text/html"입니다.
  if (strstr(filename, ".html"))
    strcpy(filetype, "text/html");

  // 주어진 파일 이름에 ".gif"이 포함되어 있다면, 파일의 타입은 "image/gif"입니다.
  else if (strstr(filename, ".gif"))
    strcpy(filetype, "image/gif");

  // 주어진 파일 이름에 ".png"이 포함되어 있다면, 파일의 타입은 "image/png"입니다.
  else if (strstr(filename, ".png"))
    strcpy(filetype, "image/png");

  // 주어진 파일 이름에 ".jpg"이 포함되어 있다면, 파일의 타입은 "image/jpg"입니다.
  else if (strstr(filename, ".jpg"))
    strcpy(filetype, "image/jpg");

  // 위의 조건에 해당하지 않는 경우, 파일의 타입은 "text/plain"입니다.
  else
    strcpy(filetype, "text/plain");
}

//serve_dynamic: 동적 콘텐츠를 제공하는 함수
void serve_dynamic(int fd, char *filename, char *cgiargs) {

  char buf[MAXLINE], *emptylist[] = { NULL };

  // HTTP 응답 헤더를 생성하여 클라이언트에 전송합니다.
  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));

  // 자식 프로세스를 생성하여 CGI 프로그램을 실행합니다.
  if (Fork() == 0) {
    // CGI 프로그램에게 쿼리 문자열을 전달하기 위해 환경 변수를 설정합니다.
    setenv("QUERY_STRING", cgiargs, 1);
    // 자식 프로세스의 표준 에러를 클라이언트 소켓 파일 디스크립터로 재지정합니다.
    Dup2(fd, STDERR_FILENO);

    // CGI 프로그램을 실행합니다.
    Execve(filename, emptylist, environ);
  }
  // 부모 프로세스는 자식 프로세스가 종료될 때까지 기다립니다
  Wait(NULL);
}

이 코드의 구조와 기능을 이해하는 데 어려움을 겪었습니다 특히, 웹 서버의 동작 방식과 HTTP 프로토콜에 대한 이해가 필요했습니다. 코드의 일부 함수와 변수들의 역할과 상호작용을 파악하는 데 시간이 걸렸지만 이 코드를 통해 C 프로그래밍에서의 파일 입출력, 프로세스 생성과 통신, 문자열 처리 등의 기술을 익힐 수 있었습니다. 코드의 각 부분을 읽고 분석하는 과정에서 C 언어의 기능과 문법에 대한 이해를 높일 수 있을 것 같습니다.
이러한 경험을 통해 점차적으로 복잡한 코드와 프로젝트에 대한 이해도를 높여가는 데 도움이 되었으면 좋겠습니다

profile
^^

0개의 댓글