[네트워크] Caching Web Proxy 만들기 (in C)

Youngeui Hong·2023년 9월 21일
0

🔬 Proxy Lab

이번 포스팅에서는 CMU의 Proxy Lab 과제를 통해 프록시 서버에 대해 이해해보자.

💚 proxy.c

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

/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define HTTP_PORT 80

void *handle_thread(int *socket_fd_pointer);
void handle_client_request(int clientfd);
void parse(const char *uri, char *hostname, char *path, char *port);
void build_request_header(rio_t *rio, char *header, char *path);

/* Global Variables */
cache* cache_list;

int main(int argc, char **argv)
{
  int listen_fd;
  int *conn_fd_p;
  struct sockaddr_storage clientaddr;
  char hostname[MAXLINE], port[MAXLINE];
  int clientlen = sizeof(clientaddr);
  pthread_t tid;

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

  // Listen for connections
  listen_fd = Open_listenfd(argv[1]);

  // Initiate cache list
  cache_list = init_cache();

  // Execute server loop
  while (1)
  {
    // Allocate a memory for connection descriptor
    conn_fd_p = Malloc(sizeof(int));
    if (conn_fd_p == NULL)
    {
      fprintf(stderr, "Memory allocation failed.\n");
      exit(1);
    }

    // Accept a connection
    *conn_fd_p = Accept(listen_fd, (SA *)&clientaddr, (socklen_t *)&clientlen);

    // Create a thread
    Pthread_create(&tid, NULL, handle_thread, conn_fd_p);
  }

  return 0;
}

/*
Handle each client request in a separate thread
- This function is called for each new connection.
- After processing it frees allocated resources and terminates thread.
*/
void *handle_thread(int *socket_fd_pointer)
{
  int conn_fd = *((int *)socket_fd_pointer);
  Pthread_detach(pthread_self());
  Free(socket_fd_pointer);
  handle_client_request(conn_fd);
  Close(conn_fd);
  return NULL;
}

/* Handle the request of client */
void handle_client_request(int clientfd)
{
  rio_t rio_client, rio_server; // Robust I/O package
  char request_buf[MAXLINE], header_buf[MAXLINE], response_buf[MAXLINE], cache_buf[MAXLINE];
  char method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char hostname[MAXLINE], port[MAXLINE], path[MAXLINE];
  int proxyfd;

  Rio_readinitb(&rio_client, clientfd);
  if (Rio_readlineb(&rio_client, request_buf, MAXLINE) <= 0)
  {
    printf("Invalid Request.\n");
    return;
  }
  sscanf(request_buf, "%s %s %s", method, uri, version);

  // If cached data exists, return the cached data
  node *cached_node = find_cache(cache_list, uri);
  if (cached_node != NULL) 
  {
    Rio_writen(clientfd, cached_node->data, cached_node->size);
    return;
  }

  parse(uri, hostname, path, port);

  build_request_header(&rio_client, header_buf, path);

  // Connect to the remote server and forward request from client
  proxyfd = Open_clientfd(hostname, port);
  Rio_readinitb(&rio_server, proxyfd);
  Rio_writen(proxyfd, header_buf, strlen(header_buf));

  // Forward the response from remote server back to the client
  size_t n;
  while ((n = Rio_readnb(&rio_server, response_buf, MAXLINE)) > 0)
  {
    sprintf(cache_buf, "%s%s", cache_buf, response_buf);
    Rio_writen(clientfd, response_buf, n);
  }

  // Store the server's response in the cache
  add_cache(cache_list, uri, cache_buf, sizeof(cache_buf));
}

/* Parse URI to extract host name, path, and port */
void parse(const char *uri, char *hostname, char *path, char *port)
{
  const char *protocol = "http://";
  const int protocol_len = strlen(protocol);

  const char *host_start = uri + protocol_len;
  const char *host_end = strchr(host_start, '/');

  // hostname & path
  if (host_end == NULL)
  {
    strcpy(hostname, host_start);
    strcpy(path, "/");
  }
  else
  {
    strncpy(hostname, host_start, host_end - host_start);
    hostname[host_end - host_start] = '\0';
    strcpy(path, host_end);
  }

  // port
  char *colon = strchr(hostname, ':');
  if (colon != NULL)
  {
    sscanf(colon + 1, "%[^/]", port);
    *colon = '\0';
  }
  else
  {
    strcpy(port, HTTP_PORT);
  }
}

/* Build HTTP request header based on the client's request */
void build_request_header(rio_t *rio, char *header, char *path)
{
  char buf[MAXLINE];
  // Modify http version from 1.1 to 1.0
  sprintf(header, "GET %s HTTP/1.0\r\n", path);
  // Copy rest of header except above one
  while (Rio_readlineb(rio, buf, MAXLINE) > 2)
  {
    if (strncasecmp(buf, "GET", 3) != 0)
    {
      sprintf(header, "%s%s", header, buf);
    }
  }
  sprintf(header, "%s\r\n", header);
}

💚 cache.h

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

/* Recommended max cache and object sizes */

#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400


/* 캐시 노드 */
typedef struct cache_node node;
struct cache_node
{
    int size;
    char* url;
    char* data;
    node *next;
    node *prev;
    
};

/* 캐시 리스트 (이중 연결 리스트) */
typedef struct cache_header cache;
struct cache_header
{
    node *front;
    node *end;
    int empty_size;
    sem_t mutex_r, mutex_w;
    int readcnt
};

cache* init_cache();

node* new_node(char* url, char* data, int length);

void delete_node(cache* list, node* p);

void free_node(node* p);

node* search_node(cache* list, char* url);

void insert_node_front(cache* list, node* p);

void insert_node_end(cache* list, node* p);

node* find_cache(cache* list, char* url);

void add_cache(cache* list, char* url, char* data, int length);

void evict_cache(cache* list, node* p);

💚 cache.c

#include "csapp.h"
#include "cache.h"

/* 캐시 초기화 */
cache *init_cache()
{
    cache *cache_list = (cache *)malloc(sizeof(struct cache_header));

    cache_list->front = NULL;
    cache_list->end = NULL;
    cache_list->empty_size = MAX_CACHE_SIZE;
    // int sem_init(sem_t *sem, int pshared, unsigned int value);
    // pshared를 0으로 설정하여 세마포어가 프로세스 내의 스레드간에 공유되도록 함. 다른 프로세스와는 공유되지 않음.
    // 세마포어의 초기값(value)을 1로 설정. 한 번에 하나의 스레드만이 캐시에 접근할 수 있도록 함
    Sem_init(&cache_list->mutex_w, 0, 1);
    Sem_init(&cache_list->mutex_r, 0, 1);
    cache_list->readcnt = 0;
    return cache_list;
}

/* 인자로 주어진 정보를 담은 노드 생성 */
node *new_node(char *url, char *data, int length)
{
    node *p;
    if (url == NULL || data == NULL)
        return NULL;
    if ((p = (node *)malloc(sizeof(struct cache_node))) == NULL)
        return NULL;

    // size
    p->size = length;

    // url
    if ((p->url = (char *)malloc(sizeof(char) * (strlen(url) + 1))) == NULL)
        return NULL;
    strcpy(p->url, url);

    // data
    if ((p->data = (char *)malloc(length)) == NULL)
        return NULL;
    memcpy(p->data, data, length);

    // next, prev
    p->next = NULL;
    p->prev = NULL;

    return p;
}

/* 캐시에서 노드 삭제하기 */
void delete_node(cache *list, node *p)
{
    if (p == NULL || list == NULL)
        return;

    if (p == list->front) // 리스트의 가장 앞에 있는 노드라면
    {
        if (p == list->end) // 리스트의 유일한 노드라면
        {
            list->front = NULL;
            list->end = NULL;
            return;
        }
        list->front = p->next;
        list->front->prev = NULL;
    }
    else if (p == list->end) // 리스트의 가장 마지막에 있는 노드라면
    {
        list->end = p->prev;
        list->end->next = NULL;
    }
    else // 리스트의 중간에 있는 노드라면
    {
        p->prev->next = p->next;
        p->next->prev = p->prev;
    }
    list->empty_size += p->size; // 삭제한 노드의 크기만큼 가용 사이즈 늘리기

    return;
}

/* 삭제한 노드의 메모리 해제하기 */
void free_node(node *p)
{
    if (p == NULL)
        return;

    if (p->url != NULL)
        Free(p->url);
    if (p->data != NULL)
        Free(p->data);
    Free(p);

    return;
}

/* url에 해당되는 노드 찾기 */
node *search_node(cache *list, char *url)
{
    node *p;
    if (url == NULL || list == NULL)
        return NULL;

    p = list->front;
    while (p != NULL)
    {
        if (strcmp(p->url, url) == 0)
            return p;
        p = p->next;
    }

    return NULL;
}

/* 연결 리스트의 맨 앞에 노드 삽입하기 */
void insert_node_front(cache *list, node *p)
{
    if (p == NULL || list == NULL)
        return;

    if (list->front == NULL) // 빈 리스트인 경우
    {
        list->front = p;
        list->end = p;
        p->next = NULL;
        p->prev = NULL;
        list->empty_size -= p->size;
        return;
    }

    p->next = list->front;
    list->front->prev = p;
    list->front = p;
    p->prev = NULL;
    list->empty_size -= p->size;

    return;
}

/* 연결 리스트의 맨 뒤에 노드 추가하기 */
void insert_node_end(cache *list, node *p)
{
    if (p == NULL || list == NULL)
        return;

    if (list->front == NULL) // 빈 리스트인 경우
    {
        list->front = p;
        list->end = p;
        p->next = NULL;
        p->prev = NULL;
        list->empty_size -= p->size;
        return;
    }

    p->prev = list->end;
    list->end->next = p;
    list->end = p;
    p->next = NULL;
    list->empty_size -= p->size;

    return;
}

/* url에 해당되는 노드 찾기 (LRU, 세마포어 적용)*/
node *find_cache(cache *list, char *url)
{
    if (list == NULL || url == NULL)
        return NULL;

    P(&(list->mutex_r));
    list->readcnt++;
    if (1 == list->readcnt)
        P(&(list->mutex_w));
    V(&(list->mutex_r));

    node *p = search_node(list, url);

    P(&(list->mutex_r));
    list->readcnt--;
    if (0 == list->readcnt)
        V(&(list->mutex_w));
    V(&(list->mutex_r));

    if (p == NULL)
        return NULL;

    P(&list->mutex_w);
    delete_node(list, p);
    insert_node_end(list, p);
    V(&list->mutex_w);

    return p;
}

/* 캐시 추가하기 */
void add_cache(cache *list, char *url, char *data, int length)
{
    node *p;

    // MAX_OBJECT_SIZE를 초과하면 추가하지 않고 리턴
    if (length > MAX_OBJECT_SIZE)
        return;

    p = new_node(url, data, length);
    if (p == NULL)
        return;

    P(&list->mutex_w);
    if (list->empty_size < p->size) // MAX_CACHE_SIZE를 초과하는 경우 가용 공간이 확보될 떄까지 evict
        evict_cache(list, p);
    insert_node_end(list, p);
    V(&list->mutex_w);

    return;
}

/* 캐시의 Maximum Size를 초과하는 경우 least-recently-used 노드 삭제 */
void evict_cache(cache *list, node *p)
{
    if (list == NULL || p == NULL)
        return;

    node *bp = list->front;
    P(&list->mutex_w);
    while (list->empty_size < p->size)
    {
        delete_node(list, bp);
        free_node(bp);
        bp = list->front;
    }
    V(&list->mutex_w);

    return;
}

0개의 댓글