[c언어] Proxy Lab :: Proxy Server 구현하기 (Sequential proxy, Concurrent proxy, Caching)
2023. 4. 20. 23:21ㆍC언어
728x90
반응형
🔎 Proxy Lab
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 수신
- 브라우저에서 ‘http://www.cmu.edu/hub/index.html’과 같은 URL을 입력하면, 아래와 같은 HTTP 요청을 받게 된다.
GET http://www.cmu.edu/hub/index.html HTTP/1.1
◾️ 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"로 지정한다.
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;
}
728x90
반응형
'C언어' 카테고리의 다른 글
[c언어] Malloc Lab :: 동적 할당기 구현하기 (Implicit List, Explicit List, Segregated List, Buddy System) (2) | 2023.04.13 |
---|---|
[c언어] Red-Black Tree 구현하기 (0) | 2023.04.06 |
MacOS에서 Ubuntu 개발 환경 설치하기 (AWS, VSC) (0) | 2023.03.31 |