[Week07] proxy.c sequential

ella·2023년 4월 20일
0

🌳정글 6기🌳

목록 보기
18/39
post-thumbnail

이전 malloc lab에서는 동적 메모리 할당이란 개념도 생소했고, c언어 포인터도 익숙치 않아 다른 사람 코드를 많이 참고했다. 하지만 이번 커리큘럼에서는 malloc 경험을 삼아 메뉴얼을 정독하고 tiny.c라는 연습문제도 풀어봐서 proxy.c를 내 손으로 구현할 수 있어 자신감이 생겼다.

이 글을 보는 다른 사람들도 그럴 수 있길 바라며, 내가 구현한 순서를 부끄럽지만 적어본다.

먼저, proxy.lab 매뉴얼을 보면 다음과 같이 시작하라고 알려준다.

1단계는 HTTP/1.0 GET 요청을 처리하는 기본 순차 프록시를 구현하는 것
proxy가 해야할 일:
1. 지정된 포트 번호에서 client 연결 수신 대기
2. 연결시, 요청 구문 분석 - HTTP가 유효한 요청인지 확인
3. 적절한 웹서버 연결인 경우, 연결 설정 및 server단에 객체 요청
4. 서버의 응답을 읽고, 클라이언트에 전달

처음엔 막막해 보일 수 있지만 순서대로 진행하면 proxy서버를 구현할 수 있다.


1단계: client가 무슨말을 하는지 들어보자.

1단계, client,proxy의 소켓을 만들어주고, client의 요청을 잘 들을 수 있도록 해주었다. 해당 코드는 tiny.c와 동일하므로 tiny.c를 옆에 켜두고 따라해보자.

code

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

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

/*
1단계는 HTTP/1.0 GET 요청을 처리하는 기본 순차 프록시를 구현하는 것

proxy가 해야할 일:
1. 지정된 포트 번호에서 client 연결 수신 대기
2. 연결시, 요청 구문 분석 - HTTP가 유효한 요청인지 확인
3. 적절한 웹서버 연결인 경우, 연결 설정 및 server단에 객체 요청
4. 서버의 응답을 읽고, 클라이언트에 전달
*/

//argc는 proxy와 연결할 때 쓰는 listning 포트
int main(int argc, char **argv)
{
  // client, server의 listenfd, connfd socket 생성
  int client_listenfd, client_connfd;

  //client, server 의 이름, 포트 저장하는 변수
  char clientName[MAXLINE], clientPort[MAXLINE];

  socklen_t clientlen;

  struct sockaddr_storage clientaddr;

  /* Check command line args './proxy 8080'처럼 2개의 argc가 입력이 되지 않았다면,*/
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }
  // 일단 client에서 연결
  client_listenfd = Open_listenfd(argv[1]);

  while (1)
  {
    clientlen = sizeof(clientaddr);
    // Accept(): 새로운 소켓 디스크립터가 생성 -> client_connfd = client fd.
    client_connfd = Accept(client_listenfd, (SA *)&clientaddr, &clientlen);
    // 소켓 주소 구조체를 대응되는 호스트와 서비스 이름 스트링으로 변환
    Getnameinfo((SA *)&clientaddr, clientlen, clientName, MAXLINE, clientPort, MAXLINE, 0);
    
    //proxy 터미널에서 요청이 왔는지 show
    printf("Accepted connection from (%s, %s)\n", clientName, clientPort);
    
    /* 여기서 부터는 doit함수에서 가져온거*/
    
    rio_t rio;
    char buf[MAXLINE];
    // rio 초기화
    Rio_readinitb(&rio, client_connfd);
    //client에서 들어온 내용을 buf에 담기
    Rio_readlineb(&rio, buf, MAXLINE);
    printf("client에서 온 내용입니다.:\n");
    printf("%s \n", buf);

    Close(client_connfd); 
  }

  return 0;
}

결과

client

한개의 터미널로 proxy의 listen 포트를 10500로 열어주고, 다른 터미널로 telnet을 이용해 proxy의 listen port와 연결 뒤, 내가 보내고 싶은 메세지를 보내봤다. 음 proxy가 잘 듣고있다.

proxy


2단계: client가 문법을 가지고 말하는지 검열을 하는 기능을 만들자.

client는 앞서 test한 것처럼 아무말이나 내뱉으면 안된다. http문법을 지키면서 말해야 원하는 바를 만들어 줄 수 있다. 말하는 문법에 대한 건 CS:APP 916page부터 나와있고, proxy 매뉴얼 4.1에도 나와있다.

client는 browser를 통해 <GET http://www.cmu.edu/hub/index.html HTTP/1.1> 이라는 '말'을 한다. 이 때 문법은 "method uri version" 인것이다. 따라서 method, uri, version 의 값을 제대로 말했는지 검열하는 판단문을 만들어 보자.

  • tiny.c에서 쓰이는 '검열하는 판단문'은 doit()함수에 다음과 같이 적용되어있다.
if (strcasecmp(client_method, "GET") && strcasecmp(client_method, "HEAD"))
...

여기에 검열문을 좀 더 추가하는 연습을 해보았다. 입력이 없거나, '/'가 없을 경우 안내문을 띄우며 종료하도록 해봤다.

코드

void doit(int connfd){
  rio_t client_rio, server_rio;
  int server_connfd;
  char method[MAXLINE], uri[MAXLINE], version[MAXLINE], 
  uri_ptos[MAXLINE], hostname[MAXLINE], port[MAXLINE], buf[MAXLINE], body[MAXBUF];

  // client측에서 전달된 메세지 수신
  Rio_readinitb(&client_rio, connfd);               // rio 초기화
  Rio_readlineb(&client_rio, buf, MAXLINE);         // client에서 들어온 내용을 buf에 담기

  printf("client에서 온 내용입니다.:\n");            // client에서 들어온 내용을 proxy 터미널에서 출력
  printf("%s \n", buf);

  sscanf(buf, "%s %s %s", method, uri, version);   // buf를 공백을 기준으로 3덩어리로 나누기.
  // printf("method,: %s,  uri: %s, version: %s \n", method, uri, version); /* 확인용 코드 */

  // 메세지 입력 예외처리1 : method - get 또는 head가 아닌 경우, 에러 메세지 안내.
  if (strcasecmp(method, "GET") && strcasecmp(method, "HEAD")){
    sprintf(body, "Proxy는 GET, HEAD method만 처리할 수 있습니다.\n");
    Rio_writen(connfd, body, strlen(body));

    return;
  }

  // 메세지 입력 예외처리2 : uri - 입력하지 않았거나, '/'가 없을 경우
  if (strlen(uri) == 0 || (strchr(uri, '/') == NULL)){
    sprintf(body, "보내주신 uri가 이상해요. 한번 확인해주세요.\n");
    Rio_writen(connfd, body, strlen(body));

    return;
  }

  // 메세지 입력 예외처리3: version - 입력하지 않았거나, '/'가 없을 경우
  if (strlen(version) == 0 || (strchr(version, '/') == NULL)){
    sprintf(body, "HTTP버전이 이상해요. 한번 확인해주세요.\n");
    Rio_writen(connfd, body, strlen(body));

    return;
  }

  // read_requesthdrs(&client_rio); /*option header를 rio구조체에 표기-> http1.0버전은 option header를 무시하므로 주석처리 */

  parse_uri(uri, uri_ptos, hostname, port);               // client 메세지를 분할해서 hostname, path, port를 포인터에 저장.

  server_connfd = Open_clientfd(hostname, port);          // server에 연결
  // printf("server_connfd: %d\n", server_connfd);        /* 확인용 코드 */

  do_request(server_connfd, method, uri_ptos, hostname);  //client에서 입력된 데이터를 기반으로 proxy가 헤더를 만들고 server에 전달
  do_response(connfd, server_connfd);                     //server에서 데이터를 받아서 client에 전달

  close(server_connfd);
  return;
}

터미널 결과창

  • 입력을 'first second thrid' 로 넣었을 때
    오른쪽 proxy 터미널을 보면,
    client_method: third , client_uri: second, client_version: third 로 분할되어 변수들어 저장되는 것을 확인할 수 있다.

  • 입력을 'weird data put' 으로 넣었을 때
    method가 GET이나 HEAD가 아니라면, 안내문구를 보이고 종료되는 것을 확인할 수 있다.

  • 입력을 'GET weirduri HTTP/1.1'(이상한 uri), 'GET weirduri weirdversion'(이상한 버전), 'GET google.com/ HTTP/1.1'(정상 입력) 일 때를 넣어서 원하는대로 안내문구가 나오는지 확인한다.

이러한 에러코드를 규정해 놓은 http정식

사실 이렇게 아마추어같이 잘못되었다고 안내문구를 내면 안된다. 외국인도 어린이도, 어른도, 외계인도 모두 알 수 있게 http버전별로 에러코드가 정해져있다. 해당 오류코드는 http공식 사이트에 가면 자세하게 볼 수 있다.
HTTP1.0버전 공식 사이트 : https://datatracker.ietf.org/doc/html/rfc1945

보통 우리가 많이 보는 403, 404 에러는 모두 client단에서 문제가 있을 때 안내해주는 에러코드다. 다음 안내문을 변경하는 건 쉬우니 설명은 넘어가도록 한다.


3단계: 말을 몇번 들어줘야할까?

http1.0과 http1.1 버전은 지금 구현 단계에서 한가지 차이점이있는데, http1.1버전은 말을 한번만 요구하지 않는다. 다음줄에서 host가 누구인지 요청하도록 해야한다. 아래 cs:app 책을 보면, client에서 요청하는 코드가 한줄이아니라 두줄이다. 그럼, 내가 만드는 서버는 도대체 몇줄까지가 client의 말이 끝나는 시점이라는 것을 알아차릴까? 모든 답은 proxy lab 메뉴얼에 있다.
메뉴얼 4.2 Request headers에 "RFC 1945의 적절한 섹션에 따라 HTTP 요청을 완전히 robust하게 파싱해야 하지만, 멀티라인 요청 필드를 제대로 처리할 필요는 없습니다. 또한, RFC 1945에서는 "\r\n"로 끝나는 빈 줄로 HTTP 요청이 종료되는 것이 중요합니다." 라고 안내문이 쓰여있다. 즉, 엔터를 두번치면 = '\r\n\r\n'이라는 입력이 주어지면 server는 아 client말이 다 끝났구나 알아차릴 수 있게 되는 것이다.

하지만 이렇게 모든 옵션에 따라 서버를 구현하기에는 너무 복잡하니, proxy lab 메뉴얼에 따라 http1.0버전처럼 1줄만 명령을 받고 응답을 주도록 구현해볼 것이다.

그렇다면 1줄 이외의 입력들은 어떻게 처리해야 할까?
tiny.c의 경우 read_requesthdrs(&client_rio) 함수를 이용해서 여러줄의 메세지를 받았을 경우 경우, rio구조체에 담도록 하는 구문이 있다. 우리는 명령 한줄만 받아서 처리하게 하자.


rio 구조체에 대해 알고가자

rio구조체는 cs:app 10장에 자세하게 설명되어 있다. 간단하게 이해한 바를 서술하자면, proxy-lab은 buf라는 변수에 rioreadlineb()함수를 통해서 한줄씩 읽은 뒤, 메모리에 저장한다. rio는 입력된 fd(file descriptor), 그리고 몇줄이 입력됐는지(rio_cnt), 첫줄이 입력되어있는 ptr을 저장하고, 그 buf크기를 저장한다. 그렇다면, ptr을 기준으로 몇줄(몇 바이트)까지 읽는다면, client의 긴 줄이 메모리 어디에 저장되어있고, 그내용을 읽을 수 있을 것이다.

read_requesthdrs(&client_rio);

// 여러줄 요구할 때 rio구조체에 표시
void read_requesthdrs(rio_t *rp)
{
  char buf[MAXLINE];

  Rio_readlineb(rp, buf, MAXLINE);
  while (strcmp(buf, "\r\n"))
  {
    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
  }
}

4단계: parsing하고 server에 보낼 header를 만들자.

client가 전달한 말을 '/'단위로 잘라서 uri, name, path, port포인터에 해당 값을 넣어주자.
이러한 작업을 pasing이라고 하는데, 이때 도메인으로 들어온 값을 리눅스가 제공하는 강력한 함수 getaddrinfo()함수를 통해 소켓 주소 구조체를 대응되는 호스트와 서비스 이름 스트링으로 변환받아서 포인터에 저장하고 출력해보면 다음과 같이 잘 나오는 것을 확인할 수 있다.
이 때, 도메인(www.google.com)으로 넘어온 값을 IP로 바꾸는 규칙을 DNS라고 하며, DNS를 지원하는 비영리 기관/단체의 이름은 "ICANN(Internet Corporation for Assigned Names and Numbers)"이다.
한국에서는 한국에서는 "한국 인터넷 진흥원(Korea Internet & Security Agency, KISA)"이 ICANN과 협력하여 DNS와 관련된 정책 수립 및 운영 업무를 수행하고 있다고 한다.

    Getnameinfo((SA *)&clientaddr, clientlen, clientName, MAXLINE, clientPort, MAXLINE, 0); // 소켓 주소 구조체를 대응되는 호스트와 서비스 이름 스트링으로 변환

    printf("Accepted connection from (%s, %s)\n", clientName, clientPort); // 요청이 왔는지 proxy 터미널에 표시


5단계: 서버와 연결하고, client에게 전달하자.

이번에는 내가 작성하고 있는 코드가 client가 되고, server에게 연결을 요청하는 코드를 작성해 본다. 다행히 함수는 proxy lab에서 이미 구현을 해줬다. Open_clientfd()함수로, open_clientfd()함수를 한 겹 에러시 에러코드와 int로 반환하는 함수로 감싸준 함수다.

  server_connfd = Open_clientfd(hostname, port); // server에 연결
  // printf("server_connfd: %d\n", server_connfd);

이제 포인터에 적힌 값을 바탕으로 원하는 적은 header를 써서 서버에게 보낼준비를하고, Rio_writen()함수로 client에게 전달하자.

void do_request(int server_connfd, char *method, char *uri_ptos, char *hostname)
{
  char proxy_hdr[MAXLINE];

  printf("Request headers to server: \n");
  printf("%s %s %s\n", method, uri_ptos, "HTTP/1.0");

  // server에 전달할 헤더 만들기
  sprintf(proxy_hdr, "GET %s HTTP/1.0\r\n", uri_ptos);
  sprintf(proxy_hdr, "%sHost: %s\r\n", proxy_hdr, hostname);          // Host: www.google.com
  sprintf(proxy_hdr, "%s%s", proxy_hdr, user_agent_hdr);              // User-Agent: Mozilla/5.0 (X11;...
  sprintf(proxy_hdr, "%sConnections: close\r\n", proxy_hdr);          // Connections: close
  sprintf(proxy_hdr, "%sProxy-Connection: close\r\n\r\n", proxy_hdr); // Proxy-Connection: close

  Rio_writen(server_connfd, proxy_hdr, (size_t)strlen(proxy_hdr));
}
                           


여기까지가 sequential proxy 구현의 순서다. 나처럼 직접 코드를 짜보고 싶은 사람에게 도움이 되었으면 싶어서 일부러 전체 코드를 넣지 않았다. 자신이 코드를 짜보고 자신감을 얻었으면 좋겠다. 물론 이후에 thread나 cashing을 적용해야한다. thread는 코드 몇줄만 추가하면 개선이 되지만, cashing은 구조체를 이용해야해서 블로그에 적기에는 시간적으로 다소 무리가 있어 이정도로 마무리 한다.

profile
^^*

0개의 댓글