[Proxy-lab] 소켓과 Tiny 웹 서버 분석
![[Proxy-lab] 소켓과 Tiny 웹 서버 분석](https://firebasestorage.googleapis.com/v0/b/cruz-lab.firebasestorage.app/o/images%2Fheroes%2Fhero-1766487567121.webp?alt=media&token=939e383a-dc27-4706-b627-a0767c4cdf61)
Proxy Lab의 시작: 소켓이란?
이번 주차는 Proxy Lab이다.
나만의 프록시 서버를 만들기 전에, 가장 기초가 되는 소켓(Socket)과 HTTP가 어떻게 굴러가는지,
그리고 CSAPP에서 제공하는 아주 간단한 웹 서버인 Tiny Web Server를 분석해보자.
1. 소켓(Socket)
소켓은 프로그램이 네트워크를 통해 데이터를 주고받을 수 있게 해주는 창구 혹은 인터페이스다.
Linux 철학 중 "모든 것은 파일이다"라는 말이 있다.
소켓도 예외는 아니다.
커널 입장에서는 소켓도 그저 파일 디스크립터(File Descriptor)일 뿐이다.
💡 파일 디스크립터 (File Descriptor)
시스템이 파일(또는 소켓)을 다루기 위해 할당한 음이 아닌 정수.
open,read,write같은 시스템 콜을 통해 이 번호로 파일에 접근한다.네트워크 소켓도 결국
read,write로 데이터를 주고받는다!
클라이언트와 서버의 소켓 흐름
네트워크 통신은 기본적으로 클라이언트-서버 모델을 따른다. 이 둘이 연결되는 과정을 식당에 비유해보자.
-
socket(): 전화기(소켓)를 설치한다. -
bind(): 전화번호(IP, Port)를 전화기에 할당한다. (서버) -
listen(): 영업 시작! 전화벨이 울릴 수 있게 대기 상태로 만든다. (서버) -
connect(): 손님(클라이언트)이 식당에 전화를 건다. -
accept(): 직원이 전화를 받는다. 이때 새로운 전화기(연결 소켓)를 꺼내서 손님과 대화한다. (서버)
여기서 가장 헷갈렸던 점! 🤯
바로 listenfd(듣기 소켓)와 connfd(연결 소켓)의 구분이다.
-
listenfd: 식당 입구에서 안내해주는 지배인. 계속 입구에 서서 손님을 맞이할 준비만 한다. (1개) -
connfd: 손님을 테이블로 안내하고 주문을 받는 웨이터. 손님 1명당 1명씩 배정된다. (N개)
2. Tiny 웹 서버 분석하기
tiny/tiny.c 코드를 보며 실제로 웹 서버가 어떻게 요청을 처리하는지 살펴보자.
Tiny 서버는 말 그대로 아주 작아서, GET 메서드만 지원하고 정적/동적 컨텐츠를 제공하는 기능만 있다.
메인 루프 (Main Loop)
int main(int argc, char **argv) {
// ... 초기화 코드 생략 ...
listenfd = Open_listenfd(argv[1]); // 듣기 소켓 오픈
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); // 연결 수락
doit(connfd); // 트랜잭션 처리
Close(connfd); // 연결 종료
}
}
아주 심플하다. Accept로 연결을 받고, doit으로 일을 처리하고, Close로 끊는다.
HTTP 1.0 비지속 연결(Non-persistent connection)을 구현하고 있기 때문에 한 번 요청-응답이 끝나면 바로 끊어버리는 쿨한 모습을 볼 수 있다.
트랜잭션 처리 (doit 함수)
doit 함수가 핵심이다. 여기서 HTTP 요청을 파싱하고 적절한 응답을 보낸다.
-
요청 라인 읽기:
RIO패키지를 이용해 요청 라인(예:GET /index.html HTTP/1.1)을 읽는다. -
메서드 확인:
GET이 아니면 "501 Not Implemented" 에러를 뱉고 끝낸다. -
URI 파싱:
parse_uri함수를 통해 정적 컨텐츠 요청인지, 동적 컨텐츠(CGI) 요청인지 구분한다.-
cgi-bin이 포함되어 있으면 동적 컨텐츠! -
아니면 정적 컨텐츠!
-
-
컨텐츠 제공:
-
정적(
serve_static): 파일을 열어서 메모리에 매핑(mmap)한 뒤, 클라이언트 소켓으로 복사한다. -
동적(
serve_dynamic):Fork()를 뜨고 자식 프로세스에서 프로그램을 실행(Execve)한다.
-
❓
mmap은 또 뭐임?
serve_static 함수를 보다가 mmap이라는 함수를 발견했다.
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);
파일을 읽어서 변수에 저장하는 게 아니라, 파일 내용을 그대로 가상 메모리 주소 공간에 매핑해버리는 것이다.
이렇게 하면 커널 공간의 버퍼를 거치지 않고(Zero-copy와 유사한 효과) 바로 메모리처럼 접근할 수 있어서, 파일 전송 시 성능상 이점이 있다고 한다. (물론 여기서는 Rio_writen으로 복사하긴 하지만...)
마무리
소켓 인터페이스라는 추상화 덕분에 우리는 복잡한 TCP/IP 패킷 구조를 몰라도 read, write 만으로 데이터를 주고받을 수 있다.
하지만 그 이면에는 3-way handshake나 흐름 제어 같은 복잡한 과정이 숨어있다는 것을 잊지 말자.