Web Proxy #4 : Proxy

지난 논함을 계속 하다보니 우리는 Echo 서버, Tiny 서버를 모두 만들 수 있었다. 이제 우리는 진짜 유의미한 무언가를 만들어보기 위해 CS:APP에 있는 Proxy Lab에 대해 논할 것이다. 그리 어렵진 않으면서도 뭔가 막히는 경험을 한다는건 흔하지 않은 것 같다.

2025.05.05 - [구현하기] - Tiny Web Server 개발기록 #3

 

Tiny Web Server 개발기록 #3

CS:APP 책 프로젝트 중 일부로, 11단원에서 웹 서버를 만드는 내용인 Tiny Web Server를 계속 진행하고있다. 최종 목표는 Proxy Lab을 완료하는 것인데, 이 부분에 있어 천천히 그리고 빠르게 접근해야 할

hyeonistic.tistory.com

나는 다음과 같은 자료들을 참고했고, 약간의 파일럿 Co도 동원했다.

참고자료

https://csapp.cs.cmu.edu/3e/proxylab.pdf

  • 요구 조건이 기재되어있는 내용이다.

https://velog.io/@jing93/Week07-proxy-lab

  • 난 나의 영어 실력을 못미더워했기 때문에 자연스러운 번역문을 확인했다.

간단히만 이라도 읽고 와주길 바랍니다.

 

프록시는 서버와 클라이언트 사이에 존재한다. 즉, 클라이언트에게서는 서버의 역할을 해주어야하고, 서버에게서는 클라이언트의 역할을 해주어야한다. 대체 뭘 원하는건가 생각했는데, 그럼 우선 이런 생각을 했다 :

  • 프록시의 테스트 환경은 기존에 만들었던 tiny 기반이니 프록시의 서버 연결 대상은 오직 tiny만 생각하면 된다.
  • 반면에 클라이언트는 몇 개가 들어올지 모르기도 하고, 또 멀티쓰레딩에 대한 코드 확장 여지도 고려해야한다.

그래서 처음에 이 코드의 main에 서버에 대한 디스크립터와 클라이언트에 대한 디스크립터를 생성하고 무한루프를 돌며 클라이언트를 받자 라고 생각했었다.

파일럿 코 : 그따구로 하시면 안됩니다.

어쨌든 서버의 역할을 하기 위해 무한루프를 도는건 맞지만, 서버에 대한 디스크립터는 클라이언트에 요청에 대응하는 식으로 만들고 폐쇄하는게 더 좋다는 결론에 도달했다. 그래서 메인 함수에서 그 내용을 좀 미뤄서 진행하게끔 수정하면서 진행했다.

 

기존의 Echo와 Tiny를 두 눈 또박 뜨고 코드친게 다행일 정도로 닮은 부분이 많았다. 또 Client 부분은 Echo에서 진짜 별로 안쓰여 있어서, 여러번 로그아닌 로그 printf를 찍어나가며 확인해야 했다.

 

내가 고난을 겪었던 건 두 개가 더 있었다 :

  1. 헤더의 요구조건을 뭐 어떻게 하라는거야? 순서도 지켜야 하는건가?
  2. 받은걸 또 Tiny 서버처럼 메모리를 배정해다가 배치하고 그걸 전달해줘야하나?

 

 

고난 #1 해결하기

  • 헤더는 순서쌍을 맞춰 구성시키면 된다. A옵션값: 뭐시기 
    • 그리고, "\r\n" 단위로 끝나야한다는 점을 상기하고 있도록 한다.

고난 #2 해결하기

  • 메모리를 배정하진 않고 그냥 받은거 그대로 클라이언트에 다시 갖다주면 된다.
    • 그럼 어떻게하냐고요?? 그냥 되나?
    • Rio_readnb로 받아와서, Rio_writen으로 쏜다. 끝!
#include <stdio.h>
#include "csapp.h"

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

/* 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";
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36

void task(int fd);
void fowarding(int fd, rio_t *destServer);
void valueSelection(char *uri, char* link, char *cgiargs);
void read_requesthdrs(rio_t *rp);
void get_filetype(char *filename, char *filetype);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);


int main(int argc, char **argv)
{
  int listenfd, connfd;
  char hostname[MAXLINE], port[MAXLINE];
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;
  rio_t rio;

  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);
    task(connfd);
    Close(connfd);
  }
  
  exit(0);
}

void task(int fd)
{
  // fd를 위한 출력 값을 만들어야한다.
  // 즉 여기서 tiny에 값을 보내서, 다시 받아오고, 그걸 다시 뿌려주게 된다.
  struct stat sbuf;
  char buf[MAXLINE], _buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char newHeader[MAXLINE], recieved[MAXLINE];
  char filename[MAXLINE], cgiargs[MAXLINE];
  int destServer_fd;
  rio_t fromClientrio, toServerrio;

  destServer_fd = Open_clientfd("localhost", "12321");
  Rio_readinitb(&toServerrio, destServer_fd);

  Rio_readinitb(&fromClientrio, fd);

  // 프록시랑 연결된 클라이언트로부터 요청 내용을 긁어온다.
  Rio_readlineb(&fromClientrio, buf, MAXLINE);
  printf("Request Headers?!\n");
  printf("%s", buf);
  sscanf(buf, "%s %s %s", method, uri, version);

  sprintf(newHeader, "%s %s HTTP/1.0\r\n", method, uri);
  sprintf(newHeader, "%sHost: localhost\r\n", newHeader);
  sprintf(newHeader, "%sServer: Tiny Web Server\r\n", newHeader);
  sprintf(newHeader, "%sUser-Agent: %s",newHeader, user_agent_hdr);
  sprintf(newHeader, "%sConnection: close\r\n", newHeader);
  sprintf(newHeader, "%sProxy-Connection: close\r\n\r\n", newHeader);
  Rio_writen(destServer_fd, newHeader, strlen(newHeader));

  // 여기서 클라이언트에 최종 전달한다.
  fowarding(fd, &toServerrio);
  Close(destServer_fd);
}

void fowarding(int fd, rio_t *destServer)
{
  char buf[MAXLINE];
  ssize_t n;

  while ((n = Rio_readlineb(destServer, buf, MAXLINE)) > 0)
  {
    Rio_writen(fd, buf, n);
    if(strcmp(buf, "\r\n") == 0)
      break;
  }

  while ((n = Rio_readnb(destServer, buf, MAXLINE)) > 0)
  {
    Rio_writen(fd, buf, n);
  }
}



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>Tiny 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));
}

// 1차 구현 완료

다음에는 멀티쓰레딩 개념을 적용한 Proxy를 만나보시겠다.

사실 이건 Proxy Lab에서 점수가 안찍힌다. 우리는 2단계부터 점수를 찍기 위해 고민해볼것이다.