csapp 이어서 하겠다
개같은 거 졸라게 많다
아무래도 이것저것 고유명사 비스무리한게 많아
하나하나 다 정리해가며 짚어야 오히려 이해하기 편할 거 같다
소켓 인터페이스는 프로그래머가 네트워크 통신을 구현하기 위해 사용하는 표준 API1
소켓 함수들은 프로토콜에 따라 다른 주소 체계를 지원해야 하므로, 이를 위해 소켓 주소를 표현하는 공용 구조체를 사용
IPv4 구조체: sockaddr_in
16바이트 크기
주소 패밀리: sin_family
IPv4 주소체계라는 의미
16비트 포트 번호: sin_port
32비트 IP주소: sin_addr
일반 주소 구조체: struct sockaddr
껍데기 구조체 (주소 데이터 담으려 만든게 아님)
struct sockaddr *
sockaddr_storage
struct sockaddr_storage clientaddr;
으로 선언하면 IPv4/IPv6에 상관없이 쓸 수 있다sockaddr *
로 캐스팅해 사용한다socket
함수 (The socket Function)socket()
함수는 새 소켓 생성해 파일 디스크립터(fd) 를 반환한다
소켓 생성 기 통신에 사용할 도메인(domain), 타입(type), 프로토콜(protocol) 을 지정한다
int socket(int domain, int type, int protocol);
으로 정의도메인 (주소 체계):
첫 번째 인자는 주소 체계 지정
AF_INET
: IPv4AF_INET6
:IPv6AF_UNIX
: 로컬 유닉스 도메인소켓 타입:
두 번째 인자는 통신의 특성을 지정
0
을 주어 기본 프로토콜 선택을 사용소켓은 초기에 완전히 열린 상태가 아니라 바로 데이터 읽기/쓰기가 불가능하다
클라이언트의 경우 connect()
호출을 통해 연결해야 하고
서버의 경우 bind()
로 주소를 지정하고 listen()
으로 대기 상태로 만들어야한다
프로토콜 독립성:
소켓 만들 때에 AF_INET
등을 넣는 대신에
getaddrinfo 같은 함수를 활용하여 도메인과 타입을 동적으로 얻어내는 것이 권장된다
이를 통해 IPv6 등도 지원 가능케 된다
connect
함수 (The connect Function)클라이언트 측에서 사용하며 연결을 요청한다
호출 시 블로킹되어, 상대 서버와의 연결이 완전히 설정되거나 에러가 발생할 때까지 반환하지 않는다
연결에 성공하면 소켓은 통신 가능한 상태(established state) 가 되고, 이후 해당 소켓 디스크립터를 통해 데이터를 주고받을 수 있다
실패할 경우 -1을 반환하며 errno
에 에러 정보를 남긴다
함수 원형: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
형식
반환값은 성공시 0
, 실패시 -1
이다
sockfd
는 socket()
로 생성한 클라이언트 소켓
addr
는 서버의 소켓 주소
addrlen
은 주소 구조체의 크기
addr
(주소)인자는 보통 서버의 IP와 포트를 설정한 뒤 그 포인터를 캐스팅한 것이다inet_pton
)하거나 DNS이름을 IP로 해석 (getaddrinfo
)하는 선행 작업 필요bind
함수 (The bind Function)bind()
함수는 서버 측에서 사용
생성된 소켓에 로컬 주소(아이피와 포트)를 할당
이를 통해 자신의 소켓이 특정 포트 번호를 수신 대기하도록 커널에 알리고, 필요하면 네트워크 인터페이스(특정 IP나 모든 IP)에 바인드할 수 있다
없이 listen()
하면 임의의 포트 할당하지만
서버는 클라가 아는 고정 포트 사용해야 하므로 bind()
호출 필수적
함수 원형: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
정상 수행 시 0
, 실패시 -1
반환
sockfd
는 서버가 생성한 소켓
addr
은 바인드할 자신의 주소
addrlen
은 주소 구조체 크기
listen
함수 (The listen Function)bind()
까지 마친 서버 소켓을 수동 대기 모드(passive listen) 로 전환한다
이를 통해 해당 소켓은 클라이언트의 연결 요청을 받을 수 있응 상태가 됨
listen()
을 호출하지 않을 시 클라이언트처럼 간주되어 반드시 listen()
을 실행해야함
함수 원형: int listen(int sockfd, int backlog);
반환 성공시 0
, 실패시 -1
sockfd
는 이전에 바인드한 소켓 디스크립터
backlog
는 커널 접속 대기 큐의 최대 길이 힌트
accept
함수 (The accept Function)accept()
함수는 서버에서 사용되며, 대기 중인 연결 요청을 하나 수락하여 새로운 소켓(fd) 을 생성
일반적으로 블로킹 모드로 호출되며,
클라이언트의 연결 시도가 대기열에 없으면 해당 소켓에서 연결이 들어올 때까지 프로세스를 수면(sleep) 시킨다
accept()
가 반환하는 새 소켓은 특정 클라이언트와 연결된 개별 통신 채널이며
기존의 listening 소켓은 계속 남아 다른 요청을 기다린다
함수 원형: int accept(int listenfd, struct sockaddr *addr, socklen_t *addrlen);
오류시 -1
반환, 정상 시 새로 연결된 소켓 fd 반환
listenfd
는 listen()
중인 소켓 디스크립터
addr
는 클라이언트 주소 정보를 받을 버퍼5
addrlen
은 입력으로 버퍼 크기, 출력으로 실제 주소 구조체 크기를 얻는 포인터
accept()
호출 시 커널은 listen 대기열에서 완료된 연결(TCP의 3-way handshake가 끝난 것)을 하나 꺼내온다addr
버퍼에 클라이언트의 소켓 주소(IP:포트)를 써준다문자열 형태의 호스트명/서비스명과 이진 소켓 주소 구조체 간 변환을 도와주는 함수에 대한 내용이다
이들은 재진입 가능(reentrant) 하고 프로토콜 독립적이다
getaddrinfo
함수호스트와 서비스 문자열을 소켓 주소로 변환한다
// 원형:
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints,
struct addrinfo **result);
성공 시 0을 반환하고 결과로 addrinfo 구조체 연결리스트의 첫 노드 포인터를 result
에 담아준다
사용후에 freeaddrinfo(result)
로 메모리를 해제해야 한다
host
인자는 도메인 이름, 숫자 IP문자열, NULL
을 줄 수 있다service
인자는 서비스 이음, 포트 번호 문자열, NULL
을 줄 수 있다NULL
주면 안됨struct addrinfo
:
getaddrinfo
가 반환하는 연결 리스트의 각 노드는 addrinfo
구조체 타입이다
주요 필드:
ai_family
(주소 패밀리)ai_socktype
(소켓 타입)ai_protocol
(프로토콜)ai_addr
(소켓 주소 구조체 포인터)ai_addrlen
(그 크기)ai_canonname
(호스트의 공식 이름)hints
인자는 getaddrinfo
의 결과를 필터링하는 데 사용된다
ai_family
를 AF_INET
또는 AF_INET6
로 지정하면 IPv4 또는 IPv6 결과로 제한할 수 있고, AF_UNSPEC
(0)으로 두면 제한없이 둘 다 가능하다
ai_socktype
을 SOCK_STREAM
으로 설정하면 TCP용 주소만 (연결형 소켓만) 얻고, 설정하지 않으면 UDP용까지 포함한 여러 항목이 나올 수 있다
ai_flags에
는 여러 상수를 OR해 설정할 수 있다:
AI_PASSIVE
: 호스트가 NULL
일 때 와일드카드 주소를 반환하도록 함
주로 서버에서 사용하며, 모든 로컬 IP에 바인드하기 위해 사용
AI_ADDRCONFIG
: 로컬 호스트가 IPv4로 설정된 경우 IPv4 주소만, IPv6이면 IPv6 주소만 반환하도록 함
(사용하지 않는 프로토콜의 주소는 제외)
AI_CANONNAME
: 첫 결과의 ai_canonname
에 호스트의 정규화된 공식 이름을 채워주도록 함
AI_NUMERICHOST
: 호스트 문자열을 도메인 이름으로 보지 말고 숫자 주소로만 해석하게 함
(DNS 조회 안 함)
AI_NUMERICSERV
: 서비스 문자열을 이름으로 보지 않고 숫자로만 해석하게 함
getnameinfo
함수:소켓 주소 → 호스트/서비스명 문자열 변환 함수
// 원형:
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen, int flags);
sa
에 소켓 주소, salen
에 그 크기, host
와 service
버퍼 및 길이를 주면 대응되는 호스트 이름과 서비스 이름을 넣어준다
성공 시 0, 실패 시 0이 아닌 에러코드
host
나 service
에 NULL
을 전달하거나 길이를 0으로 주면 해당 변환 스킵한다
기본적으로 호스트 이름을 반환하기 위해 DNS 조회를 할 수도 있고,
서비스 이름은 /etc/services
참조하여 알려진 이름을 줄 수도 있다
옵션 플래그로 이를 제어한다:
NI_NUMERICHOST
: 무조건 숫자 형식의 호스트 주소 문자열을 반환 (예: “93.184.216.34”)
NI_NUMERICSERV
: 무조건 숫자 형식의 포트 번호 문자열을 반환 (예: “8080”)
NI_NOFQDN
, NI_NAMEREQD
등의 추가 플래그도 존재한다 (표준 참고)
별 거 아니고 CSAPP저자들이 만든 소켓 프로그래밍용 함수다
open_clientfd
는 클라이언트 측에서 호스트와 포트로 쉽게 연결하는 함수이며,
open_listenfd
는 서버 측에서 포트 번호 하나로 간단히 듣기 소켓을 열어준다
open_clientfd
함수:
int open_clientfd(char *hostname, char *port);
성공 시 소켓 fd, 실패 시 -1
리턴
open_listenfd
함수:
int open_listenfd(char *port);
서버 예제 나온다
에코 클라이언트 동작:
에코 클라이언트는 사용자 입력 -> 서버 송신 -> 서버로부터 같은 내용 수신 -> 출력 의 동작을 반복
연결 단계:
프로그램 시작 시 명령줄 인자로 호스트 이름과 포트 번호를 받아, open_clientfd
(host, port)를 호출하여 해당 서버에 연결
입출력 설정:
표준 입출력의 편의를 위해 CSAPP의 RIO 버퍼를 초기화한다 (Rio_readinitb(&rio, clientfd)
호출)
요청/응답 루프:
읽고fgets
전송Rio_writen(clientfd, buf, strlen(buf))
하고
읽어Rio_readlineb
와 출력을 입력 종료(EOF)까지 반복
종료:
close(clientfd)
로 서버와의 연결을 닫는다
에코 서버 동작: (iterative server, 접속 순차 처리)
준비 단계:
open_listenfd(port)
를 호출함으로써 해당 포트에서 듣기 소켓(listenfd) 을 연다
성공 시 반환되는 listenfd를 가지고 while(1)
무한 루프에 들어간다
연결 수락:
루프 안에서 Accept(listenfd, (SA *)&clientaddr, &clientlen)
으로 클라이언트 접속을 기다리다가 어떤 클라이언트가 접속하면 새로운 connfd
를 반환
반환과 동시에 clientaddr
에는 상대의 주소가 채워짐
서버는 Getnameinfo
함수를 이용해 clientaddr
로부터 클라이언트의 호스트 이름과 포트 번호 문자열을 얻어 로그를 출력
서비스 처리:
echo(connfd)
함수를 호출하여, 해당 연결에 대한 에코 서비스를 수행
echo
함수는 인자로 넘긴 connfd
소켓에 대하여
EOF가 나올 때까지 Rio_readlineb
로 한 줄을 읽고 그대로 Rio_writen
으로 돌려주는 작업을 반복
정리:
echo
함수가 끝나면 서버는 해당 클라이언트와의 connfd
를 Close(connfd)
하여 연결을 닫는다
이후 루프가 돌며 다시 accept()
로 다음 클라이언트를 기다린다
echo
함수에서 Rio_readlineb
가 0
을 리턴하면, 이는 클라이언트 쪽에서 EOF(연결 닫힘) 를 보낸 것을 뜻한다서로 다른 소프트웨어들이 통신하고 데이터를 교환할 수 있도록 하는 규칙과 프로토콜의 집합
데이터를 전송하기 전에 미리 연결을 설정하는 과정(호 설정)이 필요 없다
데이터가 데이터그램이라는 독립적인 패킷 단위로 분할되어 전송