컴퓨터 구조 CS:APP 11장
11.1 클라이언트-서버 프로그래밍 모델
클라이언트-서버 모델에서 근본적인 연산은 트랜잭션
트랜잭션은 네 단계로 구성
- 클라이언트는 한 개의 요청을 서버에 보내는 것으로 트랜잭션을 개시(웹 브라우저가 파일을 필요로 할 때, 웹서버로 요청을 보냄)
- 서버는 요청을 받고, 해석하고, 자신의 자원들을 적절한 방법으로 조작, 웹 서버가 브라우저로부터 요청을 받을 때, 디스크 파일을 읽는다
- 서버는 응답을 클라이언트로 보내고, 요청을 기다린다
- 클라이언트는 응답을 받고 이것을 처리
클라이언트와 서버는 프로세스이다
11.2 네트워크
- 클라이언트와 서버는 서로 다른 호스트에서 실행
- 호스트에게 네트워크는 단지 또 다른 I/O 디바이스이다
- LAN은 근거리 네트워크 - 이더넷, 유일한 48비트 주소를 가지며 프레임을 주고 받는다
- 브릿지(bridge) - 여러 이더넷 세그먼트를 연결해 더 큰 LAN을 만든다
- WAN - 지리적으로 더 큰 영역을 연결하는 네트워크
- 세그먼트 - 동일한 전송 매체를 공유하는 네트워크 영역
- 각 호스트의 네트워크 어댑터는 48비트 MAC 주소를 확인하여, 자신에게 온 프레임인지 판별한다

- 여러 LAN, WAN을 라우터가 연결하면, 하나의 인터넷으로 묶임
- 이름 지정(Naming scheme) - 네트워크마다 다른 주소 체계를 통일하기 위해 IP를 사용
- 전달 메커니즘(Delivery mechanism) - 데이터를 일정 형식(packet)으로 묶어 목적지까지 전달
- 패킷은 헤더(주소,길이 등)와 페이로드로 구성됨
11.3 글로벌 IP 인터넷
- 인터넷은 1969년 ARPANET에서 시작되어 발전해온 가장 유명하고 성공적인 인터넷 구현체
- 인터넷 호스트는 TCP/IP 프로토콜 스택을 실행하며, 이는 거의 모든 현대 운영체제에서 지원
TCP/IP 프로토콜 계층
- IP - 기본적인 네이밍(주소 지정), 전달 메커니즘 제공
- 패킷(데이터그램)한 호스트에서 다른 호스트로 전송
- 신뢰성 없음: 손실 발생 시 회복을 하지 않음
- UDP - IP 위에 구축된 단순 확장, 프로세스-프로세스 간 데이터그램 전송 지원
- TCP - IP 위에 구축된 복잡한 신뢰성 프로토콜, 양뱡향 연결을 제공, 연결 기능 담당
프로그래머 관점에서 본 인터넷
- 호스트 집합은 32비트 IP 주소에 매핑, IPv4 주소는 32비트 부호 없는 정수
- 네트워크 전송될 때는 빅엔디언으로 저장
- 변환 함수: htonl,htons, ntohl, ntohs
- IP 주소 집합은 도메인 이름 집합에 매핑
- 도메인 이름과 IP 주소 간 매핑은 DNS 이라는 분산 데이터베이스에서 관리
프로세스-프로세스 간 연결이 가능
- TCP 연결은 소켓을 통해 표현
- 각 소켓은 IP 주소, 포트 번호로 식별
- 에페메럴 포트가 자동 할당, 서버는 잘 알려진 포트를 사용
11.3.2 도메인 이름
- 도메인 이름은 계층적 구조를 가짐
- www.cs.cmu.edu -> edu 최상위, cmu 2단계 도메인, cs 하위 도메인, www 특정 호스트
- 오른쪽에서 왼쪽으로 갈수록 더 구체적
DNS(Domain Name System)
- 도메인 이름과 IP 주소 간의 매핑은 DNS라는 분산 데이터베이스가 관리
11.3.3 인터넷 연결
- 인터넷은 단순히 호스트-호스트 간 연결만 제공하는 것이 아니라 프로세스-프로세스 간의 논리적 연결도 제공
- point-to-point 연결, 완전양방향(full-duples)
소켓
- 프로세스-프로세스 연결을 표현하는 추상화 -> 소켓
- 소켓은 운영체제 커널 안에서 관리되는 엔드포인트
- 각 소켓은 ip 주소, 포트 번호 쌍으로 식별
포트 번호
- 16비트 비부호 정수
- Well-known post
- Ephemeral ports
TCP 연결의 식별
- TCP 연결은 네 개의 값(클라이언트 IP, 클라이언트 포트, 서버 IP, 서버 포트)
- 서버는 같은 포트 번호에서 동시에 여러 클라이언트와 연결할 수 있음
11.4 소켓 인터페이스
- 네트워크 프로그래밍용 함수 집합
- 파일 입출력처럼 소켓을 통해 데이터를 읽고 쓴다
소켓 주소 구조체
- 소켓 통신에서 가장 중요한 데이터 구조 - 소켓 주소 구조체
- IPv4의 경우 struct sockaddr_in을 사용
- sin_family: 프로토콜 패밀리(항상 AF_INET)
- sin_port: 포트 번호 (네트워크 바이트 오더)
- sin_addr: IP 주소 (네트워크 바이트 오더)
주요 함수들
- socket(): 소켓 디스크립터를 생성
- connect(): 클라이언트가 서버와 연결을 설정
- bind(): 서버가 소켓 디스크립터에 자신의 주소를 연결
- listen(): 소켓을 수동 대기 상태로 전환, 연결 요청을 받을 준비
- accept(): 클라이언트 연결 요청을 수락하고 새로운 연결용 디스크립터 생성
- close(): 소켓을 닫음
호스트 서비스 변환
- IP 주소, 도메인 이름, 포트 번호 간 변환을 돕는 getaddrinfo / getnameinfo 함수 제공
- IPv4, IPv6에 독립적인 코드 작성
헬퍼 함수
- open_clientfd(host, port) - 클라이언트 소켓 생성 후 서버와 연결
- open_listenfd(port) - 서버가 리스닝 소켓을 생성

클라이언트: socket -> connect -> read/write
서버: socket -> bind -> listen -> accept -> read/write
socket 함수
- 새로운 소켓 디스크립터 생성
connect 함수 -클라이언트 소켓을 서버 소켓과 연결
bind 함수
- 소켓에 자신의 주소를 할당
listen 함수
- 소켓을 리스닝 모드로 전환
11.4.7 호스트와 서비스 변환
네트워크 프로그래밍을 할 때는 보통 문자열 형태의 주소와 포트를 다룬다
소켓 함수들은 이들을 바이트 형태의 주소 구조체로 다뤄야 함
getaddrinfo와 getnameinfo는 네트워크 프로그래밍에서 표준적이고 이식성 있는 방법이다
리눅스의 함수
- getaddrinfo 함수
-
호스트이름, 호스트주소, 서비스이름, 포트번호의 스트링 표시를 소켓 주소 구조체들로 변환한다
- getnameinfo 함수
- 소켓 주소 구조체를 호스트와 서비스이름 스트링으로 변환된다
11.4.8 소켓 인터페이스를 위한 도움함수들
- open_clientfd
-
주어진 호스트(hostname)와 포트(port)에 대해 서버와 연결된 소켓을 생성, connect로 서버와 연결 시도
- open_listenfd
- 주어진 포트 번호에서 클라이언트 요청을 받을 준비가 된 리스닝 소켓을 생성, bind, listen으로 대기 상태 전환
11.5.2 웹 컨텐츠
- 각 콘텐츠에는 반드시 MIME(Multipurpose Internet Mail Extensions type) 타입이 붙어, 데이터의 의미와 형식을 설명
- 정적 콘텐츠, 동적 컨텐츠
tiny server
전체 동작 개요
- main에서 리스닝 소켓을 열고(Open_listenfd) 무한 루프
- 새 연결을 Accept로 받으면 클라이언트 주소/포트를 출력하고 doit에 파일 디스크립터 connfd를 넘긴다
- doit이 요청 라인과 헤더를 읽고, 정적 파일인지(CG I가 아닌지) 또는 CGI 실행인지 판단한다
- 정적이면 serve_static, 동적이면 serve_dynamic으로 응답을 보낸 후 연결을 닫는다
함수별 해설
-
포트 인자 체크: 인자가 2개가 아니면 사용법 출력 후 종료.
-
리스닝 소켓 생성: listenfd = Open_listenfd(argv[1]);
-
반복 처리:
- Accept로 한 연결씩 수락
- Getnameinfo로 원격 호스트/포트 문자열 얻어 로그 출력
- doit(connfd)로 요청 처리(동기·단일 처리)
- Close(connfd)로 연결 종료
- 특징: 스레드/프로세스 풀 없이 순차적(iterative) 서버입니다. 동시성 X
int main(int argc, char **argv)
- 포트 인자 체크: 인자가 2개가 아니면 사용법 출력 후 종료
- 리스닝 소켓 생성: listenfd = Open_listenfd(argv[1]);
- 반복 처리: Accept로 한 연결씩 수락
- Getnameinfo로 원격 호스트/포트 문자열 얻어 로그 출력
- doit(connfd)로 요청 처리(동기·단일 처리)
- Close(connfd)로 연결 종료
- 특징: 스레드/프로세스 풀 없이 순차적(iterative) 서버입니다. 동시성 X
void doit(int fd)
- 버퍼/변수 준비: 요청 라인 파싱을 위한 method, uri, version 등
- 리오 버퍼 초기화: Rio_readinitb(&rio, fd); (안전한 I/O 래퍼)
- 요청 라인 읽기: Rio_readlineb로 첫 줄(“GET /path HTTP/1.1\r\n”)을 읽어 로그 출력
- sscanf(buf, “%s %s %s”, method, uri, version)으로 메소드/URI/버전 분리
- 메소드 제한: strcasecmp(method, “GET”)가 0이 아니면(=GET이 아니면) 501 응답 (clienterror) 후 반환
- 헤더 읽기: read_requesthdrs로 빈 줄(“\r\n”)까지 요청 헤더를 소진
- URI 파싱: is_static = parse_uri(uri, filename, cgiargs);
- 정적이면 filename에 실제 파일경로(“./…”), 동적이면 CGI 인자(cgiargs)도 세팅
- 파일 메타 조회: stat(filename, &sbuf) 실패 시 404 반환
- 정적/동적 분기:
- 정적: 일반 파일인지 S_ISREG, 읽기권한 S_IRUSR 확인. 위반 시 403, 통과 시 serve_static(fd, filename, sbuf.st_size)
- 동적(CGI): 일반 파일인지, 실행권한 S_IXUSR 확인. 위반 시 403, 통과 시 serve_dynamic(fd, filename, cgiargs)
- 포인트: 정적은 read 권한, CGI는 execute 권한을 검사
void read_requesthdrs(rio_t *rp)
- 한 줄 읽고(Rio_readlineb) 헤더를 출력하면서 빈 줄(“\r\n”)이 나올 때까지 반복
- HTTP/1.x에서 요청 헤더와 바디를 구분하는 CRLF CRLF의 첫 CRLF까지 소진하는 단계
int parse_uri(char *uri, char *filename, char *cgiargs)
- cgi-bin이 없으면 ⇒ 정적:
- cgiargs 빈 문자열로 세팅
- filename = “.” + uri (상대경로로 매핑)
- URI가 /로 끝나면 디폴트 문서로 “home.html”을 덧붙임
- return 1 (정적 표시)
- cgi-bin이 있으면 ⇒ 동적(CGI):
- index(uri, ‘?’)(혹은 strchr)로 쿼리스트링 분리
- ?가 있으면 cgiargs = (ptr+1)로 복사하고 *ptr = ‘\0’로 URI 본체를 끝내기(파일 경로만 남김)
- filename = “.” + uri
- return 0 (동적 표시)
void serve_static(int fd, char *filename, int filesize)
- Content-Type 결정: get_filetype(filename, filetype)
- 응답 헤더 작성/전송: HTTP/1.0 200 OK, Server, Connection: close
- Content-length:
- Content-type:
- 바디 전송: Open으로 파일 열고 Mmap으로 파일 전체를 메모리에 매핑
- 매핑 영역을 Rio_writen으로 한번에 소켓으로 전송
- Munmap, Close 정리
- 포인트: mmap을 써서 커널이 파일을 페이지 캐시에서 바로 송신할 수 있게(복사 감소). 단순·효율적
webproxy
스크립트가 기대하는 “프록시의 핵심 요건”
- HTTP/1.0/1.1 GET 요청 중계(Lab 스펙 기준)
- 요청 파싱, 헤더 정리(Host, Connection: close 등), 오리진과 통신, 응답을 원형 그대로 클라이언트로 전달
- 바이너리 안전성: 텍스트/바이너리 파일 모두 손실/변형 없이 전달
- 동시성: 느린 요청과 무관하게 다른 요청이 진행 가능
- 캐시: 이전에 받은 응답을 저장하고 오리진 다운 시에도 동일 응답 제공
핵심 구성요소
- 소켓 래퍼: Socket, Setsockopt, Bind, Listen, Accept, Connect 등 시스템콜을 감싼 함수들. 실패 시 공통 에러 핸들러 호출
- 주소 변환/검색: Getaddrinfo, Getnameinfo, Freeaddrinfo, Inet_ntop, Inet_pton 등 프로토콜 독립적 도우미
- 클라이언트/서버 헬퍼: open_clientfd(host,port), open_listenfd(port)로 연결 수립/리스닝 소켓 개설을 간단히
- 안정적 입출력(RIO): Rio_readn, Rio_writen, Rio_readlineb, Rio_readinitb. 짧은 읽기/시그널 인터럽트 등을 견고하게 처리
서버 측 통신 플로우
- 리스닝 소켓 생성
listenfd = open_listenfd(port)
내부적으로: getaddrinfo → socket → setsockopt(SO_REUSEADDR) → bind → listen
연결 수락
- connfd = Accept(listenfd, (SA*)&clientaddr, &clientlen) → 클라이언트 전용 소켓 획득
데이터 송수신
- RIO로 요청 읽기/응답 쓰기: Rio_readinitb, Rio_readlineb, Rio_writen
종료
- Close(connfd)
클라이언트 측 통신 플로우
서버에 연결
- clientfd = open_clientfd(host, port)
- 내부적으로: getaddrinfo → 후보 주소들 순회 socket → connect 성공 시 소켓 반환
데이터 송수신
- Rio_readn / Rio_writen / Rio_readlineb 등으로 견고하게 송수신
종료
- Close(clientfd)
RIO(robust I/O) 한눈에 보기
- 초기화: Rio_readinitb(&rp, fd) — 내부 버퍼와 fd 연결
- 라인 단위 읽기: Rio_readlineb(&rp, buf, MAXLINE) — 텍스트 라인을 안전하게 읽음
- 바이트 단위 읽기: Rio_readn(fd, buf, n) / Rio_readnb(&rp, buf, n) — 원하는 바이트 수를 견고히 읽음.
- 쓰기: Rio_writen(fd, buf, n) — 부분 쓰기/인터럽트 처리