[TIL] 정글 52일차 - 회고

신승준·2022년 5월 19일
0

TIL

목록 보기
25/34

네트워크

네트워크

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"

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);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs, char *method);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg,
                 char *longmsg);
                 
// listenfd = 3
// connfd = 4
// srcfd = 5

/* 
 * tiny main routine - main 함수는 서버 측에서 listenfd(소켓)를 생성하고 커맨드 라인에 입력한 포트로(./tiny 5000이라고 입력했다면 포트는 5000) 클라이언트의 connect 요청이 들어오길 기다리는 함수이다.
 * tiny 서버는 동시성 서버가 아니다. 따라서 하나의 통신 혹은 서비스 밖에 처리할 수 없다.(라고 이해함)
 */
// ./tiny 5000처럼 입력 시 argc = 2, argv[0] = tiny, argv[1] = 5000이 된다.
// port 번호를 인자로 받는다. 
int main(int argc, char **argv) {
  int listenfd, connfd;                                                               // 서버 측에서 생성하는 listenfd, connfd 소켓
  char hostname[MAXLINE], port[MAXLINE];                                              // 클라이언트의 hostname(IP 주소)과 port 번호를 저장할 것이다.
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;                                                 // 클라이언트에서 connect 요청을 보내면, 서버 측에서 알 수 있게 되는 클라이언트 소켓의 주소이다.
  
  // 클라이언트 입력의 argument(인자) 갯수가 2가 아니면 에러를 출력한다. 서버 측에 주소랑 포트 번호를 알려줘야, 서버는 거기에 맞는 프로세스를 찾아서 클라이언트 측으로 넘겨줄 수 있으니까!
  // int fprintf(FILE* stream, const char* format, ...) : 해당 파일을 열어 format을 작성한다.
  if (argc != 2) {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);                                   // 인자 2개를 입력한 것이 아니면 usage: ./tiny <port>라는 문구가 보일 것이다. 이는 ./tiny <port> 형식으로 쓰라는 의미이다.
    exit(1);                                                                          // 해당 파일 자체가 종료되므로 서버 종료
  }
  
  listenfd = Open_listenfd(argv[1]);                                                  // 해당 포트 번호에 해당하는 listen 소켓 디스크립터를 열어준다.

  // 서버는 listenfd 소켓을 생성해놓은 상태에서, 클라이언트가 connect 요청을 하여 서버 자신이 accept할 때까지 계속 기다리게 된다.
  while (1) {
    clientlen = sizeof(clientaddr);                                                     
    
    // 서버가 생성한 listenfd 소켓에 클라이언트의 connect 요청(클라이언트의 IP 주소와 클라이언트의 port 번호를 보내는 것으로 생각하자)이 맞물린다고 이해하자.
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);                         // 클라이언트가 connect 요청을 하면 서버 측은 accept하게 된다. 그 이후 서로 데이터를 읽고 쓸 수 있는 공간(소켓)인 connfd가 생성된다.
                                                                                      // 이후 네트워크 통신에서 사용하기 좋은 RIO 패키지를 이용하여 이 connfd에 입출력을 하게 된다.
                                                                                      // 클라이언트가 자신의 IP 주소와, 클라이언트 프로세스를 유일하게 식별할 수 있는 포트를 보낸다. 이 때 포트 번호는 클라이언트의 포트 번호인데, 임의로 설정될 것이다.
    
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);   // 서버 측에서 클라이언트의 hostname과 클라이언트의 port 정보를 가져온다.
    printf("Accepted connection from (%s %s)\n", hostname, port);    
    
    doit(connfd);                                                                     // 서버 측에서 서비스를 실행하는 부분이다.
    
    Close(connfd);                                                                    // 클라이언트로부터의 요청을 통해 생성된 connfd로 서비스(doit)를 처리하고 나면 해당 connfd 소켓은 닫는다. 더 이상 필요하지 않고, tiny는 동시성 서버가 아니니까
  }
}

/*
 * doit - HTTP 트랜잭션, 즉 클라이언트의 요구를 서버가 그에 맞게 서비스하는 함수이다.
 */
// 한 개의 HTTP 트랜잭션을 처리한다.
// 이 때 fd는 connfd이다.
void doit(int fd) {
  int is_static;                                                                      // 클라이언트가 보낸 요청이 정적 컨텐츠를 요구하는 것인지, 혹은 동적 컨텐츠를 요구하는 것인지 판단할 flag이다. 후에 parse_uri에서 더 자세히 설명된다.
  struct stat sbuf;                                                                   // server buffer?
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];                 // 클라이언트에게서 받은 요청(클라이언트의 request header)을 바탕으로 해당 변수들을 채우게 된다.
  char filename[MAXLINE], cgiargs[MAXLINE];                                           // 클라이언트가 요구한 filename과, (동적 컨텐츠를 요구했다면 존재할) 인자들. 나중에 parse_uri를 통하여 채워지게 된다.
  rio_t rio;                                                                          // 네트워크 통신에 효과적인 RIO 패키지 구조체
  
  // 클라이언트가 보낸 request header를 RIO 패키지로 읽고 분석한다.
  Rio_readinitb(&rio, fd);                                                            // connfd를 rio에 위치한 rio_t 타입의 읽기 버퍼(그냥 RIO I/O를 쓸 수 있는 공간이라고 이해하자)와 연결한다. 네트워크 통신에 적합한(short count를 자동으로 처리하는) RIO I/O로 connfd를 통해 데이터를 읽고 쓸 수 있게 만든다.
  printf("Request headers:\n");
  Rio_readlineb(&rio, buf, MAXLINE);                                                  // rio에 있는, 즉 connfd에 있는 문자열 한 줄을 읽어와서 buf로 옮긴다. 그리고 그 문자열 한 줄을 NULL로 바꾼다.
  printf("%s", buf);                                                                  // 위 Rio_readlineb에서 한 줄을 읽었다. buf = "GET /godzilla.gif HTTP/1.1\0"를 출력해준다. 아마 connfd의 첫째 줄에는 클라이언트가 보낸 저 문구가 있지 않았을까?
  sscanf(buf, "%s %s %s", method, uri, version);                                      // buf에서 문자열 3개를 가져와서 method, uri, version이라는 문자열에 저장한다. 즉 위 예시대로라면 GET, godzilla.gif, HTTP/1.1 정도가 저장될 것이다.
  
  // 클라이언트의 request header의 method가 GET or HEAD가 아니면 doit 함수는 끝나고 main으로 돌아가서 Close가 된다. 하지만 while(1)이므로 서버는 계속 연결 요청을 기다리게 된다.
  if (!(strcasecmp(method, "GET") == 0 || strcasecmp(method, "HEAD") == 0)) {         // 우리가 만드는 Tiny 서버는 GET, HEAD만 지원하므로 GET이 아닐 경우 connfd에 에러 메세지를 띄운다. strcasecmp는 대문자 상관없이 인자를 비교하고 같으면 0을 반환한다.
    clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
    return;
  }
  
  // request line을 뺀 나머지 request header 부분을 무시한다. 그냥 읽어서 프린트할 뿐 실제로 무언가를 하지 않는다.
  read_requesthdrs(&rio);
  
  // 클라이언트 요청을 분석해서 클라이언트가 요구한 것이 무엇인지(정적 혹은 동적 컨텐츠인지, 요구한 filename은 무엇인지(추출), 요구한 인자는 무엇인지(추출)) 클라이언트가 보낸 uri를 통해 분석한다. 아마 open_clientfd로 다 보내지 않았을까? accept에서 받았을 것이다.
  is_static = parse_uri(uri, filename, cgiargs);                                      // request가 정적 컨텐츠를 위한 것인지, 동적 컨텐츠를 위한 것인지 flag를 설정한다.
                                                                                      // 이 때 클라이언트가 보낸 connect 요청을 분석해서 (클라이언트가 서버 측에 요구한)filename과 인자를 채운다.
                                                                                      
  // 요청한 파일이 서버의 디스크 상에 있지 않다면 connfd에 에러 메세지를 띄운다(클라이언트에게 에러 메세지를 띄운다). 그리고 main으로 돌아가서 서버는 또 연결 요청을 기다린다.
  // stat은 서버 측에 filename과 일치하는 file이 있으면 그에 대한 정보를 sbuf(filename에 대한 정보를 저장하는 stat 구조체)에 업데이트 시킨다.
  // 즉 클라이언트가 요구한 filename에 대해, 서버는 자신한테 그 파일이 있는지 확인하고 있다면 그 파일에 대한 정보를, 따로 구조체를 만들어 저장 시킨다.
  // stat은 성공 시 0을 리턴하고, 실패 시 -1을 리턴한다.
  if (stat(filename, &sbuf) < 0) {                                                    
    clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");    // connfd
    return;                                                                           
  }
  
  // 정적 컨텐츠라면
  if (is_static) {
    // 동적 컨텐츠라면, 이번에는 클라이언트가 요구한 서버의 파일에 대해 검증한다.
    // 일반 파일(regular file)인지, 읽기 권한이 있는지 확인한다.
    // 일반 파일이 아니거나 읽기 권한이 없다면 에러 메세지를 띄우고 이 또한 doit을 종료해 main으로 돌아가 연결 요청을 기다리게 한다.
    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 {
    // 동적 컨텐츠라면, 이번에는 클라이언트가 요구한 서버의 파일에 대해 검증한다.
    // 일반 파일인지, 실행 권한이 있는지 확인한다.
    // 일반 파일이 아니거나 실행 권한이 없다면 에러 메세지를 띄우고 이 또한 doit을 종료해 main으로 돌아가 연결 요청을 기다리게 한다.
    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);
  }
}

// 에러메세지와 응답 본체를 connfd를 통해 클라이언트에 보낸다.
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)
{
  char buf[MAXLINE], body[MAXBUF];
  
  // HTTP 응답 body를 구성한다.
  // 즉 에러가 발생했을 때 클라이언트 측에 보여줄 에러 HTML 파일을 구성하는 것이다.
  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로 한번에 저장해뒀다가 Rio_writen으로 보내주면 안될까?
  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));

  // HTTP 응답 body와 HTTP 응답 메세지를 connfd에게 보내준다.
  Rio_writen(fd, buf, strlen(buf));
  Rio_writen(fd, body, strlen(body));
}

/*
 * read_requesthdrs - method / IP 주소 / version 한 줄 읽고 read_requesthdrs를 만난다. 즉 1줄 다음을 읽고 프린트한다.
 */
void read_requesthdrs(rio_t *rp)
{
  char buf[MAXLINE];

  Rio_readlineb(rp, buf, MAXLINE);

  /* 버퍼 rp의 마지막 끝을 만날 때까지("Content-length: %d\r\n\r\n에서 마지막 \r\n) */
  /* 계속 출력해줘서 없앤다. */
  while(strcmp(buf, "\r\n")) {
    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
  }
  return;
}

/*
 * parse_uri - GET Request URI form을 분석한다.
 */
// URI를 받아 요청 받은 파일의 이름(filename)과 요청인자(cgiargs)를 채운다.
// 정적 컨텐츠 URI 예시 : http://54.180.101.138:5000/home.html, uri = /home.tml
// 동적 컨텐츠 URI 예시 : http://54.180.101.138:5000/cgi-bin/adder?123&123, uri = /cgi-bin/adder?123&123
int parse_uri(char *uri, char *filename, char *cgiargs)
{
  char *ptr;

  /* uri에 cgi-bin이 없다면, 즉 정적 컨텐츠를 요청한다면 1을 리턴한다.*/
  // 예시 : GET /godzilla.jpg HTTP/1.1 -> uri에 cgi-bin이 없다
  
  // URI에 cgi-bin이 없다면, 즉 클라이언트가 정적 컨텐츠를 요청했다면
  // strstr은 해당 문자열이 있으면 그 문자열을 가르키는 포인터를, 없으면 NULL을 반환한다.
  if (!strstr(uri, "cgi-bin")) {
    strcpy(cgiargs, "");                                                              // 정적이니까 cgiargs는 필요가 없다. 있지도 않을 것이다. 아마
    strcpy(filename, ".");                                                            // 상대 경로에서, ./이란 현재 폴더, 즉 현재 경로를 말한다. filename은 .이 된다.
    strcat(filename, uri);                                                            // filename에 URI 문자열을 이어붙인다. 이 때 URI는 아마 home.html과 같을 것이다. 따라서 ./home.html이 될 것이다.
    
    // 만약 클라이언트가 http://54.180.101.138:5000/과 같이 접속한다면, 알아서 http://54.180.101.138:5000/home.html이 되도록 한다.
    // 그래서 http://54.180.101.138:5000/home.html/으로 접속하면 http://54.180.101.138:5000/home.html/home.html이 돼서 에러 메세지가 뜰 수도 있다.
    // 어쨋든, URI 뒤의 맨 마지막이 /이라면 그냥 home.html이라는 정적 컨텐츠가 제공되도록 한다.
    if (uri[strlen(uri)-1] == '/')
      strcat(filename, "home.html");
    
    // 정적 컨텐츠면 parse_uri는 1 리턴
    return 1;
    
    // uri = /home.html에서
    // cgiargs = ""
    // filename = ./home.html로 된다.
  }
  
  // URI에 cgi-bin이 있다면, 즉 클라이언트가 동적 컨텐츠를 요청했다면
  else {
    ptr = index(uri, '?');                                                            // uri에서 ?을 가리키는 포인터를 만든다. QUERY_STRING, 즉 클라이언트가 보낸 인자를 추출해내기 위해서이다.

    // '?'가 있으면 cgiargs를 '?' 뒤 인자들과 값으로 채워주고 ?를 NULL로 만든다.
    if (ptr) { 
      strcpy(cgiargs, ptr+1);
      *ptr = '\0';
    }
    
    // '?'가 없으면 그냥 아무것도 안 넣어준다.
    else {
      strcpy(cgiargs, "");
    }
    
    strcpy(filename, ".");  // 현재 폴더에서 시작
    strcat(filename, uri);  // uri를 붙여준다.
                            // 동적 컨텐츠일 때 인자가 존재할 경우인 if를 만났었더라도 ptr이 가리키는 ?을 \0으로 바꿨으니 뒤의 인자는 날아간 형태로 붙게 된다.
                            
    // 동적 컨텐츠면 parse_uri는 0 리턴
    return 0;
    
    // uri = /cgi-bin/adder?123&123에서
    // cgiargs = 123&123
    // filename = ./cgi-bin/adder이 된다.
  }
}

/*
 * serve_static - 응답 메세지를 구성하고 정적 컨텐츠를 처리(connfd로)한다.
 */   
void serve_static(int fd, char *filename, int filesize, char *method)
{
  int srcfd;
  char *srcp, filetype[MAXLINE], 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, "%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);
  
  /* 응답 메세제지를 클라이언트에게 보냄 */
  Rio_writen(fd, buf, strlen(buf));                                                   // connfd를 통해 클라이언트에게 보낸다.
  printf("Response headers:\n");                                                      // 서버 측에서도 출력한다.
  printf("%s", buf);
  
  if (strcasecmp(method, "HEAD") == 0) {
    return;
  }                                                                

  // Mmap, Munmap 이용 시
  // srcfd는 home.html을 가리키는 식별자
  // 이 떄 이 srcfd를 가상 메모리에 할당한다.
  // 이 가상 메모리를 다시 connfd로 옮긴다.
  // 그리고 이 srcfd를 프리시킨다.
  // srcfd = Open(filename, O_RDONLY, 0);                                                // filename의 이름을 갖는 파일을 읽기 권한으로 불러온다.
  // srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);                         // 메모리에 파일 내용을 동적할당한다.
  // Close(srcfd);                                                                       // 파일을 닫는다. 
  // Rio_writen(fd, srcp, filesize);                                                     // 동적 할당을 받아 메모리에 복사한, 즉 srcp가 가리키는 메모리에 있는 파일 내용들을 fd에 보낸다.
  // Munmap(srcp, filesize);                                                             // 할당 받은 것을 해제시킨다.
  
  // Malloc, free 이용시
  srcfd = Open(filename, O_RDONLY, 0);
  srcp = (char *)Malloc(filesize);
  Rio_readn(srcfd, srcp, filesize);
  Close(srcfd);
  Rio_writen(fd, srcp, filesize);
  Free(srcp);
}

/*
 * get_filetype - 클라이언트가 서버 측에 요구한 filename에서 파일의 유형(html, png, jpg, mp4 등등)을 파악하고 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(filetype, "image/png");
  else if (strstr(filename, ".jpg"))
    strcpy(filetype, "image/jpeg");
  else if (strstr(filename, ".mp4"))
    strcpy(filetype, "video/mp4");
  else if (strstr(filename, ".mpeg"))
    strcpy(filetype, "video/mpeg");
  else
    strcpy(filetype, "text/plain");
}

/*
 * serve_dynamic - 응답 메세지를 구성하고 동적 컨텐츠를 처리(connfd로)한다.
 */   
void serve_dynamic(int fd, char *filename, char *cgiargs, char *method)
{
  char buf[MAXLINE], *emptylist[] = { NULL };

  // 응답 메세지를 구성한다.
  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));

  // 자식 프로세스를 생성한다. fork시, 자식 프로세스의 반환 값은 0이다. 부모는 0이 아닌 다른 값이다. 즉 자식 프로세스일 경우 Fork()는 0이라서 if문에 들어가게 된다.
  // fork()하면 파일 테이블도 같이 복사된다. 다른 것은 fork 반환 값인 프로세스 id인 것으로 알고 있다.
  if (Fork() == 0) {
    // 환경 변수에서 QUERY_STRING에 클라이언트가 요구한 인자들을 등록한다.
    // 여기서 QUERY_STRING은 URI에서 클라이언트가 보낸 인자인 id=HTML&name=egoing와 같은 부분이다.
    // 이를 통해서 우리의 동적 컨텐츠 처리 애플리케이션(응용)인 adder.c가 이 환경변수의 QUERY_STRING으로 동적인 처리를 할 수 있게 된다. 여기서 동적인 처리는 그냥 더하기이다. 단순히 인자를 통해서 새로운 결과물을 낼 수 있기 때문에 동적 처리라고 할 수 있고 우리는 그 예로 아주 간단한 더하기를 사용하였다.
    setenv("QUERY_STRING", cgiargs, 1);
    setenv("REQUEST_METHOD", method, 1);

    // 클라이언트의 표준 출력을 CGI 프로그램의 표준 출력과 연결한다.
    // 이제 CGI 프로그램에서 printf하면 클라이언트에서 출력된다.
    Dup2(fd, STDOUT_FILENO);                                                          // 표준 출력을 connfd로 항하게 한다. 이렇게 하면 Execve를 통해 adder.c가 실행되어 출력되는 값이 connfd에 출력되게 할 수 있는 느낌이다.
    Execve(filename, emptylist, environ);                                             // adder.c가 실행된다. 현 코드 영역을 모두 지우고 adder.c의 코드로 채워지게 된다.
                                                                                      // Execve는 프로그램을 실행시키는 함수이다.
                                                                                      
    // Execve는 현 영역의 코드를 모두 지우고, 실행할 파일의 코드로 코드 영역을 채운다. 그렇기 때문에 부모 프로세스로만 하면, adder.c의 코드로 채워져서 더 이상 서버 역할을 할 수 없게 된다. 서버 측의 코드가 모두 지워져 버렸으니까
    // 따라서 자식 프로세스를 만들고, 자식 프로세스만 Execve를 하게 해 adder.c 파일을 수행하도록 한다.
    // 부모는 if문을 피하여 Wait(NULL)을 만나, 자식 프로세스의 동작이 끝나 자식 프로세스의 status가 null이 될 때까지 기다리게 된다.
    // 자식 프로세스의 status가 NULL이 되면 부모 프로세스는 다시 동작하게 되고, serve_dynamic 함수를 빠져나가 또 main 함수의 while 문을 돌게 되어 클라이언트의 새로운 connect 요청을 기다리게 된다.
  }
  Wait(NULL); /* Parent waits for and reaps child */
}

Proxy.c (concurrent까지)

#include <stdio.h>
#include "csapp.h"

// 주로 추천되어지는 캐시와 캐시 오브젝트의 최대 사이즈
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

static const char *user_agent_hdr = "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv;10.0.3) Gecko/20120305 Firefox/10.0.3\r\n";
static const char *conn_hdr = "Connection: close\r\n";
static const char *prox_hdr = "Proxy-Connection: close\r\n";
static const char *host_hdr_format = "Host: %s\r\n";
static const char *requestlint_hdr_format = "GET %s HTTP/1.0 \r\n";
static const char *endof_hdr = "\r\n";

static const char *connection_key = "Connection";
static const char *user_agent_key = "User_Agent";
static const char *proxy_connection_key = "Proxy-Connection";
static const char *host_key = "Host";

void doit(int connfd);
void parse_uri(char *uri, char *hostname, char *path, int *port);
void build_http_header(char *http_header, char *hostname, char *path, int port, rio_t *client_rio);
int connect_endServer(char *hostname, int port, char *http_header);
void *thread(void *vargsp);                                                                 // 쓰레드를 가리키는 포인터

// listenfd = 3
// connfd = 4
// end_serverfd = 5

int main(int argc, char **argv) {
    int listenfd, connfd;
    socklen_t clientlen;                                                                    // buf에 clientlen만큼 넣을려고
    char hostname[MAXLINE], port[MAXLINE];                                                  // client hostname(IP 주소), client port
    struct sockaddr_storage clientaddr;
    pthread_t tid;                                                                          // 쓰레드 구조체
    
    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);                                     // 버퍼 없이 바로 출력한다. fprintf는 버퍼에 담았다가 출력한다. 하지만 stderr는 버퍼없이 출력한다. 그냥 printf는 버퍼를 쓰지 않는다.
        exit(1);                                                                            // 1이면 에러 시 강제 종료
    }
    
    listenfd = Open_listenfd(argv[1]);
    while(1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);                           // 대문자 Accept는 오류가 났을 때 무슨 메세지를 띄울지 내부적으로 코드가 담겨 있다.
        
        Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
        printf("Accepted connection from (%s %s). \n", hostname, port);
        
        // doit(connfd);
        
        // Close(connfd);
        
        // 1번쨰 인자 : 성공적으로 쓰레드 생성 시 이 쓰레드를 식별하기 위해 사용되는 쓰레드 식별자
        // 2번째 인자 : 쓰레드 특성. 기본 쓰레드 특성을 이용하고자 한다면 NULL을 입력
        // 3번쨰 인자 : 분기시켜서 실행할 쓰레드 함수, 즉 쓰레드가 할 일을 정해주는 함수이다. 생성된 쓰레드는 thread라는 함수의 코드를 수행하게 된다.
        // 4번째 인자 : 쓰레드 함수의 매개변수. client와 proxy간 데이터를 쓰고 읽는 공간인 connfd에서 thread가 동작하게 된다.
        Pthread_create(&tid, NULL, thread, (void *)connfd);                                 // 왜 void로 캐스팅해줄까? 어떤 자료형의 connfd일지 몰라서 그런가?
    }
    
    return 0;
}

// 새로운 요청이 들어올 때마다 새로 생겨난 쓰레드가 connfd에서 클라이언트의 요청을 수행하게 된다.
// 하나의 프로세스에 여러 개의 쓰레드가 있게 된다.
void *thread(void *vargs) {
    int connfd = (int)vargs;                                                                // 위에서 넘겨진 connfd를, 여기서 생성된 변수인 connfd에 넣는다. 위에서 void로 넘겨줬으니 int로 캐스팅한다.
    Pthread_detach(pthread_self());                                                         // 해당 함수는, 쓰레드가 종료되고 나면 쓰레드가 사용하던 자원을 반납하게 하는 함수이다. 쓰레드는 스택만 따로 가지고 있고, 나머지 힙이나 코드 영역 등은 공유한다고 한다.
    doit(connfd);                                                                           // 해당 쓰레드가 doit 함수를 처리한다.
    Close(connfd);
}

void doit(int connfd) {
    int end_serverfd;                                                                       // end_server를 가리키는 파일 디스크립터
    
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char endserver_http_header[MAXLINE];
    
    char hostname[MAXLINE], path[MAXLINE];
    int port;
    
    rio_t rio, server_rio;                                                                  // 이 때 그냥 rio는 client의 rio이고, server_rio는 end_server의 rio이다.
    
    Rio_readinitb(&rio, connfd);
    Rio_readlineb(&rio, buf, MAXLINE);
    sscanf(buf, "%s %s %s", method, uri, version);
    
    if (strcasecmp(method, "GET")) {
        printf("Proxy does not implement the method");
        return;
    }
    
    // client가 보낸 uri를 분석하여 hostname, path, port 값을 갱신한다.
    parse_uri(uri, hostname, path, &port);
    // printf("1. %s\n", uri);
    // printf("2. %s\n", hostname);
    // printf("3. %s\n", path);
    // printf("4. %d\n", port);
    
    // proxy는, end_server로 보낼 HTTP header를 구성한다.
    // build_http_header함수를 거쳐 완성될 end_server의 응답 헤더, end_server의 hostname(IP 주소), path?, 서버 port 번호, client와 proxy간의 connfd
    build_http_header(endserver_http_header, hostname, path, port, &rio);
    
    // proxy를 end_server와 연결시킨다.
    // 여기서 end_serverfd는, tiny에서 connfd와 역할이 같다.
    // 이 때 end_serverfd의 값은 5이다.
    end_serverfd = connect_endServer(hostname, port, endserver_http_header);
    
    // 연결 실패 시 에러 문구 띄우고 end_server와 연결할 수 없다는 의미이므로 main 함수가 종료된다. 즉 proxy 서버 또한 종료된다.
    // hostname이나 port에, 즉 client가 end_server에 잘못된 주소로 요청을 할 때, 정확히는 존재하지 않는 주소 및 포트 번호로 요청할 때 이러한 에러가 발생할 수 있다.(뇌피셜)
    if (end_serverfd < 0) {
        printf("connection failed\n");
        return;
    }
    
    // end_serverfd, 즉 proxy와 end_server간의 데이터를 쓰고 읽는 공간에 RIO 패키지를 연결해, RIO 방식으로 proxy와 end_server간 RIO 패키지를 이용해 데이터를 읽고 쓸 수 있도록 만든다.
    Rio_readinitb(&server_rio, end_serverfd);
    
    // proxy에서 end_server로 데이터를 보내는 부분
    Rio_writen(end_serverfd, endserver_http_header, strlen(endserver_http_header));         // endserver_http_header에서 end_serverfd로 endserver_http_header만큼의 바이트를 전송한다.
    
    size_t n;
    while((n = Rio_readlineb(&server_rio, buf, MAXLINE)) != 0) {                            // end_server의 응답 한줄 한줄을 buf에 저장한다. 한줄이 0바이트이면, 즉 다 읽었으면 while문을 빠져나간다. end_server의 응답을 proxy가 받은 셈이다.
        printf("proxy received %d bytes, then send\n", n);                                  // 한줄 한줄 얼마만큼의 바이트를 읽었는지 출력시킨다.
        Rio_writen(connfd, buf, n);                                                         // proxy는 end_server로부터 받은 응답 한줄인 buf를, client와 proxy가 연결된 공간(소켓)인 connfd을 통해 client에게 데이터를 전송한다.
    }
    
    Close(end_serverfd);                                                                    // end_server로부터 원하는 데이터를 다 받았으니 proxy와 end_server간의 공간인 end_serverfd를 닫는다.
}

// doit 함수에서 build_http_header를 사용하는 부분 : build_http_header(endserver_http_header, hostname, path, port, &rio);
// requestlint_hdr_format은 static const char *requestlint_hdr_format = "GET %s HTTP/1.0 \r\n"으로 선언되었다.
void build_http_header(char *http_header, char *hostname, char *path, int port, rio_t *client_rio) {
    char buf[MAXLINE], request_hdr[MAXLINE], other_hdr[MAXLINE], host_hdr[MAXLINE];
    
    // path는 /home.html같은 모양
    sprintf(request_hdr, requestlint_hdr_format, path);                                     // requestlint_hdr_format의 %s에 path가 들어가고, 그렇게 완성된 requestlint_hdr_format이 reqeust_hdr이 된다.
    
    while (Rio_readlineb(client_rio, buf, MAXLINE) > 0) {                                   // 여기서 client_rio는, client와 proxy 사이의 공간인 connfd와 연결되어있다. 즉, 클라이언트가 보낸 데이터 한줄 한줄을 buf에 담게 된다.
        
        // 응답 헤더를 다 읽으면 읽는 동작 break
        if (strcmp(buf, endof_hdr) == 0) {                                                  // endof_hdr = "\r\n"이다. 즉 buf가 "\r\n"이면 client의 요청 헤더 끝줄이라는 뜻이므로 읽는 것을 그만하도록 한다.
            break;
        }
        
        // host만 있는 host header를 따로 저장
        if (!strncasecmp(buf, host_key, strlen(host_key))) {                                // host_key = "Host"였다. 대소문자 구분 없이 n만큼 buf와 host_key를 비교한다. Host: ~~~~라는 부분을 만났을 때, 앞이 Host가 맞으면 buf의 문자열을 host_hdr에 복사한다.
            strcpy(host_hdr, buf);                                                          // ex. Host : 123.123.123.123에서 앞 글자가 Host(host, HoSt라도 상관없다)이니 해당 if 조건을 만족한다.
            continue;                                                                       // continue이므로 아래 코드를 읽는 것이 아니라 다시 while문으로 간다.
        }
        
        // 다른 헤더들 저장
        if(strncasecmp(buf,connection_key,strlen(connection_key)) &&strncasecmp(buf,proxy_connection_key,strlen(proxy_connection_key)) &&strncasecmp(buf,user_agent_key,strlen(user_agent_key))) {
            strcat(other_hdr, buf);
        }        
    }
    
    // 만약 client가 따로 host header를 날리지 않았다면 인가?
    // static const char *host_hdr_format = "Host: %s\r\n";
    if (strlen(host_hdr) == 0) {
        sprintf(host_hdr, host_hdr_format, hostname);
    }
    
    // http_header는 doit 함수에서 endserver_http_header였다. 즉, endserver_http_header를 구성한다.
    // request : METHOD PATH HTTP 버전
    // host : 서버 주소 + 포트 번호
    sprintf(http_header,"%s%s%s%s%s%s%s",
            request_hdr,
            host_hdr,
            conn_hdr,
            prox_hdr,
            user_agent_hdr,
            // 이 위에는 순서가 중요하다. 하지만 밑에는 변동될 수 있다고 한다. 따라서 그냥 other로 퉁친다.
            other_hdr,
            endof_hdr);
            
    return;
}

// proxy와 end_server를 연결한다.
inline int connect_endServer(char *hostname, int port, char *http_header) {
    char portStr[100];
    sprintf(portStr, "%d", port);
    return Open_clientfd(hostname, portStr);
}

// path는 tiny의 filename이다.
void parse_uri(char *uri, char *hostname, char *path, int *port) {
    *port = 80;                                                                             // HTTP 기본 포트 설정 값
    char* pos = strstr(uri, "//");
    
    pos = ((pos != NULL) ? (pos + 2) : (uri));                                              // pos가 NULL이 아니라면 pos는 // 바로 뒤를 가리키게 된다. NULL이면 uri의 처음을 가리키게 된다.
    
    char *pos2 = strstr(pos, ":");
    
    //ex. localhost:5000/home.html
    if (pos2 != NULL) {
        *pos2 = '\0';
        sscanf(pos, "%s", hostname);
        sscanf(pos2 + 1, "%d%s", port, path);
        
    // port 번호를 입력안해줬으면 80을 기본값으로 해준다.
    } else {
        pos2 = strstr(pos, "/");
        if(pos2 != NULL)
        {
            *pos2 = '\0';
            sscanf(pos, "%s", hostname);
            *pos2 = '/';
            sscanf(pos2, "%s", path);
        }
        else
        {
            sscanf(pos, "%s", hostname);
        }
    }
    
    return;
}

코치님 말씀

SOCK_STREAM(TCP) 말고 dgram(UDP) 등이 있다.

일부 HTTP를 구현한 셈이다.

소켓 인터페이스를 익히다보면 flask 등을 통해 서버를 돌릴 때, listen을 때리고 돌리는 등을 이해할 수 있다.

Fork()를 통해 1개가 2개의 프로세스로 쪼개진다. 부모, 자식 프로세스 둘다 쓰레드가 1개씩 있다.

계속 새로운 요청이 오면 오히려 response 시간이 늘어나게 된다.

컨텐츠가 바뀌면 캐시를 이용하는 이유가 사라진다. 현지 시간을 리턴하는 등.

하루를 마치고

정말 사람은 적응의 동물인 것 같다. 나도 할 수 있다는 것을 느낀다. 다른 사람들이 막 구현을 해 나갈 때마다, 되도록 흔들리지 않고 나는 나의 길을 갔다. '개념 이해부터 하고 구현을 해보자!'. 오히려 이제 내가 물어보는 것이 아니라 가르쳐줄 기회도 늘어났다.

협력사 강연을 지난 화요일과 오늘 듣기도 했는데, 다시 한 번 열정을 일으켜주었다. 내가 왜 퇴사했고, 지금 어디에 집중해야 되고, 무엇을 해야 하는지 다시 한 번 마음을 잡을 수 있었다.

profile
메타몽 닮음 :) email: alohajune22@gmail.com

0개의 댓글