[Jungle] Week7. tiny web server

somi·2024년 5월 8일
0

[Krafton Jungle]

목록 보기
49/68

공부하면서 헷갈린 용어/개념

소켓 식별자는 네트워크를 통한 데이터 송수신을 가능하게 하는 특정 연결을 식별하는데 사용되는 고유한 값이다

  • 소켓?
    : 네트워크 통신의 endpoint,
    각 소켓은 운영 체제에 의해 제공되는 고유한 정수 값인 파일 디스크립터를 통해 식별된다.
  • 이 파일 디스크립터가 소켓의 식별자 역할을 함. -> 이 식별자를 사용해서 소켓에 데이터 보내고 받는 작업 수행.
  • listenfd
    : 서버가 클라이언트로 들어오는 연결 요청을 기다리는 소켓(listening socket) 식별자.
    : 서버가 시작되면 서버는 특정 포트에서 연결 요청을 듣기위해 소켓을 생성함
    : socket()으로 소켓 생성, bind()로 소켓에 주소(포트 번호, IP) 할당, listen()함수 호출하여 클라이언트 연결 요청 기다림.

  • connfd
    : 서버가 클라이언트의 연결 요청을 accept하고 실제 데이터 통신을 위해 생성된 소켓의 식별자
    : client로부터 연결 요청이 들어오면 서버는 accept()함수를 호출하여 그 요청을 수락. accept()함수는 새로운 연결 소켓을 생성하고 이 소켓을 위한 파일 디스크립터 반환.
    : 이 연결 식별자를 통해 서버는 특정 클라이언트와 데이터를 송수신 -> 각각의 연결은 고유한 연결 식별자를 가지며 서버는 이를 통해 개별 클라이언트와의 통신을 관리함.


Tiny Web Server 구현

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 */
  //.tiny 8000 처럼 argc 가 2개 입력되지 않았다면, 포트 번호가 전달되지 않은 것 -> 프로그램 사용 법 출력하고 프로그램 Exit
  if (argc != 2) {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }

  listenfd = Open_listenfd(argv[1]); //argv[1] 포트를 열어서 들어오는 연결 요청을 기다리는 리스닝 소켓 생성                    
 
  //무한 반복하여 클라이언트의 연결 요청 처리
  while (1) {
    clientlen = sizeof(clientaddr); //클라이언트 주소 구조체의 크기 설정
    //클라이언트의 연결 요청 수락, 통신을 위한 새로운 소켓 생성(connfd)
    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 connfd 연결 종료
  }
}

hostname, 포트 번호를 인자로 받아 클라이언트 요청이 들어올 때마다 새로운 연결 식별자(connfd) 만들어서 doit() 함수 호출한다.


doit()

// -> connfd가 인자로 들어오게 됨
void doit(int fd) {
  int is_static; //정적 콘텐츠인지 동적 컨텐츠인지 판별하는 변수
  struct stat sbuf; //파일의 상태 정보를 저장할 구조체
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];// 클라이언트에게서 받은 요청(rio)로 채워지게 된다.
  char filename[MAXLINE], cgiargs[MAXLINE]; // 파싱된 파일 이름과 CGI 인수를 저장할 배열들
  rio_t rio; // Robust I/O 구조체

  /*Read request line and headers*/
  /*request 라인과 헤더를 읽음*/
  Rio_readinitb(&rio, fd); //RIO 버퍼 초기화 - rio 버퍼와 서버의 connfd를 연결시켜준다.
  // Rio_readlineb(&rio, buf, MAXLINE);  //request 라인 읽음
  if (!(Rio_readlineb(&rio, buf, MAXLINE))){
    return;
  }
  printf("%s", buf);
  sscanf(buf, "%s %s %s", method, uri, version); //request line 파싱 -> 메소드, URI, 버전 추출
  
  //strcasecmp(): 대소문자를 구분하지 않고 스트링 비교
  // 일치하면 0 return 

  //요청 메소드가 GET이 아닌 경우 -> 클라이언트에게 501 에러
  
  /*숙제 11.11*/
  // 요청 Method가 GET과 HEAD가 아니면 종료.
  //main으로 가서 연결 닫고 다음 요청 기다림
  // if (strcasecmp(method, "GET")) {
  if (!(strcasecmp(method, "GET") == 0 || strcasecmp(method, "HEAD") == 0)) {
    clienterror(fd, method, "501", "Not implemented",
        "Tiny does not implement this method");
        return;
  }

  //Request header 읽음
  read_requesthdrs(&rio);

  /*Parse URI from GET request, GET 요청에서 URI 파싱*/
  is_static = parse_uri(uri, filename, cgiargs); //URI 파싱해서 정적/동적 콘텐츠 판별 - 정적(1), 동적(0)
  
  //파일 상태 정보를 가져오는데 실패한 경우 => 클라이언트에게 404 에러
  if (stat(filename, &sbuf) < 0) {
    clienterror(fd, filename, "404", "Not found",
          "Tiny couldn't find this file");
    return;
  }

  /*Serve static content, 정적 콘텐츠 제공*/
  if (is_static) { 

    /*파일이 일반 파일이 아니거나 읽기 권한이 없는 경우 -> 403 에러(웹 페이지를 볼 수있는 권한이 없음)*/
    //S_ISREG(sbuf.st_mode): 파일 모드가 정규 파일을 가맄키는지 ->false 반환하면 정규 파일이 아님(디렉토리나 링크일 수 있음)
    //S_IRUSR :소유자의 읽기 권한, sbuf.st_mode : 권한 비트 -> 해당 권한이 설정되었는지 검사
    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); //정적 콘텐츠 제공
  }
  /*Serve dynamic content 동적 콘텐츠 제공*/
  else { 
    //파일이 일반 파일이 아니거나 소유자가 실행 권한을 갖고 있지 않은 경우 -> 403 에러 
    //S_IXUSR: 파일 소유자의 실행 권한
    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); //동적 콘텐츠 제공
  }

}

클라이언트로부터 요청을 받고, 해당 요청이 static or dynamic 콘텐츠인지 판단한 후 요청에 맞는 콘텐츠 제공한다.

strcasecmp(): 대소문자를 구분하지 않고 스트링 비교하는 함수인데, 일치하면 0을 return한다.

Rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen):
: Rio 구조체와 연결된 네트워크 소켓으로부터 한 줄을 읽어 버퍼에 저장 -> 읽은 byte 수를 반환하며 파일의 끝(EOF)에 도달하거나 오류가 발생하면 0반환

  • rio_t *rp: RIO 구조체의 포인터. 이 구조체는 파일이나 네트워크 소켓과 관련된 버퍼링된 I/O 작업을 위해 사용됨
  • void *usrbuf: 읽은 데이터를 저장할 사용자 버퍼의 주소
  • size_t maxlen: 버퍼의 최대 길이
 if (!(Rio_readlineb(&rio, buf, MAXLINE))){
    return;
  }

-> 클라이언트로부터 연결이 닫히거나 읽을 데이터가 없을 때 무한 루프에 빠지지 않고 doit 함수를 즉시 종료시키게 Return -> 서버는 더 이상 처리할 데이터가 없을 때 불필요하게 리소스 소모하거나 오류 상태에서 계속 작업 시도하는 것 방지할 수 있을듯.


clienterror()

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

  /* Build the HTTP response body, 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); 


  /*Print the HTTP response*/
  sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
  Rio_writen(fd, buf, strlen(buf)); //buf에 저장된 HTTP 헤더 정보를 fd를 통해 연결된 클라이언트에게 정확한 길이만큼 전송
  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()으로 buf와 body를 서버 소켓(connfd)을 통해 클라이언트에게 전송
  Rio_writen(fd, buf, strlen(buf));
  Rio_writen(fd, body, strlen(body));
}

클라이언트에게 에러 메시지 전송 -> HTML 형식의 에러 페이지를 구성하여 에러 메시지와 body를 클라이언트에게 전송

sprintf() 함수: printf()와 유사하게 동작하지만 출력 결과를 화면에 표시하는 대신 지정된 문자 배열(buffer)에 저장
지정된 형식에 따라 다양한 데이터를 문자열로 변환하여 지정된 문자열 버퍼에 저장함

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

read_requesthdrs()

void read_requesthdrs(rio_t *rp) {
  char buf[MAXLINE];

  Rio_readlineb(rp, buf, MAXLINE); //첫번째 헤더 라인 읽음
  
  //strcmp(): 두 문자열 비교 
  //HTTP의 헤더 끝까지(루프를 통해 \r\n만 포함된 빈 줄을 만날 떄까지) 데이터 읽어옴
  while(strcmp(buf, "\r\n")) {
    Rio_readlineb(rp, buf, MAXLINE); // 다음 헤더 라인 한줄씩 읽고 
    printf("%s", buf); //출력
  }
  return;
}

HTTP request header를 끝까지 한줄씩 읽고 그냥 출력만 하고 있음 - 헤더 내의 어떤 정보도 사용하고 있지 않음 그냥 읽고 무시
현재는 요청의 본문을 처리하기 위해 헤더 부분을 넘어가기 위한 용도로만 사용되고 있음


parse_uri()

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

  /*Static content 정적 콘텐츠 처리*/
  //strstr() : 첫번째 인자(uri)에서 두번쨰 인자("cgi-bin")의 첫 번째 발생을 찾아 그 위치의 포인터 반환
  // 존재하지 않으면 NULL 반환  -> 동적 콘텐츠를 처리하는 CGI 프로그램과 관련이 없는 것 => 정적 콘텐츠 요청하는 것
  if (!strstr(uri, "cgi-bin")) {
    strcpy(cgiargs, ""); // 정적 콘텐츠 요청하는 경우 CGI 인자가 필요없으니 빈 문자열 복사
    strcpy(filename, "."); //현재 디렉토리로 기본 경로 설정
    strcat(filename, uri); //URI를 파일 이름에 추가

    //URI가 /로 끝나면 기본 파일 이름을 home.html로 설정
    //strlen(url)-1 : '\0'를 제외한 문자수 -> 마지막 문자가 /인지 확인 
    //uri가 디렉토리를 가리키는지 체크
    if (uri[strlen(uri)-1] == '/') {
      strcat(filename, "home.html");  //filename 문자열의 끝에 home.html 문자열 붙임 
      //웹 서버가 디렉토리에 대한 요청을 받았을 때 사용자에게 보여줄 기본적인 웹 페이지 설정
      //strcat 함수 -> 두개의 문자열을 연결하는데 사용.
    }
    return 1; 
  }

  /*Dynamic content 동적 콘텐츠 처리*/
  else {
    //index(): 주어진 문자열에서 특정 문자를 받아 그 위치의 포인터 반환
    ptr = index(uri, '?');

    // ?가 존재한다면 cgiargs를 ? 뒤 인자들과 값으로 채워주고 ?를 NULL(\0)로 대체
    if (ptr) {
      strcpy(cgiargs, ptr+1); //쿼리 스트링을 cgiargs로 복사
      *ptr = '\0'; //? 위치를 널 문자로 대체 ->  URI의 경로 부분만 남기기 위함
    }
    // '?'가 없다면 CGI 인자 비움
    else 
      strcpy(cgiargs, "");

    strcpy(filename, "."); //파일 이름의 기본 경로를 현재 디렉토리로 설정
    strcat(filename, uri); // ? 이전 부분을 파일 이름에 추가 
    return 0;
  }
}

URI를 분석하여 정적, 동적 콘텐츠 처리
URI에 따라 웹 서버가 제공해야 할 콘텐츠가 정적인지 동적인지 결정 -> 그에 따른 적절한 filename과 CGI 인자(cgiarg) 설정


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); //파일 이름을 바탕으로 파일의 MIME 타입 결정
  sprintf(buf, "HTTP/1.0 200 OK\r\n"); // HTTP 응답 시작 
  sprintf(buf, "%sServer: Tiny Web Server\r\n", buf); //서버 정보
  sprintf(buf, "%sConnection: close\r\n", buf); //연결 닫음
  sprintf(buf, "%sContent-length: %d\r\n", buf, filesize); //콘텐츠 길이
  sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype); //콘텐츠 타입

  /*connfd를 통해 clinetfd에게, 응답라인과 헤더를 클라이언트에게 보냄.*/
  Rio_writen(fd, buf, strlen(buf)); 
  printf("Response headers: \n");
  printf("%s", buf);

  if (strcasecmp(method, "HEAD")==0) {
    return;
  }
  
  /*Send response body to client*/
  srcfd = Open(filename, O_RDONLY, 0); //요청받은 파일을 읽기 전용 모드(O_RDONLY)로 열기 
    
  srcp = (char *)Malloc(filesize); //파일 크기만큼 메모리를 동적 할당
  Rio_readn(srcfd, srcp, filesize); //파일 내용을 읽어서 동적할당한 메모리에 값을 저장.
  Close(srcfd);  //파일 닫음
  Rio_writen(fd, srcp, filesize);  //해당 메모리에 있는 파일 내용들을 클라이언트에 보낸다.
  free(srcp); //메모리 해제
}

serve_dynamic()

동적 컨텐츠를 처리하기 위해 웹 서버에서 사용되는 함수
CGI 프로그램을 실행하고 그 출력을 클라이언트에게 직접 전송.
CGI 자식 프로세스를 fork() => 자식 프로세스의 표준 출력을 서버 연결 식별자를 거쳐서 클라이언트에 출력됨

void serve_dynamic(int fd, char *filename, char *cgiargs, char* method) {
  char buf[MAXLINE], *emptylist[] = {NULL};

  /*Return first part of HTTP response*/
  //HTTP 응답의 첫 부분을 클라이언트에게 반환 -> 200: 요청이 성공적으로 처리되었음
  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));



  /*Child*/
  //자식 프로세스에서 실행되는 코드
  if (Fork() == 0) {
    /*Real server would set all CGI vars here*/ 
    //CGI 프로그램에 전달될 QUERY_STRING 환경 변수 -> cgiargs로 설정
    setenv("QUERY_STRING", cgiargs, 1);

    //요청 메소드를 환경 변수에 추가
    setenv("REQUEST_METHOD", method, 1);

    Dup2(fd, STDOUT_FILENO); /*Redirect stdout to client, 
    CGI 프로세스의 표준 출력을 connfd에 복사 -> CGI 프로세스에서 표준 출력 하면 서버 연결 식별자를 거쳐 클라이언트에 출력됨*/

    Execve(filename, emptylist, environ); /*Run CGI program* CGI 프로그램 실행 */
    //filename 변수에는 실행할 CGI 프로그램의 경로가 저장되어 있음. 
    //emptylist는 CGI 프로그램으로 전달될 인자 목록, 여기서는 인자 없이 실행됨
  }
  /*Parent waits for and reaps child 
  부모는 자식 프로세스가 종료될 때까지 기다림, 자식 프로세스가 종료되면 시스템 자원 회수*/
  Wait(NULL); 
}
  • Fork(): 현재 실행중인 프로세스(부모 프로세스)의 정확한 복사본 생성 - 자식 프로세스 생성
    자식 프로세스는 부모 프로세스와 메모리 공간을 공유하지 않음. 자신만의 독립적인 메모리 공간을 가짐
    부모 프로세스에게는 자식 프로세스의 PID가 반환, 자식 프로세스에서는 0이 반환.
  • Fork() == 0이 참이면 현재 자식 프로세스의 컨텍스트에서 실행되고 있음을 의미. 부모 프로세스에서는 이 조건이 거짓이 됨
  • 동적 콘텐츠를 제공하기 위해 CGI 프로그램을 별도의 프로세스로 실행하기 위해 자식 프로세스는 독립적인 환경에서 CGI 프로그램 실행 -> 그 출력을 클라이언트에게 전송. 웹 서버의 메인 프로세스는 다른 요청을 처리할 수 있음
  • Dup2(): File descriptor를 지정된 파일 디스크립터 번호로 복사하는 함수

get_filetype()

Derive file type from filename
파일 이름 기반으로 파일 유형(MIME 타입) 결정


filename: 파일 유형을 결정할 파일의 이름
filetype: 결정된 파일 유형을 저장할 문자열 주소

void get_filetype(char *filename, char *filetype) {
  //파일 이름에 .html 확장자가 포함되어 있으면 
  // HTML 문서로 간주하고 MIME Type을 text/html로 설정
  if (strstr(filename, ".html")) {
    strcpy(filetype, "text/html");
  }
  //파일 이름에 .gif 확장자가 포함되어 있으면
  //MIME Type - GIF 이미지로 간주
  else if (strstr(filename, ".gif")) {
    strcpy(filetype, "image/gif");
  }
  //파일 이름에 .png 확장자가 포함되어 있으면
  //PNG 이미지로 간주 -> MIME type : "iamge/png"
  else if (strstr(filename, ".png")) {
    strcpy(filetype, "image/png");
  }
  //파일 이름에 .jpg 확장자가 포함되어 있으면
  //JPEG 이미지로 간주 -> MIME type: "image/jpeg"
  else if (strstr(filename, ".jpg")) {
    strcpy(filetype, "image/jpeg");
  }
  else if (strstr(filename, ".mp4")) {
    strcpy(filetype, "video/mp4");
  }
  //으로 "text/plain"으로 설정
  else 
    strcpy(filetype, "text/plain");
}

HTTP HEAD Method

  • GET 요청과 유사하지만 GET 요청은 실제 리소스의 내용까지 가져오지만, HEAD 요청은 리소스의 헤더 정보만 가져옴.
    => HEAD: 리소스의 내용(content)가 아니라 메타데이터(metadata)만을 요청한다.
    웹 서버로부터 해당 리소스의 본문 데이터(body)는 전송받지 않고, 헤더 정보(Header)만을 받게 된다.
  • 리소스의 유효성 검사: Last-Modified, ETag
    Last Modified: 마지막으로 수정된 날짜와 시간 -> 버전의 리소스가 최신 버전인지 확인할 수 있음
    Etag(Entity Tag): 리소스의 특정 버전을 나타내는 식별자.
  • Content-Length -> 데이터의 양/크기 예측. 리소스의 크기를 바이트 단위로 나타냄.
  • 리소스의 유형 확인: Content-Type: 어떤 MIME 타입인지. 어떻게 처리할지 결정 가능
  • 서버의 응답 헤더 확인
  • 서버의 생존 확인

=> HTTP HEAD 메소드는 네트워크 트래픽을 최소화하면서 필요한 데이터를 효과적으로 수집할 수 있음

(예시) HTTP HEAD 요청이 아닐 때만 content를 출력하도록

 if (strcasecmp(method, "HEAD")!=0){
    printf("%s", content); 
  }

serve_static()

기존의 정적 컨텐츠 처리 코드 - 메모리 매핑 방식

srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);


Mmap(): 특정 파일을 가상 주소에 매핑

  • 프로세스는 파일을 처리하는게 아니라 메모리에 있는 데이터를 읽거나 쓰면 됨.
  • 파일의 내용을 메모리 주소 공간에 직접 매핑.
  • 파일 I/O 연산 없이 메모리에 직접 데이터를 읽어올 수 있음.

파일을 읽기 전용 모드로 엶.
-> 가상 메모리에 매핑 Mmap
-> Close(fd)
: 파일이 메모리에 매핑된 이후에는 fd가 더이상 필요하지 않음
-> 닫지 않으면 메모리 누수가 발생할 수있음
-> Rio_writen : connfd를 통해 가상 메모리에 매핑된 파일 내용을 클라이언트에 보냄
-> Munmap() : 파일 전송이 완료되면 더이상 메모리 매핑 불필요하니까 Unmap


malloc으로 대체한 코드 - 동적 할당

srcfd = Open(filename, O_RDONLY, 0);
srcp = (char *)malloc(sizeof(filesize));
Rio_readn(srcfd, srcp, filesize);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
free(srcp);

파일을 읽기 전용 모드로 엶.
-> malloc()을 사용해서 파일 크기만큼의 메모리를 동적 할당.
-> Rio_readn(srcfd, srcp, filesize): 파일의 내용을 읽어서 동적할당한 메모리에 파일 값 저장
-> Close(srcfd): 파일 닫음
-> Rio_writen: 해당 메모리에 있는 파일 내용들을 클라이언트에 보냄
-> free : 동적할당한 메모리 해제

  • 메모리 매핑 방식
    : 큰 파일을 처리할 경우 파일 전체를 메모리 매핑해야하기 때문에 사용 가능한 메모리 양에 제한을 받을 수 있따는 단점.
    메모리 매핑 실패시의 에러 처리가 필요하다.
    운영체제가 파일 캐싱을 활용해서 여러 프로세스에서 같은 파일을 사용할 경우 메모리 사용 최적화할 수 있음.
  • 동적 할당 방식
    : 필요한 메모리만 할당하여 사용하므로 작은 파일 다룰 때 더 효율적일 수 있다.
    파일 i/o -> 데이터를 읽고 쓰기 때문에 메모리 매핑 방식에 비해 성능이 떨어질 수 있다.
    파일의 내용을 메모리로 복사하는 과정에서 시간이 추가로 소요

=> 성능이 중요하고 처리해야할 파일의 크기가 크고 메모리 사용량에 여유가 있으면 메모리 매핑이,
작은 파일을 주로 다루는 경우 -> 동적 할당 방식이 더 적합할 수 있을 것이다.
!!

profile
📝 It's been waiting for you

0개의 댓글