[Week07] tiny.c

ella·2023년 4월 16일
0

🌳정글 6기🌳

목록 보기
7/39

tiny.c는 내가 직접 만들어보는 작고 소중한 서버다. tiny.c 코드는 CS:APP책에 모두 적혀있으며, 해당 코드에 관련 설명을 첨부하면서 정리하려고 한다.

tiny.c 찐 통 코드

/* $begin tinymain */
/*
 * tiny.c - A simple, iterative HTTP/1.0 Web server that uses the
 *     GET method to serve static and dynamic content.
 *
 * Updated 11/2019 droh
 *   - Fixed sprintf() aliasing issue in serve_static(), and clienterror().
 */
#include "csapp.h"
#include "stdio.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
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);

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

//client의 http요청을 처리하는 함수
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;

   /* Read request line and headers */
  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);

  /* Parse URI from GET request */
  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)
  { /* Serve static content */

    //일반파일이 아니거나, 파일 읽기권한이 없는 경우 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 실행
    serve_static(fd, filename, sbuf.st_size);
  }
  //동적 컨텐츠를 요청 받았다면, 
  else
  { /* Serve dynamic content */
    // S_IXUSR는 사용자(user)에 대한 실행 권한을 나타내는 상수
    // st_mode는 stat 구조체에서 파일의 모드를 나타내는 필드
    // 일반 파일이 아니거나, 실행가능한 파일이 아니거나, 실행 권한이 없는 경우 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);
  }
}

// HTTP 클라이언트에게 오류 응답을 보내기 위한 함수.
void clienterror(int fd, char *cause, char *errnum, char *shortmsg,char *longmsg){
  char buf[MAXLINE], body[MAXBUF];

  /* Build the 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</p>\r\n", body, longmsg, cause);
  sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body); 

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



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

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

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

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

  /* Send response headers to client */
  get_filetype(filename, filetype);
  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  sprintf(buf, "%sServer: Tiny Web Server\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);

  /* Send response body to client */
  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);
}

void get_filetype(char *filename, char *filetype)
{
  if (strstr(filename, ".html"))
    strcpy(filetype, "text/html");
  else if (strstr(filename, ".gif"))
    strcpy(filetype, "image/gif");
  else if (strstr(filename, ".jpg"))
    strcpy(filetype, "image/jpeg");
  else if (strstr(filename, ".png"))
    strcpy(filetype, "image/png");
  else if (strstr(filename, ".css"))
    strcpy(filetype, "text/css");
  else if (strstr(filename, ".js"))
    strcpy(filetype, "application/javascript");
  else if (strstr(filename, ".ico"))
    strcpy(filetype, "image/x-icon");
  else
    strcpy(filetype, "text/plain");
}

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)
  { /* child */
    /* Real server would set all CGI vars here */
    setenv("QUERY_STRING", cgiargs, 1);
    Dup2(fd, STDOUT_FILENO);              /* Redirect stdout to client */
    Execve(filename, emptylist, environ); /* Run CGI program */
  }
  Wait(NULL); /* Parent waits for and reaps child */
}

헤더파일 및 함수 원형선언

우리가 정의해야할 함수에 대해 먼저 선언해준다.

#include "csapp.h"
#include "stdio.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
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);

int main(int argc, char **argv)

설명

위에 있는 그림을 참고해서보면, server의 기능들은 모두 int main()함수에 구현되어있다.
getaddrinfo(),socket(),bind(),listen()함수는 open_listenfd()함수 안에 구현되어있어 순차적으로 실행된다. while문안에는 accept()함수와 doit()함수가 포함되어 있다. doit()함수는 rio_readlineb, rio_writen, rio_readlineb()함수를 포함하고 있어 client단에서 원하는 서비스를 제공한다. 서비스 제공이 완료되면 close()함수가 실행되고, 다시 client에서 요청이 들어올때까지 대기를 기다린다.

해당 main함수의 코드는 아래와 같다.

코드

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

매개변수: argc, * *argv

main함수의 인자인 argc는 커맨드 라인 인자의 개수를 나타낸다. char '**argv' 는 커맨드 라인 인자들을 가리키는 포인터 배열이다. 예를들어, 다음과 같이 커멘드 명령을 내였을 경우 argc=2이며, argv[0] = "./tiny" 이라는 문자열이고, argv[1] = 8000 이 된다.

$ ./tiny 8000

이를 통해, tiny프로그램을 실행시킬때 인자값으로 http 연결에 필요한 포트 번호를 받는다. 이 때, 포트번호는 서버 소프트웨어가 수신 대기할 포트를 지정하는 데 사용되며, 클라이언트 소프트웨어는 이 포트 번호를 사용하여 서버와 연결을 설정한다.

사용된 변수들

int listenfd

소켓을 생성하고 바인딩한 후, 클라이언트의 연결 요청을 대기하기 위한 파일 디스크립터이다.

int connfd

클라이언트와의 통신을 위한 파일 디스크립터다. 클라이언트의 연결 요청을 accept() 함수로 받아들인 후, connfd에 할당된다.

char hostname[MAXLINE], port[MAXLINE]

클라이언트와의 연결이 수립되면, 클라이언트의 호스트 이름과 포트 번호를 저장하기 위해 사용한다.
MAXLINE의 정의를 찾아보면('#define MAXLINE 8192 / Max text line length / )이름과 포트의 배열길이 최대를 정의하는 것을 알 수 있다.

socklen_t clientlen

socklen_t는 sys/socket.h 헤더 파일에 정의된 데이터 타입으로, 일반적으로 소켓 주소를 저장하는 데 사용된다. 따라서 client 변수는 클라이언트에게서 받은 주소 구조체의 크기를 저장하는 변수이다.

struct sockaddr_storage clientaddr


CS:APP을 참고해서 보면, 일반적인 socket struct를 그림과 같다. socket은 커널의 관점에서 보자면 통신을 위한 종점이다. socket을 추상화를 해보자면, 편지(파일)을 받을 수신자이며, 수신자에 관한 수신주소 등이 써있는 정보덩어리를 struct sockarr라고 보면 되겠다.

구조체의 일반적인 내용은 sin_family, sin_port, sin_addr, sin_zero로 이루어져 있다.
sin_family 필드로 AF_INT의 값이 들어가면, IPv4을 나타낸다. sin_port 필드는 16비트 포트 번호이며, sin_addr 필드에는 32비트 IP 주소가 포함되며, IP 주소와 포트 번호는 항상 네트워크 (빅 엔디언) 바이트 순서로 저장된다.

정리하자면, 해당 변수는 클라이언트의 소켓 주소를 저장하기 위해 사용된다.

코드 설명

if (argc != 2) {}

입력이 './tiny 8080'처럼 2개의 argc가 입력이 되지 않았다면, 포트 번호가 전달되지 않은 것이다. 따라서 프로그램 사용법을 출력하고(exit(1)), 프로그램을 종료한다.

  • fprintf()는 파일 스트림으로 출력할 때 사용하는 함수이다. fprintf(fp, "The value of x is %d\n", x)는 파일 포인터 fp가 가리키는 파일에 문자열과 변수 x의 값을 출력한다.

listenfd = Open_listenfd(argv[1]);

Open_listenfd(argv[1])는 argv[1] 포트를 열어서 들어오는 연결 요청을 수신할 수 있는 리스닝 소켓 파일 디스크립터를 반환하는 함수다. open_listenfd 함수는 socket, setsockopt, bind, listen 함수를 순서대로 호출하여 리스닝 소켓을 생성하고 초기화한다.
(ps: 아래에 좀 더 자세한 설명을 첨부해 놓았다.)

while (1){}문

  • clientlen = sizeof(clientaddr)
    : clientaddr 구조체의 크기를 바이트 단위로 반환한다.

  • connfd = Accept(listenfd, (SA *)&clientaddr,&clientlen);
    : 여기서 사용되는 accept() 함수는 시스템 콜함수 중 하나로 <sys/socket.h>에 선언되어있다. 이 함수가 호출되면, 새로운 소켓 디스크립터가 생성되고, 이 소켓 디스크립터는 클라이언트와의 통신에 사용된다. 이 함수의 성공적인 호출 이후에는, 서버 소켓은 여전히 클라이언트의 연결 요청을 수락할 수 있는 상태를 유지한다.

  • Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE,0);
    : 리눅스에서 제공하는 강력한 함수로, getaddringo의 역이다. 해당 함수는 소켓 주소 구조체를 대응되는 호스트와 서비스 이름 스트링으로 변환한다.

  • printf("Accepted connection from (%s, %s)\n", hostname, port);
    : 연결이 되었음을 알리기위해, hostname과 port를 출력한다.

  • doit(connfd);
    : 클라이언트와의 데이터 통신을 수행한다. (ps: 아래에 좀 더 자세한 설명을 첨부해 놓았다.)

  • Close(connfd);
    :close() 함수는 파일 디스크립터를 닫는 함수다. unistd.h 헤더 파일에 선언되어 있으며, 열린 파일 또는 소켓과 같은 리소스를 반환하고, 다른 프로세스나 쓰레드가 해당 파일에 접근하지 못하게 한다.


아래에는 main함수에 나오는 int open_listenfd()함수에 대해 좀 더 자세히 정리해 놓았다.

int open_listenfd(char *port)

해당 함수는 csapp.c 파일에 구현되어 있으며, 앞서 말했듯이 getaddrinfo, socket, bind, listen 기능을 순차적으로 실행하는 함수이다. 성공시에는 listenfd를 반환하며, 실패시 -1값을 반환한다. 해당 함수를 한줄한줄 자세하게 공부해 보자.


여기서! getaddrinfo()라는 함수가 사용되는데 해당 함수는 리눅스에서 제공하는 강력한 함수로, 소켓 주소 구조체들과 호스트 이름, 호스트 주소, 서비스 이름, 포트번호를 소켓 주소 구조체로 변환해 준다.


여기서! socket(), setsockopt(), bind(), listen() 함수는 리눅스 시스템 콜(System Call) 중 하나이다. 이 함수들은 시스템 콜 라이브러리에 정의되어 있고, unistd.h 헤더 파일에 선언되어있다.

코드

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

사용된 변수들

struct addrinfo는 주소정보를 저장하기 위한 구조체이다. 해당 구조체는 아래와 같이 구성되어 있다.

struct addrinfo {
    int              ai_flags;     // hint flags
    int              ai_family;    // address family
    int              ai_socktype;  // socket type
    int              ai_protocol;  // protocol type
    size_t           ai_addrlen;   // length of address
    struct sockaddr *ai_addr;      // address
    char            *ai_canonname; // canonical name of host
    struct addrinfo *ai_next;      // pointer to next addrinfo struct
};

'hints': 리눅스가 제공하는 기능인 getaddrinfo()가 사용할 주소 정보의 힌트를 담은 'addrinfo' 구조체 변수이다.
'listp': getaddrinfo()가 주소 정보를 반환할 때 사용되는 연결리스트의 첫번째 노드를 가리키는 포인터이다.
'p': 주소 정보를 순회하면서 각각의 노드를 가리키는 포인터이다.

'int listenfd'는 'open_listenfd' 함수가 반환하는 리스닝 소켓 디스크립터, 즉 결과값이다.
'int rc'는 getaddrinfo()를 호출할 때 반환되는 오류 코드이다.
'int optval': setsockopt 함수를 호출할 때 사용하는 옵션 값이다.

자세한 코드 설명

memset(&hints, 0, sizeof(struct addrinfo));

: 리눅스가 제공하는 기능인 getaddrinfo()를 사용할 때, 말그대로 힌트를 주기위한 구조체, struct addrinfo hints를 만들기 앞서 memset() 함수를 사용하여 구조체 변수의 모든 멤버 변수가 0으로 초기화 한다.

hints.ai_socktype = SOCK_STREAM;

: hints의 ai_socktype은 소켓의 타입을 나타내며, SOCK_STREAM으로 설정되어 있어 TCP 스트림 소켓을 나타낸다.

hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;

:getaddrinfo()가 검색할 소켓 주소 유형에 대한 힌트를 제공한다. AI_PASSIVE 플래그는 소켓 주소가 서버 주소인 것을 나타내고, AI_ADDRCONFIG 플래그는 시스템 구성에 맞는 주소만 반환하도록 지정한다.

hints.ai_flags |= AI_NUMERICSERV;

: getaddrinfo()를 호출할 때 port 인자가 문자열 대신 숫자형태의 포트 번호임을 나타내는 플래그이다. getaddrinfo 함수에서 port 인자를 정수형으로 변환하여 처리할 수 있도록 하기 위함이며, 만약 이 플래그를 설정하지 않으면 port 인자가 문자열일 경우에만 처리할 수 있다.

if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {...}

getaddrinfo 함수를 사용하려면 매개변수로 getaddrinfo(node, service, hints, res)를 전달한다.

  • node: 호스트명 또는 IP 주소값이다. 여기서 NULL을 전달하면 로컬 호스트에 대한 주소를 얻게된다.
  • service: 서비스명 또는 포트 번호를 넣는다. 만약 service에 NULL을 전달하면 "0"이 사용된다.
  • hints: getaddrinfo() 함수에서 어떤 종류의 소켓을 생성할 것인지에 대한 힌트다.
  • res: getaddrinfo() 함수가 호출되고 결과를 담을 매개변수다.

getaddrinfo()함수는 반환값으로 해당 포트 번호에 대한 대상 주소를 찾아 listp에 저장한다.
대상 주소를 찾을 수 없으면 getaddrinfo() 함수는 비음수 정수 값을 반환하고, 해당 if문의 {}문이 실행되며 오류를 출력하고 -2 값을 반환한다.

for (p = listp; p; p = p->ai_next){...}

getaddrinfo 함수를 통해 가져온 서버 주소 리스트를 하나씩 탐색하여, socket()를 사용하여 서버 소켓을 생성하고 바인딩(bind)하는 코드다.
첫번째 if문에서는 소켓 생성에 실패하면 socket() 반환값을 담은 listenfd이 음수가 되며, cotinue문을 통해 다음 주소를 탐색한다.
다음 줄에서는 소켓 생성이 성공했을 때, setsockopt 함수를 사용하여 SO_REUSEADDR 옵션을 설정한다. 이 옵션은 TIME_WAIT이 아닌 상태에서 포트를 재사용할 수 있게 한다.
다음 if문에서는 bind()함수를 호출하여 서버 소켓을 해당 주소에 바인딩한다. 이때, 바인딩이란, 소켓(socket)을 특정 IP 주소와 포트 번호에 연결하는 과정을 말한다. 바인딩에 성공하면 bind()는 0을 반환하며 이어서 break 문으로 루프를 빠져나가고 다음 freeaddrinfo()함수를 실행한다. 바인딩에 실패하면 생성된 소켓을 close 함수로 닫고 에러 문구를 파일에 출력하며 open_listenfd()함수를 -1을 반환하며 종료한다.

freeaddrinfo(listp);

: <sys/socket.h> 헤더 파일과 <netdb.h> 헤더 파일에 선언되어 있으며, getaddrinfo() 함수를 통해 할당한 addrinfo 구조체 메모리를 해제하는 역할을 한다.

if (!p) return -1;

: getaddrinfo()를 통해 생성한 서버 주소 리스트의 끝에 도달했을 때,p는 NULL을 가진다. 따라서, 끝가지 탐색했는데도 바인딩에 성공하지 못했다면 -1값을 반환하고 open_listenfd()함수를 종료한다.

if (listen(listenfd, LISTENQ) < 0){...}

:listen() 함수는 서버 측 소켓에서 클라이언트의 연결 요청을 수신할 수 있도록 소켓을 대기 상태로 만든다. 이 함수가 호출될 때, 대기 큐의 길이도 함께 지정된다. LISTENQ는 대기 큐의 길이로 정의된다.
listen() 함수가 실패하면, listenfd를 닫고 -1을 반환한다.

return listenfd;

: 모든 조건이 정상적으로 작동했다면 listenfd를 반환하고 open_listenfd()함수를 종료한다.


doit(int fd)

client의 http요청을 처리하는 함수이다.
Rio_readlineb()함수, read_requesethdrs()함수, parse_uri()함수,stat()함수 등을 이용해 client의 요청 정보, 파일 정보들을 추출하고 그에 따른 오류 처리나 정적,동적 컨텐츠 서비스를 제공하는 함수다.

코드

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;

   /* Read request line and headers */
  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);

  /* Parse URI from GET request */
  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)
  { /* Serve static content */
    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
  { /* Serve dynamic content */
    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);
  }
}

사용된 변수들

  • int is_static;
    :요청된 URI가 정적 컨텐츠인지 동적 컨텐츠인지 여부를 저장하는 변수

  • struct stat sbuf;
    :파일 상태 정보를 저장하기 위한 구조체

  • char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    : doit() 함수의 지역 변수(local variable)로서, 요청 메시지를 파싱(parsing)할 때 사용된다.

    • buf : 클라이언트로부터 받은 요청 메시지(request message)를 저장하는 버퍼
    • method : HTTP 요청 메시지의 method를 저장하는 문자열 변수
    • uri : HTTP 요청 메시지의 URI(uniform resource identifier)를 저장하는 문자열 변수
    • version : HTTP 요청 메시지의 version을 저장하는 문자열 변수
  • char filename[MAXLINE], cgiargs[MAXLINE];
    :

    • filename: 클라이언트가 요청한 파일의 경로를 저장하는 배열
    • cgiargs: CGI 프로그램에 전달되는 인수를 저장하는 배열
  • rio_t rio;
    : Robust I/O(RIO) 패키지에서 사용되는 I/O 버퍼링 및 오류 처리를 담당하는 구조체

자세한 코드 설명

Rio_readinitb(&rio, fd);

rio를 초기화하는 함수다. rio_t 구조체와 파일 디스크립터 fd를 인자로 받는다. rio_t 구조체의 rio_bufptr, rio_cnt, rio_fd 멤버 변수를 0, 0, fd로 초기화하여, Rio_readlineb()함수를 사용하기전 준비를 한다.

Rio_readlineb(&rio, buf, MAXLINE);

: 파일 디스크립터에서 한 줄씩 읽는 동작을 하므로, HTTP 요청의 경우, 요청 라인이나 헤더 필드를 처리할 때 사용된다.
요청라인의 예시는 "GET /index.html HTTP/1.1\r\n" 와 같으며 해당 값을 읽어서 buf에 저장하는 역할을 수행한다. (해당 코드는 csapp.c에 정의되어있다.)

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen) 
{
    int n, rc;
    char c, *bufp = usrbuf;

    for (n = 1; n < maxlen; n++) { 
        if ((rc = rio_read(rp, &c, 1)) == 1) {
	    *bufp++ = c;
	    if (c == '\n') {
                n++;
     		break;
            }
	} else if (rc == 0) {
	    if (n == 1)
		return 0; /* EOF, no data read */
	    else
		break;    /* EOF, some data was read */
	} else
	    return -1;	  /* Error */
    }
    *bufp = 0;
    return n-1;
}

printf("Request headers:\n"); printf("%s", buf);

Rio_readlineb()함수를 통해 읽어드린 request headers 내용을 출력한다.

sscanf(buf, "%s %s %s", method, uri, version);

HTTP 요청 라인에서 method, URI, HTTP 버전을 추출하기 위해 buf 문자열에서 공백으로 구분된 문자열 3개를 추출하여 변수 method, uri, version에 저장한다.
클라이언트에서 "GET /index.html HTTP/1.1"과 같은 요청 라인을 보내면 buf는 "GET /index.html HTTP/1.1\r\n"과 같은 문자열을 가지고 있을 것이다. sscanf() 함수는 buf 문자열에서 3개의 공백으로 분리된 문자열을 추출하여 method에는 GET, uri에는 index.html, version에는 HTTP/1.1를 각 변수에 저장한다.

if (strcasecmp(method, "GET")){...}

strcasecmp() 함수: 두 개의 문자열을 대소문자를 구분하지 않고 비교하는 함수, 같으면 0. 0이 아닐 때는 clienterror()함수를 통해 클라이언트에게 501에러 응답 메세지를 보낸다.

read_requesthdrs(&rio);

HTTP 요청 메시지에서 헤더 필드를 읽어들이는 함수다. 예를들어 아래와 같은 client 요청이 있다고 해보자.

GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:87.0) Gecko/20100101 Firefox/87.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

read_requesthdrs()함수는 헤더 필드인 "Host", "User-Agent", "Accept", "Accept-Language", "Accept-Encoding", "Connection", "Upgrade-Insecure-Requests"을 읽어들일 것이다.

is_static = parse_uri(uri, filename, cgiargs);

parse_uri()함수는 앞서 sscanf()를 통해 추출한 URI 문자열에서 파일이름과 CGI인자를 추출한다.
만약 URI가 정적 콘텐츠를 가리키는 경우, is_static을 1로 설정하고 파일 이름만 추출한다. 만약 URI가 동적 콘텐츠를 가리키는 경우, is_static을 0으로 설정하고 파일 이름과 CGI 인자를 추출한다.
예를 들어, URI가 /home.html인 경우 is_static은 1로 설정되며 filename은 ./home.html이 된다. 반면에 URI가 /cgi-bin/adder?1&2인 경우 is_static은 0으로 설정되며 filename은 ./cgi-bin/adder이고 cgiargs는 1&2다.

** CGI는 Common Gateway Interface의 약어로, 웹 서버와 다른 응용 프로그램이 데이터를 교환하는 방법 중 하나이다. CGI 프로그램은 브라우저가 웹 서버에 요청한 웹 페이지를 만들기 위한 동적인 데이터를 생성한다.

if (stat(filename, &sbuf) < 0){...}

stat()함수도 시스템콜 함수 중 하나다. stat시스템콜은 주어진 파일의 상태 정보를 가져오는 함수다. 파일이 존재하는지 여부, 파일 종류, 파일의 소유자, 생성시간, 마지막 수정시간, 파일 크기 등의 정보를 제공한다. 이렇게 탐색이 성공하면 0을 반환하고, 파일이 존재하지 않으면 404오류를 알리고 함수를 종료한다.

자, 이렇게 앞에서 client의 요청에 대한 데이터 추출이 끝났다. 이제 해당 데이터를 바탕으로 데이터를 생성해서 서비스를 제공해주는 코드를 보도록 하자.

if (is_static)

추출한 client 요청파일이 정적 콘텐츠일 경우, sbuf에 저장된 파일 정보를 기준으로 if와 else문에 대해 판단해서 문제가 없을 시 결과값을 전달한다.

if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)){...}

S_ISREG(sbuf.st_mode) : 파일이 일반 파일 (regular file)인 경우 참
S_IRUSR & sbuf.st_mode : 파일의 소유자가 읽기 권한을 가지고 있는 경우 참
따라서 일반파일이 아니거나, 파일읽기 권한이 없는 경우 403에러를 안내하고 함수를 종료한다.

serve_static(fd, filename, sbuf.st_size);

파일이 일반파일이고, 파일의 읽기권한이 있다면, serve_static()함수를 통해 static컨텐츠 서비스를 제공한다.(관련 함수 내용은 아래에 정리했다.)

else

추출한 client 요청파일이 동적 콘텐츠일 경우, sbuf에 저장된 파일 정보를 기준으로 if와 else문에 대해 판단해서 문제가 없을 시 결과값을 전달한다.

if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)){...}

정적 컨텐츠와 마찬가지로 sbuf에 저장된 파일정보가 일반파일이 아니거나, 파일읽기 권한이 없는 경우 403에러를 안내하고 함수를 종료한다.

serve_dynamic(fd, filename, cgiargs);

일반파일이고, 실행가능하고 권한이 있는 경우, serve_dynamic 실행한다. (관련 함수 내용은 아래에 정리했다.)


clienterror()

HTTP 클라이언트에게 오류 응답을 보내기 위한 함수이다.
이 함수는 먼저 HTTP 응답 헤더를 보내고, 그 다음에 오류 메시지를 보낸다.
여기서 쓰인 Rio_writen() 함수는 소켓 파일 디스크립터 fd에 문자열 buf를 쓰는 함수이다.


sprintf()함수는 출력을 화면에 출력하지 않고, 대신 index0의 변수에 문자열로 저장한다. 반환값은 생성된 문자열의 길이다.


html의 < em > 태그는 텍스트를 강조하기 위해 이탤릭체로 표시한다.


strlen(buf)는 쓰여질 문자열의 길이를 나타낸다.

코드

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

 /* Build the HTTP response body */
 //  sprintf()함수는 출력을 화면에 출력하지 않고, 대신 index0의 변수에 문자열로 저장합니다. 반환값은 생성된 문자열의 길이
 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</p>\r\n", body, longmsg, cause);
 sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body); //<em> 태그는 텍스트를 강조하기 위해 이탤릭체로 표시

 /* Print the HTTP response */
 sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
 // Rio_writen() 함수는 소켓 파일 디스크립터 fd에 문자열 buf를 쓰는 함수입니다. 
 //함수의 세 번째 인자 strlen(buf)는 쓰여질 문자열의 길이를 나타냅니다.
 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));
}

parse_uri(char uri, char filename, char *cgiargs)

parse_uri()함수는 doit()함수에서 URI 문자열에서 파일이름과 CGI인자를 추출함으로써 정적 콘텐츠인지 동적콘텐츠인지를 가리키기위해 사용된다.

URI에 "cgi-bin"이 없으면 정적 컨텐츠를 의미한다. filename은 현재 디렉토리를 나타내는 "."과 URI를 결합하여 파일 이름을 만들고, uri의 끝에 "/"가 있으면 "home.html"로 끝나는 파일을 사용한다.

그러나 URI에 "cgi-bin"이 있으면 동적 컨텐츠를 의미한다. filename은 현재 디렉토리를 나타내는 "."과 URI를 결합하여 파일 이름을 만듭니다. URI에서 '?' 이후에 있는 인자를 cgiargs에 저장하고 '?'를 '\0'로 바꾸어서 filename 문자열이 완성된다.

코드

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

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

serve_static(int fd, char *filename, int filesize)

정적 컨텐츠를 처리하는 함수다.
첫 번째 매개변수 fd는 클라이언트와의 연결을 나타내는 파일 디스크립터다. 두 번째 매개변수 filename은 요청받은 파일의 이름을 가리키는 문자열 포인터다. 세 번째 매개변수 filesize는 요청받은 파일의 크기다.

이 함수는 filename에서 파일의 확장자를 추출하여, 해당 파일이 어떤 타입의 파일인지를 결정한다. 그리고 이 정보를 이용하여 HTTP 응답 헤더를 생성하고 클라이언트에게 전송한다.

그 후, 파일 디스크립터를 열고, 파일의 크기만큼 메모리를 할당하여 파일을 읽는다. 읽어들인 파일 내용을 클라이언트에게 전송한 후, 메모리를 해제하고 파일 디스크립터를 닫는다.

응답 메시지의 형식은 다음과 같다.

makefile
Copy code
HTTP/1.0 200 OK\r\n
Server: Tiny Web Server\r\n
Content-length: filesize\r\n
Content-type: filetype\r\n\r\n

여기서 filesize는 파일 크기이고, filetype은 파일 타입을 나타낸다. Content-type 헤더는 클라이언트에게 보내는 파일의 타입을 알려주는 역할을 한다. Content-length 헤더는 응답 바디의 크기를 나타낸다. 이 헤더가 없으면, 클라이언트는 전체 응답 메시지를 수신하지 못하고 오류가 발생할 수 있다.

코드

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

  /* Send response headers to client */
  get_filetype(filename, filetype);
  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  sprintf(buf, "%sServer: Tiny Web Server\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);

  /* Send response body to client */
  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(char filename, char filetype)

get_filetype 함수는 serve_static()함수나 serve_dynamic()함수에 쓰이며, 파일 확장자를 기반으로 해당 파일의 Content-Type을 결정하는 역할을한다.


함수 인자로 전달된 filename 문자열에서 ".html", ".gif", ".jpg", ".png", ".css", ".js", ".ico" 문자열 중 하나가 포함되어 있다면 해당 파일이 각각 HTML 문서, GIF 이미지, JPEG 이미지, PNG 이미지, CSS 파일, JavaScript 파일, 아이콘 파일임을 인식하고 각각에 맞는 Content-Type을 filetype에 할당한다.


만약 해당 파일이 위의 확장자 중 어느 것에도 해당하지 않는 경우, 일반적인 text/plain 유형으로 filetype을 할당한다.

코드

void get_filetype(char *filename, char *filetype)
{
  if (strstr(filename, ".html"))
    strcpy(filetype, "text/html");
  else if (strstr(filename, ".gif"))
    strcpy(filetype, "image/gif");
  else if (strstr(filename, ".jpg"))
    strcpy(filetype, "image/jpeg");
  else if (strstr(filename, ".png"))
    strcpy(filetype, "image/png");
  else if (strstr(filename, ".css"))
    strcpy(filetype, "text/css");
  else if (strstr(filename, ".js"))
    strcpy(filetype, "application/javascript");
  else if (strstr(filename, ".ico"))
    strcpy(filetype, "image/x-icon");
  else
    strcpy(filetype, "text/plain");
}

serve_dynamic(int fd, char filename, char cgiargs)

동적 콘텐츠를 처리하는 함수다.
동적 컨텐츠를 처리하기 위해서 fork(), setenv(), dup2(), execve(),wait() 함수들이 사용되는 데 이는 모두 유닉스 및 유닉스 기반 시스템에서 사용되는 함수이며, 시스템 콜이다. 이 함수들은 유닉스 시스템콜 인터페이스를 제공하는 헤더 파일인 unistd.h, stdlib.h, stdio.h 에 선언되어 있다.

코드

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)
  { /* child */
    /* Real server would set all CGI vars here */
    setenv("QUERY_STRING", cgiargs, 1);
    Dup2(fd, STDOUT_FILENO);              /* Redirect stdout to client */
    Execve(filename, emptylist, environ); /* Run CGI program */
  }
  Wait(NULL); /* Parent waits for and reaps child */
}

자세한 코드 설명

해당 함수의 인자로 라이언트와 연결된 파일 디스크립터(fd), 실행할 CGI 프로그램의 파일 이름(filename), 그리고 CGI 프로그램에 전달할 인자(cgiargs)를 받는다.

설정변수로는

  • buf: HTTP응답의 첫 부분의 내용을 넣는다.

  • emptyList[]: 문자열 포인터 배열로, Execve() 함수에서 CGI 프로그램에 전달할 인자를 저장한다.

    / Return first part of HTTP response / 구문에서 client에게 전달할 HTTP 응답 헤더를 작성하는 부분이다.
    sprinf()함수를 통해 buf 변수에 응답 헤더를 한줄씩 넣고, Rio_writen()함수를 통해 fd에 buf를 전달한다.

if (Fork() == 0){...}구문에서는 Fork() 함수를 호출하여 자식 프로세스를 생성한다. 자식프로세스는 CGI프로그램을 실행하기 위해 사용된다.
만약 Fork()==0이라면, 자식프로세스가 실행되는 것이고 setenv() 함수를 사용하여 QUERY_STRING 환경 변수를 cgiargs로 설정한다.
Fork() 함수가 0보다 큰 값을 리턴한다면, 즉 부모 프로세스라면, Wait() 함수를 사용하여 자식 프로세스의 종료를 기다린다.

사용된 시스템콜

  • fork() 함수는 새로운 프로세스를 생성하는 시스템 호출입니다. 이 함수가 호출되면 현재 실행 중인 프로세스는 완전히 동일한 프로세스를 하나 더 생성합니다. 이 두 개의 프로세스는 동일한 코드, 데이터, 스택 등을 가지지만, 서로 다른 프로세스 ID(PID)와 부모-자식 프로세스 관계를 가집니다.
  • setenv() 함수는 환경 변수를 설정하는 함수입니다. 환경 변수는 프로세스의 실행 환경에 영향을 미치는 변수로, 예를 들어 PATH나 LD_LIBRARY_PATH 등이 대표적인 환경 변수입니다.
  • dup2() 함수는 파일 디스크립터를 복제하거나 리디렉션하는 시스템 콜입니다. 서버에서 Dup2(fd, STDOUT_FILENO)를 호출하면, 현재 연결된 소켓 파일 디스크립터를 표준 출력으로 리디렉션하게 됩니다. 따라서, 서버의 CGI 프로그램은 표준 출력에 데이터를 출력하면, 연결된 클라이언트 소켓으로 데이터를 보낼 수 있게 됩니다.

이 외에 정리하고 싶은 내용

:

  • 빅 엔디언(Big Endian)은 바이트(byte)를 저장할 때, 가장 높은 자리의 바이트부터 저장하는 방식입니다. 즉, 가장 높은 자리의 바이트가 메모리의 낮은 번지에 저장되고, 그 다음 바이트는 바로 앞 번지에 저장됩니다
    빅 엔디언(Big Endian)은 바이트(byte)를 저장할 때, 가장 높은 자리의 바이트부터 저장하는 방식입니다. 즉, 가장 높은 자리의 바이트가 메모리의 낮은 번지에 저장되고, 그 다음 바이트는 바로 앞 번지에 저장됩니다

  • 파일 디스크립터(file descriptor)는 운영 체제에서 파일이나 소켓 같은 입출력 장치를 다루기 위해 사용되는 정수값입니다.
    일반적으로 0부터 시작하여 연속된 정수값으로 할당됩니다. 각각의 파일 디스크립터는 해당 파일이나 장치에 대한 참조를 나타내며,
    이를 통해 입출력 작업을 수행할 수 있습니다.
    프로그래밍에서 파일 디스크립터는 open(), socket()과 같은 시스템 호출을 통해 생성됩니다.
    생성된 파일 디스크립터는 read(), write()와 같은 시스템 호출을 통해 읽기와 쓰기 작업이 수행됩니다.
    파일 디스크립터는 프로그램에서 열린 파일, 네트워크 소켓, 파이프, 터미널, 시리얼 포트 등 다양한 입출력 장치를 나타낼 수 있습니다.

profile
^^*

0개의 댓글