Ethernet과 Wi-Fi는 LAN(Local Area Network) 기술로, 실제 데이터를 전기 신호나 무선 전파로 변환해 전송하는 역할을 한다. MAC 주소를 기반으로 프레임이 전달되며, 전송 매체와 방식에 따라 성능 특성이 달라진다.
항목 | Ethernet | Wi-Fi |
---|---|---|
전송 매체 | 유선 (UTP, 광케이블 등) | 무선 (2.4GHz, 5GHz 등 전파) |
연결 방식 | Point-to-Point (스위치 기반) | 공유 채널 (AP 기반) |
전송 속도 | 일반적으로 1Gbps 이상 | 최대 수 Gbps (Wi-Fi 6 이상) |
지연 및 안정성 | 낮고 안정적 | 간섭에 취약하고 변동 있음 |
충돌 제어 | CSMA/CD (사실상 미사용) | CSMA/CA (충돌 회피 방식) |
보안 | 물리적 접근 제약으로 안전 | WPA2/WPA3 암호화 필요 |
인터넷 계층은 데이터를 목적지까지 전달하기 위한 주소 지정과 라우팅을 담당한다. 핵심 프로토콜은 IP이며, 데이터를 패킷 단위로 전송한다. 연결을 설정하지 않고 전송하는 비신뢰성 기반 방식이며, 전송 중 손실되거나 순서가 바뀔 수 있다. 이러한 한계를 전송 계층이 보완한다.
전송 계층은 애플리케이션 간 종단 간(end-to-end) 데이터 전송을 담당한다. 대표적인 프로토콜은 TCP와 UDP다.
항목 | TCP | UDP |
---|---|---|
연결 방식 | 연결 지향 (3-way handshake) | 비연결 지향 |
신뢰성 | 있음 (순서 보장, 재전송 등) | 없음 (손실/중복 가능) |
전송 단위 | 바이트 스트림 | 메시지 단위 (Datagram) |
흐름/혼잡 제어 | 있음 | 없음 |
속도 | 느리지만 안정적 | 빠르지만 손실 가능 |
헤더 크기 | 20바이트 이상 | 8바이트 |
대표 용도 | 웹, 이메일, 파일 전송 | 실시간 영상, 게임, DNS 등 |
애플리케이션 계층은 사용자가 직접 마주하는 영역이다. 웹 브라우징, 이메일, 파일 전송 등 고수준 애플리케이션이 위치하며, 여기서는 DNS와 HTTP를 중심으로 살펴본다.
항목 | DNS | HTTP |
---|---|---|
역할 | 도메인 이름 → IP 주소 해석 | 리소스 요청 및 전송 |
프로토콜 유형 | 요청-응답 (단일 질의) | 요청-응답 (다양한 리소스 요청) |
전송 프로토콜 | UDP (기본), TCP (fallback) | TCP (HTTP), TLS 기반 TCP (HTTPS) |
사용 시점 | 통신 시작 전 | IP 확보 후 리소스 요청 시 |
연결 지속성 | 없음 | 있음 (keep-alive 지원) |
보안 확장 | DNSSEC, DoH, DoT 등 | HTTPS (TLS 기반 암호화) |
www.example.com
을 입력한다. 항목 | 설명 | 관여 계층 | 주체 | 성능에 미치는 영향 | 현실 비유 |
---|---|---|---|---|---|
MTU(Maximum Transmission Unit) | 하나의 IP 패킷이 전송될 수 있는 최대 크기 (IP 헤더 포함) | 네트워크 계층 (IP) | 네트워크 장비 (라우터, NIC 등) | 너무 작으면 IP fragmentation 증가, 전송 효율 감소 | 도로 제한 높이 |
MSS(Maximum Segment Size) | 하나의 TCP 세그먼트에서 데이터 영역의 최대 크기 (헤더 제외) | 전송 계층 (TCP) | 송신자 (TCP 스택) | 작으면 전송 단위 증가 → 헤더 오버헤드 증가 | 트럭 짐칸 크기 |
TCP Window Size(rwnd: 수신 윈도) | 수신자가 한 번에 받아들일 수 있는 총 데이터 양 | 전송 계층 (TCP) | 수신자 (애플리케이션 or 커널) | 작으면 송신자 정지, Throughput 감소 | 목적지 창고 용량 |
Congestion Window(cwnd) | 혼잡 제어에 따라 송신자가 한 번에 보낼 수 있는 최대 양 | 전송 계층 (TCP) | 송신자 (TCP 스택) | 네트워크 혼잡도 반영. 손실 발생 시 줄어들고 서서히 증가 | 트럭 출발 빈도 제한 (정체 고려) |
Effective Window Size | 실질 전송 가능 크기: min(cwnd, rwnd) | 전송 계층 | 양측 | 실제 전송량 결정 | 창고 용량 vs 도로 상황 중 좁은 쪽 |
Send Buffer | 송신 애플리케이션이 write한 데이터를 커널이 보관하는 버퍼 | 소켓 계층 (OS 커널) | 송신자 (커널/OS) | 작으면 write()가 막힘. 커널 레벨 처리량 제약 | 트럭 출발 전 대기 장소 |
Receive Buffer | 수신한 데이터를 read() 전까지 보관하는 커널 버퍼 | 소켓 계층 (OS 커널) | 수신자 (커널/OS) | 작으면 rwnd 작아짐 → 송신 제어 발생 | 창고 내부에 쌓인 짐 |
TCP는 MSS 단위로 나눠진 데이터를 MTU에 맞게 포장해서, rwnd와 cwnd의 합의점을 고려하며, Send/Receive 버퍼의 상태에 따라 유동적으로 전송량을 조절한다. 이 전 과정은 지연(latency), 손실률(loss rate), 대역폭(bandwidth)을 모두 고려하는 동적인 최적화 과정이며, TCP의 위대함은 바로 이 조율 능력에 있다.
파일 디스크립터(File Descriptor)는 유닉스 및 리눅스 운영체제에서 프로세스가 입출력 자원(I/O resource)에 접근하기 위해 사용하는 정수형 핸들이다. 이것은 커널 내부의 파일 디스크립터 테이블에서 해당 자원을 가리키는 인덱스 역할을 하며, 파일뿐 아니라 소켓, 파이프, 디바이스, 표준 입출력 등 다양한 자원을 추상화하여 다룰 수 있게 해준다. 예를 들어 open() 시스템 호출을 통해 파일을 열면 커널은 내부에서 파일 구조체를 생성하고, 해당 파일을 가리키는 FD를 사용자 프로그램에 정수값으로 반환한다. 이후 이 FD를 통해 read(), write(), close() 같은 시스템 호출을 사용할 수 있다.
이러한 구조를 바탕으로 유닉스 철학에서는 흔히 “유닉스에서는 모든 것이 파일이다”라고 표현한다. 이는 단순히 일반적인 텍스트 파일뿐 아니라, 소켓, 디바이스 파일(/dev/null 같은 특수 파일), 파이프, 터미널, 심지어 네트워크 연결까지도 모두 파일로 취급된다는 의미다. 이러한 일관된 추상화 덕분에 프로그래머는 다양한 입출력 자원을 동일한 방식으로 접근하고 조작할 수 있다. 실제로 네트워크 프로그래밍에서 socket()으로 생성된 소켓도 내부적으로는 FD로 표현되며, write()나 read()와 같은 함수로 데이터를 송수신할 수 있다.
리눅스 시스템에서는 표준 입력, 출력, 에러 스트림도 각각 FD 0, 1, 2로 관리된다. stdin은 0번, stdout은 1번, stderr는 2번으로, 이 값들을 통해 셸과 프로그램 간의 입력 및 출력 흐름을 제어할 수 있다. 또한 이러한 FD는 프로세스의 복제(fork()), 복사(dup()), 이벤트 기반 I/O(select(), poll() 등)에서 핵심적인 역할을 하며, 유닉스의 강력한 프로세스와 I/O 모델의 기반이 된다.
결과적으로, 파일 디스크립터는 유닉스 시스템의 강력한 I/O 추상화를 가능하게 하는 핵심 개념이며, “모든 것은 파일”이라는 철학은 이 FD 기반 구조 덕분에 구현된다고 할 수 있다.
모든 네트워크 애플리케이션은 클라이언트-서버 모델에 기반한다. 이 모델에서 애플리케이션은 서버와 하나 이상의 클라이언트로 구성되며, 서버는 자원을 관리하고 클라이언트에게 서비스를 제공한다. 이 과정은 클라이언트의 요청(request)과 서버의 응답(response)으로 이루어진 트랜잭션(transaction) 단위로 진행된다.
클라이언트와 서버는 인터넷이라는 글로벌 네트워크를 통해 통신하며, 프로그래머의 관점에서는 인터넷을 다음과 같은 성질을 지닌 전 세계적인 호스트 집합으로 볼 수 있다:
이러한 연결(connection)을 만들기 위해 클라이언트와 서버는 소켓(sockets) 인터페이스를 사용한다.
소켓은 연결의 종단점(end point)으로, 애플리케이션에게는 파일 디스크립터 형태로 제공된다.
소켓 인터페이스는 소켓 디스크립터를 열고 닫는 함수들과, 이 디스크립터를 통해 읽고 쓰는 함수들을 제공하여 클라이언트와 서버 간 통신을 가능하게 한다.
소켓(Socket)은 네트워크 통신을 위한 논리적 엔드포인트(endpoint)로, 서로 다른 컴퓨터 혹은 같은 컴퓨터 내의 프로세스 간에 데이터를 교환할 수 있도록 운영체제가 제공하는 인터페이스다. 보통 TCP나 UDP 같은 전송 계층 프로토콜 위에서 작동하며, 프로그래머는 이를 통해 마치 파일을 읽고 쓰듯 네트워크 통신을 수행할 수 있다. 실제로 유닉스 시스템에서는 소켓도 하나의 파일처럼 간주되며, read(), write() 같은 시스템 호출을 통해 다룰 수 있다. 이러한 구조는 "유닉스에서는 모든 것이 파일"이라는 설계 철학을 그대로 반영한다.
socket() 함수를 호출하면 운영체제는 커널 내부에 새로운 소켓 객체를 생성하고, 사용자 공간의 애플리케이션에는 정수형 파일 디스크립터(File Descriptor)를 반환한다. 이 디스크립터는 커널의 기술자 테이블 내에 있는 해당 소켓 객체에 대한 포인터 역할을 하며, 애플리케이션은 이 디스크립터를 통해 커널에 작업을 요청한다. 파일과 소켓 모두 같은 기술자 테이블을 공유하며, 예를 들어 파일을 먼저 열면 3번 디스크립터가 할당되고, 그 다음 생성된 소켓은 4번을 사용할 수 있다. 단, 이 번호는 프로세스 내부에서만 고유하면 되기 때문에, 다른 프로세스에서 같은 번호를 사용하는 것은 문제가 되지 않는다.
커널은 각 소켓에 대해 송신 버퍼(send buffer)와 수신 버퍼(receive buffer)를 유지한다. 프로그램이 write()로 데이터를 보내면, 그 데이터는 먼저 커널의 송신 버퍼에 저장되고, 커널이 비동기적으로 네트워크를 통해 전송한다. 반대로 외부에서 도착한 데이터는 수신 버퍼에 임시 저장되며, 애플리케이션이 read()를 호출할 때 이 버퍼에서 데이터를 읽는다. 이처럼 소켓의 I/O는 비동기적으로 동작하며, 속도 차이가 큰 네트워크와 프로세스 간의 데이터 흐름을 안정적으로 중계해준다. 특히 TCP는 이 버퍼를 기반으로 슬라이딩 윈도우, 흐름 제어, 혼잡 제어 등의 메커니즘을 통해 높은 신뢰성을 보장한다.
![]() | ![]() |
---|
파일 디스크립터는 유닉스/리눅스 시스템에서 열린 파일, 소켓, 파이프 등을 추상화하여 다루기 위한 정수형 식별자다. 각 프로세스는 자체적인 파일 디스크립터 테이블을 가지며, 그 안에서만 유효한 지역적 식별자로 사용된다. 관례적으로, 운영체제는 프로세스를 시작할 때 표준 입력(0), 표준 출력(1), 표준 에러(2)의 세 가지 스트림을 미리 열어 두기 때문에, 이후 open(), socket() 등을 통해 생성되는 디스크립터는 보통 3번부터 시작하게 된다.
하지만 이 번호는 절대적인 규칙이 아니라 운영체제 구현과 프로세스 상태에 따라 달라질 수 있다. 예를 들어, 어떤 파일 디스크립터가 먼저 닫히면 그 번호가 재활용될 수 있고, dup() 같은 호출을 통해 번호를 직접 조작할 수도 있다. 따라서 올바른 표현은 “일반적으로 3번부터 시작하지만, 항상 그런 것은 아니다”라는 것이다.
한편, 네트워크 관점에서 보면 포트 번호(port number)는 시스템 전체에서 전역적으로 고유한 식별자다. 하나의 컴퓨터에서 여러 소켓이 동시에 존재할 수 있기 때문에, TCP/IP 계층은 포트 번호를 기준으로 수신된 데이터를 어떤 응용 프로그램으로 전달할지를 결정한다. 운영체제는 이 포트 번호에 해당하는 소켓 디스크립터를 찾아서 해당 프로세스에 데이터를 전달한다. 요약하면, 소켓 번호(FD)는 프로세스 내부에서만 의미 있는 지역 식별자, 반면 포트 번호는 네트워크 상에서 프로세스를 식별하기 위한 전역적 식별자다. 이 두 체계를 연결하여 커널은 네트워크와 프로세스 간 안정적이고 유연한 데이터 통신을 중개한다.
소켓 쌍(socket pair)은 TCP 연결을 고유하게 식별하기 위한 주소 쌍으로, 항상 (클라이언트 IP:포트, 서버 IP:포트)(cliaddr:cliport, servaddr:servport) 형태의 튜플로 구성된다. 이 네 가지 요소를 결합한 소켓 주소 쌍은 각각의 연결을 구분하는 유일한 식별자로 작용하며, 커넥션 소켓 쌍(connection socket pair) 또는 소켓 엔드포인트 쌍이라고도 불린다. TCP는 이러한 쌍을 기준으로 수많은 연결을 동시에 관리하고 추적할 수 있으며, 이를 통해 동일한 서버 포트로 들어오는 여러 클라이언트 요청을 충돌 없이 구분할 수 있다.
소켓 인터페이스(Socket interface)는 네트워크 애플리케이션을 만들기 위해 UNIX I/O 함수들과 함께 사용되는 함수들의 집합이다. 각각의 함수들에 대한 설명은 CS:APP 책의 순서를 따라 작성했지만, 구조 그림을 먼저 이해하고 이게 client쪽 함수인지, server쪽 함수인지 인지한 상태로 학습하는 게 좋을 것이다.
함수 이름 | 사용 위치 | 주요 역할 |
---|---|---|
getaddrinfo | 클라이언트/서버 | 호스트명과 포트명을 소켓 주소 정보로 변환하는 함수 |
socket | 클라이언트/서버 | 소켓(통신 엔드포인트)을 생성하는 함수 |
connect | 클라이언트 | 서버에 연결 요청을 보내는 함수 |
bind | 서버 | 소켓에 IP 주소와 포트번호를 할당하는 함수 |
listen | 서버 | 소켓을 연결 요청 수신 대기 상태로 전환하는 함수 |
accept | 서버 | 클라이언트의 연결 요청을 수락하고 통신 소켓을 생성하는 함수 |
rio_writen | 클라이언트/서버 | 데이터를 소켓을 통해 전송하는 함수 |
rio_readlineb | 클라이언트/서버 | 소켓에서 한 줄씩 데이터를 읽는 함수 |
close | 클라이언트/서버 | 소켓 연결을 종료하는 함수 |
open_clientfd | 클라이언트 | 클라이언트 소켓을 생성하고 서버에 연결하는 헬퍼 함수 (getaddrinfo → socket → connect) |
open_listenfd | 서버 | 서버용 소켓을 생성하고 수신 대기 상태로 설정하는 헬퍼 함수 (getaddrinfo → socket → bind → listen) |
/* IP socket address structure */
struct sockaddr_in {
uint16_t sin_family; /* Protocol family (always AF_INET) */
uint16_t sin_port; /* Port number in network byte order */
struct in_addr sin_addr; /* IP address in network byte order */
unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */
};
/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
uint16_t sa_family; /* Protocol family */
char sa_data[14]; /* Address data */
};
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// Returns: nonnegative descriptor if OK, −1 on error
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
// Returns: 0 if OK, −1 on error
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// Returns: 0 if OK, −1 on error
서버가 listen()을 호출하면 커널은 두 개의 큐를 생성한다. 첫 번째는 incomplete connection queue로, 아직 3-way handshake가 완료되지 않은 클라이언트들의 SYN 요청을 임시 저장한다. 클라이언트가 SYN을 보내면 서버는 자동으로 SYN-ACK를 응답하고, 클라이언트가 최종 ACK를 보내 3-way handshake가 완료되면 해당 연결은 completed connection queue (accept queue)로 이동된다. 이후 서버가 accept()를 호출하면, 이 큐에서 가장 오래된 연결을 꺼내고, 이 연결을 위한 새로운 소켓 디스크립터를 반환한다. 이 새 소켓은 고유한 송수신 버퍼를 커널 공간에 가지며, 클라이언트와의 실제 데이터 전송은 이 디스크립터를 통해 이루어진다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// Returns: 0 if OK, −1 on error
(1) 서버: accept() 호출로 블로킹 중
listenfd는 리스닝 소켓으로 클라이언트의 연결 요청을 기다린다.
그림에서 listenfd(3)은 파일 디스크립터 번호 3번으로 열려 있음.
(2) 클라이언트: connect() 호출
클라이언트는 clientfd를 통해 서버의 IP와 포트로 SYN을 전송한다.
서버 커널은 해당 요청을 **incomplete queue(SYN 큐)**에 저장한다.
(3) 3-way handshake 완료 후
클라이언트가 마지막 ACK를 보내고, 서버는 이 연결을 completed queue로 이동시킨다.
이제 accept()는 대기 중이던 연결을 수락하고, **connfd(4)**라는 새 파일 디스크립터를 반환한다.
connfd는 클라이언트와의 연결을 위한 전용 소켓이다.
블로킹은 어떤 함수 호출이 결과가 준비될 때까지 멈춰있는 상태를 의미한다.
이 그림에서 accept() 함수는 블로킹 호출이다.
즉, 클라이언트가 연결 요청을 보내기 전까지는 accept()가 리턴되지 않고 기다린다. 연결 요청이 오고 3-way handshake가 끝나야만 accept()가 연결된 소켓 디스크립터를 반환하면서 깨어난다.
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
// Returns: nonnegative connected descriptor if OK, −1 on error
getaddrinfo() 함수는 문자열 기반의 호스트명(예: "www.google.com")과 서비스명(예: "http")을 받아 내부적으로 DNS 프로토콜을 통해 해당 도메인의 IP 주소 정보를 조회한다. 이 과정에서 glibc 라이브러리의 내부 함수들이 DNS 질의를 수행하고, 응답을 파싱한 뒤, 결과를 표현할 수 있도록 struct addrinfo 구조체들의 연결 리스트를 동적으로 생성한다.
이 구조체들은 malloc()을 통해 사용자 프로세스의 heap 영역에 동적으로 할당되며, 이 메모리는 가상 주소 공간 상에 위치한다. 운영체제 커널은 이 가상 주소 공간을 실제 물리 메모리(RAM)의 페이지에 매핑함으로써 사용자 프로그램이 해당 주소를 통해 안전하게 접근할 수 있도록 한다. 따라서 getaddrinfo()로 얻은 결과는 DNS 프로토콜 기반으로 조회된 IP 정보가 heap에 저장된 구조체로 변환된 형태이며, 사용이 끝난 후에는 반드시 freeaddrinfo()로 메모리를 해제해야 한다.
#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);
// Returns: 0 if OK, nonzero error code on error
struct addrinfo {
int ai_flags; // 힌트 플래그
int ai_family; // 주소 체계 (AF_INET 등)
int ai_socktype; // 소켓 타입 (SOCK_STREAM 등)
int ai_protocol; // 프로토콜 (보통 0)
size_t ai_addrlen; // ai_addr의 크기
struct sockaddr *ai_addr; // 실제 주소 정보
char *ai_canonname; // 호스트의 정식 이름
struct addrinfo *ai_next; // 다음 항목 포인터 (연결 리스트)
};
host
)과 서비스명(service
)을 소켓 주소 구조체 리스트로 변환하는 함수hints
를 통해 주소 체계, 소켓 타입 등의 조건을 설정할 수 있음result
에는 연결 리스트 형태의 주소 목록이 반환됨freeaddrinfo()
로 메모리 해제 필요void freeaddrinfo(struct addrinfo *result);
// Returns: nothing
getaddrinfo
로 생성된 주소 리스트의 메모리를 해제하는 함수const char *gai_strerror(int errcode);
// Returns: error message
getaddrinfo
, getnameinfo
등에서 발생한 에러 코드를 문자열 메시지로 변환해줌#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);
// Returns: 0 if OK, nonzero error code on error
getaddrinfo
의 반대 역할 수행: 소켓 주소 → 호스트명 및 서비스명 문자열로 변환flags
에 따라 IP 문자열 또는 포트 숫자를 그대로 반환 가능 (예: NI_NUMERICHOST
, NI_NUMERICSERV
)#include "csapp.h"
int open_clientfd(char *hostname, char *port);
// Returns: descriptor if OK, −1 on error
int open_clientfd(char *hostname, char *port) {
int clientfd;
struct addrinfo hints, *listp, *p;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a connection */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
Getaddrinfo(hostname, port, &hints, &listp);
/* Walk the list for one that we can successfully connect to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Connect to the server */
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* Success */
Close(clientfd); /* Connect failed, try another */
}
/* Clean up */
Freeaddrinfo(listp);
if (!p) /* All connects failed */
return -1;
else /* The last connect succeeded */
return clientfd;
}
getaddrinfo
로 주소 리스트 획득socket
생성 후 connect
시도AI_NUMERICSERV
)#include "csapp.h"
int open_listenfd(char *port); // Returns: descriptor if OK, −1 on error
int open_listenfd(char *port) {
struct addrinfo hints, *listp, *p;
int listenfd, optval=1;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
Getaddrinfo(NULL, port, &hints, &listp);
/* Walk the list for one that we can bind to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Eliminates "Address already in use" error from bind */
Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int));
/* Bind the descriptor to the address */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break; /* Success */
Close(listenfd); /* Bind failed, try the next */
}
/* Clean up */
Freeaddrinfo(listp);
if (!p) /* No address worked */
return -1;
/* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, LISTENQ) < 0) {
Close(listenfd);
return -1;
}
return listenfd;
}
getaddrinfo
로 모든 수신 가능한 주소 목록 획득 (host는 NULL, AI_PASSIVE
)socket
생성 및 bind
시도SO_REUSEADDR
옵션 설정으로 주소 재사용 가능listen
호출함수명 | 역할 | 내부 구성 함수 | 특징 |
---|---|---|---|
getaddrinfo | 도메인/포트 → sockaddr 구조체 | 없음 | 멀티 프로토콜, 재진입 가능 |
getnameinfo | sockaddr → 문자열 변환 | 없음 | 역변환, 플래그로 세부 제어 가능 |
open_clientfd | 클라이언트 연결 시도 | getaddrinfo → socket → connect | 실패 시 fallback 주소 시도 |
open_listenfd | 서버 리스닝 소켓 설정 | getaddrinfo → socket → bind → listen | 주소 재사용, 수신 전용 설정 |
정글에서 docker를 통해 작업을 하고 있다면 환경 구축에 있어 아래의 글이 도움이 될 것이다.
at_this_moment님이 작성하신 글
CS:APP을 따라 작성한 코드와 각각의 코드에 대한 설명은 아래와 같다.
// echo.c
#include "csapp.h" // 표준 라이브러리와 에러 처리, IO 처리를 포함하는 사용자 정의 헤더
void echo(int connfd)
{
size_t n;
char buf[MAXLINE]; // 클라이언트로부터 읽어올 데이터를 저장할 버퍼 (최대 MAXLINE 바이트)
rio_t rio; // Robust I/O 처리를 위한 구조체 변수 (연결된 파일 디스크립터, 내부적으로 사용할 읽기 버퍼, 현재 버퍼 내 위치, 남은 바이트 수 등 저장)
Rio_readinitb(&rio, connfd);
// connfd에 연결된 소켓을 rio 객체에 연결해서, 그걸로 안전하게 읽겠다는 준비 작업임임
// rio 구조체를 초기화, 어떤 소켓 디스크립터(connfd)에서 읽을지 설정
// 이 함수를 호출한 이후, rio를 통해 connfd에서 데이터를 읽을 수 있음. Rio_readlineb() 같은 함수들이 이 rio 구조체를 활용해서 읽기를 수행
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
// 클라이언트로부터 한 줄씩 입력을 읽음
// 0을 반환하면 EOF (연결 종료)
printf("server received %zu bytes\n", n);
// 몇 바이트를 수신했는지 출력 (디버깅용)
Rio_writen(connfd, buf, n);
// 받은 내용을 클라이언트에게 그대로 다시 전송 (에코)
}
}
// rio를 쓰는 이유
// 일반적인 read() 호출은 버퍼링이 없고 부분 읽기(partial read)가 발생할 수 있어 번거로움
// RIO는 내부 버퍼를 활용해 라인 단위 또는 지정된 바이트 수만큼 안정적으로 읽을 수 있도록 도와줌
// 줄 단위로 데이터를 주고받는 TCP 서버를 만들 때 매우 유용
// echoclient.c
#include "csapp.h" // CS:APP에서 제공하는 래퍼 함수들과 상수, 자료구조가 정의된 헤더 파일 포함
int main(int argc, char **argv)
{
int clientfd; // 서버와의 연결에 사용할 소켓 디스크립터
char *host, *port, buf[MAXLINE]; // 서버 호스트명, 포트번호, 입출력 버퍼
rio_t rio; // 버퍼링된 입출력용 구조체
if (argc != 3) // 명령행 인자가 3개가 아닌 경우 (프로그램 이름 + 호스트 + 포트), 사용법 출력
{
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0);
}
host = argv[1]; // 첫 번째 인자는 접속할 서버의 호스트 주소
port = argv[2]; // 두 번째 인자는 접속할 포트 번호
clientfd = Open_clientfd(host, port); // 서버에 연결하고, 연결된 소켓의 디스크립터를 clientfd에 저장, 실패 시 −1 반환 (내부적으로 getaddrinfo, socket, connect 사용)
Rio_readinitb(&rio, clientfd); // clientfd를 기반으로 버퍼링된 읽기 구조체 rio를 초기화, 이후 Rio_readlineb()로 안전한 라인 기반 읽기가 가능함
while (Fgets(buf, MAXLINE, stdin) != NULL) // 표준 입력으로부터 한 줄 입력을 받아 buf에 저장 (ex. 사용자 타이핑)
{
Rio_writen(clientfd, buf, strlen(buf)); // buf 내용을 서버에 전송 (clientfd는 서버와 연결된 소켓), strlen으로 정확한 바이트 수만큼 전송
Rio_readlineb(&rio, buf, MAXLINE); // 서버로부터 한 줄 응답을 읽어 buf에 저장, rio는 내부적으로 버퍼를 사용하여 read 호출을 최적화함
Fputs(buf, stdout); // 서버로부터 받은 응답을 표준 출력에 출력 (즉, 사용자에게 보여줌)
}
Close(clientfd); // 모든 작업이 끝난 후 소켓을 닫아 리소스를 정리
exit(0); // 프로그램 정상 종료
}
// echoserveri.c
#include "csapp.h" // csapp 라이브러리에는 에러 처리 및 입출력 관련 유틸 함수가 정의되어 있음
void echo(int connfd); // 클라이언트와 연결된 소켓을 통해 echo 동작을 수행하는 함수 (정의는 echo.c에 있음)
int main(int argc, char **argv) // argc : argument count(인자의 개수), argv : argument vector(입력된 인자 문자열 배열)
{
int listenfd, connfd;
// listenfd: 리스닝 소켓의 파일 디스크립터(File Descriptor) - 서버에서 단 하나, 클라이언트의 연결 요청을 기다림
// 서버가 socket() → bind() → listen()을 통해 생성, 클라이언트의 연결 요청을 기다리는 수동 소켓(passive socket), 단 하나만 만들어서 accept()에 계속 넘겨줌
// connfd: accept() 호출 결과로 반환되는 클라이언트와 연결된 소켓 디스크립터 - 클라이언트마다 하나씩, 각각의 연결을 위해 따로 생성됨
// 이 소켓은 특정 클라이언트와의 통신 전용으로 사용됨, read(), write() 또는 echo() 함수 내부에서 메시지를 주고받을 때 사용
// 이 두 개는 둘 다 소켓이며, 내부적으로는 유닉스 커널이 관리하는 파일 디스크립터임. 네트워크 연결도 마치 "파일"처럼 다룰 수 있다는 UNIX 철학
socklen_t clientlen; // clientaddr의 구조체 크기를 담는 변수, accept()에서 클라이언트 주소를 받기 위해 주소 크기를 미리 알려주는 용도
struct sockaddr_storage clientaddr; // 클라이언트의 주소 정보를 담을 범용 구조체(sockaddr_in, sockaddr_in6 모두 수용), 나중에 getnameinfo()로 IP 주소나 도메인명을 얻는 데 사용됨
char client_hostname[MAXLINE], client_port[MAXLINE]; // 클라이언트의 호스트 이름(또는 IP 문자열)과 클라이언트가 접속한 포트 번호 문자열을 저장하는 버퍼, getnameinfo()를 통해 clientaddr에서 추출됨
if (argc != 2) // 포트 번호를 명령행 인자로 받지 않으면 사용법 출력 후 종료
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
// ./echoserveri 8080라는 명령을 받으면, 인자가 2개임("./echoserveri"(argv[0]), "8080"(argv[1]))
listenfd = Open_listenfd(argv[1]); // 해당 포트에 바인딩된 리스닝 소켓을 생성하고, 그 디스크립터를 반환함(클라이언트 연결 요청을 받는 데 사용)
while (1)
{
clientlen = sizeof(struct sockaddr_storage); // 클라이언트 주소 정보를 받을 구조체 크기 설정
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); // 클라이언트 연결 수락. connfd는 클라이언트와 통신할 소켓
Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE, client_port, MAXLINE, 0); // 클라이언트의 IP 주소와 포트 번호를 문자열로 변환
printf("Connected to (%s, %s)\n", client_hostname, client_port); // 클라이언트 정보 출력
echo(connfd); // echo 서비스 수행 (받은 내용을 그대로 돌려줌)
Close(connfd); // 클라이언트 연결 종료
}
exit(0);
}
// 단일 프로세스 기반의 TCP 에코 서버
// 클라이언트와 연결을 수락한 후, 해당 연결에 대해 echo() 함수를 실행하고, 종료되면 연결을 닫고 다음 연결을 수락
// Accept() → echo() → Close()의 순환을 무한 반복하는 구조