[네트워크 프로그래밍 - Tiny 웹 서버 코드 분석]

Junyeong Fred Kim·2021년 12월 21일
3

Tiny Web Server

Tiny라고 부르는 작지만 동작하는 웹서버 입니다. 이는 프로세스 제어, Unix I/O, 소켓, 인터페이스, HTTP와 같은 개념들을 결합하였습니다.

함수

  1. 기초선언
  2. main()
  3. doit()
  4. clienterror()
  5. read_requesthdrs()
  6. parse_uri()
  7. get_filetype()
  8. serve_static()
  9. serve_dynamic()
  10. adder.c

기초선언()

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 *filenmae, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);

1. main()

// 입력 ./tiny 8000 / argc = 2, argv[0] = tiny, argv[1] = 8000
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);
    }

    /* Open_listenfd 함수를 호출해서 듣기 소켓을 오픈한다. 인자로 포트번호를 넘겨준다. */
    // Open_listenfd는 요청받을 준비가된 듣기 식별자를 리턴한다 = listenfd
    listenfd = Open_listenfd(argv[1]);

    /* 전형적인 무한 서버 루프를 실행*/
    while (1) {

        // accept 함수 인자에 넣기 위한 주소 길이를 계산
        clientlen = sizeof(clientaddr);

        /* 반복적으로 연결 요청을 접수 */
        // accept 함수는 1. 듣기 식별자, 2. 소켓주소구조체의 주소, 3. 주소(소켓구조체)의 길이를 인자로 받는다.
        connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);

        // Getaddrinfo는 호스트 이름: 호스트 주소, 서비스 이름: 포트 번호의 스트링 표시를 소켓 주소 구조체로 변환
        // Getnameinfo는 위를 반대로 소켓 주소 구조체에서 스트링 표시로 변환.
        Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);

        /* 트랜젝션을 수행 */
        doit(connfd);

        /* 트랜잭션이 수행된 후 자신 쪽의 연결 끝 (소켓) 을 닫는다. */
        Close(connfd); // 자신 쪽의 연결 끝을 닫는다.
        printf("===============================================\n\n");

    }
}

main 함수의 인자

main 함수에서 첫 번째 매개변수 argc는 옵션의 개수이며, argv는 옵션 문자열의 배열이다.

int main(int argc, char * argv[] (== char **argv) )

It is fundamental to c that char* x and char x[] are two ways of expressing the same thing. Both declare that the parameter receives a pointer to an array of pointers. Apart from the use of sizeof


2. 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];

  // rio_readlineb를 위해 rio_t 타입(구조체)의 읽기 버퍼를 선언
  rio_t rio;

  /* Read request line and headers */
  /* Rio = Robust I/O */
  // rio_t 구조체를 초기화 해준다.
  Rio_readinitb(&rio, fd); // &rio 주소를 가지는 읽기 버퍼와 식별자 connfd를 연결한다.
  Rio_readlineb(&rio, buf, MAXLINE); // 버퍼에서 읽은 것이 담겨있다.
  printf("Request headers:\n");
  printf("%s", buf); // "GET / HTTP/1.1"
  sscanf(buf, "%s %s %s", method, uri, version); // 버퍼에서 자료형을 읽는다, 분석한다.

 if (strcasecmp(method, "GET") && strcasecmp(method, "HEAD")) {
    clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
    return;
  }
  
  /* GET method라면 읽어들이고, 다른 요청 헤더들을 무시한다. */
  read_requesthdrs(&rio);
  
  /* Parse URI form GET request */
  /* URI 를 파일 이름과 비어 있을 수도 있는 CGI 인자 스트링으로 분석하고, 요청이 정적 또는 동적 컨텐츠를 위한 것인지 나타내는 플래그를 설정한다.  */
  is_static = parse_uri(uri, filename, cgiargs);
  printf("uri : %s, filename : %s, cgiargs : %s \n", uri, filename, cgiargs);

  /* 만일 파일이 디스크상에 있지 않으면, 에러메세지를 즉시 클라아언트에게 보내고 메인 루틴으로 리턴 */
  if (stat(filename, &sbuf) < 0) { //stat는 파일 정보를 불러오고 sbuf에 내용을 적어준다. ok 0, errer -1
    clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
    return;
  }

    /* Serve static content */
  if (is_static) {
    // 파일 읽기 권한이 있는지 확인하기
    // S_ISREG : 일반 파일인가? , S_IRUSR: 읽기 권한이 있는지? S_IXUSR 실행권한이 있는가?
    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, method);
  } 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, method);
  }
}

Unix I/O 그리고 RIO (Robust input output) 란

우선, Unix I/O의 개념들

리눅스에서 파일은 연속된 m개의 바이트이다.
네트워크, 디스크, 터미널 같은 모든I/O 디바이스들은 파일로 모델링 된다. 모든 입력과 출력은 해당 파일을 읽거나 쓰는 형식으로 수행된다.

  • 읽기 연산은 현재 파일 위치 k에서 시작해서 n > 0 바이트를 파일에서 메모리로 복사하고 k를 n만큼 증가 시킨다.

k > m (파일의 최대 길이)의 읽기 연산을 수행하면 End-Of-File (EOF)라고 알려진 조건이 발생하며, 이것은 응용 프로그램에서 감지할 수 있다. 하지만 실제 파일 끝에서 "EOF 문자"가 명시적으로 존재하는 것은 아니다.

  • 쓰기 연산은 현재 파일 위치 k에서 시작해서 n > 0 바이트를 메모리에서 파일로 복사하고 k를 갱신시킨다.

리눅스의 파일 타입 혹은 종류

파일의 타입은 여러개로 나뉜다.

  • Regular file : 일반 파일로, 2진수, text 등 데이터를 담고 있다. OS에서 이 파일을 보면, 정확한 포맷은 알지 못한 채, 그저 "일련의 바이트"라고 생각한다고 한다.
  • Directory file : 문서 파일이다. 파일의 이름과 주소를 담고 있다.
  • Character special and block special file : 말그대로 문자/블록 특수 파일이다. 사용자와 프로그램이 하드웨어(ex 키보드)등으로 소통하게 해주는 파일이다. 간단히 설명하자면, 키보드로 입력된 입력값과, 모니터에 나타낼 블록 단위의 값을 담고 있다고 보면 된다.
  • Pipe : 로컬 프로세스 간 커뮤니케이션 용도로 사용된다.
  • Socket : 네트워크 커뮤니케이션 용도로 사용된다.
  • link : 바로가기라고 생각하면 편하다. 원본 파일을 가리키는 포인터의 역할을 한다.

Rio, Robust I/O

Rio 패키지는 짧은 카운트 (short count)를 자동으로 처리한다. (Unix I/O 와 차이).
짧은 카운트가 발생할 수 있는 네트워크 프로그램 같은 어플리케이션에서 편리하고 안정적이고 효율적인 I/O 패키지이다.

짧은 카운트란?

일부의 경우에 Unix I/O의 Read, Write 함수는 어플리케이션이 요청하는 것보다 더 적은 바이트를 전송한다. 이를 short count라고 명시한다. (Short Count는 에러를 나타내는 것은 아니다.)

보통 short count는 다음과 같은 상황일 떄 발생.

  • EOF(End-Of-File)을 읽는 도중 만난 경우.
  • text lines을 터미널로부터 읽어올 때 (예측이 힘듦)
  • 네트워크 소켓 통신시

3. clienterror()

void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
  char buf[MAXLINE], body[MAXBUF];
  /* Build the HTTP response body */
  /* 브라우저 사용자에게 에러를 설명하는 응답 본체에 HTML도 함께 보낸다 */
  /* HTML 응답은 본체에서 컨텐츠의 크기와 타입을 나타내야하기에, HTMl 컨텐츠를 한 개의 스트링으로 만든다. */
  /* 이는 sprintf를 통해 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\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));
}

sprintf는?

#include <stdio.h>

int sprintf(char* str, const char* format, ...);

간단히 설명하자면,
str라는 문자열 배열에 format(형식 문자열)에서 지정한 방식대로 ...에 들어갈 인자들을 넣어준다.


4. read_requesthdrs

/* Tiny는 요청 헤더 내의 어떤 정보도 사용하지 않는다
 * 단순히 이들을 읽고 무시한다. 
 */
void read_requesthdrs(rio_t *rp)
{
  char buf[MAXLINE];

  Rio_readlineb(rp, buf, MAXLINE);
  
  /* strcmp 두 문자열을 비교하는 함수 */
  /* 헤더의 마지막 줄은 비어있기에 \r\n 만 buf에 담겨있다면 while문을 탈출한다.  */
  while (strcmp(buf, "\r\n"))
  {
  	//rio 설명에 나와있다 싶이 rio_readlineb는 \n를 만날때 멈춘다.
    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
    // 멈춘 지점 까지 출력하고 다시 while
  }
  return;
}

👉 요청 헤더에 대한 자세한 설명


5. parse_uri

int parse_uri(char *uri, char *filename, char *cgiargs) {
  char *ptr;
  
  /* strstr 으로 cgi-bin이 들어있는지 확인하고 양수값을 리턴하면 dynamic content를 요구하는 것이기에 조건문을 탈출 */
  if (!strstr(uri, "cgi-bin")) { /* Static content*/
    strcpy(cgiargs, "");
    strcpy(filename, ".");
    strcat(filename, uri);

    //결과 cgiargs = "" 공백 문자열, filename = "./~~ or ./home.html
	  // uri 문자열 끝이 / 일 경우 home.html을 filename에 붙혀준다.
    if (uri[strlen(uri) - 1] == '/') {
      strcat(filename, "home.html");
    }
    return 1;

  } else { /* Dynamic content*/
    // uri 예시: dynamic: /cgi-bin/adder?first=1213&second
    ptr = index(uri, '?');
    // index 함수는 문자열에서 특정 문자의 위치를 반환한다
    // CGI인자 추출
    if (ptr) {
      // 물음표 뒤에 있는 인자 다 갖다 붙인다.
      // 인자로 주어진 값들을 cgiargs 변수에 넣는다.
      strcpy(cgiargs, ptr + 1);
      // 포인터는 문자열 마지막으로 바꾼다.
      *ptr = '\0'; // uri물음표 뒤 다 없애기
    } else {
      strcpy(cgiargs, ""); // 물음표 뒤 인자들 전부 넣기
    }
    strcpy(filename, "."); // 나머지 부분 상대 uri로 바꿈,
    strcat(filename, uri); // ./uri 가 된다.
    return 0;
  }
}

strstr, strcpy, strcat???

1. strstr()

문자열 안에서 문자열로 검색하는 기능

strstr(대상 문자열, 검색할 문자열);

char *strstr(char * const _String, char const * const _SubString);

문자열을 찾았으면 문자열로 시작하는 문자열의 포인터를 반환, 문자열이 없으면 NULL을 반환

2. strcpy()

문자열 복사하기

strcpy(대상 문자열, 원본 문자열);

char *strcpy(char *_Dest, char const *_Source);

대상 문자열의 포인터를 반환

3. strcat()

문자열은 strcat함수를 사용하여 서로 붙일 수 있다.

strcat(최종 문자열, 붙일 문자열);

char *strcat(char *_Destination, char const *_Source);

최종 문자열의 포인터를 반환


6. get_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, ".png")) {
    strcpy(filename, "image/.png");
  } else if (strstr(filename, ".jpg")) {
    strcpy(filetype, "image/jpeg");
  /* 11.7 숙제 문제 - Tiny 가 MPG  비디오 파일을 처리하도록 하기.  */
  } else if (strstr(filename, ".mp4")) {
    strcpy(filetype, "video/mp4");
  } else {
    strcpy(filetype, "text/plain");
  }
}

HTML 서버가 처리할 수 있는 파일 타입을 이 함수를 통해 제공한다.


7. serve_static

void serve_static(int fd, char *filename, int filesize, char *method) {
  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, "%sConnection : close \r\n", buf);
  sprintf(buf, "%sConnect-length : %d \r\n", buf, filesize);
  sprintf(buf, "%sContent-type : %s \r\n\r\n", buf, filetype);
  Rio_writen(fd, buf, strlen(buf));
  //rio_readn은 fd의 현재 파일 위치에서 메모리 위치 usrbuf로 최대 n바이트를 전송한다.
  //rio_writen은 usrfd에서 식별자 fd로 n바이트를 전송한다.
  //서버에 출력
  printf("Response headers : \n");
  printf("%s", buf);

  if (!strcasecmp(method, "HEAD")) {
    return; 
  }
  //읽을 수 있는 파일로 열기
  srcfd = Open(filename, O_RDONLY, 0); //open read only 읽고
  // PROT_READ -> 페이지는 읽을 수만 있다.
  // 파일을 어떤 메모리 공간에 대응시키고 첫주소를 리턴
  // srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); //메모리로 넘기고
  srcp = (char *) Malloc(filesize);
  Rio_readn(srcfd, srcp, filesize);
  // 매핑위치, 매핑시킬 파일의 길이, 메모리 보호정책, 파일공유정책,srcfd ,매핑할때 메모리 오프셋

  Close(srcfd); // 닫기


  Rio_writen(fd, srcp, filesize);
  // mmap() 으로 만들어진 -맵핑을 제거하기 위한 시스템 호출이다
  // 대응시킨 녀석을 풀어준다. 유효하지 않은 메모리로 만듦
  // void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
  // int munmap(void *start, size_t length);
  free(srcp);
//    Munmap(srcp, filesize); //메모리 해제
}

Unix I/O에서 Open 함수는 열고자하는 파일의 식별자 번호를 리턴하는데, 이를 srcfd에 담는다.


8. serve_dynamic

void serve_dynamic(int fd, char *filename, char *cgiargs, char *method) {
  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));
  //fd는 정보를 받자마자 전송하나요???
  //클라이언트는 성공을 알려주는 응답라인을 보내는 것으로 시작한다.
  if (Fork() == 0) { //타이니는 자식프로세스를 포크하고 동적 컨텐츠를 제공한다.
    setenv("QUERY_STRING", cgiargs, 1);
    setenv("REQUEST_METHOD", method, 1);    // REQUEST_METHOD: GET or POST

    //자식은 QUERY_STRING 환경변수를 요청 uri의 cgi인자로 초기화 한다.  (15000 & 213)
    Dup2(fd, STDOUT_FILENO); //자식은 자식의 표준 출력을 연결 파일 식별자로 재지정하고,

    Execve(filename, emptylist, environ);
    // 그 후에 cgi프로그램을 로드하고 실행한다.
    // 자식은 본인 실행 파일을 호출하기 전 존재하던 파일과, 환경변수들에도 접근할 수 있다.

  }
  Wait(NULL); //부모는 자식이 종료되어 정리되는 것을 기다린다.
}

9. adder.c

/*
 * adder.c - a minimal CGI program that adds two numbers together
 */
/* $begin adder */
#include "csapp.h"

int main(void)
{
  char *buf, *p;
  char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
  int n1 = 0, n2 = 0;

  /* Extract the two arguments */
  if ((buf = getenv("QUERY_STRING")) != NULL)
  {
    p = strchr(buf, '&');
    *p = '\0';
    strcpy(arg1, buf);
    strcpy(arg2, p + 1);
    n1 = atoi(strchr(arg1, '=') + 1);
    n2 = atoi(strchr(arg2, '=') + 1);
  }

  /* Make the response body */
  // content 인자에 html body 를 담는다.
  sprintf(content, "QUERY_STRING=%s", buf);
  sprintf(content, "Welcome to add.com: ");
  sprintf(content, "%sTHE Internet addition portal.\r\n<p>", content);
  sprintf(content, "%sThe answer is: %d + %d = %d\r\n<p>",
          content, n1, n2, n1 + n2);
  sprintf(content, "%sThanks for visiting!\r\n", content);

  /* Generate the HTTP response */
  printf("Connection: close\r\n");
  printf("Content-length: %d\r\n", (int)strlen(content));
  printf("Content-type: text/html\r\n\r\n");
  // 여기까지 가 헤더

  // 여기부터 바디 출력
  printf("%s", content);
  fflush(stdout);

  exit(0);
}
/* $end adder */
profile
기억보다 기록

0개의 댓글