이 전 포스팅에서 캐시 기능을 적용하기 위해 각종 함수들을 구현해 봤다. 이제 마지막으로 구현했던 함수들을 우리가 기존에 구현했던 코드에 통합하는 과정을 수행해 보겠다. 캐시와 관련된 함수들이 궁금하면 이전 포스팅을 참고하라
2025.05.05 - [크래프톤 정글] - [WebProxy-Lab] proxy 서버 구현하기 Part.4 - 캐시 기능: 구현
[WebProxy-Lab] proxy 서버 구현하기 Part.4 - 캐시 기능: 구현
이제 본격적으로 캐시 기능을 구현해 보려고 한다. 캐시 기능을 구현하기 위해 cache.c, cache.h 를 만들어 별도로 분리를 해도 될 것이며 그냥 proxy.c에 전부 담아서 개발을 진행해도 된다. 일단은 pro
www.gowoong.com
통합
통합에 있어 많은 함수들을 수정할 필요는 없다. 우리는 오직 doit() 이 딱 함수만 수정에 들어가고 추가로 main 함수에 한 줄만 추가하면 된다.
먼저 기존의 코드를 확인하겠다.
void doit(int fd){
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char host_header[MAXLINE], other_header[MAXLINE];
char hostname[MAXLINE], port[MAXLINE], path[MAXLINE];
char reqest_buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE);
printf("Request headers:\n");
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET") != 0){
clienterror(fd, method, "501", "Not implemented", "This Server does not implement this method");
return;
}
read_requesthdrs(&rio, host_header, other_header);
parse_uri(uri, hostname, port, path);
int servedf = Open_clientfd(hostname, port);
reassemble(reqest_buf, path, hostname, other_header);
Rio_writen(servedf, reqest_buf, strlen(reqest_buf));
forward_response(servedf, fd);
}
1. main() 함수에 cache_init() 추가
먼저 main 함수 내부에서 cache_init() 함수를 호출하도록 추가하라 이 코드는 서버 시작 전 전역 캐시로 선언한 cache를 초기화하도록 설정한다.
2. doit() 함수에 사용 로직 추가
- cache_find() 호출 → 캐시 hit 시 바로 응답 후 리턴
- 캐시 miss 시 서버에 요청 → 응답을 읽으면서 버퍼에 저장
- 전체 응답 크기가 MAX_OBJECT_SIZE 이하일 경우 cache_insert() 호출
/* 캐시 관련 함수 추가 */
char cache_buf[MAX_OBJECT_SIZE];
int cache_size;
if (cache_find(&cache, uri, cache_buf, &cache_size)) {
Rio_writen(fd, cache_buf, cache_size);
return;
}
위 코드를 uri를 parsing 하고 목적지 서버와 소켓 연결을 맺기 전인 즉 int servedf = Open_clientfd(hostname, port); 전에 추가하면 된다.
- 목적지 서버와 먼저 연결을 수행 하기 전에 캐시에 저장된 데이터가 있는지 먼저 확인하고 캐시 히트 면 데이터를 반환하고 return 한다.
- 캐시 미스 상태이면 목적지 서버와 통신을 진행해야 하니 다음으로 넘어간다.
3. forward_response() 함수 제거 및 캐시 버퍼 추가
- 캐시 삽입 여부를 판단하려면 바이트 단위로 응답을 수집해야 하므로, 직접 Rio_readnb() 반복문을 doit() 안에 넣어 처리
- 기존에 doit() 내부에서 forward_response()를 호출하던 로직을 제거하고 doit() 내부에서 처리하도록 수정한다.
- temp_cache[MAX_OBJECT_SIZE]를 이용해 캐시에 저장할 응답 누적
char response_buf[MAXBUF];
char temp_cache[MAX_OBJECT_SIZE];
int total_size = 0;
rio_t server_rio;
Rio_readinitb(&server_rio, servedf);
ssize_t n;
while ((n = Rio_readnb(&server_rio, response_buf, MAXBUF)) > 0) {
Rio_writen(fd, response_buf, n);
if (total_size + n <= MAX_OBJECT_SIZE) {
memcpy(temp_cache + total_size, response_buf, n);
}
total_size += n;
}
Close(servedf);
if (total_size <= MAX_OBJECT_SIZE) {
cache_insert(&cache, uri, temp_cache, total_size);
}
캐시 저장용 버퍼 누적
기존 forward_response의 기능들을 doit() 함수와 통합을 하고 추가로 조건문을 추가한다.
if (total_size + n <= MAX_OBJECT_SIZE) {
memcpy(temp_cache + total_size, response_buf, n);
}
total_size += n;
- 지금까지 누적한 크기 + 이번에 읽은 바이트가 MAX_OBJECT_SIZE 이하면:
- temp_cache 버퍼에 이번에 읽은 데이터를 복사
- total_size를 누적 증가
즉 캐시에 넣을 수 있는 크기인지 판단하는 로직이다.
서버 응답 완료 후 캐시에 삽입
Close(servedf);
if (total_size <= MAX_OBJECT_SIZE) {
cache_insert(&cache, uri, temp_cache, total_size);
}
- 서버 연결 종료
- 만약 전체 응답 크기가 MAX_OBJECT_SIZE보다 작거나 같으면:
- 캐시에 삽입
이때 cache_insert() 함수는 내부적으로 LRU 정책을 적용해 오래된 항목을 제거하고 새 응답을 저장한다.
이렇게 해서 캐시에 대한 모든 구현을 마쳤다. 이제 실제 테스트에 통과하는지 확인을 해야 한다.
make clean && make
빌드를 진행하고 문제없이 완료되는지 확인한다.
./driver.sh
테스트를 위해 driver.sh를 실행한다. 그러면 채점을 시작할 것이다.
*** Basic ***
Starting tiny on 8993
Starting proxy on 12728
1: home.html
Fetching ./tiny/home.html into ./.proxy using the proxy
Fetching ./tiny/home.html into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
2: csapp.c
Fetching ./tiny/csapp.c into ./.proxy using the proxy
Fetching ./tiny/csapp.c into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
3: tiny.c
Fetching ./tiny/tiny.c into ./.proxy using the proxy
Fetching ./tiny/tiny.c into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
4: godzilla.jpg
Fetching ./tiny/godzilla.jpg into ./.proxy using the proxy
Fetching ./tiny/godzilla.jpg into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
5: tiny
Fetching ./tiny/tiny into ./.proxy using the proxy
Fetching ./tiny/tiny into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
Killing tiny and proxy
basicScore: 40/40
*** Concurrency ***
Starting tiny on port 4369
Starting proxy on port 7157
Starting the blocking NOP server on port 1860
Trying to fetch a file from the blocking nop-server
Fetching ./tiny/home.html into ./.noproxy directly from Tiny
Fetching ./tiny/home.html into ./.proxy using the proxy
Checking whether the proxy fetch succeeded
Success: Was able to fetch tiny/home.html from the proxy.
Killing tiny, proxy, and nop-server
concurrencyScore: 15/15
*** Cache ***
Starting tiny on port 11683
Starting proxy on port 14082
Fetching ./tiny/tiny.c into ./.proxy using the proxy
Fetching ./tiny/home.html into ./.proxy using the proxy
Fetching ./tiny/csapp.c into ./.proxy using the proxy
Killing tiny
Fetching a cached copy of ./tiny/home.html into ./.noproxy
Success: Was able to fetch tiny/home.html from the cache.
Killing proxy
cacheScore: 15/15
totalScore: 70/70
그리고 가장 마지막에 점수를 확인하면 70 점으로 모두 통과한 것을 볼 수 있다.
회고 및 개선 방안 탐색
이렇게 크래프톤 정글 8주 차 목표인 WebProxy-Lab의 구현이 마무리 되었다. 사실 개인적으로는 이번 8주차 내용이 훨씬 재미가 있었다 기존에 2년 4개월 가까이 백엔드 개발자로 근무하면서 웹 서버인 nginx를 개인적으로 많이 다뤘었는데 nginx도 C로 구현되었다는 것을 확인하는 것도 재미있었다. 그리고 그간 리버스 프록시를 많이 다뤘는데 이번에 포워드 프록시도 다뤄보면서 그 개념을 다질 수 있었다.
아직 주차가 끝이 나지 않았으니 가능하다면 PDF에서 요구하던 O(1)에 근접하게 삽입/탐색/삭제를 가능하도록 해야 한다는 요구사항을 구현하기 위해 해시맵을 적용해 보는 것도 좋을 것 같다. 다만 해시키 충돌을 해결하기 위해 고난이 예상된다.
마지막으로 이번 주차 역시 쉽지는 않았다. 어떻게 구현해야 할지 각종 개념과 아이디어는 잡을 수 있었는데 구현을 할 때 막히는 부분이 많아 부분적으로 GPT의 도움(캐시 기능은 특히 많았다)을 받았다. 그래도 완전히 GPT에 의존하지 않고 구현 아이디어를 생각해 보고 전체적인 틀을 잡고 구현해 보려고 노력해 보며 재미있게 구현을 이어나갈 수 있었다.