웹 브라우징, 이메일 전송, 온라인 게임 등 네트워크 애플리케이션은 어디에나 있습니다. 흥미롭게도, 모든 네트워크 애플리케이션은 동일한 기본 프로그래밍 모델, 유사한 전체 논리 구조, 그리고 동일한 프로그래밍 인터페이스를 기반으로 합니다.
네트워크 애플리케이션은 이미 시스템 공부에서 배운 많은 개념(예: 프로세스, 신호, 바이트 순서(byte ordering), 메모리 매핑, 동적 저장소 할당)에 의존합니다.
물론 마스터해야 할 새로운 개념도 있습니다.
마지막에는 이 모든 아이디어를 종합하여, 실제 웹 브라우저에 텍스트와 그래픽으로 정적(static) 및 동적(dynamic) 콘텐츠를 모두 제공할 수 있는, 작지만 기능적인 웹 서버를 개발할 것입니다.
모든 네트워크 애플리케이션은 클라이언트-서버 모델을 기반으로 합니다. 이 모델에서 애플리케이션은 하나의 서버 프로세스(server process)와 하나 이상의 클라이언트 프로세스(client process)로 구성됩니다.

클라이언트-서버 모델의 기본 동작은 트랜잭션(transaction)입니다 (그림 11.1). 트랜잭션은 4단계로 구성됩니다.
클라이언트와 서버는 (이 문맥에서 종종 호스트(host)라고 불리는) 머신(machine)이 아니라 프로세스(process)라는 것을 인지하는 것이 중요합니다.
클라이언트와 서버는 종종 분리된 호스트에서 실행되며, 컴퓨터 네트워크의 하드웨어 및 소프트웨어 자원을 사용해 통신합니다. 네트워크는 정교한 시스템이며, 여기서는 프로그래머의 관점에서 작동 가능한 mental model을 제공하는 것이 목표입니다.
호스트에게 네트워크는 데이터를 위한 소스(source)이자 싱크(sink) 역할을 하는 또 하나의 I/O 장치일 뿐입니다 (그림 11.2).

물리적으로 네트워크는 지리적 근접성에 따라 구성된 계층적 시스템입니다.

케이블: 주로 UTP(Unshielded Twisted Pair, 꼬임쌍선) 케이블을 사용합니다. (이것이 우리가 "랜선"이라고 부르는 것입니다.)
네트워크 어댑터 (랜 카드): 모든 호스트(PC, 노트북 등)에 장착되며, 데이터를 프레임으로 만들거나 해석합니다.
연결 장치 (허브 vs 스위치):
- 허브 (Hub): 11.2에서 언급된 구형 장치입니다. 한 포트에서 들어온 데이터를 모든 포트로 맹목적으로 복사하여 전송합니다. (비효율적)
- 스위치 (Switch): 현재의 표준 장치입니다. 11.2의 브리지(Bridge)와 유사하게 작동하며, 각 포트에 연결된 장치의 MAC 주소를 학습합니다. 데이터가 들어오면 목적지 MAC 주소가 연결된 포트로만 프레임을 선택적으로 전송하여 효율이 매우 높습니다.
이더넷은 LAN 내부에서 장치들이 "어떻게" 통신할지 정의합니다.
MAC 주소 (Media Access Control Address)
프레임 (Frame)
- 이더넷에서 데이터를 주고받는 기본 패키지 단위입니다.
- 본문 11.2에서 설명한 것처럼 [헤더 + 페이로드] 구조를 가집니다.
- 이더넷 헤더에는 목적지 MAC 주소와 출발지 MAC 주소가 포함됩니다.
- (참고: 11.2의 캡슐화 예시처럼, 인터넷 통신을 할 때는 IP 패킷이 이 이더넷 프레임의 '페이로드' 부분에 담겨서 전송됩니다.)
[MAC 주소 | 포트 번호]를 한 쌍으로 저장합니다. 이 학습과 처리 과정은 두 부분으로 나뉩니다.네트워크의 컴퓨터 A(MAC 주소: AA-AA)가 스위치의 1번 포트에 연결되어 있다고 가정합니다.
A가 컴퓨터 B에게 데이터를 보내기 위해 프레임(frame)을 생성합니다. 이 프레임의 헤더에는 [출발지: AA-AA | 목적지: BB-BB]가 기록됩니다.
프레임이 1번 포트로 들어오는 순간, 스위치는 프레임 헤더를 엿봅니다.
스위치는 '출발지(Source) MAC 주소'인 AA-AA를 확인합니다.
스위치는 "아, AA-AA라는 MAC 주소는 1번 포트에 연결되어 있구나!"라는 사실을 알게 됩니다.
이 정보를 자신의 MAC 주소 테이블에 즉시 기록(갱신)합니다.
| MAC 주소 | 포트 번호 |
|---|---|
| AA-AA | 1 |
학습의 목적은 데이터를 효율적으로 전달하기 위함입니다.
이제 컴퓨터 B(MAC 주소: BB-BB, 포트: 2번)가 A에게 답장을 보낸다고 가정합니다.
B가 보낸 프레임 [출발지: BB-BB | 목적지: AA-AA]가 2번 포트로 들어옵니다.
(학습) 스위치는 출발지 BB-BB를 보고 "아, BB-BB는 2번 포트에 있구나!"라고 테이블에 기록합니다.
(전달) 스위치는 '목적지(Destination) MAC 주소'인 AA-AA를 봅니다.
스위치가 자신의 MAC 주소 테이블을 검색합니다.
테이블에 AA-AA는 1번 포트에 있다고 기록되어 있습니다.
스위치는 이 프레임을 다른 모든 포트가 아닌 오직 1번 포트로만 정확하게 전송(Forwarding)합니다. (허브처럼 모든 곳에 뿌리지 않습니다.)
맨 처음, 스위치의 MAC 주소 테이블은 비어있습니다.
이때 A(AA-AA)가 B(BB-BB)에게 프레임을 보내면, 스위치는 AA-AA가 1번 포트에 있음을 학습합니다.
하지만 목적지인 BB-BB가 테이블에 없습니다. 스위치는 이 프레임을 어디로 보내야 할지 모릅니다. 이때 스위치는 허브(Hub)처럼 작동합니다.
프레임이 들어온 1번 포트를 제외한 모든 포트로 이 프레임을 복제해서 뿌립니다 (Flooding).
B를 제외한 모든 컴퓨터는 "이거 내 거 아니네" 하고 프레임을 무시하지만, B는 "내 거다!" 하고 프레임을 받습니다. 그리고 B가 답장을 보내는 순간, 스위치는 B의 위치(BB-BB는 2번 포트)를 학습하게 됩니다.
이 MAC 주소 테이블에 대해 더 궁금한 점이나, IP 주소와 MAC 주소가 함께 작동하는 방식(ARP)에 대해 이어서 설명해 드릴까요?
192.168.0.10 같은 IP 주소를 사용합니다. 이것은 "논리적 주소"입니다. 문제가 발생합니다:192.168.0.10(B)에게 데이터를 보내고 싶은데, 스위치에게 데이터를 전달하려면 B의 MAC 주소를 알아야만 이더넷 프레임 [헤더]에 적어 보낼 수 있습니다. ARP의 역할:192.168.0.10을 쓰시는 분, MAC 주소가 어떻게 되시나요?"라고 물어보는 데 사용하는 프로토콜이 바로 ARP입니다.192.168.0.5)가 PC B (192.168.0.10)에게 데이터를 보내려 함 ]ARP 캐시 확인 (Cache Check)
192.168.0.10의 MAC 주소를 이미 알고 있나?"ARP Request (Broadcast)
192.168.0.10을 가진 분, 당신의 MAC 주소를 192.168.0.5(나)에게 알려주세요."FF:FF:FF:FF:FF:FF (이것이 "모두에게"라는 뜻의 특수 주소입니다.)수신 및 응답 결정
192.168.0.10) 찾는 거 아니네." ➔ 요청을 무시(discard)합니다.192.168.0.10)를 찾네!" ➔ 응답(Reply)을 준비합니다.ARP Reply (Unicast)
192.168.0.10이고, 제 MAC 주소는 BB-BB-BB-BB-BB-BB입니다."캐시 저장 및 통신 시작
- PC A는 응답을 받고, 자신의 ARP 캐시에 [192.168.0.10 = BB-BB-BB-BB-BB-BB] 라고 기록합니다.
- 이제 PC A는 PC B의 MAC 주소를 알았으므로, 원래 보내려던 데이터를 이더넷 프레임에 담아 (목적지 MAC: BB-BB...) 전송을 시작합니다.
- PC B 역시 PC A의 MAC 주소를 ARP 테이블에 캐싱합니다.
cmd나 터미널에서 arp -a 명령어를 치면 현재 내 PC의 ARP 캐시 목록을 볼 수 있습니다.)L3 (IP) 헤더는 불변: 패킷이 최종 목적지에 도착할 때까지 Source IP (A)와 Destination IP (B)는 절대 변하지 않습니다.
L2 (MAC) 헤더는 가변: 이더넷 프레임의 L2(MAC) 헤더는 각 라우터를 통과(hop)할 때마다 매번 벗겨지고 새로 포장됩니다. L2 헤더는 오직 '다음 홉(next hop)'까지만 유효합니다.
PC A가 인터넷을 거쳐 PC B에게 데이터를 보냅니다.
경로: PC A → R1 (A의 게이트웨이) → R2 (중간 라우터) → R3 (B의 게이트웨이) → PC B
IP 패킷 (L3): [Source: A-IP | Dest: B-IP] (이것은 절대 변하지 않음)
PC A → R1 (LAN A 내부)PC A의 판단: "목적지 B-IP는 내 LAN(A)에 없다. 무조건 게이트웨이 R1에게 보내야지."
필요한 정보: R1의 MAC 주소
ARP #1 (LAN A): PC A가 LAN A에 브로드캐스트. "IP R1-IP 가진 분, MAC 주소 뭐예요?"
R1의 응답: "제 MAC은 R1-MAC-A입니다."
PC A의 프레임 포장:
A-MAC | Dest: R1-MAC-A]A-IP | Dest: B-IP]PC A가 이 프레임을 LAN A로 전송합니다.
R1 → R2 (라우터 간)R1의 수신: R1이 프레임을 받고, L2 헤더(목적지가 R1-MAC-A임)를 벗겨냅니다.
R1의 판단 (L3): L3 헤더를 봅니다. "최종 목적지는 B-IP구나."
라우팅 테이블 조회: "내 라우팅 테이블을 보니, B-IP로 가려면 '다음 홉'은 R2이네."
필요한 정보: R2의 MAC 주소
ARP #2 (R1-R2망): R1이 R2와 연결된 망에 ARP 요청. "IP R2-IP 가진 분, MAC 주소 뭐예요?"
R2의 응답: "제 MAC은 R2-MAC입니다."
R1의 새 프레임 포장:
R1-MAC-WAN | Dest: R2-MAC]A-IP | Dest: B-IPR1이 이 새로운 프레임을 R2로 전송합니다.
R2 → R3 (라우터 간)2번 과정과 완전히 동일한 로직이 반복됩니다.
R2의 수신: 프레임을 받고, L2 헤더(목적지가 R2-MAC임)를 벗겨냅니다.
R2의 판단 (L3): L3 헤더를 봅니다. "최종 목적지는 B-IP."
라우팅 테이블 조회: "내 테이블을 보니, B-IP로 가려면 '다음 홉'은 R3이네."
필요한 정보: R3의 MAC 주소
ARP #3 (R2-R3망): R2가 R3와 연결된 망에 ARP 요청. "IP R3-IP 가진 분, MAC 주소 뭐예요?"
R3의 응답: "제 MAC은 R3-MAC입니다."
R2의 새 프레임 포장:
R2-MAC-WAN2 | Dest: R3-MAC]A-IP | Dest: B-IPR2가 이 새로운 프레임을 R3으로 전송합니다.
R3 → PC B (LAN B 내부)마지막 단계입니다. 여기서 '다음 홉'은 라우터가 아닌 최종 목적지 PC B가 됩니다.
R3의 수신: 프레임을 받고, L2 헤더(목적지가 R3-MAC임)를 벗겨냅니다.
R3의 판단 (L3): L3 헤더를 봅니다. "최종 목적지는 B-IP."
라우팅 테이블 조회: "내 테이블을 보니, B-IP가 속한 네트워크(LAN B)는 나에게 직접 연결되어 있네!"
R3의 결론: "이제 내가 이 패킷을 PC B에게 직접 전달하면 끝이다."
필요한 정보: PC B의 MAC 주소
ARP #4 (LAN B): R3이 LAN B에 브로드캐스트. "IP B-IP 가진 분, MAC 주소 뭐예요?"
PC B의 응답: "저예요! 제 MAC은 B-MAC입니다."
R3의 마지막 프레임 포장:
R3-MAC-B | Dest: B-MAC]A-IP | Dest: B-IPR3이 이 프레임을 LAN B로 전송하고, PC B가 최종적으로 수신합니다.
R2, R3)이든, 최종 목적지(PC B)이든 상관없이, 라우터는 '다음 홉'의 MAC 주소를 알아내기 위해 ARP를 사용합니다.R1-IP를 알게 되는 방법(즉, 네트워크 설정)은 크게 두 가지입니다.가장 간단한 방식입니다.
사용자가 직접 PC의 [네트워크 설정]에 들어가서 IP 정보를 모두 키보드로 입력합니다.
- IP 주소: 192.168.0.5
- 서브넷 마스크: 255.255.255.0
- 기본 게이트웨이: 192.168.0.1 (이것이 바로 R1-IP입니다)
우리가 집이나 카페에서 Wi-Fi에 연결할 때 자동으로 IP를 받는 방식입니다.
DHCP (Dynamic Host Configuration Protocol) 라는 프로토콜이 사용됩니다.
과정:
1. PC A가 네트워크에 연결되면, "저 IP 주소 필요해요!"라고 DHCP Discover 브로드캐스트를 보냅니다.
2. 일반적으로 라우터(R1)가 이 DHCP 서버 역할을 겸합니다.
3. R1이 PC A에게 "DHCP Offer"를 보냅니다. 이 안에는 다음 정보가 모두 들어있습니다.
- IP 주소: "넌 192.168.0.5를 쓰렴."
- 서브넷 마스크: "네트워크 울타리는 255.255.255.0이야."
- 기본 게이트웨이: "그리고 모르는 곳(외부망)으로 갈 땐, 나(192.168.0.1)에게 보내렴." (이것이 R1-IP입니다.)
- (부가 정보: DNS 서버 주소 등)
R1-IP를 "기본 게이트웨이" 주소로 미리 저장해 둡니다.B-IP로 통신을 시도합니다.B-IP가 외부망임을 확인합니다.R1-IP)를 꺼냅니다.R1-IP로 보내야 하는데... 혹시 R1-IP의 MAC 주소 아는 사람?" (ARP 브로드캐스트)

허브 (Hub) - Layer 1 (물리 계층)
스위치 (Switch) - Layer 2 (데이터 링크 계층)
- 스위치는 "영리한(smart)" 장치입니다. (제시해주신 텍스트의 브리지와 동일한 역할을 합니다.)
- 스위치는 각 포트에 연결된 장치의 MAC 주소를 학습합니다.
- A가 C에게 데이터를 보낼 때, 스위치는 해당 데이터를 C가 연결된 포트로만 선택적으로 전송합니다.
- 현대의 스위치(Switch)는 사실상 '멀티 포트 브리지(multi-port bridge)'입니다. 브리지의 기능을 여러 포트에서 고속으로 처리하도록 만든 장치입니다.
결론: 허브는 모든 포트에 소리치는 확성기(broadcast)이고, 스위치(브리지)는 특정 대상에게만 귓속말(unicast)을 전달하는 교환원입니다.
브리지 (Bridge) - Layer 2 (데이터 링크 계층)
라우터 (Router) - Layer 3 (네트워크 계층)
- 라우터는 IP 주소 (논리적 주소)를 기반으로 작동합니다.
- 주된 목적은 서로 다른 네트워크(예: 내 집 LAN과 인터넷 WAN)를 연결하고, 데이터 패킷이 목적지까지 갈 수 있는 최적의 경로를 찾는(routing) 것입니다.
결론: 브리지는 '하나의 큰 집(LAN)'을 여러 방(segment)으로 나누는 문지기 역할이고, 라우터는 '우리 집(LAN)'과 '다른 집(Another LAN/WAN)'을 연결하는 대문이자 우체부 역할입니다.
브리지 (Bridge):
스위치 (Switch):
- 현재: 브리지의 개념을 여러 포트(multi-port)로 확장한 장치입니다. (포트 수가 4, 8, 24개 등으로 많음)
- 처리: ASIC (주문형 반도체)라는 전용 하드웨어를 사용하여 MAC 주소 테이블 조회와 포워딩을 "와이어 스피드(wire speed, 회선 최고 속도)"로 매우 빠르게 처리합니다.
결론:
오늘날 우리가 "스위치"라고 부르는 장비는 브리지의 기본 원리를 계승하여 훨씬 더 빠르고, 더 많은 포트를 가진 형태로 발전시킨 것입니다. 따라서 "스위치는 브리지의 현대적이고 고성능 버전이다"라고 이해하시면 정확합니다.

internet(상호연결된 네트워크)의 결정적인 속성은 서로 급진적으로 다르고 호환되지 않는 기술의 LAN과 WAN으로 구성될 수 있다는 점입니다. 모든 호스트가 다른 모든 호스트와 물리적으로 연결된 것은 아닌데, 어떻게 출발지 호스트가 이 모든 호환되지 않는 네트워크를 거쳐 목적지 호스트로 데이터 비트를 보낼 수 있을까요?
해결책은 각 호스트와 라우터에서 실행되는 프로토콜 소프트웨어(protocol software) 계층이 서로 다른 네트워크 간의 차이점을 '없애주는(smoothes out)' 것입니다.
이 소프트웨어는 호스트와 라우터가 데이터를 전송하기 위해 어떻게 협력해야 하는지를 규율하는 프로토콜(protocol)을 구현합니다. 이 프로토콜은 두 가지 기본 기능을 제공해야 합니다.

두 개의 LAN이 하나의 라우터로 연결된 인터넷 예시입니다. LAN1의 호스트 A(클라이언트)가 LAN2의 호스트 B(서버)로 데이터를 전송하는 8단계입니다.
(물론 네트워크 최대 프레임 크기 문제, 라우팅 테이블 결정 방식, 패킷 손실 등 많은 어려운 문제를 생략했지만) 이 예제가 인터넷 아이디어의 본질을 담고 있으며, 캡슐화(Encapsulation)가 그 핵심입니다.
글로벌 IP 인터넷은 internet(상호연결된 네트워크)의 가장 유명하고 성공적인 구현체입니다. 1969년부터 어떤 형태로든 존재해왔습니다.

TCP/IP는 실제로는 각기 다른 기능을 제공하는 프로토콜들의 제품군입니다.
(논의를 단순화하기 위해, 이 책에서는 TCP/IP를 하나의 단일 프로토콜로 취급할 것입니다. UDP는 논의하지 않습니다.)
프로그래머의 관점에서, 인터넷은 다음과 같은 속성을 가진 전 세계적인 호스트의 집합으로 생각할 수 있습니다.
이어지는 섹션에서 이러한 기본적인 인터넷 아이디어를 더 자세히 논의합니다.

IP 주소는 부호 없는 32비트 정수입니다. 네트워크 프로그램은 [그림 11.9]에 보이는 IP 주소 구조체에 IP 주소를 저장합니다.
스칼라(scalar) 주소를 구조체에 저장하는 것은 소켓 인터페이스 초기 구현의 불행한 유물입니다. IP 주소를 위한 스칼라 타입을 정의하는 것이 더 합리적이었겠지만, 이미 설치된 수많은 애플리케이션 기반 때문에 지금 바꾸기에는 너무 늦었습니다.
인터넷 호스트들은 서로 다른 호스트 바이트 순서(host byte order)를 가질 수 있기 때문에, TCP/IP는 패킷 헤더에 실려 네트워크를 가로지르는 IP 주소와 같은 모든 정수 데이터 항목에 대해 통일된 네트워크 바이트 순서(network byte order) (즉, 빅 엔디안(big-endian))를 정의합니다.
IP 주소 구조체(struct in_addr) 내의 주소는, 설령 호스트 바이트 순서가 리틀 엔디안(little-endian)일지라도, 항상 (빅 엔디안) 네트워크 바이트 순서로 저장됩니다.
유닉스는 호스트와 네트워크 바이트 순서를 변환하기 위해 다음 함수들을 제공합니다.
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // Host to Network Long
uint16_t htons(uint16_t hostshort); // Host to Network Short
// 반환 값: 네트워크 바이트 순서의 값
uint32_t ntohl(uint32_t netlong); // Network to Host Long
uint16_t ntohs(uint16_t netshort); // Network to Host Short
// 반환 값: 호스트 바이트 순서의 값
htonl: 부호 없는 32비트 정수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환합니다.ntohl: 부호 없는 32비트 정수를 네트워크 바이트 순서에서 호스트 바이트 순서로 변환합니다.htons, ntohs: 부호 없는 16비트 정수에 대해 동일한 변환을 수행합니다. (64비트 값에 대한 함수는 없습니다.)IP 주소는 일반적으로 점 표기법(dotted-decimal notation)이라는 형태로 사람들에게 표시됩니다. 이는 각 바이트를 10진수 값으로 나타내고 점(.)으로 구분하는 방식입니다.
128.2.194.242는 0x8002c2f2의 점 표기법입니다.)리눅스 시스템에서는 hostname -i 명령을 사용해 자신의 호스트 주소를 확인할 수 있습니다.
애플리케이션 프로그램은 inet_pton과 inet_ntop 함수를 사용해 IP 주소와 점 표기법 문자열 간의 변환을 수행할 수 있습니다.
#include <arpa/inet.h>
int inet_pton(AF_INET, const char *src, void *dst);
// 반환 값: 성공 시 1, src가 유효하지 않으면 0, 오류 시 -1
const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);
// 반환 값: 성공 시 점 표기법 문자열 포인터, 오류 시 NULL
AF_INET)나 128-bit IPv6 주소(AF_INET6)를 조작할 수 있습니다.inet_pton 함수는 점 표기법 문자열(src)을 네트워크 바이트 순서의 바이너리 IP 주소(dst)로 변환합니다. inet_ntop 함수는 네트워크 바이트 순서의 바이너리 IP 주소(src)를 해당하는 점 표기법 문자열로 변환하고, 그 결과를 dst에 복사합니다.
인터넷 클라이언트와 서버는 IP 주소를 사용해 통신하지만, 사람은 32비트 정수(IP 주소)를 기억하기 어렵습니다. 따라서 인터넷은 도메인 이름(domain names)이라는 더 인간 친화적인 이름 집합과, 이 도메인 이름을 IP 주소로 매핑(mapping)하는 메커니즘을 정의합니다.
whaleshark.ics.cs.cmu.edu)도메인 이름 집합은 계층(hierarchy)을 형성하며, 이는 트리(tree)로 표현됩니다 (그림 11.10).

com, edu, gov, org, net 같은 1단계 도메인입니다.cmu.edu와 같이 ICANN의 공인된 에이전트가 (선착순으로) 할당하는 2단계 도메인입니다.cs.cmu.edu와 같이 자신의 하위 도메인 내에서 자유롭게 새 도메인 이름을 생성할 수 있습니다.도메인 이름과 IP 주소 간의 매핑은 1988년까지는 HOSTS.TXT라는 단일 텍스트 파일로 관리되었습니다.
nslookup 예시)리눅스 nslookup 프로그램으로 DNS 매핑의 속성을 탐색할 수 있습니다.
localhost:Bash
localhost라는 로컬 도메인 이름을 가지며, 이는 항상 루프백 주소(loopback address) 127.0.0.1로 매핑됩니다.linux> nslookup localhost
Address: 127.0.0.1
1:1 매핑 (One-to-one):Bash
linux> nslookup whaleshark.ics.cs.cmu.edu
Address: 128.2.210.175
다:1 매핑 (Many-to-one):Bash
linux> nslookup cs.mit.edu
Address: 18.62.1.6
linux> nslookup eecs.mit.edu
Address: 18.62.1.6
다:다 매핑 (Many-to-many):Bash
linux> nslookup www.twitter.com
Address: 199.16.156.6
Address: 199.16.156.70
Address: 199.16.156.102
Address: 199.16.156.230
매핑 없음 (No mapping):Bash
edu나 ics.cs.cmu.edu처럼, 유효한 도메인 이름임에도 불구하고 IP 주소에 매핑되지 않는 경우도 있습니다.linux> nslookup edu
*** Can’t find edu: No answer
인터넷 클라이언트와 서버는 연결(connections)을 통해 바이트 스트림을 주고받으며 통신합니다. 연결은 다음과 같은 속성을 가집니다.
address:port 표기법으로 나타냅니다.http(웹), smtp(이메일)와 같이 대응하는 잘 알려진 서비스 이름이 있습니다. (/etc/services 파일에 매핑이 포함됩니다.)연결은 두 종단점의 소켓 주소에 의해 고유하게 식별됩니다. 이 소켓 주소 쌍을 소켓 페어(socket pair)라고 하며, 다음과 같은 튜플(tuple)로 표기합니다.
(cliaddr:cliport, servaddr:servport)
(cliaddr: 클라이언트 IP, cliport: 클라이언트 포트, servaddr: 서버 IP, servport: 서버 포트)
예시 (그림 11.11): 웹 클라이언트와 웹 서버 간의 연결

128.2.194.242:51213 (51213은 커널이 할당한 임시 포트)208.216.181.15:80 (80은 웹 서비스의 잘 알려진 포트)(128.2.194.242:51213, 208.216.181.15:80)소켓 인터페이스는 Unix I/O 함수들과 함께 네트워크 애플리케이션을 구축하는 데 사용되는 함수들의 집합입니다.


인터넷 소켓 주소는 [그림 11.13]에 보이는 sockaddr_in 타입의 16바이트 구조체에 저장됩니다. 인터넷 애플리케이션의 경우, 필드 값은 다음과 같습니다.
sin_family: AF_INET (프로토콜 패밀리)sin_port: 16비트 포트 번호 (네트워크 바이트 순서)sin_addr: 32비트 IP 주소 (네트워크 바이트 순서)sockaddr 구조체connect, bind, accept 함수들은 프로토콜에 특화된(protocol-specific) 소켓 주소 구조체에 대한 포인터를 요구합니다.
소켓 인터페이스 설계자들의 과제는 이 함수들이 어떤 종류의 소켓 주소 구조체든(예: IPv4, IPv6 등) 받아들일 수 있도록 정의하는 것이었습니다. (오늘날 C라면 제네릭 void * 포인터를 사용했겠지만, 당시에는 void *가 존재하지 않았습니다.)
그들의 해결책은 소켓 함수들이 제네릭 sockaddr 구조체([그림 11.13])에 대한 포인터를 받도록 정의한 다음, 애플리케이션이 sockaddr_in 같은 프로토콜별 구조체 포인터를 이 제네릭 sockaddr 구조체로 캐스트(cast)하도록 요구하는 것이었습니다.
(코드 예제를 단순화하기 위해, 스티븐스(Stevens)의 선례를 따라 다음 타입을 정의합니다:
typedef struct sockaddr SA;
이후 sockaddr_in 구조체를 제네릭 sockaddr 구조체로 캐스트해야 할 때마다 이 SA 타입을 사용할 것입니다.)
클라이언트와 서버는 socket 함수를 사용하여 소켓 디스크립터를 생성합니다.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
만약 이 소켓이 연결(connection)의 종단점이 되기를 원한다면, 다음과 같이 하드코딩된 인자들로 socket을 호출할 수 있습니다:
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
AF_INET: 우리가 32비트 IP 주소를 사용함을 나타냅니다.SOCK_STREAM: 이 소켓이 연결(connection)의 종단점이 될 것임을 나타냅니다.하지만, 코드가 프로토콜에 독립적(protocol-independent)이 되도록 getaddrinfo 함수(11.4.7절)를 사용하여 이 파라미터들을 자동으로 생성하는 것이 최선의 방법입니다. (11.4.8절에서 getaddrinfo를 socket 함수와 함께 사용하는 방법을 보여줄 것입니다.)
socket에 의해 반환된 clientfd 디스크립터는 부분적으로만 열린(partially opened) 상태이며, 아직 읽기/쓰기에 사용될 수 없습니다.
소켓 열기를 완료하는 방법은 우리가 클라이언트인지 서버인지에 따라 다릅니다. 다음 섹션은 클라이언트인 경우 소켓 열기를 완료하는 방법을 설명합니다.
클라이언트는 connect 함수를 호출하여 서버와 연결을 수립합니다.
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
connect 함수는 소켓 주소 addr에 있는 서버와 인터넷 연결을 시도합니다 (여기서 addrlen은 sizeof(sockaddr_in)입니다).
connect 함수는 연결이 성공적으로 수립되거나 오류가 발생할 때까지 블록(blocks)됩니다.
성공하면, clientfd 디스크립터는 이제 읽기/쓰기가 준비된 상태가 되며, 이 연결은 다음 소켓 페어(socket pair)로 식별됩니다.
(x:y, addr.sin_addr:addr.sin_port)
(여기서 x는 클라이언트의 IP 주소이고, y는 클라이언트 호스트에서 클라이언트 프로세스를 고유하게 식별하는 임시 포트(ephemeral port)입니다.)
socket 함수와 마찬가지로, connect의 인자들을 제공하기 위해 getaddrinfo를 사용하는 것이 최선의 방법입니다 (11.4.8절 참조).
bind, listen, accept 등 나머지 소켓 함수들은 서버가 클라이언트와 연결을 수립하기 위해 사용됩니다.
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind 함수는 커널에게 addr에 있는 서버의 소켓 주소를 소켓 디스크립터 sockfd와 연결(associate)하도록 요청합니다. addrlen 인자는 sizeof(sockaddr_in)입니다.
socket, connect와 마찬가지로, bind의 인자들을 제공하기 위해 getaddrinfo를 사용하는 것이 최선의 방법입니다 (11.4.8절 참조).
클라이언트는 연결 요청을 시작하는(initiate) 능동적인 주체입니다. 서버는 클라이언트의 연결 요청을 기다리는(wait) 수동적인 주체입니다.
기본적으로 커널은 socket 함수로 생성된 디스크립터가 (클라이언트 측의) 능동 소켓(active socket) 이라고 가정합니다. 서버는 listen 함수를 호출하여, 해당 디스크립터가 클라이언트가 아닌 서버에 의해 사용될 것임을 커널에 알립니다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen 함수는 sockfd를 능동 소켓에서, 클라이언트의 연결 요청을 받을 수 있는 수신 소켓(listening socket)으로 변환합니다.
backlog 인자는 커널이 요청을 거부하기 시작하기 전에 대기열(queue)에 쌓아둘 보류 중인 연결 요청의 수에 대한 힌트(hint)입니다. (backlog의 정확한 의미는 TCP/IP에 대한 이해가 필요하므로 이 책의 범위를 벗어납니다. 일반적으로 1,024와 같은 큰 값으로 설정합니다.)
backlog 인자에 대해 더 자세히 설명해 드릴게요. 이 큐가 왜 필요하고, 커널 내부에서 실제로 어떻게 동작하는지 알려면 TCP 3-Way Handshake 과정과 커널의 두 가지 큐(Queue)를 이해해야 합니다. backlog는 서버가 accept()를 호출하여 연결을 가져갈 때까지, 이미 3-Way Handshake를 완료한 연결들을 잠시 보관하는 '완료 연결 큐'의 최대 크기를 지정하는 힌트입니다.SYNSYN 패킷을 보냅니다.SYN을 받으면, 해당 연결 정보를 SYN Queue에 넣습니다.SYN-ACKSYN-ACK로 응답하며 클라이언트의 마지막 ACK를 기다립니다.이 SYN Queue의 크기는 backlog 값이 아니라, 리눅스 시스템의 net.ipv4.tcp_max_syn_backlog 같은 시스템 전역 설정에 의해 제어됩니다.
클라이언트 → 서버: ACK
SYN-ACK를 받고, "알겠습니다"라는 마지막 ACK를 서버에 보냅니다.커널의 처리:
- 서버 커널이 이 마지막 ACK를 받으면, 3-Way Handshake가 성공적으로 완료된 것입니다.
- 커널은 이 연결을 SYN Queue에서 꺼내어, Accept Queue로 옮깁니다.
- 이 큐의 연결들은 "완전히 연결되었으며, accept() 함수가 자신을 가져가기만 기다리는" 상태입니다.
backlog의 정확한 역할listen(sockfd, backlog)에서 backlog 인자는 바로 이 2번 큐, 즉 Accept Queue의 최대 크기를 지정합니다.
listen(sockfd, 1024)를 호출하면, 커널은 "핸드셰이크가 완료된 연결을 최대 1024개까지 Accept Queue에 보관할 수 있다"고 설정합니다.
본문에서 "힌트"라고 표현한 이유는, 프로그래머가 backlog 값을 1024로 설정하더라도 커널이 그 값을 그대로 사용하지 않을 수 있기 때문입니다.
커널에는 시스템 전체에서 허용하는 Accept Queue의 최대 크기 제한(net.core.somaxconn)이 따로 있습니다.
만약 net.core.somaxconn이 512로 설정되어 있는데 프로그래머가 backlog를 1024로 요청하면, 커널은 이 값을 512로 조정(clamp)합니다.
따라서 backlog는 "이만큼 필요합니다"라는 요청(힌트)이며, 실제 크기는 커널의 정책에 따라 결정됩니다.
Accept Queue가 꽉 차면?서버 프로그램이 accept()를 충분히 빨리 호출하지 못해서 Accept Queue가 backlog 크기만큼 꽉 차버리면 어떻게 될까요?
이때 클라이언트로부터 마지막 ACK가 도착해도, 커널은 이 연결을 Accept Queue에 넣을 수 없습니다.
커널은 이 ACK를 일단 무시(drop)합니다. (연결을 SYN Queue에 그대로 둡니다.)
클라이언트 입장에서는 ACK가 유실된 것으로 보이므로, 잠시 후 ACK를 재전송합니다.
이것이 바로 Accept Queue가 가득 찼을 때 서버가 새로운 연결 수립을 지연시키는 방식입니다. (SYN Queue까지 가득 차면, 그때는 SYN 패킷 자체를 무시하여 클라이언트에게 "Connection refused"가 발생합니다.)
요약: backlog는 서버 프로그램이 accept()로 처리하는 속도(소비 속도)와 클라이언트가 연결을 요청하는 속도(생산 속도) 사이의 버퍼(buffer) 크기를 지정하는 중요한 값입니다.
서버는 accept 함수를 호출하여 클라이언트의 연결 요청을 기다립니다.
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
accept 함수는 수신 디스크립터(listening descriptor) listenfd로 클라이언트의 연결 요청이 도착하기를 기다립니다.
요청이 도착하면, addr에 클라이언트의 소켓 주소를 채워 넣습니다.
그런 다음, Unix I/O 함수를 사용해 클라이언트와 통신하는 데 사용할 수 있는 연결된 디스크립터를 반환합니다.

listenfd (예: 디스크립터 3 - 0~2는 표준 파일용)에 대해 accept를 호출하고 대기(wait)합니다.connect 함수를 호출하여, 서버의 listenfd로 연결 요청을 보냅니다.accept 함수는:connfd (예: 디스크립터 4)를 엽니다.clientfd와 connfd 간의 연결을 수립합니다.connfd (4)를 서버 애플리케이션에 반환합니다.connect 함수에서 반환됩니다. 이 시점부터 클라이언트와 서버는 각각 clientfd와 connfd에 데이터를 읽고 쓰면서 통신할 수 있습니다.리눅스는 getaddrinfo와 getnameinfo라는 강력한 함수들을 제공합니다.
이 함수들은 바이너리 소켓 주소 구조체(binary socket address structures)와, 호스트 이름, 호스트 주소, 서비스 이름, 포트 번호의 문자열 표현(string representations) 간의 상호 변환을 수행합니다.
이 함수들을 소켓 인터페이스와 함께 사용하면, IP 프로토콜의 특정 버전에 독립적인(protocol-independent) 네트워크 프로그램을 작성할 수 있습니다.
리눅스는 바이너리 소켓 주소 구조체와, 호스트 이름/주소, 서비스 이름/포트 번호의 문자열 표현 간의 상호 변환을 수행하는 getaddrinfo 및 getnameinfo 함수를 제공합니다.
이 함수들은 소켓 인터페이스와 함께 사용될 때, IP 프로토콜의 특정 버전에 독립적인(protocol-independent) 네트워크 프로그램을 작성할 수 있게 해줍니다.
getaddrinfo Functiongetaddrinfo 함수는 호스트 이름, 호스트 주소, 서비스 이름, 포트 번호의 문자열 표현을 소켓 주소 구조체로 변환합니다.
gethostbyname, getservbyname 함수를 대체하는 현대적인 함수입니다.getaddrinfo는 재진입 가능(reentrant)하며(12.7.2절 참조) 모든 프로토콜(IPv4/IPv6)과 함께 작동합니다.#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints,
struct addrinfo **result);
// 반환 값: 성공 시 0, 오류 시 0이 아닌 에러 코드
void freeaddrinfo(struct addrinfo *result);
// 반환 값: 없음 (할당된 메모리 해제)
const char *gai_strerror(int errcode);
// 반환 값: 에러 메시지 문자열
getaddrinfo는 host(호스트)와 service(서비스)를 입력받아, result가 addrinfo 구조체의 연결 리스트(linked list)를 가리키도록 반환합니다. 이 리스트의 각 노드는 host와 service에 해당하는 소켓 주소 구조체를 가리킵니다 (그림 11.15).

getaddrinfo 호출 후, socket과 connect가 성공할 때까지 이 리스트를 순회하며 각 소켓 주소를 시도합니다.socket과 bind가 성공할 때까지 이 리스트를 순회하며 각 소켓 주소를 시도합니다.freeaddrinfo를 호출하여 리스트 메모리를 해제해야 합니다.getaddrinfo가 0이 아닌 에러 코드를 반환하면, gai_strerror를 호출하여 메시지 문자열로 변환할 수 있습니다.getaddrinfo 인자 상세host: 도메인 이름 (www.google.com) 또는 숫자 주소 (128.2.194.242)가 될 수 있습니다. (NULL 가능)
service: 서비스 이름 (http) 또는 10진수 포트 번호 (80)가 될 수 있습니다. (NULL 가능)
(단, host와 service 중 적어도 하나는 지정되어야 합니다.)
hints (옵션):
getaddrinfo가 반환할 소켓 주소 리스트를 제어하기 위한 addrinfo 구조체입니다 ([그림 11.16] 참조). 
hints 인자로 사용할 때는 ai_family, ai_socktype, ai_protocol, ai_flags 필드만 설정할 수 있습니다. (나머지는 0 또는 NULL이어야 함)memset으로 구조체 전체를 0으로 초기화한 뒤, 필요한 필드만 설정합니다.)hints의 주요 필드 설정:
ai_family:AF_INET: 리스트를 IPv4 주소로 제한.AF_INET6: 리스트를 IPv6 주소로 제한.ai_socktype:addrinfo 구조체를 반환할 수 있음.SOCK_STREAM: 리스트를 연결(connection)의 종단점으로 사용될 수 있는 소켓 주소(즉, TCP)로 제한합니다. (이 책의 모든 예제 프로그램에 해당)ai_flags: (비트 마스크)AI_ADDRCONFIG: (연결 사용 시 권장) 로컬 호스트가 IPv4로 설정된 경우에만 IPv4 주소를 반환합니다 (IPv6도 마찬가지).AI_CANONNAME: 설정 시, 리스트의 첫 번째 addrinfo 구조체의 ai_canonname 필드가 host의 공식(canonical) 이름을 가리키도록 합니다.AI_NUMERICSERV: service 인자가 서비스 이름("http")이 아닌 반드시 포트 번호 문자열("80")이어야 하도록 강제합니다.AI_PASSIVE: (서버용) getaddrinfo가 (클라이언트의 connect용) 능동 소켓 주소 대신, (서버의 bind용) 수신 소켓 주소를 반환하도록 지시합니다.host 인자는 NULL이어야 합니다.getaddrinfo가 생성하는 addrinfo 구조체 리스트의 각 필드는 다음과 같습니다.
ai_addr: 소켓 주소 구조체(sockaddr_in 등)를 가리킵니다.ai_addrlen: ai_addr이 가리키는 구조체의 크기입니다.ai_next: 리스트의 다음 addrinfo 노드를 가리킵니다.getaddrinfo의 강력한 점은, 반환된 addrinfo 구조체의 필드들이 애플리케이션 코드의 추가 조작 없이 소켓 인터페이스 함수에 직접 전달될 수 있다는 것입니다.
ai_family, ai_socktype, ai_protocol ➔ socket() 함수에 직접 전달ai_addr, ai_addrlen ➔ connect() 또는 bind() 함수에 직접 전달이 강력한 속성 덕분에, 우리는 IP 프로토콜의 특정 버전에 독립적인(protocol-independent) 클라이언트와 서버를 작성할 수 있습니다.
getnameinfo Functiongetnameinfo 함수는 getaddrinfo의 반대입니다. 이 함수는 소켓 주소 구조체(sockaddr)를 해당하는 호스트(host) 및 서비스(service) 이름 문자열로 변환합니다.
gethostbyaddr, getservbyport 함수를 대체하는 현대적인 함수입니다.getnameinfo는 재진입 가능(reentrant)하며 프로토콜에 독립적(protocol-independent)입니다.#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen, int flags);
getnameinfo 인자 상세sa (입력): salen 바이트 크기의 소켓 주소 구조체 포인터입니다.salen (입력): sa 구조체의 크기입니다.host (출력): hostlen 바이트 크기의 버퍼이며, 변환된 호스트 이름 문자열이 이곳에 복사됩니다.hostlen (입력): host 버퍼의 크기입니다.service (출력): servlen 바이트 크기의 버퍼이며, 변환된 서비스 이름 문자열이 이곳에 복사됩니다.servlen (입력): service 버퍼의 크기입니다.gai_strerror를 사용해 에러 코드를 문자열로 변환할 수 있습니다.)host를 NULL로, hostlen을 0으로 설정할 수 있습니다. service도 마찬가지이며, 단 둘 다 NULL일 수는 없습니다.)flags (입력): 기본 동작을 수정하는 비트 마스크입니다.NI_NUMERICHOST: (Domain Name ➔ IP String)getnameinfo는 host에 도메인 이름을 반환하려 시도합니다.NI_NUMERICSERV: (Service Name ➔ Port String)getnameinfo는 /etc/services 파일을 참조하여 포트 번호 대신 서비스 이름(예: "http")을 반환하려 시도합니다.hostinfo 예제 프로그램 (그림 11.17)getaddrinfo와 getnameinfo를 함께 사용하여, 도메인 이름을 IP 주소로 매핑하는 간단한 프로그램 예시입니다. (nslookup과 유사)

hints 구조체를 초기화하여 getaddrinfo가 원하는 주소(IPv4, TCP)를 반환하도록 설정합니다.getaddrinfo를 호출하여 addrinfo 연결 리스트를 받습니다.p->ai_addr)를 getnameinfo 함수에 전달합니다.getnameinfo가 소켓 주소를 점 표기법(dotted-decimal) 문자열로 변환하여 출력합니다.freeaddrinfo로 리스트 메모리를 해제합니다.twitter.com이 4개의 IP 주소로 매핑되는 것을 보여주며, 이는 nslookup 결과와 일치합니다.linux> ./hostinfo [twitter.com](http://twitter.com/)
199.16.156.102
199.16.156.230
199.16.156.6
199.16.156.70
getaddrinfo 함수와 소켓 인터페이스는 처음 배울 때 다소 위협적으로(daunting) 보일 수 있습니다.
이것들을 상위 수준의 헬퍼 함수(helper functions)로 감싸는 것이 편리하며, 여기서는 클라이언트와 서버가 서로 통신하고자 할 때 사용할 수 있는 open_clientfd와 open_listenfd라는 헬퍼 함수를 호출할 것입니다.
open_clientfd Function클라이언트는 open_clientfd를 호출하여 서버와 연결을 수립합니다.
#include "csapp.h"
int open_clientfd(char *hostname, char *port);
open_clientfd 함수는 hostname 호스트에서 실행 중이고 port 번호에서 연결 요청을 수신 대기 중인 서버와 연결을 수립합니다. 이 함수는 Unix I/O 함수를 사용한 입출력이 준비된 열린 소켓 디스크립터를 반환합니다.
[그림 11.18]은 open_clientfd의 코드를 보여줍니다.

getaddrinfo를 호출하여 addrinfo 구조체의 리스트를 반환받습니다. (이 리스트의 각 노드는 서버와 연결을 수립하는 데 적합한 소켓 주소 구조체를 가리킵니다.)socket과 connect 호출이 성공할 때까지, 이 리스트를 순회하며 각 항목을 차례로 시도합니다.connect가 실패하면, 다음 항목을 시도하기 전에 close를 호출하여 소켓 디스크립터를 닫습니다.connect가 성공하면, 리스트 메모리를 freeaddrinfo로 해제하고, 소켓 디스크립터를 클라이언트에게 반환합니다.코드 어디에도 특정 IP 버전에 대한 의존성이 없다는 점에 주목하십시오. socket과 connect의 인자들은 getaddrinfo에 의해 자동으로 생성되며, 이는 코드를 깔끔하고 이식성 있게(portable) 만들어 줍니다.
open_listenfd Function서버는 open_listenfd 함수를 호출하여 연결 요청을 받을 준비가 된 수신 디스크립터(listening descriptor)를 생성합니다.
#include "csapp.h"
int open_listenfd(char *port);
open_listenfd 함수는 port 포트에서 연결 요청을 받을 준비가 된 수신 디스크립터를 반환합니다. [그림 11.19]는 이 코드를 보여줍니다.

open_clientfd와 스타일이 비슷합니다. getaddrinfo를 호출한 다음, socket과 bind가 성공할 때까지 반환된 리스트를 순회합니다.setsockopt 함수(여기서 설명하진 않음)를 사용하여, 서버가 종료되고 재시작되었을 때 즉시 연결 요청을 수락할 수 있도록 설정합니다.getaddrinfo를 AI_PASSIVE 플래그와 NULL host 인자로 호출했기 때문에, 각 소켓 주소 구조체의 주소 필드는 와일드카드 주소(wildcard address)로 설정됩니다.listen 함수를 호출하여 listenfd를 수신 디스크립터로 변환한 후 호출자에게 반환합니다. (listen이 실패하면, 반환하기 전에 디스크립터를 close하여 메모리 누수를 방지합니다.)소켓 인터페이스를 배우는 가장 좋은 방법은 예제 코드를 연구하는 것입니다.

open_clientfd를 호출하여 서버와 연결을 수립합니다.stdin)에서 텍스트 한 줄을 읽습니다. (fgets)Rio_writen)Rio_readlineb)stdout)에 인쇄합니다. (Fputs)fgets가 표준 입력에서 EOF (Ctrl+D 또는 파일 끝)를 만나면 루프가 종료됩니다.Close(clientfd)).close 호출은 서버에게 EOF 알림을 전송하게 됩니다.rio_readlineb가 0을 반환할 때 이 EOF를 감지합니다.)close가 필수는 아니지만, 명시적으로 닫는 것이 좋은 프로그래밍 습관입니다.)
open_listenfd를 호출하여 수신 디스크립터를 엽니다.accept를 호출하여 클라이언트의 연결 요청을 기다립니다.Getnameinfo 사용)echo 함수를 호출합니다.echo 루틴이 반환되면 (클라이언트가 연결을 끊으면), 연결된 디스크립터(connfd)를 닫습니다. (Close(connfd))clientaddr (9행) 변수는 accept에 전달됩니다. accept는 clientaddr를 클라이언트의 소켓 주소로 채웁니다.struct sockaddr_storage: clientaddr의 타입을 sockaddr_in이 아닌 sockaddr_storage로 선언한 것에 주목하십시오.sockaddr_storage 구조체는 모든 타입의 소켓 주소(IPv4, IPv6 등)를 담을 수 있을 만큼 충분히 크도록 정의되어 있습니다.echo 루틴 (Function) - [그림 11.22]
rio_readlineb 함수가 (클라이언트의 close로 인한) EOF를 만나 0을 반환할 때까지 (10행), 텍스트 라인을 반복적으로 읽고 다시 써넣습니다(echo).EOF의 개념은 (특히 인터넷 연결 문맥에서) 종종 혼란스러울 수 있습니다.
read 함수로부터 0 (zero)의 반환 값을 받았을 때 EOF 상태임을 알게 됩니다.EOF가 발생하는 시점은 대상이 무엇이냐에 따라 다릅니다.
연결의 다른 쪽 끝에 있는 프로세스는 (상대방이 연결을 닫은 후) 스트림의 마지막 바이트를 지나서 read를 시도할 때, read가 0을 반환하면서 이 EOF 상태를 감지하게 됩니다.
웹 클라이언트와 서버는 HTTP (Hypertext Transfer Protocol)라는 텍스트 기반 애플리케이션 수준 프로토콜을 사용하여 상호 작용합니다.
<b> Make me bold! </b>는 텍스트를 굵게 표시하라고 지시합니다.)<a href="http://www.cmu.edu/index.html">Carnegie Mellon</a>)웹 클라이언트와 서버에게 콘텐츠(content)는 연관된 MIME(Multipurpose Internet Mail Extensions) 타입([그림 11.23])을 가진 바이트의 시퀀스입니다.

웹 서버는 두 가지 다른 방식으로 클라이언트에게 콘텐츠를 제공합니다.
웹 서버가 반환하는 모든 콘텐츠는 URL(Universal Resource Locator)이라는 고유한 이름을 가진 파일과 연관됩니다.
http://www.google.com:80/index.htmlwww.google.com 호스트의 80번 포트에서 수신 대기 중인 웹 서버가 관리하는 /index.html 파일을 식별합니다. (포트 80은 HTTP의 잘 알려진 포트이며 생략 가능합니다.)http://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?15000&213/cgi-bin/adder라는 실행 파일을 식별하며, 15000과 213이라는 두 문자열 인자와 함께 호출될 것입니다.? 문자는 파일 이름과 인자를 구분합니다.& 문자는 각 인자를 구분합니다.클라이언트와 서버는 트랜잭션 동안 URL의 서로 다른 부분을 사용합니다.
http://www.google.com:80 같은 접두사(prefix)를 사용하여, 어떤 종류의 서버에, 어디로, 무슨 포트로 접속할지 결정합니다./index.html 같은 접미사(suffix)를 사용하여, 자신의 파일 시스템에서 파일을 찾고, 이 요청이 정적인지 동적인지 결정합니다.서버가 URL의 접미사를 해석하는 방법에 대해 이해해야 할 몇 가지 사항이 있습니다.
cgi-bin처럼 모든 실행 파일이 있어야 하는 디렉터리 집합을 식별하는 것입니다.)/는 루트 디렉터리가 아님: 접미사의 첫 번째 /는 리눅스 루트 디렉터리를 의미하지 않습니다. 대신, 요청되는 콘텐츠 종류에 대한 홈 디렉터리를 나타냅니다. (예: 서버는 정적 콘텐츠는 /usr/httpd/html에, 동적 콘텐츠는 /usr/httpd/cgi-bin에 저장하도록 구성될 수 있습니다.)/: 최소 URL 접미사는 / 문자이며, 모든 서버는 이를 /index.html과 같은 기본 홈 페이지로 확장합니다. (이것이 브라우저에 도메인 이름만 입력해도 사이트의 홈 페이지를 가져올 수 있는 이유입니다. 브라우저는 빠진 /를 URL에 추가하고, 서버는 이 /를 기본 파일 이름으로 확장합니다.)HTTP는 인터넷 연결을 통해 텍스트 라인을 기반으로 하므로, 리눅스 telnet 프로그램을 사용하여 인터넷의 모든 웹 서버와 트랜잭션을 수행할 수 있습니다.
telnet 프로그램은 (원격 로그인 도구로서는 ssh에 의해 대체되었지만) 텍스트 라인으로 통신하는 서버를 디버깅하는 데 매우 유용합니다.
[그림 11.24]는 telnet을 사용해 AOL 웹 서버의 홈 페이지를 요청하는 예시입니다.

telnet을 실행하여 AOL 웹 서버(www.aol.com 80)로의 연결을 엽니다.telnet은 터미널에 3줄을 출력하고, 연결을 연 다음, 사용자가 텍스트를 입력하기를 기다립니다 (5행).telnet은 그 라인을 읽어 \r\n (캐리지 리턴 및 라인 피드) 문자를 덧붙여 서버로 전송합니다.\r\n 쌍으로 종료되어야 한다는 HTTP 표준과 일치합니다.)HTTP 요청은 요청 라인(request line)(5행), 0개 이상의 요청 헤더(request headers)(6행), 그리고 헤더 목록을 종료하는 빈 텍스트 라인(7행)으로 구성됩니다.
요청 라인은 다음과 같은 형식을 가집니다.
method URI version
method (메서드):URI (Uniform Resource Identifier):version (버전):[그림 11.24]의 5행 요약: 서버에 /index.html 파일을 가져와 반환하도록 요청하며, 나머지 요청은 HTTP/1.1 형식임을 알립니다.
요청 헤더는 브라우저 이름이나 브라우저가 이해하는 MIME 타입 같은 부가적인 정보를 서버에 제공합니다.
header-name: header-data
Host 헤더 (6행):Host 헤더의 데이터(원본 서버의 도메인 이름)는 프록시가 요청된 콘텐츠의 캐시된 복사본을 로컬에 가지고 있는지 판별할 수 있게 해줍니다.HTTP 응답은 HTTP 요청과 유사합니다. HTTP 응답은 다음과 같이 구성됩니다.
응답 라인은 다음과 같은 형식을 가집니다.
version status-code status-message
version: 응답이 준수하는 HTTP 버전입니다.status-code: 요청의 처리 상태를 나타내는 3자리 양의 정수입니다.status-message: 상태 코드에 해당하는 영어 메시지입니다.
응답 헤더(9-13행)는 응답에 대한 부가적인 정보를 제공합니다. 우리에게 가장 중요한 두 헤더는 다음과 같습니다.
Content-Type (12행): 클라이언트에게 응답 본문에 포함된 콘텐츠의 MIME 타입을 알려줍니다.Content-Length (13행): 응답 본문의 크기(바이트)를 나타냅니다.응답 헤더를 종료하는 빈 텍스트 라인(14행) 뒤에는, 요청된 실제 콘텐츠가 포함된 응답 본문(response body)이 이어집니다.
서버가 클라이언트에게 동적 콘텐츠를 어떻게 제공하는지 생각해보면 몇 가지 질문이 생깁니다.
이러한 질문들은 CGI (Common Gateway Interface)라는 사실상의 표준(de facto standard)에 의해 해결됩니다.
? 문자가 파일 이름과 인자들을 구분합니다.& 문자가 각 인자를 구분합니다.%20 문자열로 표현(인코딩)되어야 합니다. (다른 특수 문자에도 유사한 인코딩이 존재합니다.)서버가 다음과 같은 요청을 받으면:
GET /cgi-bin/adder?15000&213 HTTP/1.1
fork를 호출하여 자식 프로세스를 생성합니다.execve를 호출하여 자식 프로세스의 컨텍스트에서 /cgi-bin/adder 프로그램을 실행합니다.adder와 같이 CGI 표준 규칙을 따르는 프로그램을 CGI 프로그램이라고 합니다.execve를 호출하기 전에, 자식 프로세스는 CGI 환경 변수(environment variable)인 QUERY_STRING의 값을 15000&213으로 설정합니다.adder (CGI) 프로그램은 실행 시점에 리눅스의 getenv 함수를 사용하여 이 QUERY_STRING 변수를 참조할 수 있습니다.CGI는 CGI 프로그램이 실행될 때 설정될 것으로 기대할 수 있는 여러 다른 환경 변수들을 정의합니다. [그림 11.26]은 이 변수들의 일부를 보여줍니다.

CGI 프로그램은 자신의 동적 콘텐츠를 표준 출력(standard output)으로 보냅니다.
dup2 함수를 사용합니다.dup2를 이용해, 자식 프로세스 자신의 표준 출력(FD 1)을 클라이언트와 연결된 연결 디스크립터(connfd)로 리디렉션(redirection)시킵니다.부모(서버)는 자식이 생성할 콘텐츠의 타입이나 크기를 모르기 때문에, Content-type과 Content-length 응답 헤더, 그리고 헤더를 종료하는 빈 줄을 생성하는 것은 자식(CGI 프로그램)의 책임입니다.

[그림 11.27]은 두 인자를 합산하고 그 결과가 담긴 HTML 파일을 클라이언트에게 반환하는 간단한 CGI 프로그램(adder)을 보여줍니다. [그림 11.28]은 이 adder 프로그램의 동적 콘텐츠를 제공하는 HTTP 트랜잭션 예시를 보여줍니다.

네트워크 프로그래밍에 대한 논의를 Tiny라는 작지만 작동하는 웹 서버를 개발하며 마무리합니다.
main 루틴 (The Tiny main Routine)[그림 11.29]는 Tiny의 main 루틴을 보여줍니다.

open_listenfd 함수를 호출하여 수신 소켓을 연 후, Tiny는 전형적인 무한 서버 루프를 실행합니다.accept)doit)close).
read_requesthdrs (헤더 읽기 및 무시)
이 함수의 목적은 HTTP 요청 메시지에서 요청 라인(request line) 뒤에 따라오는 헤더들 (예: User-Agent: ..., Accept: ... 등)을 읽되, 실제로 사용하지 않고 그냥 넘겨버리는(무시하는) 것입니다.
Rio_readlineb(rp, buf, MAXLINE); (5행): 루프에 진입하기 전, 첫 번째 헤더 라인을 읽습니다.while(strcmp(buf, "\r\n")) { ... } (6행): 이것이 핵심입니다.\r\n (CRLF) 문자로만 구성됩니다.strcmp(buf, "\r\n")는 buf의 내용이 \r\n과 다르면 0이 아닌 값(true)을 반환하고, 같으면 0(false)을 반환합니다.while 루프는 "읽어들인 줄(buf)이 빈 줄(\r\n)이 아닌 동안" 계속 반복 실행됩니다.Rio_readlineb(...): 다음 헤더 줄을 읽습니다. (이전 헤더 내용은 덮어써짐)printf(...): 읽은 헤더를 (파싱하지 않고) 서버의 콘솔에 그냥 출력합니다. (디버깅 목적)\r\n을 만나면) 함수가 return됩니다.결론: read_requesthdrs는 서버가 당장 처리할 필요가 없는 HTTP 헤더들을 모두 읽어서 소켓 버퍼에서 비우는(consume) 역할을 합니다.
clienterror (오류 응답 전송)
이 함수는 함께 올려주신 텍스트 설명 그대로, 클라이언트에게 완전한 HTTP 오류 응답을 생성하여 전송하는 역할을 합니다.
이전 질문에서 보신 if (strcasecmp(method, "GET")) { ... } 코드 블록이 이 함수를 호출하는 예시입니다.
sprintf를 반복 사용해 body 버퍼에 오류 메시지를 담은 간단한 HTML 페이지를 만듭니다.sprintf(buf, "HTTP/1.0 %s %s\r\n", ...): 상태 라인 (예: HTTP/1.0 404 Not Found\r\n)sprintf(buf, "Content-type: text/html\r\n"): 응답 본문이 HTML임을 알리는 헤더sprintf(buf, "Content-Length: %d\r\n\r\n", ...): 응답 본문의 길이를 알리는 헤더 (뒤에 \r\n이 두 번 붙어 헤더의 끝을 알림)Rio_writen(fd, body, ...): 실제 HTML body 내용을 전송합니다.결론: clienterror는 서버가 요청을 처리할 수 없을 때(예: 404 Not Found, 501 Not Implemented), HTTP 규칙에 맞는 응답을 만들어 클라이언트에게 전송하는 함수입니다.

parse_uri이 parse_uri 함수는 클라이언트가 요청한 URI를 분석(parsing)하여, 이것이 정적 컨텐츠(Static Content) 요청인지 동적 컨텐츠(Dynamic Content) 요청인지 구분하는 역할을 합니다.
분석한 결과를 filename과 cgiargs라는 두 개의 출력 변수에 저장하고, 정적/동적 여부를 정수(1 또는 0)로 반환합니다.
함께 제공된 텍스트의 설명처럼, 이 함수는 "URI에 cgi-bin이 포함되어 있으면 동적, 아니면 정적"이라는 규칙을 따릅니다.
if (!strstr(uri, "cgi-bin"))strstr 함수는 문자열에서 "cgi-bin"을 찾습니다.! (NOT)이 붙었으므로, URI에 cgi-bin이라는 문자열이 없을 때 이 블록이 실행됩니다.strcpy(cgiargs, "");: 정적 컨텐츠는 CGI 인자가 필요 없으므로 cgiargs를 빈 문자열로 초기화합니다.strcpy(filename, "."); strcat(filename, uri);: filename을 . (현재 디렉토리)와 uri를 합쳐 (예: ./index.html) 상대 경로로 만듭니다.if (uri[strlen(uri)-1] == '/'): 만약 URI가 /로 끝난다면 (예: /), 기본 파일명인 home.html을 filename 뒤에 덧붙입니다.return 1;1을 반환합니다.elsecgi-bin 문자열이 있을 때 실행됩니다.ptr = index(uri, '?');: index 함수(오래된 strchr 함수)를 사용해 ? 문자를 찾습니다. ?는 실행 파일 경로와 CGI 인자를 구분하는 문자입니다.?가 있다면 (인자가 있다면):strcpy(cgiargs, ptr+1);: ? 문자 다음부터 끝까지의 모든 문자열(예: a=1&b=2)을 cgiargs 변수에 복사합니다.ptr = '\0';: (핵심) 원본 uri 문자열에서 ?가 있던 위치를 \0 (NULL 문자)로 덮어써서 문자열을 강제로 끝내버립니다.?가 없다면 (인자가 없다면):strcpy(cgiargs, "");: cgiargs를 빈 문자열로 초기화합니다.strcpy(filename, "."); strcat(filename, uri);: filename을 .와 (이제는 ? 뒤가 잘려나간) uri를 합쳐 (예: ./cgi-bin/adder) 실행 파일 경로를 만듭니다.return 0;0을 반환합니다.| 입력 uri | filename (출력) | cgiargs (출력) | 반환 값 |
|---|---|---|---|
"/favicon.ico" | "./favicon.ico" | "" | 1 (정적) |
"/" | "./home.html" | "" | 1 (정적) |
"/cgi-bin/adder" | "./cgi-bin/adder" | "" | 0 (동적) |
"/cgi-bin/adder?a=1&b=2" | "./cgi-bin/adder" | "a=1&b=2" | 0 (동적) |
serve_static 함수이 함수는 HTTP 응답의 전체 과정을 처리합니다. 즉, HTTP 헤더를 만들고, 요청된 파일의 내용을 읽어 클라이언트에게 전송합니다.

get_filetype(filename, filetype);get_filetype 헬퍼 함수를 호출하여 전송할 파일의 Content-Type (예: "text/html", "image/gif")을 결정합니다.sprintf를 사용해 buf 버퍼에 HTTP 응답 헤더를 만듭니다.HTTP/1.0 200 OK: 성공 상태 코드를 보냅니다.Server: Tiny Web Server: 서버 이름을 알립니다.Connection: close: 응답이 끝나면 연결을 닫겠다고 알립니다.Content-length: %d: 전송할 파일의 크기(filesize)를 보냅니다.Content-type: %s: get_filetype으로 알아낸 파일 타입(MIME 타입)을 보냅니다.\r\n)까지 포함하여 Rio_writen으로 클라이언트에게 전송합니다.mmap을 사용합니다.srcfd = Open(...): 전송할 파일을 read-only로 엽니다.srcp = Mmap(...): (중요) mmap 시스템 콜을 사용해, srcfd 파일의 filesize 바이트만큼을 이 프로세스의 가상 메모리 영역에 직접 매핑합니다. srcp는 해당 메모리 영역의 시작 주소를 가리키는 포인터가 됩니다.Close(srcfd): 파일이 메모리에 매핑되었으므로, 파일 디스크립터(fd)는 더 이상 필요 없어 바로 닫아줍니다. (리소스 누수 방지)Rio_writen(fd, srcp, filesize): 메모리에 매핑된 파일의 내용 (srcp가 가리키는 곳)을 filesize만큼 읽어서 클라이언트 소켓(fd)으로 직접 전송합니다.(참고) read()로 버퍼에 읽고 write()로 소켓에 쓰는 방식보다, 커널 버퍼에서 유저 버퍼로의 복사 단계를 줄일 수 있어 더 효율적입니다.
Munmap(srcp, filesize): 파일 전송이 완료되면 mmap으로 할당했던 가상 메모리 영역을 해제합니다. (메모리 누수 방지)get_filetype 함수이 함수는 serve_static에서 호출하는 헬퍼 함수입니다.

strstr 함수를 사용해 filename 문자열에 특정 파일 확장자가 포함되어 있는지 확인합니다..html이 있으면 filetype에 "text/html"을 복사합니다..gif가 있으면 "image/gif"를 복사합니다..png나 .jpg도 마찬가지로 처리합니다.
이 serve_dynamic 함수는 'Tiny' 서버가 CGI(Common Gateway Interface) 표준을 사용해 동적 컨텐츠(예: 스크립트 실행 결과)를 처리하는 방법을 보여줍니다.
핵심 아이디어는 새로운 자식 프로세스를 생성(fork)하여 클라이언트의 요청을 처리할 외부 프로그램을 실행(execve)시키는 것입니다.
fork를 호출하기 전에, 서버(부모 프로세스)는 먼저 클라이언트에게 HTTP 응답의 첫 부분을 보냅니다."HTTP/1.0 200 OK\r\n" (성공 상태 라인)"Server: Tiny Web Server\r\n" (서버 정보 헤더)\r\n\r\n이나 Content-Type 같은 헤더를 보내지 않습니다. 나머지 응답을 채우는 것은 전적으로 실행될 CGI 프로그램의 책임입니다. (함께 올려주신 텍스트 설명에도 이 부분이 언급됩니다.)이 부분이 이 함수의 핵심입니다.
if (Fork() == 0) (11행):Fork()는 자식 프로세스에게는 0을 반환하므로, if 블록 안의 코드는 자식 프로세스만 실행합니다.setenv(...) (13행):cgiargs, 예: "a=1&b=2")를 QUERY_STRING이라는 환경 변수로 설정합니다. CGI 프로그램은 이 환경 변수를 읽어서 입력을 받습니다.Dup2(fd, STDOUT_FILENO) (14행):Dup2 함수를 사용해 자식 프로세스의 표준 출력(Standard Output, STDOUT_FILENO)의 대상(destination)을 클라이언트와 연결된 소켓(fd)으로 재지정(redirect)합니다.Execve(...) (15행):filename (예: ./cgi-bin/adder)으로 지정된 CGI 프로그램을 실행합니다.execve가 호출되면, 현재 실행 중인 자식 프로세스(Tiny 서버의 복사본)의 코드는 filename의 코드로 완전히 대체됩니다.printf 같은 표준 출력 함수를 사용해 무언가를 출력하면, 그 데이터는 (14행의 Dup2 때문에) 콘솔이 아닌 클라이언트 소켓으로 바로 전송됩니다.Wait(NULL) (17행):if 블록 밖, 즉 부모 프로세스(서버)에 의해 실행됩니다.Wait는 자식 프로세스를 정리(reap)하여, 시스템에 리소스를 낭비하는 좀비 프로세스(zombie process)가 남지 않도록 합니다.write() (데이터 전송)를 시도합니다.write() 호출은 성공할 수 있습니다. 데이터가 커널의 TCP 전송 버퍼로 복사되기만 해도 함수는 성공을 반환하기 때문입니다.write()를 시도하면, 커널은 이 프로세스에게 "더 이상 데이터를 보낼 수 없다"는 신호로 SIGPIPE 시그널을 보냅니다.SIGPIPE 시그널의 기본 동작(Default behavior)은 해당 프로세스를 즉시 종료시키는 것입니다. 이로 인해 서버 전체가 다운(crash)될 수 있습니다.SIGPIPE 무시 및 EPIPE 오류 처리이 텍스트는 "강건한(Robust) 서버"가 되기 위한 두 가지 해결책을 제시합니다.
SIGPIPE 시그널 무시 (Catch or Ignore):signal(SIGPIPE, SIG_IGN);과 같은 코드를 사용해 SIGPIPE 시그널을 명시적으로 무시하도록 설정해야 합니다.write() 반환 값 확인:SIGPIPE가 무시되면, 두 번째 write() 함수는 프로세스를 종료시키는 대신 1을 반환합니다.errno*는 EPIPE** ("Broken pipe")로 설정됩니다.write()의 반환 값이 1이고 errno가 EPIPE인지 확인해야 합니다.이 글은 write()가 실패하는 것 자체는 정상이지만, 그 실패로 인해 서버 프로세스 전체가 종료되는 것은 버그임을 강조합니다. 따라서 안정적인 서버는 반드시 SIGPIPE 시그널을 처리하고, write()의 EPIPE 오류를 확인하여 예외를 처리해야 한다는 것입니다.
serve_static 함수)serve_dynamic 함수)