Web Proxy #5 : 멀티쓰레딩 Proxy

 

CS:APP에서 Proxy Lab 두번째 과정인 멀티쓰레딩 구현 과정이다. 마냥 딥하지않은 실습 코드를 통해 멀티쓰레딩 개념을 구현하는 것이 목표이다. 우리는 지난 번 제일 기초적인 Proxy를 구현을 완료하였으니 이어서 논해보자.

2025.05.06 - [구현하기] - Tiny Web Server 개발기록 #4

 

Tiny Web Server 개발기록 #4

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

hyeonistic.tistory.com

CS:APP에서 Proxy Lab을 구현하는 것은 11장과 12장의 내용 이수가 필요하다.

하지만 멀티쓰레딩으로 전환하는 것으로써의 코드는 생각보다 그렇게 많이 변경하진 않는다.

그러나 개념을 좀 확실하게 해야 할 필요가 있다.

main 함수 내의 변경

  listenfd = Open_listenfd(argv[1]);
  while (1)
  {
    int *connfd_ptr = Malloc(sizeof(int));
    *connfd_ptr = Accept(listenfd, (SA *)&clientaddr, &clientlen);
    pthread_t pthr;
    pthread_create(&pthr, NULL, task, connfd_ptr);
  }

 

이제 보니 free 할 생각을 까먹었던 것 같다. 이대로 서버가 시작되면 몇 분 이내로 회사 건물에 불이 나겠지.. 반성해야겠다.

기존 코드와 결은 비슷하되, 이번엔 쓰레드를 새롭게 만드는 상황에 봉착했다. 쓰레드는 함수 하나를 배정받아서 배치 된다.

쓰레드를 소환하는 일종의 spawner 역할을 수행하는 하는 함수는 pthread_create 라는 함수인데, 네 개의 변수를 필요로 한다 :

  • &pthr : 생성된 쓰레드의 ID를 저장할 변수의 포인터로써 배치되었다. 이걸로 나중에 해당 쓰레드와 상호작용한다.
  • NULL : 쓰레드의 특성을 결정하는 데 사용한다. 하지만 특별히 건들 것이 없어 NULL로써 기본 속성을 주었다.
  • task : 쓰레드가 생성 된 후 실행 할 함수를 지정한다. 나는 기존에 task()함수를 만들었고 그것을 쓰기로 했다.
    • 일종의 thread 단위의 main 함수를 논하는 것.
  • connfd_ptr : 나는 task에 파일 디스크립터를 넘겼었는데, 여기서는 파일 디스크립터의 포인터를 인자로써 넘겨주었다. 인자를 넘겨준다는 것은 같지만, 기본값이 call by ref 이니 쓰레드가 실행 할 함수 내에서 신중한 수정이 이루어져야한다.
    • 이건 (void *)라서, 어떤 타입의 데이터든 전달이 가능하다. 단, 한 개만 전달이 가능하니 여러 개가 필요하면 구조체를 활용한다.
  • 쓰레드를 생성하면서 쓰레드에게 실행시킬 함수와 그 함수의 매개변수를 주었다. 정도로 요약 가능하다.
void *task(void *connfd)
{
    int fd = *((int *)connfd);
    free(connfd);
    Pthread_detach(pthread_self());
    realTask(fd);
    Close(fd);
}

쓰레드는 이 함수를 받아서 본격적인 작동을 시작한다. 앞에서 논했듯 이건 쓰레드의 main 함수 같은 것이다.

fd라는 실제 값을 받은 다음 매개변수로 받은 건 즉시 메모리 해제를 해주고 있다.
그도 그럴것이 쓰레드는 값만 얻으면 되는거라 그렇다.

 

 

연이어서, Pthread_detach를 통해 이 쓰레드에 대한 옵션을 조정해주어야 한다.

종료되는 즉시 메모리가 해제되는 형태로 조정하는 것이 이 함수의 핀트이다. 그리고 realTask를 실행해서 실질적인 작동을 시작한다.

void realTask(int client_fd)
{
  // #1. 선언부
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char hostHeader[MAXLINE], otherHeader[MAXLINE];
  char hostname[MAXLINE], path[MAXLINE], port[MAXLINE];
  char request_buf[MAXLINE], response_buf[MAXLINE];
  int server_fd;
  ssize_t n;
  rio_t client_rio, server_rio;

  // #2. 연결된 클라이언트로부터 요청 내용 받아오기
  Rio_readinitb(&client_rio, client_fd);
  Rio_readlineb(&client_rio, buf, MAXLINE);
  printf("Request Headers:\n");
  printf("%s", buf);
  sscanf(buf, "%s %s %s", method, uri, version);
  
  // #3. 요청 내용을 본격적으로 유의미한 값으로 재구성
  parseURI(uri, hostname, port, path);
  read_requesthdrs(&client_rio, hostHeader, otherHeader); // read HTTP request headers
  // HTTP 1.1->HTTP 1.0으로 변경 
  format_http_header(request_buf, path, hostname, otherHeader);

  // #4. 서버 최초 연결 시도, #3 내용을 바탕으로 요청
  server_fd = Open_clientfd(hostname, port);
  Rio_writen(server_fd, request_buf, strlen(request_buf));
  Rio_readinitb(&server_rio, server_fd);
  
  // #5. #4에서 연결된 정보를 바탕으로 정보를 얻어오고, 그대로 클라이언트에게 전송
  while ((n = Rio_readnb(&server_rio, response_buf, MAXBUF)) > 0)
  {
     Rio_writen(client_fd, response_buf, n);
  }


  Close(server_fd);
  // Close(server_fd);
  return NULL;
}

#1. 선언부

char에 대한 선언부가 무척 많아졌다. 나도 이정도인가 싶었는데 진짜 이정도 쓰길래 좀 당황했다. 전부 사용처가 있다.

 

 

#2. 연결된 클라이언트로 부터 요청문 받아오기

여긴 특별히 다룰 부분이 없다. 왜냐하면 이전 내용이랑 다를게 없어서 그렇다.

 

 

#3. 요청 내용을 본격적으로 유의미한 내용으로 재구성하기

새로운 함수 세 개를 확인 할 수 있다 : parseURI read_requesthdrs format_http_header

void parseURI(char *uri, char *hostname, char *port, char *path)
{
    char *hostBegin, *hostEnd, *portBegin, *pathBegin;
    int hostLen, portLen, pathLen;

    hostBegin = strstr(uri, "//");
    if (hostBegin != NULL)
        hostBegin += 2;
    else
        hostBegin = (char *)uri;
    // hostbegin, 즉 본격적인 위치를 확인해야함. //의 시작 위치니 //를 스킵한 위치로 던져주는 것
    
    pathBegin = strchr(hostBegin, '/');
    if(pathBegin != NULL)
    {
        hostEnd = pathBegin;
        pathLen = strlen(pathBegin);
        strncpy(path, pathBegin, pathLen);
        path[pathLen] = '\0';
    }
    else
    {
        hostEnd = hostBegin + strlen(hostBegin);
        path[0] = '\0';
    }
    // http://www.naver.com/ << 즉, hostbegin은 //를 이미 넘어간 상태에서 다음 /를 찾는 것이다.

    portBegin = strchr(hostBegin, ':');
    if (portBegin != NULL && portBegin < hostEnd)
    {
        hostLen = portBegin - hostBegin;
        strncpy(hostname, hostBegin, hostLen);
        hostname[hostLen] = '\0';

        portBegin++;
        portLen = hostEnd - portBegin;
        strncpy(port, portBegin, portLen);
        port[portLen] = '\0';
    }
    else
    {
        hostLen = hostEnd - hostBegin;
        strncpy(hostname, hostBegin, hostLen);
        hostname[hostLen] = '\0';
        port[0] = '\0';
    }
    // 포트 번호 탐색.
}

parseURI 함수는 URI 라는 명목으로 입력된 내용을 바탕으로 http:// www.naver.com  / :80 을 다 찢어서 각기 다른 변수에 저장한다.

 

 

void read_requesthdrs(rio_t *rp, char *host_header, char *other_header){
    char buf[MAXLINE];
  
    host_header[0]='\0';
    other_header[0]='\0';
  
    while(Rio_readlineb(rp, buf, MAXLINE) > 0 && strcmp(buf, "\r\n")){
      if(!strncasecmp(buf, "Host:",5)){
        strcpy(host_header, buf);
      }
      else if(!strncasecmp(buf, "User-Agent:", 11)||!strncasecmp(buf, "Connection:", 11)||!strncasecmp(buf, "Proxy-Connection:", 17)){
        continue;
      }else{
        strcat(other_header, buf);
      }
    }
  }

 

read_requestheaders 함수는 받아온 정보를 바탕으로 선언부에서 정의했던 내용에 내용들을 저장한다.

분기는 세 개의 선택지로 나뉜다 : 

  • Host로 시작하는 내용, 이 내용은 건져가야되서 host_header에 빼둔다.
  • else if(...)쪽은 필요 없어서 조건문에서 그냥 넘겨버린다.
  • else 쪽은 없을거라고 기대하고 있지만 뭐가 들어올지 모르니 other_header에 저장한다.

 

 

format_http_header 함수는 본격적으로 서버에게 보내야할 클라이언트의 요청을 양식에 맞게 재작성 하는 것이다. 최근에 백수를 경력기술서에 기깔나게 환경 관리 같은 워딩으로 표현한 글을 봤는데 갑자기 그게 생각난다.

void format_http_header(char *client_rio, char *path, char *hostname, char *other_header){
    sprintf( client_rio,
    "GET %s HTTP/1.0\r\n"
    "Host: %s\r\n"
    "%s"
    "Connection: close\r\n"
    "Proxy-Connection: close\r\n"
    "%s"
    "\r\n",
    path, hostname, user_agent_hdr, other_header);
  }

Proxy Lab에서는 Connection과 Proxy-Connection을 close로 하길 기대하기 때문에 이 부분을 수동으로 조정해준다. 그리고 잔여 내용들을 채워준다. 쌍으로써 매칭이 되어야하고 동시에 \r\n으로 끝나야 하는 부분을 신경써주면 된다.

 

 

#4. 서버 최초 연결 시도, #3 내용을 바탕으로 요청

여기선 특별하게 나눌 이야기가 없다. 요청이 본격적이 되었기 때문에 수령하는 내용이 바뀐다면 요청서가 잘 쓰여진 덕이 아닐까요..

 

 

#5. #4에서 연결된 정보를 바탕으로 정보를 얻어오고, 그대로 클라이언트에게 전송한다.

이 부분이 좀 어려웠는데, 라인 단위로 받아오는건지 byte 단위로 받아오는건지가 헷갈렸다.

response_buf에 MAXBUF만큼 끌어와보니 n만큼 가져올거고 그걸 그대로 client를 향해 response_buf를 보낸다.

 

이렇게 Step 2를 구현 할 수 있었다.

'구현하기' 카테고리의 다른 글

PintOS #0 : 서론  (0) 2025.05.10
Web Proxy #6 : Caching Proxy  (0) 2025.05.08
Web Proxy #4 : Proxy  (0) 2025.05.06
Web Proxy #3 : 기본적인 Tiny 완성하기  (0) 2025.05.05
Web Proxy #2 : 기본적인 Echo 서버 만들기  (1) 2025.05.03