[c언어] Proxy Lab :: Proxy Server 구현하기 (Sequential proxy, Concurrent proxy, Caching)

이주희·2023년 4월 20일
3

C언어

목록 보기
6/9
post-thumbnail

🔎 Proxy Lab

🖥 Github에서 전체 코드 보기

Carnegie Mellon University의 Proxy Lab 과제

  • Web Proxy는 웹 브라우저와 end server 사이에서 중간자 역할을 하는 프로그램이다.
  • 웹 페이지를 가져오기 위해서 브라우저가 end server에 직접 연결하는 대신에, proxy server에 연결하여 요청을 전달할 수 있다.
  • end server가 proxy server에 응답을 하면, proxy server가 응답을 브라우저에 전달한다.

🤔 Proxy의 역할

  • Firewall: 프록시는 방화벽 외부에 있는 브라우저가 종단 서버에 접근할 때 프록시를 통해서만 접근이 가능하게 만들어주는 중개자 역할을 할 수 있다.
    👉🏻 이를 통해 내부 네트워크는 외부로부터 보호될 수 있다.
  • Anonymizers: 클라이언트가 프록시를 통해 요청을 보낼 경우, 프록시는 요청에서 모든 식별 정보(ex: IP 주소 등)을 제거하고 익명으로 변경할 수 있다.
    👉🏻 클라이언트가 직접 서버와 통신하면, 클라이언트의 IP 주소가 서버에 노출되므로, 이를 악용하여 클라이언트를 추적하거나 공격할 수 있다.
  • Cache: 웹 객체를 캐싱할 수 있다.
    👉🏻 이를 통해 클라이언트가 이전에 요청한 웹 객체를 다시 요청할 경우, 프록시는 원격 서버에 재요청하는 대신에 캐시된 객체를 바로 제공함으로써 웹 페이지 로딩 시간을 줄일 수 있다.

요구사항

1) Implementing a sequential web proxy

  • 프록시를 구성하여 들어오는 연결을 수락하고, 요청을 읽고 구문을 분석한다.
  • 웹 서버에 요청을 전달하고 서버의 응답을 읽고 해당 클라이언트에게 응답을 전달한다.

🌟 목적) HTTP 동작 및 소켓을 사용하여 네트워크 연결을 통신하는 프로그램을 작성하는 방법을 배우게 된다.

2) Dealing with multiple concurrent requests

  • 다중 동시 연결을 처리할 수 있는 프록시로 업그레이드한다.

🌟 목적) 동시성 처리에 대한 이해를 높이게 되며, 시스템에서 중요한 개념 중 하나인 동시성을 다루는 방법을 배운다.

3) Caching web objects

  • 최근 액세스한 웹 콘텐츠의 간단한 메인 메모리 캐시를 사용하여 프록시에 캐싱을 추가한다.

추가 요구사항

  • 소켓 입력 및 출력을 위해 표준 I/O 함수를 사용하는 것은 문제가 될 수 있으므로 Robust I/O(RIO) 패키지를 사용한다. (csapp.c에 내장되어 있다.)
  • 모듈화 등을 고려하여 모든 파일을 수정할 수 있다.
    예를 들어 cache 기능을 구현할 때에는 모듈성을 고려하여 cache.c 및 cache.h 파일과 같은 라이브러리를 만들 수 있으며, 그럴 경우 Makefile도 수정해야 한다.
  • 프록시 서버는 SIGPIPE(닫힌 소켓에 데이터 보냈을때 발생) 시그널을 무시해야 한다. (928p 참고)
  • 웹에서 전송되는 모든 콘텐츠가 ASCII 텍스트가 아니다.
    네트워크 I/O와 바이너리 데이터(ex: 이미지, 동영상 등의 이진 데이터)를 다룰 때는 이진 데이터를 처리하기 위한 적절한 함수를 사용해야 한다.
  • 원래 요청이 HTTP/1.1인 경우에도 모든 요청은 HTTP/1.0으로 전달되어야 한다.

📝 테스트 하는 방법

  • 우분투 터미널에서 자체 테스트 (tiny와 proxy 프로그램을 각 포트에서 실행시킨 상태에서 테스트한다.)
    # 형식
    curl --proxy {http://localhost:proxy포트/path} {http://localhost:tiny포트/path}
    
    # 예시
    curl --proxy http://localhost:7000/ http://localhost:8000/
  • 과제에서 주어진 자동 테스트
    # 테스트를 위한 툴 설치 (한 번만 하면 됨)
    sudo apt install net-tools
    
    # 테스트 수행 명령어 (🔥tiny 상위 폴더에서🔥 입력해야 함)
    ./dirver.sh

1. Implementing a sequential web proxy

😃 HTTP/1.0 GET 요청을 처리하는 sequential proxy를 구현한다.

💡 Sequential Proxy 개요

  • 지정된 포트 번호에서 들어오는 연결을 수신한다.
  • 연결이 수립되면, 클라이언트로부터 전송된 요청(request)을 읽고 파싱한다.
  • 클라이언트가 유효한 HTTP 요청을 보냈는지 여부를 확인한 후, 웹 서버와의 연결을 수립하고 클라이언트가 지정한 object를 요청한다.
  • 서버의 응답을 읽고 클라이언트로 전달한다.

1) HTTP/1.0 GET requests

◾️ Request 수신

◾️ URI Parsing

  • 이 요청을 아래와 같이 파싱한다.
    • 호스트 네임 : www.cmu.edu
    • 쿼리, path 등: /hub/index.html

◾️ Request 전송

  • 프록시 서버는 1) www.cmu.edu와의 연결을 열고, 2) 아래와 같은 HTTP 요청을 보낸다.
    GET /hub/index.html HTTP/1.0

2) 필수 Request headers 목록

◾️ Host header

  • Host header : 요청하는 호스트의 이름이나 IP 주소
  • 항상 Host header를 보내야 한다.
  • 브라우저가 이미 Host header를 포함한 요청을 보낸 경우에는 그대로 전달한다.

◾️ User-Agent header

  • User-Agent header : 클라이언트 소프트웨어의 정보
  • 아래와 같은 User-Agent 문자열을 한 줄로 전달한다.
    User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
  • User-Agent를 통해 서버는 적절한 콘텐츠를 제공하거나 화면을 최적화하여 보여줄 수 있다.
  • 위 User-Agent는 클라이언트가 Mozilla Firefox 브라우저를 사용하는 클라이언트로 인식하게 하며, 이를 통해 더욱 적절한 콘텐츠를 제공할 수 있다.

◾️ Connection/Proxy-Connection header

  • Connection header : Connection의 유형
  • Proxy-Connection header : ****프록시 서버와 웹 서버 간의 Connection 유형
  • 이 헤더는 연결 유지와 관련된 정보를 전달한다.
    각 요청마다 새로운 연결을 열도록 "close"로 지정한다.
💡 **각 요청마다 새로운 연결을 여는 이유**
  • 리소스 사용을 효율적으로 관리하기 위함이다.
  • 이 방법을 사용하면 프록시 서버가 요청 후에 연결을 즉시 끊게 되어 클라이언트와 서버 사이의 연결 수가 최소화된다.
  • HTTP 프로토콜에서 연결을 유지하는 것은 일반적으로 네트워크 리소스와 서버 자원을 많이 사용하게 된다.
  • 각각의 요청마다 새로운 연결을 열게 하면 각 요청이 완료될 때 각 리소스가 즉시 해제될 수 있다.
  • 따라서, 이전 연결을 계속해서 재사용하는 것보다 더욱 효율적인 관리가 가능하다.
  • 반면, 헤더 값으로 "keep-alive"를 지정하면, 웹 서버와의 연결을 유지하고 다음 요청에서 동일한 연결을 재사용할 수 있다.

3) Port numbers

◾️ HTTP 요청 포트

  • HTTP 요청 포트는 URL에 선택적으로 포함된다.
  • URL에 포트가 지정되어 있으면 요청에 지정된 포트에 연결해야 한다.

◾️ 프록시 서버의 수신 포트

  • 프록시 서버가 들어오는 연결을 수신할 포트이다.
  • 프록시 서버의 포트 번호는 명령 줄 인자로 수신 대기 포트 번호가 지정된다.
  • 예를 들어, 아래의 경우에는 15213번 포트에서 프록시 서버가 연결을 수신한다.
    linux> ./proxy 15213

Sequential Proxy 구현

📝 설계

1) Client의 요청을 수신한다.

  • 요청 라인에서 hostname과 port 및 path를 파악한다.

    • parse_uri 함수에서 http://www.cmu.edu:3012/hub/index.html 이렇게 생긴 uri를
      hostname, port, path로 나눠지도록 파싱한다.
    • uri에 port가 없을 수도 있다. 그럴 경우 기본 port인 80으로 지정한다.
    • 요청 라인에 HTTP version이 1.1로 들어와도 end server에 보낼 때는 1.0으로 바꿔서 보낸다.
  • 🖥 Code

    void doit(int clientfd)
    {
    
    ...
    
      /* 1️⃣ -1) Request Line 읽기 [🙋‍♀️ Client -> 🚒 Proxy] */
      Rio_readinitb(&request_rio, clientfd);
      Rio_readlineb(&request_rio, request_buf, MAXLINE);
      printf("Request headers:\n %s\n", request_buf);
    
      // 요청 라인 parsing을 통해 `method, uri, hostname, port, path` 찾기
      sscanf(request_buf, "%s %s", method, uri);
      parse_uri(uri, hostname, port, path);
    
      // Server에 전송하기 위해 요청 라인의 형식 변경: `method uri version` -> `method path HTTP/1.0`
      sprintf(request_buf, "%s %s %s\r\n", method, path, "HTTP/1.0");
    
      // 지원하지 않는 method인 경우 예외 처리
      if (strcasecmp(method, "GET") && strcasecmp(method, "HEAD"))
      {
        clienterror(clientfd, method, "501", "Not implemented", "Tiny does not implement this method");
        return;
      }
    
    ...
    
    }
    // uri를 `hostname`, `port`, `path`로 파싱하는 함수
    // uri 형태: `http://hostname:port/path` 혹은 `http://hostname/path` (port는 optional)
    void parse_uri(char *uri, char *hostname, char *port, char *path)
    {
      // host_name의 시작 위치 포인터: '//'가 있으면 //뒤(ptr+2)부터, 없으면 uri 처음부터
      char *hostname_ptr = strstr(uri, "//") ? strstr(uri, "//") + 2 : uri;
      char *port_ptr = strchr(hostname_ptr, ':'); // port 시작 위치 (없으면 NULL)
      char *path_ptr = strchr(hostname_ptr, '/'); // path 시작 위치 (없으면 NULL)
      strcpy(path, path_ptr);
    
      if (port_ptr) // port 있는 경우
      {
        strncpy(port, port_ptr + 1, path_ptr - port_ptr - 1); 
        strncpy(hostname, hostname_ptr, port_ptr - hostname_ptr);
      }
      else // port 없는 경우
      {
        if (is_local_test)
          strcpy(port, "80"); // port의 기본 값인 80으로 설정
        else
          strcpy(port, "8000");
        strncpy(hostname, hostname_ptr, path_ptr - hostname_ptr);
      }
    }

2) 받은 요청을 End Server에 보낸다.

  • End Server 소켓 생성

    • 1번의 parse_uri 함수에서 얻은 hostname과 port로 소켓을 생성한다.
  • 요청 라인과 요청 헤더를 끝까지 읽으면서 end server에 전송한다.

  • 전달 받은 헤더가 요구사항에 맞는지 확인한다.

  • 🖥 Code

    void doit(int clientfd)
    {
    
    ...
    
      /* 1️⃣ -2) Request Line 전송 [🚒 Proxy -> 💻 Server] */
      // Server 소켓 생성
      serverfd = Open_clientfd(hostname, port);
      if (serverfd < 0)
      {
        clienterror(serverfd, method, "502", "Bad Gateway", "📍 Failed to establish connection with the end server");
        return;
      }
      Rio_writen(serverfd, request_buf, strlen(request_buf));
    
      /* 2️⃣ Request Header 읽기 & 전송 [🙋‍♀️ Client -> 🚒 Proxy -> 💻 Server] */
      read_requesthdrs(&request_rio, request_buf, serverfd, hostname, port);
    
    ...
    
    }
    // Request Header를 읽고 Server에 전송하는 함수
    // 필수 헤더가 없는 경우에는 필수 헤더를 추가로 전송
    void read_requesthdrs(rio_t *request_rio, void *request_buf, int serverfd, char *hostname, char *port)
    {
      int is_host_exist;
      int is_connection_exist;
      int is_proxy_connection_exist;
      int is_user_agent_exist;
    
      Rio_readlineb(request_rio, request_buf, MAXLINE); // 첫번째 줄 읽기
      while (strcmp(request_buf, "\r\n"))
      {
        if (strstr(request_buf, "Proxy-Connection") != NULL)
        {
          sprintf(request_buf, "Proxy-Connection: close\r\n");
          is_proxy_connection_exist = 1;
        }
        else if (strstr(request_buf, "Connection") != NULL)
        {
          sprintf(request_buf, "Connection: close\r\n");
          is_connection_exist = 1;
        }
        else if (strstr(request_buf, "User-Agent") != NULL)
        {
          sprintf(request_buf, user_agent_hdr);
          is_user_agent_exist = 1;
        }
        else if (strstr(request_buf, "Host") != NULL)
        {
          is_host_exist = 1;
        }
    
        Rio_writen(serverfd, request_buf, strlen(request_buf)); // Server에 전송
        Rio_readlineb(request_rio, request_buf, MAXLINE);       // 다음 줄 읽기
      }
    
      // 필수 헤더 미포함 시 추가로 전송
      if (!is_proxy_connection_exist)
      {
        sprintf(request_buf, "Proxy-Connection: close\r\n");
        Rio_writen(serverfd, request_buf, strlen(request_buf));
      }
      if (!is_connection_exist)
      {
        sprintf(request_buf, "Connection: close\r\n");
        Rio_writen(serverfd, request_buf, strlen(request_buf));
      }
      if (!is_host_exist)
      {
        sprintf(request_buf, "Host: %s:%s\r\n", hostname, port);
        Rio_writen(serverfd, request_buf, strlen(request_buf));
      }
      if (!is_user_agent_exist)
      {
        sprintf(request_buf, user_agent_hdr);
        Rio_writen(serverfd, request_buf, strlen(request_buf));
      }
    
      sprintf(request_buf, "\r\n"); // 종료문
      Rio_writen(serverfd, request_buf, strlen(request_buf));
      return;
    }

3) End Server가 보낸 응답을 받고 Client에 전송한다.

  • Response Header 읽기
    • 응답 헤더를 한 줄 한 줄 읽으면서 한 줄씩 바로 Client에 전송한다.
    • 응답 바디 전송에 사용하기 위해 Content-length 헤더를 읽을 때는 값을 저장해둔다.
  • Response Body 읽기
    • 응답 바디는 이진 데이터가 포함되어 있을 수 있으므로 한 줄씩 읽지 않고, 필요한 사이즈만큼 한번에 읽는다.
      • 응답 헤더에서 파악한 Content-length를 활용해 Content-length만큼 읽고, Client에 전송할 때에도 Content-length만큼 전송한다.

🔥 Response Body를 읽는 과정에서 발생한 이슈 readlineb vs readnb vs readn

  • 잘 작동되는 코드
            // Rio_readlineb 사용
            int n;
            while ((n = Rio_readlineb(&response_rio, response_buf, MAXLINE)) > 0)
            {
                Rio_writen(clientfd, response_buf, n)
            }
            
            /* 
            	Rio_readlineb의 실제 읽은 바이트 수인 `n`을 반환받고, 
            	Rio_writen으로 `n`만큼 데이터를 전송한다.
            */
            
            // Rio_readnb 사용
            Rio_readnb(&response_rio, response_buf, content_length);
            Rio_writen(clientfd, response_buf, content_length);
            
            /* 
            	Response Header에서 Content-length를 파싱해서 파악하고,
            	Content-length만큼 읽고 전송한다.
            
            	readline()이 아닌 read() 함수를 사용하면 바이트 단위로 데이터를 읽거나 쓸 수 있다.
            	read() 함수는 문자열의 끝을 나타내는 NULL 종료 문자열이 없는 이진 데이터를 처리할 수 있도록 설계되어 있다.
            */
  • 문제가 발생하는 코드
            // 이진 데이터 처리에 문제가 발생하는 코드
            while ((Rio_readlineb(&response_rio, response_buf, MAXLINE)) > 0)
            {
                Rio_writen(clientfd, response_buf, strlen(response_buf));
            }
            
            /* 
            	Rio_writen의 사이즈를 strlen(response_buf)로 구하고 있다.
            	strlen는 문자열의 길이를 구하는 함수로, NULL 종료 문자열이 나올 때까지 문자열의 길이를 계산한다.
            	이진 데이터는 NULL 종료 문자열을 사용하지 않아 response_buf의 길이를 정확히 계산하지 못한다.
            */
            
            // Rio_readnb 대신 Rio_readn 함수를 사용해서 문제가 발생하는 코드
            srcp = malloc(content_length);
            Rio_readn(serverfd, srcp, content_length);
            Rio_writen(clientfd, srcp, content_length);
            free(srcp);
            
            /*
            	Rio_readn() 함수는 버퍼를 사용하지 않는 함수이다.
            	(어쩐지,, 이 함수는 Rio_readnb()와 다르게 인자로 버퍼가 아닌 포인터를 받는다.)
            	이전에 serverfd에서 헤더의 내용을 읽은 시점부터 이어서 읽어야 바디의 내용을 잘 받아오는데,
            	Rio_readn() 함수는 버퍼를 사용하지 않기 때문에 읽은 지점을 파악하지 않고 
            	다시 처음부터 읽어오기 때문에 바디의 내용이 아닌 다시 처음(헤더)부터 읽어와서 문제가 발생한다.
            */
  • 🖥 Code
    void doit(int clientfd)
    {
    
    ...
    
      /* 3️⃣ Response Header 읽기 & 전송 [💻 Server -> 🚒 Proxy -> 🙋‍♀️ Client] */
      Rio_readinitb(&response_rio, serverfd);
      while (strcmp(response_buf, "\r\n"))
      {
        Rio_readlineb(&response_rio, response_buf, MAXLINE);
        if (strstr(response_buf, "Content-length")) // Response Body 수신에 사용하기 위해 Content-length 저장
          content_length = atoi(strchr(response_buf, ':') + 1);
        Rio_writen(clientfd, response_buf, strlen(response_buf));
      }
    
      /* 4️⃣ Response Body 읽기 & 전송 [💻 Server -> 🚒 Proxy -> 🙋‍♀️ Client] */
      response_ptr = malloc(content_length);
      Rio_readnb(&response_rio, response_ptr, content_length);
      Rio_writen(clientfd, response_ptr, content_length); // Client에 Response Body 전송
      free(response_ptr); // 캐싱하지 않은 경우만 메모리 반환
    
      Close(serverfd);
    }

4) Dealing with multiple concurrent requests

  • 동시에 여러 요청을 처리할 수 있도록 한다.

  • 🖥 Code

    int main(int argc, char **argv)
    {
    
    ...
    
      listenfd = Open_listenfd(argv[1]); // 전달받은 포트 번호를 사용해 수신 소켓 생성
      while (1)
      {
    
    	...
    
        Pthread_create(&tid, NULL, thread, clientfd); // Concurrent 프록시
      }
    }
    
    void *thread(void *vargp)
    {
      int clientfd = *((int *)vargp);
      Pthread_detach(pthread_self());
      Free(vargp);
      doit(clientfd);
      Close(clientfd);
      return NULL;
    }

2. Caching web objects

😃 프록시에 최근에 사용된 웹 객체를 메모리에 저장하는 캐시를 추가한다.

💡 Caching 개요

  • 프록시가 서버로부터 웹 객체를 받을 때, 객체를 클라이언트로 전송하는 동안 캐시에 저장한다.
  • 같은 객체를 요청하는 다른 클라이언트가 있다면, 서버에 다시 연결할 필요 없이 캐시된 객체를 재전송한다.

1) 캐싱 조건 (크기)

  • 캐시와 웹 객체의 크기 제한
    • 100 KiB를 초과하지 않는 웹 객체만 캐시한다.
    • 캐시에는 최대 1 MiB까지만 저장한다.
      • 캐시 크기를 계산할 때, 실제 웹 객체를 저장하는 데 사용된 바이트만 계산한다.
        (메타 데이터를 포함한 기타 불필요한 바이트는 무시한다.)

2) Eviction policy

  • 사용된 지 가장 오래된 객체(LRU: least-recently-used)를 삭제하는 정책을 사용한다.
  • 객체를 읽거나 캐시하는 두 가지 경우 모두 객체를 사용한 것으로 간주한다.
  • 연결 리스트 활용
    • 웹 객체가 사용될 때마다, 해당 객체의 노드를 연결 리스트의 앞쪽으로 이동시켜, 최근에 사용된 객체임을 나타낸다.
    • 새로운 객체를 캐시해야 하는데 캐시가 가득 찬 경우, 연결 리스트의 끝에 있는 사용된 지 가장 오래된 객체를 삭제한다.
    • 연결 리스트의 각 노드는 웹 객체, 이전 노드, 다음 노드 에 대한 포인터를 포함한다.

Caching web objects 구현

📝 설계

1) 요청 라인을 읽고 나서, 캐싱된 요청인지 확인한다.

  • 캐시 연결 리스트에 동일한 요청의 캐싱된 객체를 찾는다. find_cache()

  • 캐싱되어 있는 객체가 있다면, 캐싱된 객체를 Client에 전송한다. send_cache()

  • 사용한 웹 객체의 순서를 캐시 연결 리스트의 맨 앞 순서로 갱신한다. read_cache()

  • Server로 요청을 보내지 않고 통신을 종료한다.

  • 🖥 Code

    /* 캐시 구현에 필요한 변수, 함수 등 선언 */
    
    typedef struct web_object_t
    {
      char path[MAXLINE];
      int content_length;
      char *response_ptr;
      struct web_object_t *prev, *next;
    } web_object_t;
    
    web_object_t *find_cache(char *path);
    void send_cache(web_object_t *web_object, int clientfd);
    void read_cache(web_object_t *web_object);
    void write_cache(web_object_t *web_object);
    
    extern web_object_t *rootp;  // 캐시 연결리스트의 root 객체
    extern web_object_t *lastp;  // 캐시 연결리스트의 마지막 객체
    extern int total_cache_size; // 캐싱된 객체 크기의 총합
    
    #define MAX_CACHE_SIZE 1049000
    #define MAX_OBJECT_SIZE 102400
    void doit(int clientfd)
    {
    
    ...
    
      // 현재 요청이 캐싱된 요청(path)인지 확인
      web_object_t *cached_object = find_cache(path);
      if (cached_object) // 캐싱 되어있다면
      {
        send_cache(cached_object, clientfd); // 캐싱된 객체를 Client에 전송
        read_cache(cached_object);           // 사용한 웹 객체의 순서를 맨 앞으로 갱신
        return;                              // Server로 요청을 보내지 않고 통신 종료
      }
    
    ...
    
    }
    // 캐싱된 웹 객체 중에 해당 `path`를 가진 객체를 반환하는 함수
    web_object_t *find_cache(char *path)
    {
      if (!rootp) // 캐시가 비었으면
        return NULL;
      web_object_t *current = rootp;      // 검사를 시작할 노드
      while (strcmp(current->path, path)) // 현재 검사 중인 노드의 path가 찾는 path와 다르면 반복
      {
        if (!current->next) // 현재 검사 중인 노드의 다음 노드가 없으면 NULL 반환
          return NULL;
    
        current = current->next;          // 다음 노드로 이동
        if (!strcmp(current->path, path)) // path가 같은 노드를 찾았다면 해당 객체 반환
          return current;
      }
      return current;
    }
    // `web_object`에 저장된 response를 Client에 전송하는 함수
    void send_cache(web_object_t *web_object, int clientfd)
    {
      // 1️⃣ Response Header 생성 및 전송
      char buf[MAXLINE];
      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\r\n", buf, web_object->content_length); // 컨텐츠 길이
      Rio_writen(clientfd, buf, strlen(buf));
    
      // 2️⃣ 캐싱된 Response Body 전송
      Rio_writen(clientfd, web_object->response_ptr, web_object->content_length);
    }
    // 사용한 `web_object`를 캐시 연결리스트의 root로 갱신하는 함수
    void read_cache(web_object_t *web_object)
    {
      if (web_object == rootp) // 현재 노드가 이미 root면 변경 없이 종료
        return;
    
      // 1️⃣ 현재 노드와 이전 & 다음 노드의 연결 끊기
      if (web_object->next) // '이전 & 다음 노드'가 모두 있는 경우
      {
        // 이전 노드와 다음 노드를 이어줌
        web_object_t *prev_objtect = web_object->prev;
        web_object_t *next_objtect = web_object->next;
        if (prev_objtect)
          web_object->prev->next = next_objtect;
        web_object->next->prev = prev_objtect;
      }
      else // '다음 노드'가 없는 경우 (현재 노드가 마지막 노드인 경우)
      {
        web_object->prev->next = NULL; // 이전 노드와 현재 노드의 연결을 끊어줌
      }
    
      // 2️⃣ 현재 노드를 root로 변경
      web_object->next = rootp; // root였던 노드는 현재 노드의 다음 노드가 됨
      rootp = web_object;
    }

2) Client에 응답을 전달할 때, 캐싱 가능한 크기라면 캐시 연결 리스트에 추가한다.

  • Request header에서 파싱해서 얻어낸 Content-length가 캐싱 가능한 최대 웹 객체의 크기보다 작거나 같다면, 웹 객체 구조체를 생성해 캐시 연결 리스트에 추가한다.

  • 연결 리스트에 추가하는 과정에서 최대 캐시 크기를 초과하게 된다면, 사용한 지 가장 오래된 웹 객체부터 캐시 리스트의 용량이 충분할 때까지 제거한다.

  • 🖥 Code

    void doit(int clientfd)
    {
    
    ...
    
      /* 4️⃣ Response Body 읽기 & 전송 [💻 Server -> 🚒 Proxy -> 🙋‍♀️ Client] */
      response_ptr = malloc(content_length);
      Rio_readnb(&response_rio, response_ptr, content_length);
      Rio_writen(clientfd, response_ptr, content_length); // Client에 Response Body 전송
    
      if (content_length <= MAX_OBJECT_SIZE) // 캐싱 가능한 크기인 경우
      {
        // `web_object` 구조체 생성
        web_object_t *web_object = (web_object_t *)calloc(1, sizeof(web_object_t));
        web_object->response_ptr = response_ptr;
        web_object->content_length = content_length;
        strcpy(web_object->path, path);
        write_cache(web_object); // 캐시 연결 리스트에 추가
      }
      else
        free(response_ptr); // 캐싱하지 않은 경우만 메모리 반환
    
      Close(serverfd);
    }
    // 인자로 전달된 `web_object`를 캐시 연결리스트에 추가하는 함수
    void write_cache(web_object_t *web_object)
    {
      // total_cache_size에 현재 객체의 크기 추가
      total_cache_size += web_object->content_length;
    
      // 최대 총 캐시 크기를 초과한 경우 -> 사용한지 가장 오래된 객체부터 제거
      while (total_cache_size > MAX_CACHE_SIZE)
      {
        total_cache_size -= lastp->content_length;
        lastp = lastp->prev; // 마지막 노드를 마지막의 이전 노드로 변경
        free(lastp->next);   // 제거한 노드의 메모리 반환
        lastp->next = NULL;
      }
    
      if (!rootp) // 캐시 연결리스트가 빈 경우 lastp를 현재 객체로 지정
        lastp = web_object;
    
      // 현재 객체를 루트로 지정
      if (rootp)
      {
        web_object->next = rootp;
        rootp->prev = web_object;
      }
      rootp = web_object;
    }
profile
🍓e-juhee.tistory.com 👈🏻 이사중

0개의 댓글