[C 언어] tiny 웹 서버 구현하기

유선·2024년 4월 19일
0

CS

목록 보기
19/25
post-thumbnail

proxy-lab을 위한 사전 과제로, csapp 11장을 토대로 구현하였습니다.

전체 코드

main

int main(int argc, char **argv)

  • HTTP 웹 서버를 시작하고 클라이언트의 연결을 수신하여 처리한다.

  • 매개변수:

    • argc: 인자 개수

    • argv: 인자 배열

    • 인자 예시

      예시 1: 프로그램이 명령행에 아무런 인자도 받지 않는 경우

      $ ./program
      argc: 1
      argv[0]: "./program

      예시 2: 프로그램이 명령행에 포트 번호를 인자로 받는 경우

      $ ./program 8080
      argc: 2
      argv[0]: "./program"
      argv[1]: "8080"

      ⇒ port 번호를 인자로 받는다.

  • 메인함수에 전달되는 정보의 개수가 2개여야함 (argv[0]: 실행경로, argv[1]: port num)

  • source code 👩🏻‍💻

    int main(int argc, char **argv) {
      int listenfd, connfd; // 서버 및 클라이언트 소켓 파일 디스크립터
      char hostname[MAXLINE], port[MAXLINE]; // 클라이언트 호스트네임 및 포트번호
      socklen_t clientlen; // 클라이언트 주소 구조체 크기
      struct sockaddr_storage clientaddr; // 클라이언트 주소 구조체
    
      // 명령행 인수 확인
      if (argc != 2) { // 인수 개수가 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); // 연결 수락
        Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0); // 클라이언트 호스트네임 및 포트번호 추출
        printf("Accepted connection from (%s, %s)\n", hostname, port); // 연결 확인 메시지 출력
        doit(connfd); // 클라이언트 요청 처리 함수 호출
        Close(connfd); // 연결 종료 및 소켓 닫음
      }
    }
  • argc가 2가 아니면 오류 메시지를 출력하고 프로그램을 종료

  • Open_listenfd(argv[1])를 호출하여 주어진 포트 번호에서 클라이언트의 연결 요청을 수신할 수 있는 소켓을 생성하고, 그 소켓의 파일 디스크립터를 listenfd에 저장

    • 소켓 파일 디스크립터
      • 서버 소켓 파일 디스크립터 (listenfd): 서버가 클라이언트의 연결을 수신하는 소켓을 식별하는 데 사용된다. 이 소켓은 Open_listenfd 함수를 통해 생성되며, 클라이언트의 연결 요청을 수락한다.
      • 클라이언트 소켓 파일 디스크립터 (connfd): 서버가 클라이언트와 통신하기 위해 생성하는 각각의 소켓을 식별하는 데 사용된다. 이 소켓은 클라이언트의 연결 요청이 수락되면 생성되며, 클라이언트와의 통신을 담당한다.
  • 무한 루프를 통해 클라이언트의 연결 요청을 지속적으로 대기

  • 클라이언트의 연결 요청이 수락되면 Accept를 호출하여 클라이언트와 통신할 소켓을 생성하고, 그 소켓의 파일 디스크립터를 connfd에 저장

  • 클라이언트의 호스트네임과 포트 번호를 Getnameinfo를 사용하여 추출하고 출력

  • 클라이언트의 요청을 처리하기 위해 doit 함수를 호출

  • 클라이언트와의 연결을 종료하고 할당된 소켓을 닫는다.


doit

void doit(int fd)

  • 클라이언트의 요청을 처리하는 함수이다. 요청을 읽고, 해당하는 작업(정적, 동적)을 수행하여 클라이언트에게 응답을 보낸다.
  • 매개변수:
    • fd: 클라이언트와의 통신을 위한 소켓 파일 디스크립터
  • source code 👩🏻‍💻
    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_readinitb(&rio, fd);                          //  리오 버퍼를 특정 파일 디스크립터(fd=>connfd)와 연결
      Rio_readlineb(&rio, buf, MAXLINE);                // rio(connfd)에 있는 string 한 줄(Request line)을 모두 buf로 옮긴다.
      printf("Request headers:\n");
      printf("%s", buf);                                // GET /index.html HTTP/1.1
      sscanf(buf, "%s %s %s", method, uri, version); 
      // method: "GET"
    	// uri: "/index.html"
    	// version: "HTTP/1.1"
    
      /* HTTP 요청의 메서드가 "GET"가 아닌 경우에 501 오류를 클라이언트에게 반환 */
      if (strcasecmp(method, "GET"))
      { // 조건문에서 하나라도 0이면 0
        clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
        return;
      }
      
      /* read_requesthdrs: 요청 헤더를 읽어들여서 출력 */
      /* 헤더를 읽는 것은 클라이언트의 요청을 처리하고 응답하기 위해 필요한 전처리 작업의 일부 */
      read_requesthdrs(&rio);
    
      /* parse_uri: 클라이언트 요청 라인에서 받아온 uri를 이용해 정적/동적 컨텐츠를 구분한다.*/
      /* is_static이 1이면 정적 컨텐츠, 0이면 동적 컨텐츠 */
      is_static = parse_uri(uri, filename, cgiargs);
    
      /* filename: 클라이언트가 요청한 파일의 경로 */
      if (stat(filename, &sbuf) < 0) 
      { // 파일이 없다면 -1, 404에러
        clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
        return;
      }
    
      /* 정적 컨텐츠 */
      if (is_static)
      {
        // !(일반 파일이다) or !(읽기 권한이 있다)
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode))
        {
          clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't read the file");
          return;
        }
        // 정적 서버에 파일의 사이즈를 같이 보낸다. => Response header에 Content-length 위해
        serve_static(fd, filename, sbuf.st_size);
      }
    
      /* 동적 컨텐츠 */
      else
      {
        // !(일반 파일이다) or !(실행 권한이 있다)
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode))
        {
          clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
          return;
        }
        // 동적 서버에 인자를 같이 보낸다.
        // filename은 CGI 프로그램의 경로이고, cgiargs는 CGI 프로그램을 실행할 때 필요한 인자들을 포함
        serve_dynamic(fd, filename, cgiargs);
      }
    }
  • 먼저 클라이언트로부터의 요청 라인을 읽는다. 이 요청 라인은 HTTP 메서드, 요청 URI, 및 HTTP 버전으로 구성됨. 이 정보를 통해 클라이언트가 원하는 작업과 요청 대상을 알 수 있다.
  • 읽어온 요청 라인을 파싱하여 HTTP 메서드가 "GET"인지 확인. 만약 그렇지 않다면, 서버는 501 Not Implemented 오류를 반환
  • 요청 헤더들을 읽는다. 이 단계는 클라이언트가 전송한 추가적인 정보를 처리하기 위해 필요
  • 클라이언트의 요청 URI를 분석하여 정적 컨텐츠인지 동적 컨텐츠인지를 구분
  • 요청된 파일이 존재하는지 여부를 확인. 파일이 없다면 404 Not Found 오류를 반환
  • 정적 컨텐츠인 경우, 해당 파일이 서버에서 제공될 수 있는지 여부를 확인한 후, 클라이언트에게 파일을 제공
  • 동적 컨텐츠인 경우, CGI 프로그램을 실행하여 동적으로 생성된 결과를 클라이언트에게 전달. 이때 CGI 프로그램의 경로와 필요한 인자들이 함께 전달.

read_requesthdrs

read_requesthdrs(rio_t *rp)

  • 클라이언트로부터 온 HTTP 요청 헤더를 읽어들이는 함수
  • 매개변수:
    • rp: 클라이언트와의 통신에 사용되는 rio_t 구조체의 포인터
  • source code 👩🏻‍💻
    void read_requesthdrs(rio_t *rp) {
    
      char buf[MAXLINE]; // 버퍼
      Rio_readlineb(rp, buf, MAXLINE); // 요청 라인을 읽어들여 무시한다. (요청 헤더부터 읽기 시작)
    
      // "\r\n"이 나올 때까지 요청 헤더를 읽어들여 화면에 출력
      while(strcmp(buf, "\r\n")){
        Rio_readlineb(rp, buf, MAXLINE); // 한 줄을 읽는다.
        printf("%s", buf); // 읽어들인 헤더를 화면에 출력
      }
      return;
    }
  • 먼저 rio_t 타입의 포인터 rp를 매개변수로 받는다. 이 포인터는 클라이언트와의 통신에 사용되는 rio 버퍼를 가리킨다.
  • 이후에는 반복문을 통해 요청 헤더를 한 줄씩 읽는다. 먼저 Rio_readlineb 함수를 호출하여 한 줄을 읽어들이고, 이를 버퍼 buf에 저장
  • 그리고 나서 읽어들인 헤더를 화면에 출력
  • 만약 읽어들인 헤더가 빈 줄인 경우("\r\n") 반복문을 종료

parse_uri

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

  • URI를 분석하여 요청된 콘텐츠가 정적인지 동적인지를 판단하고, 파일 경로 및 CGI 인자를 설정한다.
  • 매개변수:
    • uri: 요청된 URI
    • filename: 파일 경로를 저장할 문자열
    • cgiargs: CGI 인자를 저장할 문자열
  • 반환값: 요청된 콘텐츠가 정적인 경우 1을, 동적인 경우 0을 반환한다.
  • source code 👩🏻‍💻
    int parse_uri(char *uri, char *filename, char *cgiargs) 
    {
      char *ptr;
    
      /* 정적 콘텐츠 확인 */
      if (!strstr(uri, "cgi-bin")) { // URI에 "cgi-bin" 문자열이 포함되어 있지 않은 경우
        strcpy(cgiargs, ""); // CGI 인자를 빈 문자열로 설정
        strcpy(filename, "."); // 파일 경로를 현재 디렉토리로 설정
        strcat(filename, uri); // URI를 파일 경로에 추가
        if (uri[strlen(uri)-1] == '/') { // URI의 마지막 문자가 '/'인 경우
          strcat(filename, "home.html"); // 파일 경로에 "home.html"을 추가하여 기본 파일로 설정
        }
        return 1; // 정적 콘텐츠임을 나타내는 플래그 반환
      }
    
      /* 동적 콘텐츠 확인 */
      else { // URI에 "cgi-bin" 문자열이 포함되어 있는 경우
        ptr = index(uri, '?'); // URI에서 '?' 문자의 위치를 찾음
    
        if (ptr){ // URI에 '?' 문자가 존재하는 경우
          strcpy(cgiargs, ptr+1); // '?' 이후의 문자열을 CGI 인자로 복사
          *ptr = '\0'; // URI 문자열에서 '?' 문자 이후의 부분을 종료하는 널 문자로 대체
        }
        else { // URI에 '?' 문자가 존재하지 않는 경우
          strcpy(cgiargs, ""); // CGI 인자를 빈 문자열로 설정
        }
    
        strcpy(filename, "."); // 파일 경로를 현재 디렉토리로 설정
        strcat(filename, uri); // URI를 파일 경로에 추가
        return 0; // 동적 콘텐츠임을 나타내는 플래그 반환
      }
    }
    
  • 예시
    Request Line: GET /index.html HTTP/1.1
    uri: /index.html
    cgiargs: 없음
    filename: ./index.html
    Request Line: GET /cgi-bin/script?param1&param2 HTTP/1.0
    uri: /cgi-bin/script?param1&param2
    cgiargs: param1&param2
    filename: ./cgi-bin/script
    Request Line: GET /example/ HTTP/1.1
    uri: /example/
    cgiargs: 없음
    filename: ./example/home.html
  • 정적 콘텐츠(Static Content): URI에 "cgi-bin"이 포함되어 있지 않을 경우. ⇒ CGI 인자가 없는 정적 콘텐츠이다. URI에서 파일 경로를 추출하여 해당 파일을 서버에서 제공한다.
  • 동적 콘텐츠(Dynamic Content): URI에 "cgi-bin"이 포함되어 있을 경우. ⇒ CGI 프로그램을 실행해야 하는 동적 콘텐츠이다. URI에서 CGI 인자를 추출하고, 이후에는 URI를 파일 경로로 간주하여 서버에서 실행되어야 할 CGI 프로그램을 가리킨다.

get_filetype

void get_filetype(char filename, char filetype)

  • 기능: 파일의 확장자를 통해 파일 타입을 결정한다.
  • 매개변수:
    • filename: 파일 이름
    • filetype: 파일 타입을 저장할 문자열
  • source code 👩🏻‍💻
    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(filetype, "image/png");
      else if (strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpeg");
    
      else
        strcpy(filetype, "text/plain");
    }

serve_static

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

  • 기능: 정적 콘텐츠를 클라이언트에게 제공한다.
  • 매개변수:
    • fd: 클라이언트와의 통신을 위한 소켓 파일 디스크립터
    • filename: 요청된 파일의 경로 및 이름
    • filesize: 요청된 파일의 크기
  • source code 👩🏻‍💻
    void serve_static(int fd, char *filename, int filesize)
    {
      int srcfd;                // 파일 디스크립터
      char *srcp,               // 파일 내용을 메모리에 매핑한 포인터
           filetype[MAXLINE],   // 파일의 MIME 타입
           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, "%sConnections: 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);                                   // 메모리 할당 해제
    }

serve_dynamic

void serve_dynamic(int fd, char filename, char cgiargs)

  • 기능: 동적인 콘텐츠를 클라이언트에게 제공한다.
  • 매개변수:
    • fd: 클라이언트와의 통신을 위한 소켓 파일 디스크립터
    • filename: CGI 프로그램의 경로 및 이름
    • cgiargs: CGI 프로그램에 전달할 인자
  • source code 👩🏻‍💻
    void serve_dynamic(int fd, char *filename, char *cgiargs)
    { 
      char buf[MAXLINE], *emptylist[] = {NULL};
    
      /* 클라이언트에 HTTP 응답 라인과 헤더를 전송 */
      sprintf(buf, "HTTP/1.1 200 OK\r\n"); // HTTP 응답 라인 생성
      Rio_writen(fd, buf, strlen(buf)); // 클라이언트에 응답 라인 전송
      sprintf(buf, "Server: Tiny Web Server\r\n"); // 서버 정보를 응답 헤더에 추가
      Rio_writen(fd, buf, strlen(buf)); // 클라이언트에 응답 헤더 전송
    
    	/* CGI 실행을 위해 자식 프로세스를 생성 */
      if (Fork() == 0) // fork() 자식 프로세스 생성됐으면 0을 반환 (성공)
      { 
        setenv("QUERY_STRING", cgiargs, 1); // CGI 프로그램에 필요한 환경 변수 설정
        Dup2(fd, STDOUT_FILENO); // 자식 프로세스의 표준 출력을 클라이언트로 리다이렉션
        Execve(filename, emptylist, environ); // CGI 프로그램 실행
      }
    
      Wait(NULL); // 부모 프로세스가 자식 프로세스의 종료를 대기
    }
  1. fork()를 실행하면 부모 프로세스와 자식 프로세스가 동시에 실행된다.
  2. 만약 fork()의 반환값이 0이라면, 즉 자식 프로세스가 생성됐으면 if문을 수행한다.
  3. fork()의 반환값이 0이 아니라면, 즉 부모 프로세스라면 if문을 건너뛰고 Wait(NULL) 함수로 간다. 이 함수는 부모 프로세스가 먼저 도달해도 자식 프로세스가 종료될 때까지 기다리는 함수이다.
  4. if문 안에서 setenv 시스템 콜을 수행해 "QUERY_STRING"의 값을 cgiargs로 바꿔준다. 우선순위가 1이므로 기존의 값과 상관없이 값이 변경된다.
  5. Dup2 함수를 실행해서 CGI 프로세스의 표준 출력을 fd(서버 연결 소켓 식별자)로 복사한다. 이제 STDOUT_FILENO의 값은 fd이다. 다시 말해, CGI 프로세스에서 표준 출력을 하면 그게 서버 연결 식별자를 거쳐 클라이언트에 출력된다.
  6. execuv 함수를 이용해 파일 이름이 filename인 파일을 실행한다.

clienterror

void clienterror(int fd, char cause, char errnum, char shortmsg, char longmsg)

  • 기능: 클라이언트에게 오류 응답을 전송합니다.
  • 매개변수:
    • fd: 클라이언트와의 통신을 위한 소켓 파일 디스크립터
    • cause: 오류의 원인을 설명하는 문자열
    • errnum: 오류 번호
    • shortmsg: 짧은 오류 메시지
    • longmsg: 긴 오류 메시지
  • source code 👩🏻‍💻
    void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)
    {
      char buf[MAXLINE], body[MAXBUF];
    
      /* HTTP 응답 본문 생성 */
      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));
      Rio_writen(fd, body, strlen(body));
    }
  • HTTP 응답 본문 구성: 먼저 HTML 형식의 오류 메시지를 생성한다. 이 메시지에는 오류 번호와 간단한 오류 메시지(shortmsg), 그리고 오류의 원인(cause)과 추가 설명(longmsg)이 포함된다. 클라이언트는 이를 통해 발생한 오류에 대한 정보를 받을 수 있다.
  • HTTP 응답 헤더 생성: 다음으로, HTTP 응답 헤더를 생성한다. 이 헤더에는 HTTP 응답의 상태 코드(errnum)와 간단한 오류 메시지(shortmsg)가 포함된다. 또한 Content-type 헤더를 설정하여 응답이 HTML 형식임을 나타낸다.

adder.c

  • source code 👩🏻‍💻
    /*
     * 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(arg1); // 첫 번째 인자를 정수로 변환
            n2 = atoi(arg2); // 두 번째 인자를 정수로 변환
        }
    
        /* Make the response 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 */
        /* HTTP 응답 생성 */
        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 */

실행 방법

mac, ec2 기준입니다.

make clean

make

(proxy-lab/tiny$) cd .. (경로 주의)

(~/proxy-lab$) curl --proxy http://localhost:8080/ http://localhost:8000

cd tiny

./tiny 8000

=> 전날엔 되던게 안됨... 안될경우 ㅠㅠ
ec2 보안설정 후 ec2주소로 접속하자

실행 결과

profile
Sunny Day!

0개의 댓글