이번 포스팅에서는 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;
}