sw사관학교정글

[week07] 웹 서버 만들기 (tiny server)

D cron 2021. 12. 27. 01:06

웹 서버

웹 기초

웹 클라이언트와 서버는 HTTP라고 하는 프로토콜을 사용해서 상호 연동된다.
웹 클라이언트(브라우저)는 서버로의 인터넷 연결을 오픈하고 컨텐츠를 요청한다. 서버는 요청한 컨텐츠로 응답하고, 그 후에 연결을 닫아준다. 브라우저는 컨텐츠를 읽고 이것을 스크린에 보여준다.


웹 컨텐츠는 HTML이라는 언어로 작성될 수 있다. HTML 프로그램(페이지)는 명령들(태그)을 포함하고 있어서 브라우저에게 여러 가지 텍스트와 그래픽 객체를 페이지에 어떻게 표시할지를 알려준다.


HTML의 진정한 강점은 페이지가 인터넷 호스트에 저장된 컨텐츠로의 포인터(하이퍼링크)를 포함할 수 있다는 것이다.

웹 컨텐츠

웹 클라이언트(브라우저)와 서버에게, 컨텐츠는 연관된 MIME(Multipurpose Internet Mail Extensions) 타입을 갖는 바이트 배열이다.


웹 서버는 두 가지 방법으로 클라이언트에게 컨텐츠를 제공한다.


  1. 정적 컨텐츠
    디스크 파일을 가져와서 그 내용을 클라이언트에게 보낸다. 디스크 파일은 정적 컨텐츠라고 하며, 파일을 클라이언트에게 돌려주는 작업은 정적 컨텐츠(static content)를 처리한다고 말한다.

  2. 동적 컨텐츠
    실행파일을 돌리고, 그 출력을 클라이언트에게 보낸다. 실행파일이 런타임에 만든 출력을 동적 컨텐츠(dynamic content)라고 하며, 프로그램을 실행하고 그 결과를 클라이언트에게 보내주는 과정을 동적 컨텐츠를 처리한다고 말한다.

웹 서버가 리턴하는 모든 내용들은 서버가 관리하는 파일에 연관된다. 이 파일 각각은 URL(Universal Resource Locator)라고 하는 고유의 이름을 가진다.


http://www.google.com:80/index.html
위와 같은 URL에서 port 80에서 듣고있는 웹 서버가 관리하는 인터넷 호스트 www.google.com의 /intex.html이라는 HTML 파일을 지정한다.
실행파일을 위한 URL은 파일 이름 뒤에 프로그램의 인자를 포함할 수 있다. '?'문자는 파일 이름과 인자를 구분하며, 각 인자는 '&'로 구분된다.


http://jungle.com:8000/cgi-bin/adder?7&3
위의 URL은 /cgi-bin/adder라는 실행파일을 식별하고, 파일은 7과3의 인자와 함께 호출된다.


클라이언트와 서버는 트랜잭션 동안에 URL의 서로 다른 부분을 사용한다.
클라이언트는 다음과 같이 접두어를 사용해서 http://www.google.com:80 여기서 어떤 종류의 서버에 접속해야 하는지 결정하고, 어디에 서버가 있는지, 서버가 무슨 포트를 듣고 있는지를 결정한다.


서버는 다음과 같은 접미어를 사용해서
/index.html
자신의 파일 시스템 상의 파일을 검색하고, 이 요청이 정적 또는 동적 컨텐츠에 대한 것인지 결정한다.


URL이 정적 또는 동적 컨텐츠를 참조하는지 결정하기 위한 표준 규칙은 없다. 고전적인(구식의) 접근 방법은 cgi-bin같은 디렉토리 집합을 지정하는 것이며, 여기에 모든 실행파일들이 들어 있어야 한다(우리의 tiny server는 이 규칙을 적용할 것이다).


HTTP 요청
요청 라인은 다음과 같은 형태를 갖는다.


method URI version


HTTP는 많은 method를 지원하며 여기에는 GET, POST, HEAD, PUT 등이 포함되어 있다. 우리는 여기서 가장 많이 이용되는 GET method에 대해서만 논의한다.

GET method는 서버에게 URI(Uniform Resource Identifier)에 의해 식별되는 내용을 리턴할 것을 지시한다.


HTTP 응답
HTTP 응답은 HTTP 요청과 비슷하다. 응답 라인은 다음과 같은 형태를 가진다.


version status-code status-message


version은 응답이 준수해야 할 HTTP버전을 설명한다.
status-code는 3bit 양수로 요청의 특성을 나타낸다(200, 404등).

동적 컨텐츠의 처리

어떻게 클라이언트가 프로그램의 인자들을 서버에 전달하는가?
이 질문에 대한 대답은 CGI(Common Gateway Interface)라고 부르는 사실상의 표준으로 설명할 수 있다.


어떻게 클라이언트는 프로그램 인자들을 서버에 전달하는가?
GET 요청을 위한 인자들은 URI에서 전달된다. '?'문자는 파일 이름과 인자를 구분하며, 각 인자는 '&'문자로 구분한다. 빈칸은 '%20'으로 표시한다.


어떻게 서버는 인자들을 자식으로 전달하는가?
서버가 다음과 같은 요청을 받은 후에


GET /cgi-bin/adder?15000&213 HTTP/1.1


fork를(system call) 호출해서 자식 프로세스를 생성하고 execve를 호출해서 /cgi-bin/adder 프로그램을 자식의 컨텍스트에서 실행한다. execve를 호출하기 전에 자식 프로세스는 CGI 환경변수 QUERY_STRING을 "15000&213"으로 설정하고, adder 프로그램은 런 타임에 리눅스 getenv 함수를 사용해서 이 값을 참조가능하다.


자식은 자신의 출력을 어디로 보내는가?
CGI 프로그램은 자신의 동적 컨텐츠를 표준 출력으로 보낸다. 자식 프로세스가 CGI 프로그램을 로드하고 실행하기 전에 리눅스 dup2함수를 사용해서 표준 출력을 클라이언트와 연계된 연결 식별자로 재지정한다. 이를 통해서 CGI 프로그램이 표준 출력으로 쓰는 모든것은 클라이언트로 직접 가게 된다.

부모가 자식이 생성한 컨텐츠의 종류와 크기를 알지 못하기 때문에 자식은 content-type과 content-length 응답 헤더와 헤더를 종료하는 빈 줄까지 생성할 책임이 있다.


tiny web server 설계

우리가 이제까지 배운 것을 가지고 매우 기본적인 기능만 가진 서버를 구현해보자.
실제 서버가 가지는 기능성, 견고성, 보안성은 없지만 이것은 충분히 강력해서 실제 웹 브라우저에 정적 및 동적 컨텐츠를 모두 제공할 수 있다.


아래 자세한 주석을 달아 놓았다.


/* $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"

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, char *method); // @과제11번 수정
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs, char *method); // @과제11번 수정
void clienterror(int fd, char *cause, char *errnum, char *shortmsg,
                 char *longmsg);

int main(int argc, char **argv) {//argc: 인자 개수, argv: 인자 배열
  int listenfd, connfd;
  char hostname[MAXLINE], port[MAXLINE];
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;

  /* Check command line args */
  if (argc != 2) { // 입력 인자 2개가 아니면
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }
  // 듣기 소켓 오픈
  // argv는 main 함수가 받은 각각의 인자들
  // argv[1]은 ./tiny 8000 할때 8000 (우리가 부여하는 port번호), 듣기 식별자 return
  printf("현재 port(argv[1]):%s\n",argv[1]);
  listenfd = Open_listenfd(argv[1]);// 8000port에 연결 요청을 받을 준비가 된 듣기 식별자 return
  while (1) { 
    clientlen = sizeof(clientaddr);
    // 서버는 accept함수를 호출해서 클라이언트로부터의 연결 요청을 기다린다.
    // client 소켓은 server 소켓의 주소를 알고 있으니까 
    // client에서 server로 넘어올 때 add정보를 가지고 올 것이라고 가정
    // accept 함수는 연결되면 식별자 connfd를 return한다.
    connfd = Accept(listenfd, (SA *)&clientaddr, // 듣기식별자, 소켓 주소 구조체의 주소, 주소(소켓 구조체)
                    &clientlen);
    printf("connfd:%d\n",connfd); //connfd 확인용
    //clientaddr의 구조체에 대응되는 hostname, port를 작성한다.=
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE,
                0);
    printf("Accepted connection from (%s, %s)\n", hostname, port); //어떤 client가 들어왔는지 알려줌
    doit(connfd);   // connfd로 트랜잭션 수행
    Close(connfd);  // connfd로 자신쪽의 연결 끝 닫기
  }
}
// 클라이언트의 요청 라인을 확인해 정적, 동적 콘텐츠를 확인하고 돌려줌
void doit(int fd) // fd는 connfd라는게 중요함!
{
    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
    // 요청 라인을 읽고 분석한다
    // 식별자 fd를 rio_t 타입의 읽기 버퍼(rio)와 연결
    // 한 개의 빈 버퍼를 설정하고, 이 버퍼와 한 개의 오픈한 파일 식별자를 연결
    Rio_readinitb(&rio, fd); 
    // 다음 텍스트 줄을 파일 rio에서 읽고, 이를 메모리 위치 buf로 복사하고, 텍스트라인을 NULL로 종료시킴
    Rio_readlineb(&rio, buf, MAXLINE);
    printf("Request headers:\n");
    printf("%s",buf); // 요청된 라인을 printf로 보여줌 (최초 요청라인: GET / HTTP/1.1)
    sscanf(buf, "%s %s %s", method, uri, version);//buf의 내용을 method, uri, version이라는 문자열에 저장함
    if (!(strcasecmp(method, "GET") == 0 || strcasecmp(method,"HEAD") == 0)){ // GET,HEAD 메소드만 지원함(두 문자가 같으면 0)
        // 다른 메소드가 들어올 경우, 에러를 출력하고
        clienterror(fd, method, "501", "Not implemented",
                    "Tiny does not implement this method");
        return; // main으로 return시킴
    }

    read_requesthdrs(&rio); // 요청 라인을 제외한 요청 헤더를 출력함

    // Parse URI from GET request
    // URI를 filename과 CGI argument string으로 parse하고 
    // request가 static인지 dynamic인지 확인하는 flag를 return한다.(1이면 static)
    is_static = parse_uri(uri, filename, cgiargs); // uri 내용 바탕으로 filename, cgiargs 채워짐
    if (stat(filename, &sbuf) < 0){ // disk에 파일이 없다면 filename을 sbuf에 넣는다. 종류,크기등등이 sbuf에 저장됨, 성공시 0 실패시 -1
        clienterror(fd, filename, "404", "Not found",
                    "Tiny couldn't find this file");
        return;
    }

    if (is_static){ // Serve static content
        // S_ISREG -> 파일 종류 확인: 일반(regular) 파일인지 판별
        // 읽기 권한(S_IRUSR)을 가지고 있는지 판별
        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); // fd: connfd 정적(static) 컨텐츠를 클라이언트에게 제공
    }
    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); // 실행가능하다면 동적(dynamic) 컨텐츠를 클라이언트에게 제공
    }
}

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

    // Build the HTTP response body
    // sprintf는 출력하는 결과 값을 변수에 저장하게 해주는 기능있음
    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));
}
// Tiny는 request header의 정보를 하나도 사용하지 않는다.
//요청 라인 한줄, 요청 헤더 여러줄 받는데 
//요청 라인은 저장해주고(우리가 tiny에서 필요한 건 이거임), 요청 헤더들은 그냥 출력
void read_requesthdrs(rio_t *rp)
{
    char buf[MAXLINE];

    Rio_readlineb(rp, buf, MAXLINE); //한 줄을 읽어들인다('\n'을 만나면 break되는 식으로)
    //strcmp(str1,str2) 같은 경우 0을 반환 -> 이 경우만 탈출
    // buf가 '\r\n'이 되는 경우는 모든 줄을 읽고 나서 마지막 줄에 도착한 경우이다.
    // 헤더의 마지막 줄은 비어있다

    while(strcmp(buf, "\r\n")){
        Rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf); //한줄 한줄 읽은 것을 출력한다.(최대 MAXLINE읽기가능)
    }
    return;
}
// Tiny는 정적 컨텐츠를 위한 홈 디렉토리가 자신의 현재 디렉토리이고,
// 실행파일의 홈 디렉토리는 /cgi-bin이라고 가정한다.
int parse_uri(char *uri, char *filename, char *cgiargs)
{
    char *ptr;
    // strstr: (대상문자열, 검색할문자열) -> 검색된 문자열(대상 문자열) 뒤에 모든 문자열이 나오게 됨
    // uri에서 "cgi-bin"이라는 문자열이 없으면, static content
    if(!strstr(uri, "cgi-bin")){ // Static content
        strcpy(cgiargs, ""); //cgiargs 인자 string을 지운다.
        strcpy(filename, "."); // 상대 리눅스 경로이름으로 변환 ex) '.'
        strcat(filename, uri); // 상대 리눅스 경로이름으로 변환 ex) '.' + '/index.html'
        if (uri[strlen(uri)-1] == '/') // URI가 '/'문자로 끝난다면
            strcat(filename, "home.html"); // 기본 파일 이름인 home.html을 추가한다. -> 11.10과제 adder.html로 변경
        return 1;
    }
    else{ // Dynamic content (cgi-bin이라는 문자열 존재)
        // 모든 CGI 인자들을 추출한다.
        // index: 첫 번째 인자에서 두번째 인자를 찾는다. 찾으면 문자의 위치 포인터를, 못찾으면 NULL을 반환
        ptr = index(uri, '?');
        if(ptr){
            strcpy(cgiargs, ptr+1);
            *ptr = '\0';
        }
        else // ?없으면 빈칸으로 둘게
            strcpy(cgiargs, "");
        // 나머지 URI 부분을 상대 리눅스 파일이름으로 변환
        strcpy(filename, ".");
        strcat(filename, uri);
        return 0;
        // cgiargs: 123&123
        // filename: ./cgi-bin/adder
    }
}
// static content를 요청하면 서버가 disk에서 파일을 찾아서 메모리 영역으로 복사하고, 복사한 것을 client fd로 복사
void serve_static(int fd, char *filename, int filesize, char *method) // fd는 connfd
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    // Send response headers to client
    get_filetype(filename, filetype); // 5개 중에 무슨 파일형식인지 검사해서 filetype을 채워넣음
    //client에 응답 줄과 헤더를 보낸다.
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n",buf);
    sprintf(buf, "%sConnextion: close\r\n",buf); // while을 한번돌면 close가 되고, 새로 연결하더라도 새로 connect하므로 close가 default가됨
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype); //여기 \r\n 빈줄 하나가 헤더종료표시
    Rio_writen(fd, buf, strlen(buf));// buf에서 strlen(buf) 바이트만큼 fd로 전송한다. buf는 가만히 있고 그 함수안에서 sbuf같은걸 설정해서~.~
    printf("Response headers: \n");
    printf("%s", buf);

    // @과제11번 수정
    if(strcasecmp(method,"HEAD") == 0) //head메소드면 return해서 header값만 보여주게 하라.
        return;
    // Send response body to client
    // open(열려고 하는 대상 파일의 이름, 파일을 열 때 적용되는 열기 옵션, 파일 열 때의 접근 권한 설명)
    // return 파일 디스크립터
    // O_RDONLY : 읽기 전용으로 파일 열기
    // 즉, filename의 파일을 읽기 전용으로 열어서 식별자를 받아온다.
    srcfd = Open(filename, O_RDONLY, 0);

    // 요청한 파일을 disk에서 가상메모리 영역으로 mapping한다.
    // mmap을 호출하면 파일 srcfd의 첫 번째 filesize 바이트를 
    // 주소 srcp에서 시작하는 사적 읽기-허용 가상메모리 영역으로 mapping
    // 대충(말록이랑 유사한데 값도 복사해준다)
    // srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);

    // Mmap대신 malloc 사용 -> 빈칸에 사용해야하므로 빈 공간에 메모리를 읽어야한다.
    srcp = (char*)malloc(filesize);
    Rio_readn(srcfd,srcp,filesize);

    // 파일을 메모리로 매핑한 후에 더 이상 이 식별자는 필요 없으므로 닫기(치명적인 메모리 누수 방지)
    Close(srcfd);
    // 실제로 파일을 client로 전송
    // rio_writen함수는 주소 srcp에서 시작하는 filesize를 클라이언트의 연결 식별자 fd로 복사
    Rio_writen(fd, srcp, filesize);
    // 매핑된 가상메모리 주소를 반환(치명적인 메모리 누수 방지)
    // Munmap(srcp, filesize);
    free(srcp);
}

// get_filetype - Derive file type from filename, Tiny는 5개의 파일형식만 지원한다.
void get_filetype(char *filename, char *filetype)
{
    if(strstr(filename, ".html")) // filename 문자열 안에 ".html"이 있는지 검사
        strcpy(filetype, "text/html");
    else if(strstr(filename, ".gif"))
        strcpy(filetype, "image/gif");
    else if(strstr(filename, ".png"))
        strcpy(filetype, "image/png");
    else if(strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpeg");
    else if(strstr(filename, ".mp4"))
        strcpy(filetype, "video/mp4");
    else    
        strcpy(filetype, "text/plain");
}

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

    if(Fork() == 0){ // 새로운 Child process를 fork한다.
        // 이때 부모 프로세스는 자식의 PID(Process ID)를, 자식 프로세스는 0을 반환받는다.

        // Real server would set all CGI vars here(실제 서버는 여기서 다른 CGI 환경변수도 설정)

        // QUERY_STRING 환경변수를 요청 URI의 CGI 인자들을 넣겠소. 
        // 세 번째 인자는 기존 환경 변수의 유무에 상관없이 값을 변경하겠다면 1, 아니라면 0
        setenv("QUERY_STRING", cgiargs, 1);

        // @과제11번 수정
        // REQUEST_METHOD 환경변수를 요청 URI의 CGI 인자들로 초기화
        setenv("REQUEST_METHOD", method, 1);

        // Dup2함수를 통해 표준 출력을 클라이언트와 연계된 연결 식별자로 재지정 
        //-> CGI 프로그램이 표준 출력으로 쓰는 모든것은 클라이언트로 바로 감(부모프로세스의 간섭 없이)
        Dup2(fd, STDOUT_FILENO); // Redirect stdout to client
        Execve(filename, emptylist, environ); // Run CGI program 실행(adder를 실행)
    } 
    // 자식이 아니면 즉,부모는 자식이 종료되어 정리되는 것을 기다리기 위해 wait 함수에서 블록된다.
    Wait(NULL); // Parent waits for and reaps child

}