컴퓨터 사이언스/Network

tiny 서버 만들기

kimjingyu 2023. 11. 20. 11:00
728x90

사용되는 보조 함수들 소개

string 관련 함수들

  • sscanf
  • sprintf
  • strcasecmp
  • strcmp
  • strstr
  • strcpy
  • strcat

파일 관련 함수들

  • stat

프로세스와 관련된 함수들

  • fork
    • 자식 프로세스를 생성하는 함수로, fork 함수의 반환값은 자식 프로세스는 0, 부모 프로세스는 자식 프로세스의 PID이다.
    • fork()를 실행하면 부모 프로세스와 자식 프로세스가 동시에 실행된다.
    • 만약에 fork()의 반환값이 0이라면. 즉, 자식 프로세스라면 if문을 수행한다.
    • fork()의 반환값이 0이 아니라면. 즉, 부목 프로세스라면 if문을 건너뛰고 Wait(NULL)함수로 가는데, 이는 부모 프로세스가 먼저 도달해도 자식 프로세스가 종료될 때까지 기다리는 함수이다.
  • setenv(대상 환경변수의 값, 기존의 값과의 우선 순위)
    • 우선 순위에 따라 대상 환경변수의 값을 변경한다.
    • 우선 순위를 1로 놓으면 기존 값의 유무와 상관없이 값을 바꿔주고, 0으로 놓으면 기존 값이 있을 때는 기존 값을 유지하고, 없으면 그 값을 채워준다.
  • dup2(파일 생성자1, 파일 생성자2)
    • Redirect stdout to client
    • clientfd 출력을 CGI 프로그램의 표준 출려과 연결한다. 그러므로 이제 CGI 프로그램에서 printf하면 클라이언트에서 출력된다.
  • execve(파일이름, 파일인자의 포인터, 환경변수의 포인터)
    • 파일 이름이 가리키는 파일을 실행한다.
  • open()
    • filename이라는 파일을 읽기 전용으로 연다.
  • mmap()
    • 메모리의 0 위치에 filesize 만큼의 메모리를 할당한다. 이 메모리는 읽을 수만 있으며, 다른 프로세스에서는 접근할 수 없고, 메모리를 가져올 파일은 srcfd, 파일의 0byte 위치에서부터 불러온다.
  • munmap()
    • 할당된 메모리 공간을 해제한다.
    • 메모리의 start 위치에서부터 length 길이의 메모리를 해제해준다.

 

tiny 서버 만들기

main()

port 번호를 인자로 바아서 클라이언트의 요청이 올 때마다 새로 연결 소켓을 만들어서 doit() 함수를 호출한다.

/*
  port 번호를 인자로 받는다.
*/
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]);

  // 클라이언트의 요청이 올 때마다 새로 연결 소켓을 만들어서 doit()을 호출한다.
  while (1) {
    // 클라이언트에서 바은 연결 요청을 accept한다.
    clientlen = sizeof(clientaddr);
    
    // connfd = 서버 연결 식별자
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);  // line:netp:tiny:accept

    // 연결이 성공했다는 메시지를 위해서 Getnameinfo를 호출하면서 hostname과 port가 채워진다.
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
    printf("Accepted connection from (%s, %s)\n", hostname, port);

    // doit 함수를 실행한다.
    doit(connfd);   // line:netp:tiny:doit

    // 서버 연결 식별자를 닫아준다.
    Close(connfd);  // line:netp:tiny:close
  }
}

 

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]; // parse_uri를 통해서 채워진다.
  rio_t rio;

  // 클라이언트가 rio로 보낸 request 라인과 헤더를 읽고 분석한다.
  Rio_readinitb(&rio, fd);  // rio 버퍼와 서버의 connfd를 연결시켜준다.
  Rio_readlineb(&rio, buf, MAXLINE);  // rio에 있는 응답라인 한 줄을 모두 buf로 옮긴다.
  printf("Request headers:\n");
  printf("%s", buf);  // 요청 라인 buf = "GET /hi HTTP/1.1\0"을 표준 출력해준다.
  sscanf(buf, "%s %s %s", method, uri, version);  // buf에서 문자열 3개를 읽어와서 method, uri, version이라는 문자열에 저장한다.

  // 요청 method가 GET이 아니면 종료한다. 즉, main으로 가서 연결을 닫고, 다음 요청을 기다린다.
  if (strcasecmp(method, "GET")) {
    clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
    return;
  }

  // 요청 라인을 뺀 나머지 요청 헤더들은 무시한다.
  read_requesthdrs(&rio);

  // 클라이언트 요청 라인에서 받아온 uri를 이용해서 정적/동적 컨텐츠를 구분한다. 정적 컨텐츠면 1이 저장된다.
  is_static = parse_uri(uri, filename, cgiargs);

  // stat 함수는 file의 상태를 buffer에 넘긴다. 여기서 filename은 클라이언트가 요청한 서버의 컨텐츠 디렉토리 및 파일 이름이다.
  // 여기서 못넘기면 파일이 없다는 뜻이므로, 404 fail이다.
  if (stat(filename, &sbuf) < 0) {
    clienterror(fd, filename, "404", "Not found", "서버에 요청하신 파일이 없습니다.");
    return;
  }

  // 컨텐츠의 유형이 정적인지, 동적인지를 파악한 후에 각각의 서버에 보낸다.
  if (is_static) { // 정적 컨텐츠인 경우
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { // 일반 파일이 아니거나, 읽을 권한이 없으면
      clienterror(fd, filename, "403", "Forbidden", "권한없는 접근입니다.");
      return;
    }
    // reponse header의 content-length를 위해 정적 서버에 파일의 사이즈를 같이 보낸다
    serve_static(fd, filename, sbuf.st_size);
  } else {  // 동적 컨텐츠인 경우
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {  // 일반 파일이 아니거나, 실행 파일이 아니면
      clienterror(fd, filename, "403", "Forbidden", "권한없는 접근입니다.");
      return;
    }

    // 동적 서버에 인자를 같이 보낸다.
    serve_dynamic(fd, filename, cgiargs);
  }
}

 

clienterror()

에러 메시지와 응답 본체를 서버 소켓을 통해 클라이언트에 보낸다.

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>\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>Tiny Web Server</em>\r\n", body);

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

  // error msg와 response body를 server socket을 통해 클라이언트에 보낸다.
  Rio_writen(fd, buf, strlen(buf));
  Rio_writen(fd, body, strlen(body));
}

 

read_requesthdrs

클라이언트가 버퍼 rp에 보낸 나머지 요청 헤더들을 무시한다. 즉, 계속 출력해줘서 없앤다.

// 클라이언트가 버퍼 rp에 보낸 나머지 요청 헤더들을 무시
void read_requesthdrs(rio_t *rp) {
  char buf[MAXLINE];

  Rio_readlineb(rp, buf, MAXLINE);

  // 버퍼 rp의 마지막 끝을 만날 때까지(Content-Length의 마지막 \r\n) 계속 출력해줘서 없앤다.
  while (strcmp(buf, "\r\n")) {
    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
  }
  return;
}

 

parse_uri

uri를 받아서 요청받은 파일의 이름(filename)과 요청 인자(cgiarg)를 채워준다.

// uri를 받아서 요청받은 파일의 이름과 요청 인자를 채워준다.
int parse_uri(char *uri, char *filename, char *cgiargs) {
  char *ptr;

  // uri에 cgi-bin이 없다면, 즉 정적 컨텐츠를 요청한다면 1을 반환한다.
  // 예) GET /hi.jpg HTTP/1.1 --> uri에 cgi-bin이 없음

  /*
    static-content. 즉, uri안에 "cgi-bin"과 일치하는 문자열이 없음

    예시)
    uri : /hi.jpg
    ->
    cgiargs :
    filename : ./hi.jpg
  */
  if (!strstr(uri, "cgi-bin")) {
    strcpy(cgiargs, "");    // 정적 컨텐츠이므로, cgiargs에는 아무것도 없다.
    strcpy(filename, ".");  // 현재 경로에서부터 시작한다. ./path ~~
    strcat(filename, uri);  // filename 스트링에 uri 스트링을 이어붙인다.

    // 만약 uri뒤에 '/'이 있으면, 그 뒤에 home.html을 붙인다.
    if (uri[strlen(uri)-1] == '/') {
      strcat(filename, "home.html");
    }

    // 정적 컨텐츠이면 1을 반환한다.
    return 1;
  } 
  /*
    dynamic-content

    예시)
    uri : /cgi-bin/adder?123&123
    ->
    cgiargs : 123&123
    filename : ./cgi-bin/adder
  */
  else {
    ptr = index(uri, '?');

    // '?'이 있으면 cgiargs를 '?' 뒤의 인자들과 값으로 채워주고, '?'를 NULL으로 만든다.
    if (ptr) {
      strcpy(cgiargs, ptr + 1);
      *ptr = '\0';
    } else {
      // '?'이 없으면 아무것도 안넣어준다.
      strcpy(cgiargs, "");
    }

    strcpy(filename, '.');  // 현재 디렉토리에서 시작한다.
    strcat(filename, uri);  // uri를 넣어준다.
  }

  return 0;
}

 

serve_static()

클라이언트가 원하는 정적 컨텐츠 디렉토리를 받아온다. 응답 라인과 헤더를 작성하고 서버에게 보낸다. 그 후 정적 컨텐츠 파일을 읽어서 그 응답 본체를 클라이언트에 보낸다.

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");
  sprintf(buf, "%sContent-Length : %d\r\n", buf, filesize);
  sprintf(buf, "%sContent-Type : %s\r\n", buf, filetype);

  // 응답 라인과 헤더를 클라이언트에게 보낸다.
  Rio_writen(fd, buf, strlen(buf)); // connfd를 통해서 clientfd에게 보낸다.
  printf("Response header:\n"); // 서버에서도 출력
  printf("%s", buf);

  // 클라이언트에게 응답 바디 보내기
  srcfd = Open(filename, O_RDONLY, 0);  // filename의 이름을 갖는 파일을 읽기 권한으로 불러온다.
  srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); // 메모리에 파일 내용을 동적 할당한다.
  Close(srcfd); // 파일을 닫는다.
  Rio_writen(fd, srcp, filesize); // 해당 메모리에 있는 파일 내용들을 fd에 보낸다.(=읽는다)
  Munmap(srcp, filesize);
}

 

get_filetype()

filename을 조사해서 각각의 식별자에 맞는 MIME 타입을 filetype에 입력해준다. 이 함수는 response header의 Content-Type에 필요하다.

// Response header의 Conten-Type에 필요한 함수로, filename을 조사해서 각각의 식별자에 맞는 MIME 타입을 filetype에 입력해준다.
void get_filetype(char *filename, char *filetype) {
  if (strstr(filename, ".html")) {
    strcpy(filename, "text/html");
  } else if (strstr(filename, ".gif")) {
    strcpy(filename, "image/gif");
  } else if (strstr(filename, ".png")) {
    strcpy(filename, "iamge/png");
  } else if (strstr(filename, "jpg")) {
    strcpy(filename, "image/jpeg");
  } else {
    strcpy(filename, "text/plain");
  }
}

 

serve_dynamic()

클라이언트가 원하는 동적 컨텐츠 디렉토리를 받아온다. 응답 라인과 헤더를 작성하고, 서버에게 보낸다. CGI 자식 프로세스를 fork하고, 그 프로세스의 표준 출력을 클라이언트 출력과 연결한다.

// 클라이언트가 원하는 동적 컨텐츠 디렉토리를 받아온다. 응답 라인과 헤더를 작성하고 서버에게 보내면, CGI 자식 프로세스를 fork하고, 그 프로세스의 표준 출력을 클라이언트 출력과 연결한다.
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));

  if (Fork() == 0) {
    setenv("QUERY_STRING", cgiargs, 1);

    // 클라이언트의 표준 출력을 CGI 프로그램의 표준 출력과 연결한다. 따라서 앞으로 CGI 프로그램에서 printf하면 클라이언트에서 출력된다.
    Dup2(fd, STDOUT_FILENO);
    Execve(filename, emptylist, environ);
  }
  Wait(NULL);
}

server_dynamic의 로직을 하나하나씩 이해해보자.

  • 우선 fork()를 실행하면, 부모 프로세스와 자식 프로세스가 동시에 실행된다.
    • 만약에 fork()의 반환값이 0이라면. 즉, 자식 프로세스라면 if문을 수행한다.
    • 만약에 fork()의 반환값이 0이 아니라면. 즉, 부모 프로세스라면 if문을 건너뛰고, Wait(NULL)함수로 간다. 이 함수는 부모 프로세스가 먼저 도달해도 자식 프로세스가 종료될 때까지 기다리는 함수이다.
    • if문 안에서 setenv 시스템 콜을 수행해서 "QUERY_STRING"의 값을 cgiargs로 바꿔준다. 우선 순위가 1이므로 기존의 값과 상관없이 값이 변경된다.
    • Dup2 함수를 실행해서 CGI 프로세스의 표준 출력을 fd(서버 연결 소켓 식별자)로 복사한다. 이제 STDOUT_FILENO의 값은 fd이다. 다시말해서 CGI 프로세스에서 표준 출력을 하면 그게 서버 연결 식별자를 거쳐 클라이언트에 출력된다.
    • Execve 함수를 이용해서 파일 이름이 filename인 파일을 실행한다.

/cgi-bin/adder.c

 

숙제 11.7

MPG 비디오 파일을 처리하도록 하라. => get_filetype()함수에서 mp4 파일 형식에 대응하는 MIME 타입을 만들어준다.

 

숙제 11.9

Tiny를 수정해서 정적 컨텐츠를 처리할 때 요청한 파일을 mmap과 rip_readn 대신에 malloc, rio_readn, rie_writen을 사용해서 연결 식별자에게 복사하도록 하라.

server_static()

기존의 파일의 메모리(데이터)를 그대로 가상 메모리에 매핑하는 mmap()과 달리, 먼저 파일의 크기만큼 메모리를 동적 할당 해준 뒤에 rio_readn을 사용해서 파일의 데이터를 메모리로 읽어와야 한다.

 

728x90