블로그로 돌아가기

[Proxy-lab] 소켓과 Tiny 웹 서버 분석

3분 소요
[Proxy-lab] 소켓과 Tiny 웹 서버 분석

Proxy Lab의 시작: 소켓이란?

이번 주차는 Proxy Lab​​이다.

나만의 프록시 서버를 만들기 전에, 가장 기초가 되는 소켓(Socket)​​과 HTTP​​가 어떻게 굴러가는지,

그리고 CSAPP에서 제공하는 아주 간단한 웹 서버인 Tiny Web Server​​를 분석해보자.

1. 소켓(Socket)

소켓은 프로그램이 네트워크를 통해 데이터를 주고받을 수 있게 해주는 창구​ 혹은 ​인터페이스​다.

Linux 철학 중 "모든 것은 파일이다"라는 말이 있다.

소켓도 예외는 아니다.

커널 입장에서는 소켓도 그저 ​파일 디스크립터(File Descriptor)​일 뿐이다.

​💡 파일 디스크립터 (File Descriptor)​

시스템이 파일(또는 소켓)을 다루기 위해 할당한 음이 아닌 정수.

open, read, write 같은 시스템 콜을 통해 이 번호로 파일에 접근한다.

네트워크 소켓도 결국 read, write로 데이터를 주고받는다!

클라이언트와 서버의 소켓 흐름

네트워크 통신은 기본적으로 클라이언트-서버 모델을 따른다. 이 둘이 연결되는 과정을 식당에 비유해보자.

  1. socket(): 전화기(소켓)를 설치한다.
  2. bind(): 전화번호(IP, Port)를 전화기에 할당한다. (서버)
  3. listen(): 영업 시작! 전화벨이 울릴 수 있게 대기 상태로 만든다. (서버)
  4. connect(): 손님(클라이언트)이 식당에 전화를 건다.
  5. 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 요청을 파싱하고 적절한 응답을 보낸다.

  1. 요청 라인 읽기​​: RIO 패키지를 이용해 요청 라인(예: GET /index.html HTTP/1.1)을 읽는다.

  2. 메서드 확인​​: GET이 아니면 "501 Not Implemented" 에러를 뱉고 끝낸다.

  3. URI 파싱​​: parse_uri 함수를 통해 정적 컨텐츠 요청인지, 동적 컨텐츠(CGI) 요청인지 구분한다.

    • cgi-bin이 포함되어 있으면 동적 컨텐츠!

    • 아니면 정적 컨텐츠!

  4. 컨텐츠 제공​​:

    • 정적(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나 흐름 제어 같은 복잡한 과정이 숨어있다는 것을 잊지 말자.