[JUNGLE] TIL_53. proxy 서버 코드

모깅·2025년 11월 4일

JUNGLE

목록 보기
54/56
post-thumbnail

Proxy basic

#include "csapp.h"

/* 권장되는 최대 캐시 및 객체 크기 */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

/* 제공된 User-Agent 헤더 상수 */
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";

/* 함수 프로토타입 */
void doit(int fd);
void parse_uri(char *uri, char *host, char *port, char *path);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);

/*
 * main - 프록시의 메인 루틴. tiny.c의 main과 거의 동일합니다.
 */
int main(int argc, char **argv) {
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

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

    listenfd = Open_listenfd(argv[1]);

    while (1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
        Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE,0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);

        doit(connfd);

        Close(connfd);
    }
}

/*
 * doit - 단일 HTTP 트랜잭션을 처리합니다.
 */
void doit(int fd) {
    int serverfd;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char host[MAXLINE], port[MAXLINE], path[MAXLINE];
    char request_buf[MAXLINE]; // 서버로 보낼 요청을 저장할 버퍼
    rio_t client_rio, server_rio;
    int Does_send_host_header = 0;

    /* 1. 클라이언트로부터 요청 라인과 헤더 읽기 */
    Rio_readinitb(&client_rio, fd);
    if (Rio_readlineb(&client_rio, buf, MAXLINE) == 0) {
        return; // 빈 요청은 무시
    }

    // 요청 라인 파싱 (예: "GET http://www.cmu.edu/hub/index.html HTTP/1.1")
    sscanf(buf, "%s %s %s", method, uri, version);

    if (strcasecmp(method, "GET")) {
        clienterror(fd, method, "501", "Not Implemented",
                    "Proxy does not implement this method");
        return;
    }

    parse_uri(uri, host, port, path);

    /* 4. 서버로 보낼 HTTP 요청 재조립 (1단계: 요청 라인) */
    // 프록시는 HTTP/1.0 요청을 보냅니다.
    sprintf(request_buf, "GET %s HTTP/1.0\r\n", path);

    /* 5. 클라이언트의 나머지 헤더 읽기 및 서버 요청 재조립 (2단계: 헤더) */
    while (Rio_readlineb(&client_rio, buf, MAXLINE) > 0) {
        if (strcmp(buf, "\r\n") == 0)
            break;

        if (strstr(buf, "User-Agent:"))
            continue;
        if (strstr(buf, "Connection:"))
            continue;
        if (strstr(buf, "Proxy-Connection:"))
            continue;

        if (strstr(buf, "Host:")) {
            Does_send_host_header = 1;
        }
        sprintf(request_buf, "%s%s", request_buf, buf);
    }

    /* 6. 필수 헤더 추가 (3단계: 고정 헤더) */
    // 클라이언트가 Host 헤더를 보내지 않았다면, URI에서 파싱한 호스트로 추가
    if (!Does_send_host_header) {
        sprintf(request_buf, "%sHost: %s\r\n", request_buf, host);
    }
    // 고정된 헤더 추가
    sprintf(request_buf, "%s%s", request_buf, user_agent_hdr);
    sprintf(request_buf, "%sConnection: close\r\n", request_buf);
    sprintf(request_buf, "%sProxy-Connection: close\r\n", request_buf);

    // 헤더 섹션의 끝을 알리는 빈 줄 추가
    sprintf(request_buf, "%s\r\n", request_buf);

    /* 7. 실제 웹 서버에 연결 */
    // printf("Connecting to %s:%s\n", host, port); // 디버깅용
    serverfd = Open_clientfd(host, port);

    /* 8. 재조립된 요청을 서버에 전송 */
    Rio_writen(serverfd, request_buf, strlen(request_buf));

    /* 9. 서버의 응답을 읽어 클라이언트에 그대로 전달 (중계) */
    Rio_readinitb(&server_rio, serverfd);
    size_t n;
    while ((n = Rio_readnb(&server_rio, buf, MAXLINE)) > 0) {
        Rio_writen(fd, buf, n);
    }

    Close(serverfd);
}

/*
 * parse_uri - HTTP 프록시 URI를 파싱합니다.
 * (예: "http://www.cmu.edu:8080/hub/index.html")
 */
void parse_uri(char *uri, char *host, char *port, char *path) {
    char *ptr;

    /* "http://" 부분 건너뛰기 */
    if (!(ptr = strstr(uri, "http://"))) {
        // 이 실습에서는 "http://"가 항상 있다고 가정합니다.
        return;
    }
    ptr += 7; // "http://" 다음부터 시작 (예: "www.cmu.edu:8080/...")

    /* 경로(path) 찾기 */
    char *path_ptr = strchr(ptr, '/');
    if (path_ptr) {
        strcpy(path, path_ptr); // (예: "/hub/index.html")
        *path_ptr = '\0'; // 호스트/포트 부분과 경로 분리
    } else {
        strcpy(path, "/"); // 경로가 없으면 기본값 "/"
    }

    /* 포트(port) 찾기 */
    char *port_ptr = strchr(ptr, ':');
    if (port_ptr) {
        *port_ptr = '\0'; // 호스트와 포트 분리
        strcpy(port, port_ptr + 1); // (예: "8080")
    } else {
        strcpy(port, "80"); // 포트가 없으면 기본값 "80"
    }

    /* 남은 부분이 호스트(host) */
    strcpy(host, ptr); // (예: "www.cmu.edu")
}

/*
 * clienterror - tiny.c에서 가져온 오류 메시지 전송 함수
 */
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
    char buf[MAXLINE], body[MAXBUF];

    /* Build the HTTP response body */
    sprintf(body, "<html><title>Proxy Error</title>");
    sprintf(body, "%s<body bgcolor=\"ffffff\">\r\n", body);
    sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
    sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
    sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);

    /* Print the HTTP response */
    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-length: %d\r\n\r\n", (int) strlen(body));
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, body, strlen(body));
}

Proxy concurrency

#include "csapp.h"

/* 권장되는 최대 캐시 및 객체 크기 */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

/* 제공된 User-Agent 헤더 상수 */
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";
/* BASIC */
void doit(int fd);
void parse_uri(char *uri, char *host, char *port, char *path);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);

/* Concurrency */
void *thread_function(void *vargp);

/*
 * main - 프록시의 메인 루틴. (동시성 적용)
 */
int main(int argc, char **argv) {
    int listenfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    pthread_t tid; // 스레드 ID 변수 추가

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

    listenfd = Open_listenfd(argv[1]);

    while (1) {
        clientlen = sizeof(clientaddr);

        // 1. connfd를 위한 메모리를 힙에 할당 (스레드 경쟁 상태 방지)
        int *connfd_ptr = Malloc(sizeof(int));

        // 2. 연결 수락
        *connfd_ptr = Accept(listenfd, (SA *)&clientaddr, &clientlen);

        Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);

        // 3. 새 스레드를 생성하고 connfd_ptr을 인자로 전달
        Pthread_create(&tid, NULL, thread_function, connfd_ptr);
    }
}

/*
 * 스레드의 메인 루틴 (시작 함수)
 */
void *thread_function(void *vargp) {
    // 1. 메인 스레드가 전달한 connfd_ptr(void*)를 int*로 변환
    int connfd = *((int *)vargp);

    // 2. 스레드를 분리(detach)하여 자원을 자동 해제하도록 설정
    Pthread_detach(pthread_self());

    // 3. connfd 값을 꺼냈으므로, 힙에 할당된 포인터 메모리 해제
    Free(vargp);

    // 4. 프록시의 핵심 로직 수행 (1부의 doit 함수 재사용)
    doit(connfd);

    // 5. 작업이 끝났으므로 클라이언트 소켓을 닫음 (매우 중요)
    Close(connfd);

    return NULL;
}

/*
 * doit - 단일 HTTP 트랜잭션을 처리합니다.
 */
void doit(int fd) {
    int serverfd;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char host[MAXLINE], port[MAXLINE], path[MAXLINE];
    char request_buf[MAXLINE]; // 서버로 보낼 요청을 저장할 버퍼
    rio_t client_rio, server_rio;
    int Does_send_host_header = 0;

    /* 1. 클라이언트로부터 요청 라인과 헤더 읽기 */
    Rio_readinitb(&client_rio, fd);
    if (Rio_readlineb(&client_rio, buf, MAXLINE) == 0) {
        return; // 빈 요청은 무시
    }

    // 요청 라인 파싱 (예: "GET http://www.cmu.edu/hub/index.html HTTP/1.1")
    sscanf(buf, "%s %s %s", method, uri, version);

    if (strcasecmp(method, "GET")) {
        clienterror(fd, method, "501", "Not Implemented",
                    "Proxy does not implement this method");
        return;
    }

    parse_uri(uri, host, port, path);

    /* 4. 서버로 보낼 HTTP 요청 재조립 (1단계: 요청 라인) */
    // 프록시는 HTTP/1.0 요청을 보냅니다.
    sprintf(request_buf, "GET %s HTTP/1.0\r\n", path);

    /* 5. 클라이언트의 나머지 헤더 읽기 및 서버 요청 재조립 (2단계: 헤더) */
    while (Rio_readlineb(&client_rio, buf, MAXLINE) > 0) {
        if (strcmp(buf, "\r\n") == 0)
            break;

        if (strstr(buf, "User-Agent:"))
            continue;
        if (strstr(buf, "Connection:"))
            continue;
        if (strstr(buf, "Proxy-Connection:"))
            continue;

        if (strstr(buf, "Host:")) {
            Does_send_host_header = 1;
        }
        sprintf(request_buf, "%s%s", request_buf, buf);
    }

    /* 6. 필수 헤더 추가 (3단계: 고정 헤더) */
    // 클라이언트가 Host 헤더를 보내지 않았다면, URI에서 파싱한 호스트로 추가
    if (!Does_send_host_header) {
        sprintf(request_buf, "%sHost: %s\r\n", request_buf, host);
    }
    // 고정된 헤더 추가
    sprintf(request_buf, "%s%s", request_buf, user_agent_hdr);
    sprintf(request_buf, "%sConnection: close\r\n", request_buf);
    sprintf(request_buf, "%sProxy-Connection: close\r\n", request_buf);

    // 헤더 섹션의 끝을 알리는 빈 줄 추가
    sprintf(request_buf, "%s\r\n", request_buf);

    /* 7. 실제 웹 서버에 연결 */
    // printf("Connecting to %s:%s\n", host, port); // 디버깅용
    serverfd = Open_clientfd(host, port);

    /* 8. 재조립된 요청을 서버에 전송 */
    Rio_writen(serverfd, request_buf, strlen(request_buf));

    /* 9. 서버의 응답을 읽어 클라이언트에 그대로 전달 (중계) */
    Rio_readinitb(&server_rio, serverfd);
    size_t n;
    while ((n = Rio_readnb(&server_rio, buf, MAXLINE)) > 0) {
        Rio_writen(fd, buf, n);
    }

    Close(serverfd);
}

/*
 * parse_uri - HTTP 프록시 URI를 파싱합니다.
 * (예: "http://www.cmu.edu:8080/hub/index.html")
 */
void parse_uri(char *uri, char *host, char *port, char *path) {
    char *ptr;

    /* "http://" 부분 건너뛰기 */
    if (!(ptr = strstr(uri, "http://"))) {
        // 이 실습에서는 "http://"가 항상 있다고 가정합니다.
        return;
    }
    ptr += 7; // "http://" 다음부터 시작 (예: "www.cmu.edu:8080/...")

    /* 경로(path) 찾기 */
    char *path_ptr = strchr(ptr, '/');
    if (path_ptr) {
        strcpy(path, path_ptr); // (예: "/hub/index.html")
        *path_ptr = '\0'; // 호스트/포트 부분과 경로 분리
    } else {
        strcpy(path, "/"); // 경로가 없으면 기본값 "/"
    }

    /* 포트(port) 찾기 */
    char *port_ptr = strchr(ptr, ':');
    if (port_ptr) {
        *port_ptr = '\0'; // 호스트와 포트 분리
        strcpy(port, port_ptr + 1); // (예: "8080")
    } else {
        strcpy(port, "80"); // 포트가 없으면 기본값 "80"
    }

    /* 남은 부분이 호스트(host) */
    strcpy(host, ptr); // (예: "www.cmu.edu")
}

/*
 * clienterror - tiny.c에서 가져온 오류 메시지 전송 함수
 */
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
    char buf[MAXLINE], body[MAXBUF];

    /* Build the HTTP response body */
    sprintf(body, "<html><title>Proxy Error</title>");
    sprintf(body, "%s<body bgcolor=\"ffffff\">\r\n", body);
    sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
    sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
    sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);

    /* Print the HTTP response */
    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-length: %d\r\n\r\n", (int) strlen(body));
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, body, strlen(body));
}

Proxy cache

#include "csapp.h"
#include "cache.h"
/* 권장되는 최대 캐시 및 객체 크기 */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

/* 제공된 User-Agent 헤더 상수 */
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";
/* BASIC */
void doit(int fd);
void parse_uri(char *uri, char *host, char *port, char *path);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);

/* Concurrency */
void *thread_function(void *vargp);

/*
 * main - 프록시의 메인 루틴. (동시성 적용)
 */
int main(int argc, char **argv) {
    int listenfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    pthread_t tid; // 스레드 ID 변수 추가

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

    Signal(SIGPIPE, SIG_IGN); // SIGPIPE 무시
    cache_init();

    listenfd = Open_listenfd(argv[1]);

    while (1) {
        clientlen = sizeof(clientaddr);

        // 1. connfd를 위한 메모리를 힙에 할당 (스레드 경쟁 상태 방지)
        int *connfd_ptr = Malloc(sizeof(int));

        // 2. 연결 수락
        *connfd_ptr = Accept(listenfd, (SA *)&clientaddr, &clientlen);

        Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);

        // 3. 새 스레드를 생성하고 connfd_ptr을 인자로 전달
        Pthread_create(&tid, NULL, thread_function, connfd_ptr);
    }
}

/*
 * 스레드의 메인 루틴 (시작 함수)
 */
void *thread_function(void *vargp) {
    // 1. 메인 스레드가 전달한 connfd_ptr(void*)를 int*로 변환
    int connfd = *((int *)vargp);

    // 2. 스레드를 분리(detach)하여 자원을 자동 해제하도록 설정
    Pthread_detach(pthread_self());

    // 3. connfd 값을 꺼냈으므로, 힙에 할당된 포인터 메모리 해제
    Free(vargp);

    // 4. 프록시의 핵심 로직 수행 (1부의 doit 함수 재사용)
    doit(connfd);

    // 5. 작업이 끝났으므로 클라이언트 소켓을 닫음 (매우 중요)
    Close(connfd);

    return NULL;
}

/*
 * doit - 단일 HTTP 트랜잭션을 처리합니다.
 */
void doit(int fd) {
    int serverfd;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char host[MAXLINE], port[MAXLINE], path[MAXLINE];
    char request_buf[MAXLINE]; // 서버로 보낼 요청을 저장할 버퍼
    rio_t client_rio, server_rio;
    int Does_send_host_header = 0; // Host 헤더 전송 여부 플래그

    /* 1. 클라이언트로부터 요청 라인과 헤더 읽기 */
    Rio_readinitb(&client_rio, fd);
    if (Rio_readlineb(&client_rio, buf, MAXLINE) == 0) {
        return; // 빈 요청은 무시
    }

    // 요청 라인 파싱 (예: "GET http://www.cmu.edu/hub/index.html HTTP/1.1")
    sscanf(buf, "%s %s %s", method, uri, version);

    if (strcasecmp(method, "GET")) {
        clienterror(fd, method, "501", "Not Implemented",
                    "Proxy does not implement this method");
        return;
    }

    // [캐싱] 캐시 키로 전체 URI를 사용합니다.
    char cache_key[MAXLINE];
    strcpy(cache_key, uri);

    /*
     * [캐싱] 2. 캐시에서 객체 찾기
     */
    if (cache_find(cache_key, fd)) {
        printf("Cache hit for %s\n", cache_key);
        return; // cache_find가 클라이언트에게 전송 완료
    }
    printf("Cache miss for %s\n", cache_key);


    /*
     * 3. 캐시 미스(Miss): 서버에 요청 (1부 로직)
     */
    parse_uri(uri, host, port, path);

    // 3a. 서버로 보낼 요청 라인 재조립
    sprintf(request_buf, "GET %s HTTP/1.0\r\n", path);

    // 3b. 클라이언트의 나머지 헤더 읽기 및 재조립
    while (Rio_readlineb(&client_rio, buf, MAXLINE) > 0) {
        if (strcmp(buf, "\r\n") == 0)
            break;

        // 프록시가 직접 설정할 헤더는 건너뛰기
        if (strstr(buf, "User-Agent:"))
            continue;
        if (strstr(buf, "Connection:"))
            continue;
        if (strstr(buf, "Proxy-Connection:"))
            continue;

        // Host 헤더가 있는지 확인하고, 나머지 헤더는 그대로 추가
        if (strstr(buf, "Host:")) {
            Does_send_host_header = 1;
        }
        sprintf(request_buf, "%s%s", request_buf, buf);
    }

    // 3c. 필수 헤더 추가
    if (!Does_send_host_header) {
        sprintf(request_buf, "%sHost: %s\r\n", request_buf, host);
    }
    sprintf(request_buf, "%s%s", request_buf, user_agent_hdr);
    sprintf(request_buf, "%sConnection: close\r\n", request_buf);
    sprintf(request_buf, "%sProxy-Connection: close\r\n", request_buf);
    sprintf(request_buf, "%s\r\n", request_buf); // 헤더 끝

    /* 4. 실제 웹 서버에 연결 및 요청 전송 */
    serverfd = Open_clientfd(host, port);
    Rio_writen(serverfd, request_buf, strlen(request_buf));

    /*
     * 5. 서버 응답 중계 및 캐시 저장
     */
    Rio_readinitb(&server_rio, serverfd);
    size_t n;

    // 캐싱을 위한 임시 버퍼 할당
    char *cache_buf = Malloc(MAX_OBJECT_SIZE);
    int total_bytes_read = 0;
    int can_cache = 1; // 객체가 MAX_OBJECT_SIZE 이하인지 추적

    while ((n = Rio_readnb(&server_rio, buf, MAXLINE)) > 0) {
        // 5a. 클라이언트에게 즉시 전송
        Rio_writen(fd, buf, n);

        // 5b. 캐시 버퍼에 데이터 누적
        if (can_cache) {
            if (total_bytes_read + n <= MAX_OBJECT_SIZE) {
                // 바이너리 데이터이므로 memcpy 사용
                memcpy(cache_buf + total_bytes_read, buf, n);
                total_bytes_read += n;
            } else {
                // 객체가 너무 커서 캐시 불가.
                can_cache = 0;
            }
        }
    }
    Close(serverfd);

    // 5c. 캐시 가능하면 캐시에 저장
    if (can_cache && total_bytes_read > 0) {
        cache_store(cache_key, cache_buf, total_bytes_read);
    }

    // 임시 버퍼 해제
    Free(cache_buf);
}

/*
 * parse_uri - HTTP 프록시 URI를 파싱합니다.
 * (예: "http://www.cmu.edu:8080/hub/index.html")
 */
void parse_uri(char *uri, char *host, char *port, char *path) {
    char *ptr;

    /* "http://" 부분 건너뛰기 */
    if (!(ptr = strstr(uri, "http://"))) {
        // 이 실습에서는 "http://"가 항상 있다고 가정합니다.
        return;
    }
    ptr += 7; // "http://" 다음부터 시작 (예: "www.cmu.edu:8080/...")

    /* 경로(path) 찾기 */
    char *path_ptr = strchr(ptr, '/');
    if (path_ptr) {
        strcpy(path, path_ptr); // (예: "/hub/index.html")
        *path_ptr = '\0'; // 호스트/포트 부분과 경로 분리
    } else {
        strcpy(path, "/"); // 경로가 없으면 기본값 "/"
    }

    /* 포트(port) 찾기 */
    char *port_ptr = strchr(ptr, ':');
    if (port_ptr) {
        *port_ptr = '\0'; // 호스트와 포트 분리
        strcpy(port, port_ptr + 1); // (예: "8080")
    } else {
        strcpy(port, "80"); // 포트가 없으면 기본값 "80"
    }

    /* 남은 부분이 호스트(host) */
    strcpy(host, ptr); // (예: "www.cmu.edu")
}

/*
 * clienterror - tiny.c에서 가져온 오류 메시지 전송 함수
 */
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
    char buf[MAXLINE], body[MAXBUF];

    /* Build the HTTP response body */
    sprintf(body, "<html><title>Proxy Error</title>");
    sprintf(body, "%s<body bgcolor=\"ffffff\">\r\n", body);
    sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
    sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
    sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);

    /* Print the HTTP response */
    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-length: %d\r\n\r\n", (int) strlen(body));
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, body, strlen(body));
}

Proxy thread pool

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

/* 권장되는 최대 캐시 및 객체 크기 */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

#define NTHREADS  6  // 워커 스레드 수
#define SBUFSIZE 16  // 공유 버퍼(큐) 크기

sbuf_t sbuf; // 공유 버퍼 전역 변수

/* 제공된 User-Agent 헤더 상수 */
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";
/* BASIC */
void doit(int fd);
void parse_uri(char *uri, char *host, char *port, char *path);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);

/* Concurrency */
void *thread_function(void *vargp);

/*
 * main - 프록시의 메인 루틴. (동시성 적용)
 */
int main(int argc, char **argv) {
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    pthread_t tid;

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

    Signal(SIGPIPE, SIG_IGN);
    cache_init();

    /* [수정] 스레드 풀 초기화 */
    sbuf_init(&sbuf, SBUFSIZE);
    listenfd = Open_listenfd(argv[1]);

    /* [수정] NTHREADS 개의 워커 스레드를 미리 생성 */
    for (int i = 0; i < NTHREADS; i++) {
        Pthread_create(&tid, NULL, thread_function, NULL);
    }

    /* [수정] main 스레드는 이제 '생산자' 역할만 수행 */
    while (1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);

        /* connfd를 공유 버퍼에 삽입 */
        sbuf_insert(&sbuf, connfd);
    }
}

/*
 * 스레드의 메인 루틴 (시작 함수)
 */
void *thread_function(void *vargp) {

    // 1. 스레드를 분리(detach)하여 자원을 자동 해제
    Pthread_detach(pthread_self());

    // 2. 워커 스레드는 무한 루프를 돌며 작업을 기다림
    while (1) {
        /* 공유 버퍼에서 connfd를 꺼냄 (없으면 대기) */
        int connfd = sbuf_remove(&sbuf);

        /* 핵심 로직 수행 */
        doit(connfd);

        /* 연결 종료 */
        Close(connfd);
    }
}

/*
 * doit - 단일 HTTP 트랜잭션을 처리합니다.
 */
void doit(int fd) {
    int serverfd;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char host[MAXLINE], port[MAXLINE], path[MAXLINE];
    char request_buf[MAXLINE]; // 서버로 보낼 요청을 저장할 버퍼

    /* [수정] request_buf의 끝을 가리킬 포인터 선언 */
    char *p = request_buf;

    rio_t client_rio, server_rio;
    int Does_send_host_header = 0; // Host 헤더 전송 여부 플래그

    /* 1. 클라이언트로부터 요청 라인과 헤더 읽기 */
    Rio_readinitb(&client_rio, fd);
    if (Rio_readlineb(&client_rio, buf, MAXLINE) == 0) {
        return; // 빈 요청은 무시
    }

    sscanf(buf, "%s %s %s", method, uri, version);

    if (strcasecmp(method, "GET")) {
        clienterror(fd, method, "501", "Not Implemented",
                    "Proxy does not implement this method");
        return;
    }

    char cache_key[MAXLINE];
    strcpy(cache_key, uri);

    /*
     * [캐싱] 2. 캐시에서 객체 찾기
     */
    if (cache_find(cache_key, fd)) {
        printf("Cache hit for %s\n", cache_key);
        return;
    }
    printf("Cache miss for %s\n", cache_key);


    /*
     * 3. 캐시 미스(Miss): 서버에 요청 (1부 로직)
     */
    parse_uri(uri, host, port, path);

    /* [수정] 3a. 포인터(p)를 이용해 request_buf에 쓰기 */
    p += sprintf(p, "GET %s HTTP/1.0\r\n", path);

    /* [수정] 3b. 포인터를 이동시키며 헤더 이어 붙이기 */
    while (Rio_readlineb(&client_rio, buf, MAXLINE) > 0) {
        if (strcmp(buf, "\r\n") == 0)
            break;

        if (strstr(buf, "User-Agent:"))
            continue;
        if (strstr(buf, "Connection:"))
            continue;
        if (strstr(buf, "Proxy-Connection:"))
            continue;

        if (strstr(buf, "Host:")) {
            Does_send_host_header = 1;
        }
        /* p가 가리키는 곳(버퍼의 끝)에 안전하게 이어 씀 */
        p += sprintf(p, "%s", buf);
    }

    /* [수정] 3c. 포인터를 이용해 필수 헤더 이어 붙이기 */
    if (!Does_send_host_header) {
        p += sprintf(p, "Host: %s\r\n", host);
    }
    /* user_agent_hdr은 \r\n을 이미 포함하고 있습니다. */
    p += sprintf(p, "%s", user_agent_hdr);
    p += sprintf(p, "Connection: close\r\n");
    p += sprintf(p, "Proxy-Connection: close\r\n");
    p += sprintf(p, "\r\n"); // 헤더 끝

    /* 4. 실제 웹 서버에 연결 및 요청 전송 */
    serverfd = Open_clientfd(host, port);

    /* [수정] strlen 대신 포인터 연산으로 정확한 크기 전송 (더 안전함) */
    Rio_writen(serverfd, request_buf, (p - request_buf));

    /*
     * 5. 서버 응답 중계 및 캐시 저장 (이 부분은 문제가 없었음)
     */
    Rio_readinitb(&server_rio, serverfd);
    size_t n;

    char *cache_buf = Malloc(MAX_OBJECT_SIZE);
    int total_bytes_read = 0;
    int can_cache = 1;

    while ((n = Rio_readnb(&server_rio, buf, MAXLINE)) > 0) {
        Rio_writen(fd, buf, n);
        if (can_cache) {
            if (total_bytes_read + n <= MAX_OBJECT_SIZE) {
                memcpy(cache_buf + total_bytes_read, buf, n);
                total_bytes_read += n;
            } else {
                can_cache = 0;
            }
        }
    }
    Close(serverfd);

    if (can_cache && total_bytes_read > 0) {
        cache_store(cache_key, cache_buf, total_bytes_read);
    }
    Free(cache_buf);
}

/*
 * parse_uri - HTTP 프록시 URI를 파싱합니다.
 * (예: "http://www.cmu.edu:8080/hub/index.html")
 */
void parse_uri(char *uri, char *host, char *port, char *path) {
    char *ptr;

    /* "http://" 부분 건너뛰기 */
    if (!(ptr = strstr(uri, "http://"))) {
        // 이 실습에서는 "http://"가 항상 있다고 가정합니다.
        return;
    }
    ptr += 7; // "http://" 다음부터 시작 (예: "www.cmu.edu:8080/...")

    /* 경로(path) 찾기 */
    char *path_ptr = strchr(ptr, '/');
    if (path_ptr) {
        strcpy(path, path_ptr); // (예: "/hub/index.html")
        *path_ptr = '\0'; // 호스트/포트 부분과 경로 분리
    } else {
        strcpy(path, "/"); // 경로가 없으면 기본값 "/"
    }

    /* 포트(port) 찾기 */
    char *port_ptr = strchr(ptr, ':');
    if (port_ptr) {
        *port_ptr = '\0'; // 호스트와 포트 분리
        strcpy(port, port_ptr + 1); // (예: "8080")
    } else {
        strcpy(port, "80"); // 포트가 없으면 기본값 "80"
    }

    /* 남은 부분이 호스트(host) */
    strcpy(host, ptr); // (예: "www.cmu.edu")
}

/*
 * clienterror - tiny.c에서 가져온 오류 메시지 전송 함수
 */
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
    char buf[MAXLINE], body[MAXBUF];

    /* Build the HTTP response body */
    sprintf(body, "<html><title>Proxy Error</title>");
    sprintf(body, "%s<body bgcolor=\"ffffff\">\r\n", body);
    sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
    sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
    sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);

    /* Print the HTTP response */
    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-length: %d\r\n\r\n", (int) strlen(body));
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, body, strlen(body));
}

sbuf.c

#include "sbuf.h"

/* 공유 버퍼 초기화 */
void sbuf_init(sbuf_t *sp, int n)
{
    sp->buf = Calloc(n, sizeof(int)); 
    sp->n = n;                       /* 버퍼 크기 */
    sp->front = sp->rear = 0;        /* 큐는 비어있음 */
    Sem_init(&sp->mutex, 0, 1);      /* 뮤텍스 (초기값 1) */
    Sem_init(&sp->slots, 0, n);      /* 비어있는 슬롯 (초기값 n) */
    Sem_init(&sp->items, 0, 0);      /* 사용 가능한 아이템 (초기값 0) */
}

/* 버퍼 정리 */
void sbuf_deinit(sbuf_t *sp)
{
    Free(sp->buf);
}

/* 버퍼에 아이템(connfd) 삽입 */
void sbuf_insert(sbuf_t *sp, int item)
{
    P(&sp->slots);                          /* 비어있는 슬롯을 기다림 */
    P(&sp->mutex);                          /* 버퍼 락 */
    sp->buf[(++sp->rear)%(sp->n)] = item;   /* 아이템 삽입 */
    V(&sp->mutex);                          /* 버퍼 언락 */
    V(&sp->items);                          /* 아이템이 사용 가능함을 알림 */
}

/* 버퍼에서 아이템(connfd) 꺼내기 */
int sbuf_remove(sbuf_t *sp)
{
    int item;
    P(&sp->items);                          /* 아이템을 기다림 */
    P(&sp->mutex);                          /* 버퍼 락 */
    item = sp->buf[(++sp->front)%(sp->n)];  /* 아이템 꺼내기 */
    V(&sp->mutex);                          /* 버퍼 언락 */
    V(&sp->slots);                          /* 비어있는 슬롯이 생겼음을 알림 */
    return item;
}

cache.c

#include "cache.h"

/* 캐시 리스트의 시작(가장 최근 사용)과 끝(가장 오래된)을 가리킴 */
static CacheNode *cache_head;
static CacheNode *cache_tail;
static int total_cache_size;

/* Readers-Writers Lock */
static pthread_rwlock_t cache_lock;

/* 내부 헬퍼 함수: 꼬리에서 노드 제거 (wrlock 안에서 호출되어야 함) */
static void evict_lru_node() {
    if (cache_tail == NULL) return; // 캐시가 비어있음

    CacheNode *node_to_evict = cache_tail;

    // 꼬리 노드 업데이트
    if (cache_tail->prev) {
        cache_tail = cache_tail->prev;
        cache_tail->next = NULL;
    } else { // 노드가 하나뿐이었던 경우
        cache_head = NULL;
        cache_tail = NULL;
    }
    
    // 리소스 해제
    total_cache_size -= node_to_evict->size;
    Free(node_to_evict->key);
    Free(node_to_evict->data);
    Free(node_to_evict);
}

/* 내부 헬퍼 함수: 노드를 리스트 맨 앞으로 이동 (wrlock 안에서 호출됨) */
/* (참고: 이 랩의 "근사" 요구사항만 맞추려면 이 함수는 선택사항임) */
/*
static void move_to_front(CacheNode *node) {
    if (node == cache_head) return; // 이미 맨 앞

    // 1. 리스트에서 노드 분리
    if (node->prev) node->prev->next = node->next;
    if (node->next) node->next->prev = node->prev;
    if (node == cache_tail) cache_tail = node->prev;

    // 2. 맨 앞에 노드 삽입
    node->next = cache_head;
    node->prev = NULL;
    if (cache_head) cache_head->prev = node;
    cache_head = node;
    if (cache_tail == NULL) cache_tail = node;
}
*/

/*
 * cache_init - 캐시와 락을 초기화 (main에서 한 번 호출)
 */
void cache_init() {
    cache_head = NULL;
    cache_tail = NULL;
    total_cache_size = 0;
    pthread_rwlock_init(&cache_lock, NULL);
}

/*
 * cache_find - 'key'(URI)에 해당하는 객체를 찾아 clientfd로 전송
 * 성공 시 1, 실패(miss) 시 0 리턴
 */
int cache_find(char *key, int clientfd) {
    pthread_rwlock_rdlock(&cache_lock); // [읽기 락] 획득

    CacheNode *current = cache_head;
    while (current) {
        if (strcmp(current->key, key) == 0) {
            // [캐시 히트!]

            // 1. 데이터를 클라이언트에게 직접 전송
            Rio_writen(clientfd, current->data, current->size);

            /* * (선택사항) 만약 "읽기"도 LRU 갱신을 해야 한다면,
             * 여기서 rdlock을 풀고, wrlock을 잡은 뒤 move_to_front()를
             * 호출해야 하나, 이는 매우 복잡하고 성능 저하를 유발함.
             * "LRU 근사" 요구사항은 쓰기/퇴출 정책만으로도 만족 가능.
             */

            pthread_rwlock_unlock(&cache_lock); // [읽기 락] 해제
            return 1; // 1 (찾았음)
        }
        current = current->next;
    }

    pthread_rwlock_unlock(&cache_lock); // [읽기 락] 해제
    return 0; // 0 (못 찾음)
}

/*
 * cache_store - 'key'와 'data'를 캐시에 저장
 */
void cache_store(char *key, char *data, int size) {
    if (size > MAX_OBJECT_SIZE) {
        return; // 너무 큰 객체는 캐시하지 않음
    }

    pthread_rwlock_wrlock(&cache_lock); // [쓰기 락] 획득

    // 1. 공간 확보 (퇴출)
    while (total_cache_size + size > MAX_CACHE_SIZE) {
        evict_lru_node();
    }

    // 2. 새 캐시 노드 생성
    CacheNode *new_node = Malloc(sizeof(CacheNode));
    new_node->key = Malloc(strlen(key) + 1);
    new_node->data = Malloc(size);
    new_node->size = size;

    strcpy(new_node->key, key);
    memcpy(new_node->data, data, size); // 바이너리 데이터이므로 memcpy

    // 3. 리스트의 맨 앞에 노드 삽입 (가장 최근 사용)
    new_node->prev = NULL;
    new_node->next = cache_head;

    if (cache_head) {
        cache_head->prev = new_node;
    }
    cache_head = new_node;

    if (cache_tail == NULL) { // 리스트가 비어있었다면
        cache_tail = new_node;
    }
    
    total_cache_size += size;

    pthread_rwlock_unlock(&cache_lock); // [쓰기 락] 해제
}
profile
멈추지 않기

0개의 댓글