[WebProxy] 캐시 기능이 추가된 동시성 프록시 서버 단계별 구현 및 테스트하기

2025. 5. 7. 11:24·크래프톤 정글/Code 정글(C언어)

캐시 기능이 추가된 동시성 프록시 서버 단계별 구현 및 테스트하기

동시성 프록시 서버 구현을 마쳤다면, 이제 대망의 캐시 기능을 넣어볼 차례입니다. 주변에서 캐시 구현이 그렇게 어렵다는 소리를 듣고 겁을 먹으신 분도 계실 것 같은데요. 하지만 이제껏 해왔던 것처럼 한 단계씩 나아가다 보면 어느새 캐시라는 목적지에 다다를 수 있을 겁니다.

 

이번 글에서는 스레드 풀 기반 프록시 서버에 Lazy LRU 기반 캐시 기능을 결합하는 과정을 단계별로 소개해봅니다. 캐시 구조 설계부터 캐시 검색, 삽입/삭제, 실제 통합까지 4단계로 나눠 설명합니다.

 

 

캐시 기능이 추가된 동시성 프록시 서버 단계별로 구현하기

0단계: 목표 정리하기

  • 동시성 프록시 서버에 캐시 기능 추가하기
  • 곧, 중복된 URI 요청에 대해 응답을 재사용할 수 있게 만들기
  • Lazy LRU 기반으로 캐시 관리하기

Lazy LRU란?

Lazy LRU는 일반적인 LRU(Least Recently Used)와 달리, 접근할 때마다 노드의 위치를 바로 갱신하지 않는 전략입니다. 대신, 캐시 적중 시에는 단순히 재사용만 하고 LRU 순서(가장 오래된 항목부터 제거)는 삽입 시점 기준으로 유지합니다.

 

Lazy LRU는 접근 시 리스트 순서를 바꾸지 않기 때문에, 여러 스레드가 동시에 캐시를 읽을 수 있는데요. 곧, 캐시를 검색할 때에는 reader-writer lock을 활용해 동시 읽기 허용(read-shared)이 가능해집니다.

 

 

1단계: 캐시 구조체 설계

목표: 캐시 자료구조 2종 정의하기

  • cache_node_t
    • 각 요청(URI)에 대한 응답 데이터를 저장
    • 이중 연결 리스트의 노드 역할
  • cache_t
    • 전체 캐시 상태를 추적 (head, tail, 총 크기)
    • 읽기 병렬성을 위한 read-write 락 사용

 

1-1. 캐시 노드 구조체 (cache_node_t)

요청된 URI와 해당 응답 데이터를 저장하며, LRU 캐시 관리를 위한 이중 연결 리스트의 노드 역할을 하는 구조체입니다.

typedef struct cache_node {
    char *uri;                 // 요청된 URI
    char *data;                // 응답 데이터 전체
    int size;                  // 응답 크기

    struct cache_node *prev;  // 이전 노드
    struct cache_node *next;  // 다음 노드
} cache_node_t;

1-2. 전체 캐시 관리 구조체 (cache_t)

전체 캐시의 상태를 관리하며, Lazy LRU 순서를 유지하기 위해 이중 연결 리스트의 앞뒤 포인터와 총 크기, 동기화를 위한 read-write 락을 포함한 구조체입니다.

typedef struct {
    cache_node_t *head;         // 가장 최근 노드 (앞)
    cache_node_t *tail;         // 가장 오래된 노드 (뒤)
    int total_size;             // 현재 캐시 총 크기

    pthread_rwlock_t lock;      // 동기화용 read-write lock
} cache_t;

1-3. 캐시 초기화 함수 (cache_init)

캐시의 초기 상태를 설정하고, 동기화를 위한 read-write 락을 초기화하는 함수입니다.

void cache_init(cache_t *cache) {
    cache->head = NULL;
    cache->tail = NULL;
    cache->total_size = 0;
    pthread_rwlock_init(&cache->lock, NULL);
}

 

 

2단계: 캐시 검색 기능 구현하기

목표: 캐시 검색 함수 구현 (find_cache)

  • 요청한 URI가 캐시에 있는지 검색
  • 캐시에 해당 URI가 있으면 그 노드를 반환
  • 없으면 NULL 반환 → 이후 함수 밖에서 원 서버에 요청

 

캐시 검색 함수 (find_cache)

캐시에 저장된 URI 목록을 순회하여 요청한 URI가 있는지 확인하고, 있으면 해당 노드를 반환하는 함수입니다.

cache_node_t *find_cache(cache_t *cache, const char *uri) {
    // 읽기 전용 접근이므로 read lock 획득 (다른 reader 동시 접근 가능)
    pthread_rwlock_rdlock(&cache->lock);

    cache_node_t *node = cache->head;

    // 캐시 리스트를 앞(head)부터 순회
    while (node) {
        // 요청한 URI와 일치하는 노드를 찾은 경우 (캐시 히트)
        if (strcmp(node->uri, uri) == 0) {
            pthread_rwlock_unlock(&cache->lock);	// read lock 해제
            return node;  							// 해당 노드 반환
        }
        node = node->next;  // 다음 노드로 이동
    }

    // 찾는 URI가 없는 경우 (캐시 미스)
    pthread_rwlock_unlock(&cache->lock);  // read lock 해제
    return NULL;  // NULL 반환
}

 

 

3단계 캐시 삽입, 초과 시 제거 기능 구현하기

목표: 초과 시 제거 기능(evict_cache)이 포함된 캐시 삽입 함수 구현 (insert_cache) 

  • 새로운 응답을 캐시에 저장
  • 가장 앞(head)에 삽입
  • 캐시 총 크기 초과 시, 공간 마련을 위해 맨 뒤(tail)에서부터 제거(evict)

 

3-1. 캐시 삽입 함수 (insert_cache)

새로운 URI-응답 데이터를 캐시에 추가하고, 필요 시 오래된 항목을 제거하여 캐시 용량을 조절하는 함수입니다.

 

참고로, data의 크기가 MAX_OBJECT_SIZE(전체 캐시 수용 가능 크기)보다 크면 아예 캐시에 저장하지 않아야 하는데요. 이를 insert_cache() 함수를 호출하기 전에 검사해야 합니다.

void insert_cache(cache_t *cache, const char *uri, const char *data, int size) {
    // 쓰기 작업이므로 writer 락 획득 (읽기/쓰기 모두 차단)
    pthread_rwlock_wrlock(&cache->lock);

    // 캐시 용량 초과 시, 오래된 노드를 제거하여 공간 확보
    while (cache->total_size + size > MAX_CACHE_SIZE) {
        evict_cache(cache);
    }

    // 새로운 캐시 노드 생성 및 초기화
    cache_node_t *node = Malloc(sizeof(cache_node_t));
    node->uri = Malloc(strlen(uri) + 1);
    strcpy(node->uri, uri);  // 요청 URI 저장

    node->data = Malloc(size);
    memcpy(node->data, data, size);  // 응답 데이터 복사
    node->size = size;

    // 새 노드를 캐시의 가장 앞(head)에 삽입
    node->prev = NULL;
    node->next = cache->head;
    if (cache->head)
        cache->head->prev = node;
    cache->head = node;

    // tail이 비어있다면 (첫 삽입이라면) tail도 설정
    if (cache->tail == NULL)
        cache->tail = node;

    // 캐시 총 크기 갱신
    cache->total_size += size;

    // 락 해제
    pthread_rwlock_unlock(&cache->lock);
}

3-2. 캐시 제거 함수 (evict_cache)

캐시에서 가장 오래된 항목을 제거하여 공간을 확보하는 함수입니다.

void evict_cache(cache_t *cache) {
    // 제거할 노드가 없으면 바로 반환
    if (cache->tail == NULL) return;

    cache_node_t *node = cache->tail;

    // 이전 노드가 있다면, tail 앞쪽 노드를 새로운 tail로 만들고
    // 현재 노드를 리스트에서 제거
    if (node->prev)
        node->prev->next = NULL;
    else
        // 노드가 하나뿐이었다면 head도 NULL로 초기화
        cache->head = NULL;

    // tail 포인터 갱신
    cache->tail = node->prev;

    // 캐시 총 크기에서 제거한 노드의 크기만큼 감소
    cache->total_size -= node->size;

    // 동적으로 할당한 데이터 해제
    free(node->uri);
    free(node->data);
    free(node);
}

 

 

4단계: 요청 처리 함수에 캐시 통합하기

목표: 캐시 기능을 요청을 처리하는 메인 루틴(func())에 통합

  • func()에서 URI로 캐시를 먼저 조회(find_cache)
  • 캐시에 해당 데이터가 있으면, Rio_writen()으로 클라이언트에 바로 전송하고 종료
  • 캐시에 해당 데이터가 없으면, 원 서버에 요청 후 응답을 insert_cache로 캐시에 저장
  • 받은 응답은 클라이언트에게 그대로 전송

 

변경된 메인 루틴 함수 (func)

클라이언트 요청을 처리하여 캐시에서 응답을 반환하거나, 원서버에 요청 후 응답을 전달하고 캐시에 저장하는 함수입니다.

 

참고로, 캐시의 URI 검색은 파싱되기 전의 원본 URI 문자열을 기준으로 이루어지기 때문에, 요청을 처리하는 과정에서 uri 변수가 parse_uri() 함수에 의해 수정되기 전에 이를 별도의 uri_key 변수에 복사해 두는 것이 중요합니다.

 

이렇게 복사한 uri_key는 캐시에서 검색하거나 새로운 캐시 노드를 삽입할 때 사용되어, URI가 파싱 중 손상되거나 변형되는 문제 없이 일관된 키로 활용될 수 있습니다.

void func(int connfd) {
    rio_t client_rio, server_rio;
    char buf[MAXLINE], req[MAX_OBJECT_SIZE];
    char method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char host[MAXLINE], port[10], path[MAXLINE];

    // 클라이언트로부터 받은 요청을 읽기 위해 rio 초기화
    Rio_readinitb(&client_rio, connfd);

    // 요청 라인을 읽지 못하면 종료
    if (!Rio_readlineb(&client_rio, buf, MAXLINE)) return;

    // 요청 라인에서 HTTP method, URI, version 파싱
    sscanf(buf, "%s %s %s", method, uri, version);

    // ** 캐시에 해당 URI가 있는지 확인 **
    cache_node_t *cached = find_cache(&cache, uri);
    if (cached) {
        // 캐시 히트 시, 클라이언트에게 바로 응답 반환
        Rio_writen(connfd, cached->data, cached->size);
        return;
    }

    // URI 백업 (원본은 파싱 중 손상 가능성 있음)
    char uri_key[MAXLINE];
    strcpy(uri_key, uri);

    // URI를 host, port, path로 파싱
    if (parse_uri(uri, host, port, path) < 0) return;

    // 서버로 보낼 요청 헤더 초기화
    sprintf(req, "GET %s HTTP/1.0\r\n", path);

    // 클라이언트 요청 헤더를 읽으며 필요한 것만 필터링하여 요청 문자열에 추가
    while (Rio_readlineb(&client_rio, buf, MAXLINE) > 0 && strcmp(buf, "\r\n") != 0) {
        if (strncasecmp(buf, "Host", 4) && strncasecmp(buf, "User-Agent", 10)
            && strncasecmp(buf, "Connection", 10) && strncasecmp(buf, "Proxy-Connection", 16)) {
            strcat(req, buf);
        }
    }

    // 필수 헤더를 명시적으로 삽입
    sprintf(buf, "Host: %s\r\n", host); strcat(req, buf);
    strcat(req, user_agent_hdr);
    strcat(req, "Connection: close\r\n");
    strcat(req, "Proxy-Connection: close\r\n\r\n");

    // 원서버에 연결 시도
    int serverfd = Open_clientfd(host, port);
    if (serverfd < 0) return;

    // 원서버에 요청 전송 준비
    Rio_readinitb(&server_rio, serverfd);
    Rio_writen(serverfd, req, strlen(req));

    int n, total_size = 0;
    char *object_buf = Malloc(MAX_OBJECT_SIZE); // 캐시에 저장할 응답 버퍼
    char *p = object_buf;

    // 응답 헤더 수신 및 전송
    while ((n = Rio_readlineb(&server_rio, buf, MAXLINE)) > 0) {
        Rio_writen(connfd, buf, n);  // 클라이언트로 전송
        if (total_size + n < MAX_OBJECT_SIZE) {
            memcpy(p, buf, n);       // ** 캐시 버퍼에 복사 **
            p += n;
            total_size += n;
        }
        if (strcmp(buf, "\r\n") == 0) break;  // 헤더 끝
    }

    // 응답 본문 수신 및 전송
    while ((n = Rio_readnb(&server_rio, buf, MAXBUF)) > 0) {
        Rio_writen(connfd, buf, n);	// 클라이언트로 전송
        if (total_size + n < MAX_OBJECT_SIZE) {
            memcpy(p, buf, n);		// ** 캐시 버퍼에 복사 **
            p += n;
            total_size += n;
        }
    }

    // ** 객체 크기가 캐시 가능한 범위라면 캐시에 저장 **
    if (total_size < MAX_OBJECT_SIZE)
        insert_cache(&cache, uri_key, object_buf, total_size);

    // 메모리 해제 및 서버 소켓 종료
    free(object_buf);	// 응답 버퍼 해제
    Close(serverfd);
}

전체 흐름 요약

  1. URI로 캐시 검색
  2. 적중 시: 바로 응답
  3. 미스 시: 원 서버로 요청
  4. 응답 수신 → 전송 + 캐시 저장

주의사항

  • 응답 크기가 MAX_OBJECT_SIZE 이상이면 캐시에 저장하지 않는다
  • object_buf 할당 후 free 를 꼭 호출해야 한다
  • cache_init()는 main()에서 반드시 한 번 호출해 캐시를 초기화해야 한다

 

최종 구현 코드

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

/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define NTHREADS 4
#define SBUFSIZE 16

typedef struct {
  int *buf;                     // connfd 저장 배열
  int front;                    // dequeue 인덱스
  int rear;                     // enqueue 인덱스
  int n;                        // 현재 들어있는 아이템 수

  pthread_mutex_t mutex;        // 큐 접근 mutex
  pthread_cond_t slots;         // 빈 슬롯이 생겼을 때 signal
  pthread_cond_t items;         // 아이템이 추가되었을 때 signal
} sbuf_t; // 작업 큐 구조체

typedef struct cache_node {
  char *uri;  // 요청된 URI
  char *data; // 응답 데이터
  int size;   // data의 크기

  struct cache_node *prev;  // 이전 노드
  struct cache_node *next;  // 다음 노드
} cache_node_t; // 캐시 노드 구조체

typedef struct {
  cache_node_t *head;   // 가장 최근에 사용된 노드
  cache_node_t *tail;   // 가장 오래된 노드
  int total_size;       // 현재 캐시에 저장된 총 크기

  pthread_rwlock_t lock; // 캐시 접근 보호 mutex
} cache_t;  // 캐시 구조체

void *thread(void *vargp);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char*host, char *port, char *path);
void func(int connfd);

// 스레드 풀 함수
void sbuf_init(sbuf_t *sp, int n);        // 큐 초기화
void sbuf_insert(sbuf_t *sp, int item);   // connfd 저장 (enqueue)
int sbuf_remove(sbuf_t *sp);              // connfd 꺼내기 (dequeue)

// 캐시 함수
void cache_init(cache_t *cache);  // 캐시 초기화
cache_node_t *find_cache(cache_t *cache, const char *uri);  // 캐시 검색
void insert_cache(cache_t *cache, const char *uri, const char *data, int size); // 캐시에 새 노드 삽입
void evict_cache(cache_t *cache); // 캐시 마지막 노드 제거

/* You won't lose style points for including this long line in your code */
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";

sbuf_t sbuf;
cache_t cache;

int main(int argc, char **argv) {
  int listenfd, connfd;
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;

  /* Check command line args */
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }

  sbuf_init(&sbuf, SBUFSIZE); // 작업 큐 초기화
  cache_init(&cache); // 캐시 초기화

  // 워커 스레드 생성
  pthread_t tid;
  for (int i = 0; i < NTHREADS; i++) {
    pthread_create(&tid, NULL, thread, NULL);
  }

  // 메인 스레드: 클라이언트 연결 수락 및 큐에 삽입
  listenfd = Open_listenfd(argv[1]);
  while (1) {
    clientlen = sizeof(clientaddr);
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
    sbuf_insert(&sbuf, connfd); // connfd를 큐에 삽입
  }

  return 0;
}

void func(int connfd) {
  rio_t client_rio, server_rio;
  char buf[MAXLINE], req[MAX_OBJECT_SIZE];
  char method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char host[MAXLINE], port[10], path[MAXLINE];

  Rio_readinitb(&client_rio, connfd);

  // 1. 요청 라인 읽기
  if (!Rio_readlineb(&client_rio, buf, MAXLINE)) return;
  sscanf(buf, "%s %s %s", method, uri, version);

  // 2. 캐시 검색
  cache_node_t *cached = find_cache(&cache, uri);
  if (cached) {
    // Cache Hit -> 응답 전송 후 종료
    Rio_writen(connfd, cached->data, cached->size);
    return;
  }

  // 3. URI 파싱 (호출 전 캐시 삽입용 URI 원본 복사)
  char uri_key[MAXLINE];
  strcpy(uri_key, uri);
  if (parse_uri(uri, host, port, path) == -1) {
      fprintf(stderr, "올바른 URI가 아닙니다: %s\n", uri);
      return;
  }

  // 4. 요청 헤더 재작성
  sprintf(req, "GET %s HTTP/1.0\r\n", path);
  while (Rio_readlineb(&client_rio, buf, MAXLINE) > 0 && strcmp(buf, "\r\n") != 0) {
      if (strncasecmp(buf, "Host", 4) == 0 ||
          strncasecmp(buf, "User-Agent", 10) == 0 ||
          strncasecmp(buf, "Connection", 10) == 0 ||
          strncasecmp(buf, "Proxy-Connection", 16) == 0) {
          continue;
      }
      strcat(req, buf);  // 유효한 헤더는 저장
  }
  // 표준 헤더 추가
  sprintf(buf, "Host: %s\r\n", host); strcat(req, buf);
  sprintf(buf, "%s", user_agent_hdr); strcat(req, buf);
  sprintf(buf, "Connection: close\r\n"); strcat(req, buf);
  sprintf(buf, "Proxy-Connection: close\r\n\r\n"); strcat(req, buf);
  printf("최종 요청:\n%s\n", req);

  // 5. 원 서버에 요청
  int serverfd = Open_clientfd(host, port);
  if (serverfd < 0) {
      fprintf(stderr, "원 서버 연결 실패\n");
      return;
  }

  Rio_readinitb(&server_rio, serverfd);
  Rio_writen(serverfd, req, strlen(req)); // 요청 전체 전송

  // 6. 응답 수신 + 클라이언트로 전송 + 캐싱 준비
  int n;
  int total_size = 0;
  char *object_buf = Malloc(MAX_OBJECT_SIZE);
  char *p = object_buf;

  // 응답 헤더 전송
  while ((n = Rio_readlineb(&server_rio, buf, MAXLINE)) > 0) {
    Rio_writen(connfd, buf, n);
    if (total_size + n < MAX_OBJECT_SIZE) {
      memcpy(p, buf, n);
      p += n;
      total_size += n;
    }
    if (strcmp(buf, "\r\n") == 0) break; // 요청 끝 감지
  }

  // 응답 바디 전송
  while ((n = Rio_readnb(&server_rio, buf, MAXBUF)) > 0) {
    Rio_writen(connfd, buf, n);
    if (total_size + n < MAX_OBJECT_SIZE) {
      memcpy(p, buf, n);
      p += n;
      total_size += n;
    }
  }

  // 7. 캐시 저장 (크기 조건 만족 시)
  if (total_size <= MAX_OBJECT_SIZE) {
    insert_cache(&cache, uri_key, object_buf, total_size);
  }

  free(object_buf);
  Close(serverfd);
}

int parse_uri(char *uri, char*host, char *port, char *path) {
  // "http://www.example.com:8000/index.html"
  char *hostbegin, *hostend, *pathbegin, *portbegin;

  if (strncasecmp(uri, "http://", 7) != 0) {  // 대소문자 구분 없이 접두어 확인
    return -1;
  }
  // 파일 경로 찾기
  hostbegin = uri + 7;  // http:// 이후부터 시작
  pathbegin = strchr(hostbegin, '/'); 
  if (pathbegin) {  // '/'가 있다면
    strcpy(path, pathbegin);  // 경로에 이후 내용(파일 경로)을 저장
    *pathbegin = '\0';        // '/' 이전 이후로 문자열 분리
  } else {
    strcpy(path, "/");        // '/'가 없다면 path에 '/'를 저장
  }
  // 포트 번호 찾기
  portbegin = strchr(hostbegin, ':');
  if (portbegin) {  // ':'가 있다면
    *portbegin = '\0';  // ':'를 기준으로 host시작부를 분리
    strcpy(host, hostbegin);
    strcpy(port, portbegin + 1);
  } else {
    strcpy(host, hostbegin);  // ':'가 없다면 전부 호스트 문자열
    strcpy(port, "80"); // 기본 포트 설정
  }

  return 0;
}

void sbuf_init(sbuf_t *sp, int n) {
  sp->buf = Calloc(n, sizeof(int));     // connfd 저장용 배열 할당
  sp->n = n;                            // 버퍼 크기 저장
  sp->front = sp->rear = 0;             // 초기 인덱스 0
  pthread_mutex_init(&sp->mutex, NULL); // mutex 초기화
  pthread_cond_init(&sp->slots, NULL);  // 빈 슬롯 대기 조건 변수 초기화
  pthread_cond_init(&sp->items, NULL);  // 아이템 대기 조건 변수 초기화

}

void sbuf_insert(sbuf_t *sp, int item) {
  pthread_mutex_lock(&sp->mutex);       // 큐 접근 mutext 잠금

  while (((sp->rear + 1) % sp->n) == sp->front) { // 큐가 가득 찬 경우
    pthread_cond_wait(&sp->slots, &sp->mutex);    // 빈 슬롯이 생길 때까지 대기
  }

  sp->buf[sp->rear] = item;           // connfd를 rear 위치에 저장
  sp->rear = (sp->rear + 1) % sp->n;  // rear 인덱스 증가 (원형 회전)

  pthread_cond_signal(&sp->items);    // 대기 중인 워커 스레드에게 작업이 생겼음을 알림
  pthread_mutex_unlock(&sp->mutex);   // mutex 잠금 해제
}

int sbuf_remove(sbuf_t *sp) {
  pthread_mutex_lock(&sp->mutex); // 큐 접근 잠금

  while (sp->front == sp->rear) { // 큐가 비어있는 경우
    pthread_cond_wait(&sp->items, &sp->mutex);  // 아이템이 들어올 때까지 대기
  }

  int item = sp->buf[sp->front];        // front 위치의 connfd 가져오기
  sp->front = (sp->front + 1) % sp->n;  // front 인덱스 증가 (원형 회전)

  pthread_cond_signal(&sp->slots);      // 대기 중인 메인 스레드에게 공간이 생겼음을 알림
  pthread_mutex_unlock(&sp->mutex);     // 잠금 해제

  return item;  // connfd 반환
}

void *thread(void *vargp) {
  pthread_detach(pthread_self()); // 스레드 자원 자동 회수

  while (1) {
    int connfd = sbuf_remove(&sbuf);  // 작업 큐에서 connfd 꺼내기
    func(connfd);                     // 요청 처리 함수 호출
    close(connfd);                    // 클라이언트와의 연결 종료
  }
}

void cache_init(cache_t *cache) {
  cache->head = NULL;
  cache->tail = NULL;
  cache->total_size = 0;
  pthread_rwlock_init(&cache->lock, NULL);
}

cache_node_t *find_cache(cache_t *cache, const char *uri) {
  pthread_rwlock_rdlock(&cache->lock); // 읽기 락(읽기 시에는 동시 접근 가능)

  cache_node_t *node = cache->head;
  while (node) {
    if (strcmp(node->uri, uri) == 0) {
      // Cache Hit: 적중한 노드를 Head로 이동
      pthread_rwlock_unlock(&cache->lock); // 캐시 접근 보호 해제
      return node;
    }
    node = node->next;
  }
  // Cache Miss
  pthread_rwlock_unlock(&cache->lock); // 읽기 락 해제
  return NULL;  // 캐시에 없으면 NULL 반환
}

void insert_cache(cache_t *cache, const char *uri, const char *data, int size) {
  pthread_rwlock_wrlock(&cache->lock); // 캐시 접근 보호(동기화)

  // 필요한 공간 확보. 초과한 경우 맨 뒤 노드를 제거
  while (cache->total_size + size > MAX_CACHE_SIZE) {
    evict_cache(cache);
  }

  // 새로운 노드 생성
  cache_node_t *node = Malloc(sizeof(cache_node_t));
  node->uri = Malloc(strlen(uri) + 1);  // 널 문자가 없을 수도 있으므로 +1 할당
  strcpy(node->uri, uri);

  node->data = Malloc(size);
  memcpy(node->data, data, size);
  node->size = size;

  // 리스트 앞에 삽입
  node->prev = NULL;
  node->next = cache->head;
  if (cache->head) {
    cache->head->prev = node;
  }
  cache->head = node;

  if (cache->tail == NULL) {  
    cache->tail = node; // 첫 노드라면 tail로도 설정
  }

  cache->total_size += size;  // 데이터 양만큼 전체 데이터 크기 증가
  pthread_rwlock_unlock(&cache->lock); // 캐시 접근 보호 해제(동기화 해제)
}

void evict_cache(cache_t *cache) {
  if (cache->tail == NULL) return;

  cache_node_t *node = cache->tail;

  // 리스트에서 제거
  if (node->prev) {
    node->prev->next = NULL;
  } else {
    cache->head = NULL;
  }

  cache->tail = node->prev;

  // 메모리 해제 및 데이터 양만큼 전체 데이터 크기 감소
  cache->total_size -= node->size;
  free(node->uri);
  free(node->data);
  free(node);
}

 

 

테스트 진행하기

캐시 기능이 추가된 동시성 프록시 서버의 구현이 끝났다면, 이제 driver.sh의 모든 주석을 해제한 뒤 테스트를 진행하면 됩니다.

 

순차적 프록시 서버부터 시작해 스레드 풀을 사용해 동시성으로 전환하고, 이제 여기에 캐시 기능까지 추가해 보면서 WebProxy 서버에 대한 전체 흐름을 경험해 보았는데요. 우리가 자연스럽게 하는 웹 브라우징 속에 이러한 개념들이 숨어 있다니 정말 놀라운 것 같습니다. 그럼 여기서 마치겠습니다. 감사합니다!

저작자표시 비영리 변경금지 (새창열림)

'크래프톤 정글 > Code 정글(C언어)' 카테고리의 다른 글

[Pintos] Threads: Alarm Clock 단계별 구현 및 테스트하기  (0) 2025.05.09
[Pintos] Pintos를 시작하면서 - 협업에 대한 고민, AI의 활용 범위  (0) 2025.05.09
[VSCode] 내가 쓰는 VSCode 유용한 단축키 모음  (0) 2025.05.06
[WebProxy] 동시성 프록시 서버 단계별 구현 및 테스트하기  (0) 2025.05.06
[WebProxy] 반복형 프록시 서버 단계별 구현 및 테스트하기  (0) 2025.05.05
'크래프톤 정글/Code 정글(C언어)' 카테고리의 다른 글
  • [Pintos] Threads: Alarm Clock 단계별 구현 및 테스트하기
  • [Pintos] Pintos를 시작하면서 - 협업에 대한 고민, AI의 활용 범위
  • [VSCode] 내가 쓰는 VSCode 유용한 단축키 모음
  • [WebProxy] 동시성 프록시 서버 단계별 구현 및 테스트하기
그냥사람_
그냥사람_
IT 관련 포스팅을 합니다. 크래프톤 정글 8기 정경호
  • 그냥사람_
    그냥코딩
    그냥사람_
  • 전체
    오늘
    어제
    • 글 전체보기 N
      • 크래프톤 정글 N
        • 로드 투 정글(입학시험)
        • CS기초(키워드, 개념정리) N
        • 컴퓨터구조(CSAPP)
        • Code 정글(C언어)
        • 마이 정글(WIL, 에세이) N
      • 자료구조&알고리즘
        • 자료구조
        • 알고리즘
      • 일상
  • 블로그 메뉴

    • 홈
  • 링크

    • Github
  • hELLO· Designed By정상우.v4.10.3
그냥사람_
[WebProxy] 캐시 기능이 추가된 동시성 프록시 서버 단계별 구현 및 테스트하기
상단으로

티스토리툴바