출처
CSAPP Lab 과제 페이지 https://csapp.cs.cmu.edu/3e/labs.html
Proxy Lab 과제 안내서(2019) https://csapp.cs.cmu.edu/3e/proxylab.pdf
소개
Web proxy는 웹 브라우저와 엔드 서버 사이에서 중개자 역할을 하는 프로그램입니다. 브라우저는 웹 페이지를 가져오기 위해 엔드 서버에 직접 접근하는 대신에 엔드 서버로 요청을 포워딩해주는 proxy에 접근합니다. 엔드 서버가 proxy에게 응답을 보내면 proxy가 브라우저에게 응답을 보냅니다.
proxy는 여러 목적에서 유용하게 사용됩니다.
- 방화벽에 proxy를 사용하여 브라우저가 proxy를 통해서만 방화벽 너머의 서버와 통신할 수 있게 만들 수 있다.
- proxy는 요청에서 모든 식별 정보를 제거함으로써 웹 서버에게 브라우저가 익명으로 보이게 만들 수 있다.
- proxy는 서버로부터 온 object들의 로컬 복사본을 저장해놓고 다음에 요청이 들어오면 원격 서버와 통신하는 대신 자신의 캐시를 읽음으로써 응답을 하는 방식으로 웹 object를 캐싱하는 데 사용할 수 있다.
이번 lab에서는 web object를 캐싱할 수 있는 간단한 HTTP proxy를 작성합니다.
lab의 첫 번째 파트에서는 proxy가 들어오는 요청을 받아들이고, 요청을 읽고 파싱하고, 웹 서버에게 요청을 포워딩하고, 웹 서버의 요청을 읽고, 해당 클라이언트에게 응답을 포워딩할 수 있도록 만듭니다. 이 과제를 수행하려면 HTTP 동작과 네트워크 connection을 통해 통신하는 프로그램 작성 방법을 배워야 할 것입니다.
그다음 파트에서는 proxy가 여러 동시 요청을 다룰 수 있도록 업그레이드시킵니다. 여기서 우리는 동시성이라는 중요한 시스템 개념을 소개할 것입니다.
마지막 파트에서는 가장 최근에 접근된 웹 콘텐츠를 저장하는 간단한 메인 메모리 캐시를 이용하여 proxy에 캐싱을 추가합니다.
Part 1: sequential web proxy 구현
HTTP/1.0 GET 요청을 처리하는 기본적인 proxy를 만드세요. POST 요청 등 다른 메서드의 요청을 처리할지 말지는 선택입니다.
proxy가 시작되면 proxy는 커맨드 라인에 명시된 포트 번호에서 들어오는 connection을 listen합니다. connection이 수립되면 proxy는 클라이언트로부터 온 요청 전체를 읽어들인 다음 요청을 파싱해야 합니다. proxy는 클라이언트가 보낸 HTTP 요청이 유효한지 확인해야 합니다. 만약 유효하다면 proxy는 웹 서버와 connection을 수립하고 웹 서버에게 클라이언트가 요청한 object를 요청합니다. 그리고 proxy는 서버의 응답을 읽어들인 다음 클라이언트에게 포워딩합니다.
(1) HTTP/1.0 GET 요청
엔드 유저가 브라우저 주소창에 http://www.cmu.edu/hub/index.html 와 같은 URL을 입력하면 브라우저는 다음 request line으로 시작하는 HTTP 요청을 proxy에게 보낼 것입니다.
GET http://www.cmu/hub/index/html HTTP/1.1
proxy는 이 요청을 적어도 다음 3개의 필드로 파싱해야 합니다. hostname인 www.cmu.edu, path 또는 qeury인 /hub/index.html, 그리고 그 외 모든 것.
이렇게 하면 proxy는 www.cmu.edu로의 connection을 열어야겠다고 결정하고 다음 request line으로 시작하는 HTTP 요청을 서버에게 보낼 것입니다.
GET /hub/index HTTP/1.0
HTTP 요청의 모든 줄은 carriage return인 '\r' 다음에 new line인 '\n'이 오는 방식으로 종료됩니다. 또한 모든 HTTP 요청은 빈 줄인 "\r\n"으로 종료됩니다.
위 예시에서 당신의 웹 브라우저의 request line은 HTTP/1.1로 끝나는 반면 proxy의 request line은 HTTP/1.0으로 끝난다는 것을 확인했나요? 현대 웹 브라우저는 HTTP/1.1 요청을 생성하지만, 당신의 proxy는 이 HTTP/1.1 요청을 잘 처리해서 서버에게는 HTTP/1.0 요청을 보내야 합니다.
HTTP/1.0 GET 요청만 처리한다고 하더라도 HTTP 요청은 매우 복잡합니다. 교재에도 HTTP 트랜잭션에 대한 자세한 설명이 나와있지만 RFC 1945에 있는 완전한 HTTP/1.0 명세서를 참고하길 바랍니다. RFC 1945의 관련 섹션을 잘 따른다면 당신의 HTTP 요청 파서는 이상적으로 충분히 robust해질 수 있을 것입니다. 단, 명세서는 여러 줄의 요청 필드를 허용하지만 당신의 proxy는 이것까지 적절하게 처리하도록 요구되지 않습니다. 물론 당신의 proxy는 잘못된 요청 때문에 일찍 종료되어서는 안 됩니다.
(2) Request headers
이번 lab에서 중요한 요청 헤더는 Host, User-Agent, Connection 그리고 Proxy-Connection 헤더이다.
항상 Host 헤더를 전송하세요.
이 동작이 HTTP/1.0 명세에 의해 기술적으로 sanction되지는 않았지만, 특히 가상 호스팅을 사용하는 일부 웹 서버에서 합리적인 응답을 얻기 위해서는 Host 헤더를 전송이 필요합니다.
Host 헤더는 엔드 서버의 hostname을 설명합니다. 예를 들어 http://www.cmu.edu/hub/index.html에 접근하기 위해 당신의 proxy는 다음과 같은 헤더를 전송해야 합니다.
Host: www.cmu.edu
웹 브라우저가 자신만의 Host 헤더를 HTTP 요청에 추가하는 것도 가능합니다. 이 경우 당신의 proxy는 브라우저와 동일한 Host 헤더를 사용해야 합니다.
다음과 같은 User-Agent 헤더를 항상 전송할 수 있습니다.
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
User-Agent 헤더는 클라이언트를 식별해줍니다. 웹 서버는 그들이 서브해야 하는 콘텐츠를 조작하기 위해 이 정보를 자주 사용합니다. 이러한 특정 User-Agent: 문자열을 전송하면 telnet-style 테스팅 중에 받게 되는 콘텐츠의 질과 다양성이 향상될 수 있습니다.
다음과 같은 Connection 헤더를 항상 전송하세요.
Connection: close
다음과 같은 Proxy-Connection 헤더를 항상 전송하세요.
Proxy-Connection: close
Connection과 Proxy-Connection 헤더는 첫 번째 request/response 교환이 완료된 이후에 connection을 유지할 것인지 아닌지를 명시할 때 사용됩니다. 당신의 proxy는 각각의 요청에 대해 새로운 connection을 여는 것이 완전히 용인되며 권장됩니다. 이 헤더들에 close를 명시하면 웹 서버에게 당신의 proxy가 첫 번째 request/response 교환 이후에 connection을 종료할 것임을 알립니다.
편의를 위해 User-Agent 헤더는 proxy.c에 상수로 제공됩니다.
브라우저가 HTTP 요청에 추가적인 요청 헤더를 보낸다면 당신의 proxy는 그것을 그대로 포워딩해야 합니다.
(3) 포트 번호
이번 lab에는 두 가지 유형의 포트 번호가 있습니다. HTTP 요청 포트 번호와 당신의 proxy가 listen하는 포트 번호입니다.
HTTP 요청 포트 번호는 HTTP 요청의 URL에 있는 선택 필드입니다. 즉, URL은 http://www.cmu.edu:8080/hub/index.html 형태일 수 있습니다. 이 경우에 proxy는 기본 HTTP 포트 번호인 80번이 아니라 8080 포트 번호로 호스트 www.cmu.edu에 연결해야 합니다. 당신의 proxy는 URL에 포트 번호가 있든지 없든지 적절하게 기능해야 합니다.
listening 포트 번호는 proxy가 들어오는 connection을 listen하는 포트 번호입니다. 당신의 proxy는 당신의 proxy를 위해 listen하고 있는 포트 번호를 명시한 커맨드 라인을 받아들여야 합니다. 예를 들어, 다음과 같은 커맨드를 이용하면 당신의 proxy는 포트 번호 15213에서 connection을 listen해야 합니다.
> ./proxy 15213
당신은 다른 프로세스가 사용하고 있지만 않다면 1,024보다 크고 65,536보다 작은 포트 번호를 사용할 수 있습니다. 각각의 proxy가 고유한 listening 포트 번호를 사용해야 하고 많은 사람들이 각자의 머신에서 동시에 작업할 것이기 때문에 당신만의 개인 포트 번호를 고르는 것을 도와주는 스크립트 port-for-user.pl가 제공됩니다. 이 스크립트를 이용하여 당신의 유저 ID에 기반한 포트 번호를 생성하세요.
> ./port-for-user.pl droh
droh: 45806
port-for-user.pl에 의해 return되는 포트 번호 p는 항상 짝수입니다. 따라서 추가적인 포트 번호가 필요하다면 Tiny server에게 요청하세요. 그러면 당신은 포트 번호 p와 p+1을 안전하게 사용할 수 있습니다.
랜덤 포트 번호를 고르지 마세요. 그렇게 하면 다른 사용자를 방해할 수 있습니다.
Part 2: 여러 개의 동시 요청 처리하기
잘 작동하는 sequential proxy를 만들었다면 이제 proxy가 여러 요청을 동시에 처리할 수 있도록 수정하세요.
동시 서버를 구현하는 가장 간단한 방법은 모든 새로운 connection 요청에 대해 요청을 처리하는 새로운 (자식) 스레드를 생성하는 것입니다. 또는 prethreaded 서버를 사용하는 방법도 있습니다.
- 당신의 스레드는 메모리 누수를 피할 수 있도록 detached mode에서 실행되어야 한다.
- open_clientfd와 open_listenfd 함수는 현대적이고 프로토콜에 독립적인 getaddrinfo에 기반을 두고 있어 thread safe하다.
Part 3: web object 캐싱하기
lab의 마지막 파트에서는 가장 최근에 사용된 Web object들을 메모리에 저장하는 캐시를 당신의 proxy에 추가합니다. HTTP는 사실 상당히 복잡한 모델을 정의하고 있는데, 이 모델에 의해 웹 서버가 서브하는 object가 어떻게 캐시되어야 하는지 웹 서버가 지침을 제공할 수 있고 클라이언트는 캐시가 어떻게 사용되어야 하는지를 명시할 수 있습니다. 하지만 당신의 proxy는 간단한 접근을 취할 것입니다.
당신의 proxy가 서버로부터 web object를 받았을 때 당신의 proxy는 클라이언트에게 그 object를 보내면서 자신의 메모리에도 object를 캐시해야 합니다. 만약 다른 클라이언트가 같은 서버에게 같은 object를 요청한다면 당신의 proxy는 서버에 재연결할 필요 없이 캐시된 object를 재전송해주면 됩니다.
당신의 proxy가 요청된 모든 object를 캐시하려고 한다면 무한한 메모리가 필요할 것입니다. 어떤 web object는 다른 것보다 용량이 커서 다른 object가 캐시되는 것을 막고 혼자 전체 캐시 공간을 차지할 수 있습니다. 이러한 문제를 피하기 위해서 proxy는 maximum 캐시 크기와 maximum 캐시 object 크기를 가져야 합니다.
Maximum cache size
당신의 proxy의 캐시의 maximum 크기는 다음과 같습니다.
MAX_CACHE_SIZE = 1MiB
캐시의 크기를 계산할 때 당신의 proxy는 실제 web object를 저장하는 데 사용된 바이트만 계산해야 합니다. 메타 데이터를 포함하여 관련 없는 바이트들은 무시되어야 합니다.
Maximum object size
당신의 proxy가 캐시할 수 있는 web object의 maximum 크기는 다음과 같습니다.
MAX_OBJECT_SIZE = 100KiB
편의를 위해 이 두 개는 proxy.c에서 매크로로 제공됩니다.
올바른 캐시를 구현할 수 있는 가장 간단한 방법은 각각의 active한 connection에 대해 버퍼를 할당하고 서버로부터 데이터가 수신될 때마다 그 버퍼에 데이터를 축적하는 것입니다. 만약 버퍼의 사이즈가 maximum object 크기를 넘는다면 버퍼는 삭제됩니다. maximum object 크기를 넘기 전에 웹 서버의 응답 내용 전체를 읽기했다면 object는 캐시될 수 있습니다. 이러한 전략을 사용하면 web object를 위해 사용될 proxy의 maximum 데이터량은 다음과 같을 것입니다. 여기서 T는 active한 connection의 최대치를 의미합니다.
MAX_CACHE_SIZE + T * MAX_OBJECT_SIZE
Eviction policy
당신의 proxy의 캐시는 LRU (lest-recently-used) eviction 정책에 근사한 eviction 정책을 사용해야 합니다. 이 정책은 엄격하게 LRU일 필요는 없고 그에 근사하면 됩니다. object를 읽는 것과 쓰는 것은 모두 object를 사용하는 것으로 간주된다는 점을 기억해 주세요.
동기화 Synchronization
캐시에 대한 접근은 thread-safe해야 합니다. 이번 part의 가장 흥미로운 지점은 캐시 접근이 race condition으로부터 자유로울 수 있도록 보장하는 것입니다. 여러 개의 스레드가 캐시로부터 동시에 읽기를 해야만 하는 특별한 상황이 있습니다. 캐시에 write하는 것은 한 번에 하나의 스레드만 허용되지만, 캐시를 read하는 스레드에 대해서는 이러한 제약 사항이 없습니다.
하나의 큰 exclusive lock을 사용하여 캐시에 대한 접근을 보호하는 방법은 좋은 해결책이 아닙니다. 그 대신 당신만의 reader-writer 해결책을 구현하기 위해 캐시를 구획화 partition하거나 Pthreads readers-writers 락을 사용하거나 세마포어를 이용할 수 있습니다.
평가
총점은 70점입니다.
- 기초적인 정확성: 기초적인 proxy 동작에 대해 40점이 부여됩니다.
- 동시성: 동시 요청 처리에 대해 15점이 부여됩니다.
- 캐시: 캐시 작동에 대해 15점이 부여됩니다.
자동 채점
학습 자료는 driver.sh라는 채점 도구를 포함하고 있습니다.
> ./driver.sh
리눅스 머신에서 드라이브를 실행시켜 보세요.
Robustness
항상 그렇듯이 에러와 잘못된 또는 악의적인 입력에 대해서 robust한 프로그램을 만들어야 합니다. 서버는 전형적인 오래 실행되는 프로세스로서 web proxy도 여기서 예외가 아닙니다. 오래 실행되는 프로세스가 다양한 유형의 에러에 어떻게 반응해야 하는지를 주의 깊게 생각해 보세요. 많은 유형의 에러의 경우에 proxy가 즉시 exit하는 것은 적절하지 않습니다.
Robustness는 segmentation fault나 메모리 누수, 파일 디스크립터 누수와 같은 에러 케이스에 대한 안정성 또한 요구합니다.
테스트와 디버깅
간단한 자동채점기 이외에 당신이 구현한 프로그램을 테스트해볼 수 있는 샘플 입력이나 테스트 프로그램은 없습니다. 당신은 당신의 코드를 디버깅하고 코드의 정확성을 확인하는 데 도움이 되는 당신만의 테스트와 testing harness를 만들어내야 합니다. 실제 세계에서 테스팅 도구를 만드는 것은 중요한 스킬입니다. 실제 세계의 경우 운영 조건이 각기 특수하기 때문에 때문에 참조할 수 있는 해결책이 없는 경우가 많습니다.
다행히도 당신의 proxy를 디버깅하고 테스트할 때 사용할 수 있는 다양한 도구들이 있습니다. 기본 케이스, 전형적인 케이스, 엣지 케이스를 포함하여 모든 대표적인 입력들을 테스트해보길 바랍니다.
Tiny web server
학습 자료에 있는 소스 코드는 CSAPP 교재의 Tiny web server 코드입니다. thttpd만큼 강력하지는 않지만 Tiny seb server는 당신에게 필요한 대로 수정하기 용이할 것입니다. 또한 이 코드는 당신의 proxy를 만드는 데 좋은 시작점이 될 것입니다. 그리고 드라이버 코드가 페이지를 가져올 때 이 서버를 이용합니다.
telnet
당신의 proxy에 대해 connection을 open할 때 telnet을 사용할 수 있고 proxy에 HTTP 요청을 전송할 수 있습니다.
curl
당신의 proxy를 포함하여 어떤 서버에든지 HTTP 요청을 생성하기 위해 curl을 사용할 수 있습니다. 이는 매우 유용한 디버깅 도구입니다. 예를 들어, 당신의 proxy와 Tiny web server가 모두 로컬 머신에서 실행 중이고, Tiny가 포트 번호 15213에서 listen 중이고 proxy가 포트 번호 15214에서 listen 중이라면, 당신은 다음 curl 명령어를 이용하여 proxy를 통해 Tiny에게 페이지를 요청할 수 있습니다.
> curl -v --proxy http://localhost:15214 http://localhost:15213/home.html
netcat
nc라고도 알려진 netcat은 다양한 용도를 가진 네트워크 도구입니다. 당신은 netcat을 서버와의 connection을 open하는 telnet처럼 사용할 수 있습니다. 그래서 당신이 포트 번호 12345를 이용하여 catshark에서 실행 중이라면 당신은 당신의 proxy를 수동으로 테스트하기 위해 다음과 같은 일들을 할 수 있습니다.
> nc catshark.ics.cs.cmu.edu 12345
웹 서버에 연결할 수 있는 것뿐만 아니라, netcat은 서버 자체로서 작동할 수도 있습니다. 다음 명령어로 당신은 netcat을 포트 번호 12345에서 listen하고 있는 서버처럼 실행시킬 수 있습니다.
> nc -l 12345
당신이 한 번 netcat 서버를 셋업하면 당신은 당신의 proxy를 통해 가짜 object를 요청할 수 있고 그러면 당신은 proxy가 netcat에 보내는 실제 요청을 점검해볼 수 있을 것입니다.
웹 브라우저
마지막으로 당신은 Mozilla Firefox의 가장 최신 버전을 이용하여 당신의 proxy를 테스트해봐야 합니다. About Firefox를 방문하면 당신의 브라우저를 자동으로 가장 최신 버전으로 업데이트할 수 있습니다.
Firefox를 proxy와 함께 작업할 수 있게 구성하려면 Preferences > Advanced > Network > Settings로 방문하세요.
당신의 proxy가 실제 웹 서버를 통해 작동되는 것을 보는 일은 매우 흥미로울 것입니다. 당신의 proxy가 가지는 기능은 제한적이기는 하더라도 당신의 proxy를 통해서 많은 대다수의 웹 사이트를 브라우징할 수 있다는 것을 알게 될 것입니다.
웹 브라우저를 이용하여 캐싱을 테스트할 때는 매우 주의해야 합니다. 모든 현대 웹 브라우저는 각각 자신만의 캐시를 가지고 있습니다. 당신의 proxy를 테스트하려면 이 브라우저의 캐시를 비활성화해야 합니다.
제출 지침 사항
제공된 Makefile은 제출용 파일을 만들어주는 기능을 포함하고 있습니다.
> make handin
결과는 ../proxylab-handin.tar 파일입니다.
- 교재 10-12장은 시스템 레벨 I/O, 네트워크 프로그래밍, HTTP 프로토콜, 동시 프로그래밍에 대해 유용한 정보를 제공합니다.
- RFC 1945(http://www.ietf.org/rfc/rfc1945.txt)는 HTTP/1.0 프로토콜의 완벽한 명세서입니다.
힌트
- 소켓 입출력을 위해 표준 입출력 함수를 사용하는 것은 문제가 될 수 있습니다. 그 대신 csapp.c 파일에 제공된 RIO 패키지를 이용하세요.
- csapp.c에 있는 에러 핸들링 코드는 당신의 proxy에 적합하지 않습니다. 왜냐하면 서버가 한 번 연결을 받아들이기 시작하면 종료되지 않기 때문입니다. 이것을 수정하거나 당신만의 새로운 에러 핸들링 코드를 만들어야 합니다.
- 학습 자료에 있는 파일들은 모두 변경 가능합니다. 예를 들어, 모듈화를 위해 cache 함수들을 cache.c, cache.h와 같은 파일에 라이브러리로 구현해도 됩니다. 물론 새로운 파일을 추가한다면 제공된 Makefile을 업데이트해야 할 것입니다.
- 당신의 proxy는 SIGPIPE 시그널을 무시하고, EPIPE 에러를 return하는 wrtie 작업을 우아하게 처리해야 합니다.
- 때때로 이미 종료된 소켓에 대해 바이트를 수신받기 위하여 read를 호출하면 -1이 return되며 errno는 ECONNRESET으로 설정됩니다. 당신의 proxy는 이 에러 때문에 종료되어서는 안 됩니다.
- 모든 웹 콘텐츠가 아스키 텍스트는 아니라는 점을 기억하세요. 웹에 있는 많은 콘텐츠는 이미지나 비디오와 같은 바이너리 데이터입니다. 네트워크 입출력을 위해 함수를 사용할 때 바이너리 데이터도 처리할 수 있도록 해주세요.
- 원래의 요청이 HTTP/1.1이었을지라도 모든 요청을 HTTP/1.0으로 포워딩해주세요.
행운을 빕니다!
'Computer System > CMU-LAB' 카테고리의 다른 글
[CMU-LAB] Unix Shell Lab 리뷰 (0) | 2025.02.04 |
---|---|
[CMU-LAB] Unix Shell Lab 과제 안내서 (번역) (0) | 2025.01.31 |