본문 바로가기

[CSAPP] 11장 네트워크 프로그래밍 Network Programming (2/4)

출처  Randal E.Byrant, David R. O'Hallaron, 컴퓨터 시스템 Computer Systems A Programmer's Perspective, 김형신 옮김 (퍼스트북), Pearson

4.  소켓 인터페이스

소켓 인터페이스 sockets interface는 네트워크 어플리케이션을 만들 때 UNIX I/O 함수들과 함께 사용되는 함수들의 집합이다. 소켓 인터페이스는 Unix 계열, Windows, Macintosh 시스템을 포함하여 대부분의 현대 시스템에 구현되어 있다.

 

위 사진은 클라이언트-서버 트랜잭션에서 사용되는 전형적인 소켓 인터페이스이다.

(1) 소켓 주소 구조체 Socket Address Structure

리눅스 커널의 관점에서, 소켓은 소통의 end point이다.

리눅스 프로그램의 관점에서, 소켓은 대응되는 descriptor를 가진 open file이다.

<netinet/in.h>에 struct sockaddr_in, struct in_addr 등의 구조체가 정의되어 있다.
<sys/socket.h>에 struct sockaddr 등의 구조체가 정의되어 있다.

 

sockaddr_in 구조체 (IPv4 소켓 주소를 위한 구조체)

sockaddr_in은 IPv4 프로토콜을 이용하는 인터넷 소켓 주소를 담는 구조체이다. 16 바이트 크기이다.

 /* 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) */
};

 

sin_family 필드는 2바이트 크기로, IP 프로토콜의 버전을 나타낸다. sockaddr_in 구조체에서 sin_family 필드는 항상 IPv4를 나타내는 AF_INET이다. sin_port 필드는 2바이트 크기로, 포트 번호를 나타낸다. uint16_t는 부호가 없는 16비트 정수 타입이라서 0부터 65,536까지 표현할 수 있다. sin_addr 필드는 in_addr 구조체 타입으로 4바이트 크기이다. 호스트 IP 주소를 나타낸다. IP 주소는 네트워크 바이트 순서, 즉 빅 엔디안 big-endian 순서이다. sin_zero[8] 필드는 8바이트 크기로, 패딩을 위해 사용된다.

/* IP address structure */
struct in_addr {
    uint32_t  s_addr; /* Address in network byte order (big-endian) */
};

 

sockaddr (generic 용도의 소켓 주소를 위한 구조체)

/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
    uint16_t  sa_family;    /* Protocol family */
    char      sa_data[14];  /* Address data  */
};

 

sockaddr_in 구조체는 IPv4 프로토콜을 사용하는 소켓 주소만 다룰 수 있는 구조체이고, socketaddr_in6 구조체는 IPv6 프로토콜을 사용하는 소켓 주소만 다룰 수 있는 구조체이다. 소켓 인터페이스인 connect, bind, accept 함수는 어떤 프로토콜을 사용하는 소켓 주소가 들어오든지 모든 소켓 주소를 받아들일 수 있어야 한다. 오늘날에는 이러한 상황에서 generic void * 포인터를 사용하지만 그 당시에는 C에 generic void * 포인터가 없었다. 과거 소켓 인터페이스 설계자들은 이 과제를 해결하기 위해 generic sockaddr 이라는 구조체를 새로 정의하고, connect, bind, accept 함수를 정의할 때 이 함수들이 인자로 sockaddr 타입의 구조체를 받아들이도록 설계했다. 소켓 인터페이스를 사용하는 애플리케이션들은 소켓 인터페이스를 이용하려면 소켓 주소 구조체를 sockaddr 타입으로 캐스팅해야 한다. sa_family 필드는 2바이트 크기로, 프로토콜 버전을 나타낸다. sockaddr_in 구조체를 sockaddr로 캐스팅하는 경우 sockaddr_in 구조체의 맨 앞에 있는 2바이트 크기의 sin_family 필드가 그대로 sa_family에 담기게 된다.

<sys/socket.h>에 socket, bind, connect 등의 함수가 정의되어 있다.

 

(2) socket 함수

클라이언트와 서버는 socket (file) descriptor를 만들기 위해서 socket 함수를 사용한다.

#include <sys/types.h>
#include <sys/socket.h>

/* Returns: nonnegative descriptor if OK, −1 on error */
int socket(int domain, int type, int protocol);

 

만약 소켓이 connection의 end point가 되길 바란다면 socket 함수를 다음과 같은 하드코딩 인자와 함께 호출하면 된다.

clientfd = Socket(AF_INET, SOCK_STREAM, 0);

 

 

AF_INET은 우리가 32 비트 IP 주소를 사용한다는 것을 나타낸다. SOCK_STREAM은 소켓이 connection의 end point가 될 것임을 나타낸다. SOCK_STREAM은 소켓을 생성할 때 사용하는 소켓 유형 중 하나로, TCP 기반의 연결 지향적 통신을 지원하는 소켓을 의미한다. 이 소켓 유형은 데이터 스트림의 연속적인 전달을 보장하며, 데이터 전송 순서와 무결성을 유지한다.

 

하지만 인자를 하드코딩하는 방식보다 권장되는 방식은 인자들을 자동으로 생성해주는 getaddrinfo 함수를 사용하는 것이다. getaddrinfo 함수를 이용하여 인자를 생성하면 코드가 프로토콜에 독립적인 코드가 된다는 장점이 있다. 

 

socket 함수에 의해 반환되는 clientfd 라는 descriptor는 부분적으로만 오픈되어 있다. 읽거나 작성될 수 없다. 소켓의 opening을 끝내는 방법은 클라이언트인지 서버인지에 따라 다르다. 이제 클라이언트의 소켓 오픈을 어떻게 완료하는지 살펴보겠다.

(3) connect 함수

클라이언트는 connect 함수를 호출하여 서버와의 connection을 수립한다.

#include <sys/socket.h>

/* Returns: 0 if OK, −1 on error */
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);

 

클라이언트에서 호출되는 connect 함수는 소켓 주소 addr에 있는 서버와의 연결을 수립하기 위해 시도한다.  인자 addrlen은 sizeof(sockaddr_in)이다. connect 함수는 connection이 성공적으로 수립되거나 에러가 발생할 때까지 블로킹된다. 만약 연결이 성공적이라면, clientfd descriptor는 이제 읽기와 쓰기를 위한 준비가 완료된 것이다.

 

connect 함수의 결과로 생성된 연결은 소켓 쌍 socket pair로 규정된다.

(x:y, addr.sin_addr:Addr.sin_port)

*x는 클라이언트의 IP 주소이고, y는 클라이언트 호스트에 있는 클라이언트 프로세스를 식별하는 임시 포트이다.

 

socket 함수와 마찬가지로 connect 함수를 호출할 때 가장 좋은 방법은 getaddrinfo를 이용하여 connect 함수의 인자를 자동 생성하는 것이다.

(4) bind 함수

남아 있는 소켓 함수인 bind, listen, accept는 서버가 클라이언트와 connection을 수립할 때 사용된다.

#include <sys/socket.h>

/* Returns: 0 if OK, −1 on error */
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

 

bind 함수는 커널에게 addr에 있는 서버의 소켓 주소를 서버의 소켓 descriptor인 sockfd와 연결해달라고 요청한다. IPv4 프로토콜을 이용할 경우 addrlen 인자의 값은 sizeof(sockaddr_in)이다. socket과 connect와 마찬가지로 가장 좋은 방법은 getaddrinfo를 사용하여 bind 함수에게 인자를 제공하는 것이다.

(5) listen 함수

클라이언트는 connection 요청을 개시하는 active한 개체이다.

서버는 클라이언트로부터 connection 요청을 기다리는 passive한 개체이다.

기본적으로 커널은 socket 함수에 의해 생성된 descriptor가 connection의 클라이언트 쪽 end point에 존재하는 active 소켓에 대응한다고 가정한다. 따라서 서버의 경우에는 서버에서 수행된 socket 함수에 의해 생성된 descriptor가 클라이언트의 active 소켓이 아니라 서버의 passive 소켓이라고 커널에게 알려주는 절차가 추가적으로 필요한데 이 일을 listen 함수가 수행해준다.

#include <sys/socket.h>

/* Returns: 0 if OK, −1 on error */
int listen(int sockfd, int backlog);

 

listen 함수는 sockfd를 active 소켓에서 listening 소켓으로 변환시킨다. 그러면 sockfd는 이제 클라이언트로부터 connection 요청을 수락할 수 있게 된다. backlog 인자는 커널이 connection 요청을 거절하기 전에 줄 세워야 하는 outstanding한 연결 요청의 수에 대한 정보를 제공한다. backlog의 정확한 의미는 TCP/IP의 이해를 요구하는데 이는 우리의 범위를 넘어선다. 일반적으로 1,024와 같은 큰 값을 설정한다.

(6) accept 함수

서버는 accept 함수를 호출함으로써 클라이언트로부터 connection 요청을 기다린다.

#include <sys/socket.h>

/* Returns: nonnegative connected descriptor if OK, −1 on error */
int accept(int listenfd, struct sockaddr *addr, int *addrlen);

 

accept 함수는 클라이언트로부터 연결 요청이 listenfd라는 listening discriptor에 도착할 때까지 기다린다. 그리고 addr에 클라이언트의 소켓 주소를 채워넣고, Unix I/O 함수를 이용하여 클라이언트와 소통하는 데 사용될 수 있는 connected descriptor를 반환한다.

 

listening descriptor와 connected descriptor 사이의 차이점은 학생들을 헷갈리게 할 수 있다.

 

listening descriptor

listening descriptor는 클라이언트의 connection 요청에 대한 end point로서 역할한다. listening descriptor는 한 번 생성되고나서 서버의 살아 있는 동안 계속 존재한다.

 

connected descriptor

connected descriptor는 클라이언트와 서버 사이에 수립된 connection의 end point이다. connected descriptor는 서버가 연결 요청을 받을 때마다 생성되고 클라이언트에게 서비스하는 동안에만 존재한다.

 

왜 listening descriptor와 connected descriptor 사이에 구분을 둘까? 이러한 구분은 많은 클라이언트 연결을 처리할 수 있는 동시성 서버 concurrent server를 만들 수 있게 해주기 때문이다. 예를 들어, 클라이언트로부터 서버의 listening descriptor에 connection 요청이 도착할 때마다 서버는 connected descriptor를 통해 클라이언트와 통신하는 새로운 프로세스를 fork할 수 있다.

 

위 사진은 listening descriptor와 connected descriptor의 역할을 간략하게 보여준다.

 

단계 1

서버는 accept 함수를 호출한다. accept 함수는 listening descriptor에 연결 요청이 도착하기를 기다린다. 이 descriptor를 descriptor 3이라고 부르겠다. descriptor 0-2는 표준 파일을 위해 배정되어 있다.


단계 2

클라이언트는 connect 함수를 호출한다. connect 함수는 listenfd에게 연결 요청을 보낸다.

 

단계 3

서버의 accept 함수는 새로운 connected descriptor인 connfd를 열고 (이 descriptor를 descriptor 4라고 부르겠다.) clientfd와 connfd 사이의 연결을 수립한다. 그리고 응용 프로그램에게 connfd를 반환한다.

클라이언트는 connect 함수로부터 반환된다. 이 지점부터, 클라이언트와 서버는 각각 clientfd와 connfd를 읽고 작성함으로써 데이터를 주고받을 수 있게 된다.


(7) 호스트와 서비스 변환

리눅스는 binary 형태의 소켓 주소 구조체와 문자열 형태의 호스트 이름, 호스트 주소, 서비스 이름, 포트 번호 사이의 변환을 위해 getaddrinfo와 getnameinfo라고 불리는 강력한 함수들을 제공한다. 소켓 인터페이스를 이용할 때 이 함수들을 사용하면 IP 프로토콜 버전에 독립적인 네트워크 프로그램을 작성할 수 있다.

<netdb.h>에 getaddrinfo, freeaddrinfo, gai_strerror 등의 함수와 struct addrinfo 등의 구조체가 정의되어 있습니다.

getaddrinfo 함수

getaddrinfo 함수는 문자열 형태의 호스트 네임, 호스트 주소, 서비스 이름, 포트 번호를 소켓 주소 구조체로 변환해준다.

 

getaddrinfo는 옛날에 사용됐던 gethostbynamegetservbyname 함수를 대체하는 현대의 함수이다. gethostbyname과 getservbyname 함수와 달리, getaddrinfo는 재진입이 가능하고 모든 프로토콜과 작업할 수 있다. getaddrinfo 함수가 재진입 가능(reentrant)하다는 것은, 동시에 여러 스레드에서 안전하게 호출될 수 있다는 뜻이다. 즉, 여러 스레드에서 getaddrinfo 함수를 호출해도 서로의 결과에 영향을 주지 않고, 충돌 없이 독립적으로 동작한다

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

/* Returns: 0 if OK, nonzero error code on error */
int getaddrinfo(const char *host, const char *service, 
		const struct addrinfo *hints, struct addrinfo **result);

/* Returns: nothing */
void freeaddrinfo(struct addrinfo *result);

/* Returns: error message */
const char *gai_strerror(int errcode);

 

호스트 host와 서비스 service(소켓 주소의 두 가지 구성 요소)가 주어지면, getaddrinfo는 addrinfo 구조체의 연결 리스트를 가리키는 포인터 result를 반환한다. 

 

 

클라이언트는 getaddrinfo를 호출하고 나서 호출 결과로 반환되는 addrinfo 연결 리스트를 방문하면서 addrinfo에 있는 소켓 주소로 연결을 시도한다. socket 함수와 connect 함수에 대한 호출이 성공하고 연결이 수립될 때까지.

서버도 socket 함수와 bind 함수에 대한 호출이 성공할 때까지 addrinfo 리스트에 있는 모든 소켓 주소를 시도한다. 그 결과로 descriptor는 유효한 소켓 주소에 바인딩된다.

메모리 누수를 피하기 위해서, 응용 프로그램은 반드시 마지막에 freeaddrinfo 함수를 호출함으로써 리스트를 free해주어야 한다.

struct addrinfo {
   int 		ai_flags;		/* Hints argument flags */
   int 		ai_family;     		/* First arg to socket function */
   int 		ai_socktype;  		/* Second arg to socket function */
   int 		ai_protocol;  		/* Third arg to socket function  */
   char 	*ai_canonname; 		/* Canonical hostname */
   size_t	ai_addrlen;		/* Size of ai_addir struct */
   struct 	sockaddr *ai_addr;  	/* Ptr to socket address structure */
   struct 	addrinfo *ai_next;  	/* Ptr to next item in linked list */
};

 

만약 getaddrinfo 함수가 nonzero error code를 반환한다면, 응용 프로그램은 코드를 메시지 문자열로 변환해주기 위해서 gai_strerror 함수를 호출할 수 있다. "Non-zero error code"란 프로그래밍에서 오류나 예외 상황을 나타내기 위해 반환되는 값 중, 0이 아닌 값을 의미한다. 일반적으로, 많은 함수나 프로그램은 0을 성공적인 실행을 의미하는 값으로 사용하고, 0이 아닌 값(즉, non-zero)이 반환되면 오류나 문제 상황을 나타낸다.


getaddrinfo 함수의 인자는 다음과 같다.

 

host (문자열)

  • 도메인 이름 또는
  • 숫자 주소 (예시: dotted-decimal IP 주소)

service (문자열)

  • 서비스 이름(예시: http) 또는
  • 10진수 포트 번호

호스트 네임을 주소로 변환하는 데 관심이 없다면, 호스트에 NULL을 설정할 수 있다.

service 이름을 포트번호로 변환하는 데 관심이 없다면, service 이름에 NULL을 설정할 수 있다.

 

hints

선택적인 인자이다. 자료형은 addrinfo 구조체이다. hints를 통해 getaddrinfo 함수가 반환하는 소켓 주소의 목록을 정교하게 조작할 수 있다.


addrinfogetaddrinfo 함수의 hints 인자로 전달될 때 이 구조체에는 오직 다음 필드만 설정될 수 있다.

 

ai_family

기본적으로, getaddrinfo 함수는 IPv4와 IPv6 소켓 주소를 모두 반환할 수 있다. 

ai_family를 AF_INET으로 설정하면 반환되는 리스트를 IPv4 주소로 제한하게 된다. 

AF_INET6으로 설정하면 반환하는 리스트를 IPv6 주소로 제한하게 된다.

 

ai_socktype

getaddrinfo 함수가 반환하는 addrinfo 구조체는 3가지 ai_socktype 필드 중 하나를 가진다. 하나는 연결을 위해, 하나는 데이터그램을 위해, 하나는 raw sockets을 위한 것이다. ai_socktype을 SOCK_STREAM으로 설정하면 리스트가 각각의 고유한 주소에 대해 최대 하나의 addrinfo 구조체를 가지도록 제한한다. addrinfo의 소켓 주소는 커넥션의 끝 점으로 사용될 수 있다.

 

ai_protocol

프로토콜 버전

 

ai_flag

기본 동작을 변경할 수 있는 비트 마스크이다.

  • AI_ADDRCONFIG 
    • connection[34]을 이용한다면 이 플래그를 사용하는 것이 권장된다.
    • getaddrinfo에게 로컬 호스트가 IPv4로 설정된 경우에만 IPv4 주소를 반환하도록 요청한다.
  • AI_CANONNAME
    • ai_canonname 필드의 디폴트 값은 NULL이다.
    • 만약 이 플래그가 설정되었다면, 이 플래그는 getaddrinfo에게 리스트에 있는 첫 번째 addrinfo 구조체의 ai_canonname 필드가 호스트의 표준 (공식) 이름을 가리키도록 지시한다.
  • AI_NUMERICSERV 
    • service 인자는 서비스 이름이거나 포트 번호일 수 있다.
    • 이 플래그는 service 인자가 포트 번호가 되도록 강제한다.
  • AI_PASSIVE 
    • getaddrinfo는 소켓 주소를 반환할 때 디폴트로, 클라이언트가 connect를 호출할 때 active sockets으로 사용할 수 있는 소켓 주소를 반환한다.
    • 이 플래그가 설정되면 getaddrinfo 함수가, 서버가 listening sockets으로 사용할 수 있는 소켓 주소를 반환하라고 지시한다. 이 경우에 host 인자는 NULL이어야 한다. 소켓 주소 구조체에 있는 주소 필드는 wildcard address일 것이다. wildcard address는 커널에게 이 서버가 이 서버에 들어오는 모든 IP 주소로의 요청을 수락할 것이라고 알려준다

다른 필드들은 0 (또는 NULL)로 설정되어야 한다. 구체적으로 말하자면 memset 함수를 이용하여 전체 구조체를 0으로 설정하고 일부 선택된 필드만 설정한다.

getaddrinfo 함수가 결과 리스트에 addrinfo 구조체를 생성할 때, getaddrinfo는 ai_flags를 제외한 모든 필드를 채운다.

 

ai_addr

소켓 주소

 

ai_addrlen

소켓 주소 구조체의 크기

 

ai_next

리스트에 있는 다음 addrinfo 구조체를 가리킴

 

getaddrinfo의 멋있는 측면 중 하나는 addrinfo 구조체에 있는 필드들이 어플리케이션 코드의 추가적인 조작 없이 소켓 인터페이스에 있는 함수들에게 직접 전달될 수 있다는 점이다. 이 강력한 속성은 특정 버전의 IP 프로토콜에 의존적이지 않은 클라이언트와 서버를 작성할 수 있게 해준다.

예를 들어, ai_family, ai_socktype, ai_protocol은 socket 함수에 직접 전달될 수 있다. 비슷하게, ai_addr과 ai_addrlen은 connect와 bind 함수에게 직접적으로 전달될 수 있다.

 

getnameinfo 함수

getnameinfo 함수는 getaddrinfo의 반대이다. getnameinfo 함수는 소켓 주소 구조체를 대응되는 호스트와 서비스 이름 문자열로 변환한다. 이 함수는 옛날에 사용되던 gethostbyaddr 함수와 getservbyport 함수의 현대판 대체 버전이다. 옛날 함수들과 달리 getnameinfo 함수는 프로토콜에 독립적이다.

#include <sys/socket.h>
#include <netdb.h>

/* Returns: 0 if OK, nonzero error code on error */
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                char *host, size_t hostlen,
                char *service, size_t servlen, int flags);

 

sa

salen 바이트 크기의 소켓 주소 구조체

 

host

hostlen 바이트 크기의 버퍼

 

service

servlen 바이트 크기의 버퍼

 

getnameinfo 함수는 소켓 주소 구조체 sa를 대응되는 호스트와 서비스 이름 문자열로 변환하고 이들을 호스트와 서비스 버퍼에 복사한다.

만약 getnameinfo가 nonzero 에러 코드를 반환한다면, 응용 애플리케이션은 gai_strerror 함수를 호출함으로써 에러 코드를 문자열로 변환할 수 있다.

만약 호스트 이름을 원하지 않는다면 호스트를 NULL로 설정하고 hostelen을 0으로 설정할 수 있다. service 필드도 마찬가지이다.


flags

비트 마스크로, 기본 행동을 변경한다. 

  • NI_NUMERICHOST
    • 기본적으로, getnameinfo 함수는 host에 있는 도메인 이름을 반환한다.
    • 이 플래그를 설정하면 숫자 주소를 반환한다.
  • NI_NUMERICSERV
    • 기본적으로 getnameinfo 함수는 /etc/services를 보고, 가능하다면 포트 번호 대신에 서비스 이름을 반환할 것이다.
    • 이 플래그를 설정하면 lookup을 스킵하고 포트 번호를 반환한다.

다음은 HOSTINFO라는 간단한 프로그램이다. 이 프로그램은 getaddrinfo와 getnameinfo 함수를 사용하여 도메인 이름과 그것과 연관된 IP 주소를 보여준다. 이는 NSLOOKUP 프로그램과 비슷하다.

#include "csapp.h"

int main(int argc, char **argv) {
    struct addrinfo *p, *listp, hints;
    char buf[MAXLINE];
    int rc, flags;
    
    if (argc!=2){
    	fprintf(stderr, "usage: %s <domain name>\n", argv[0]); 
        exit(0);
	}
    
    /* Get a list of addrinfo records */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;       /* IPv4 only */
    hints.ai_socktype = SOCK_STREAM; /* Connections only */
    if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
    	exit(1); 
    }
    
    /* Walk the list and display each IP address */
    flags = NI_NUMERICHOST; /* Display address string instead of domain name */
    for (p = listp; p; p = p->ai_next) {
        Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
		printf("%s\n", buf);
    }
    
    /* Clean up */
    Freeaddrinfo(listp);
    
    exit(0);
}

 

hints 구조체를 초기화하여 getaddrinfo 함수가 우리가 원하는 주소를 반환하도록 만든다.  getaddrinfo를 호출한 후에, 우리는 addrinfo 구조체의 리스트를 방문한다. 이때 각각의 소켓 주소를 dotted-decimal address로 변환하기 위해 getnameinfo 함수를 사용한다. 리스트를 방문한 뒤에, 우리는 freeaddrinfo 함수를 호출함으로써 이들을 free해준다. (이러한 간단한 프로그램에서 freeaddrinfo 함수를 호출하는 것은 필수적인 과정은 아니긴 하다.)

*getaddrinfo와 getnameinfo 함수는 inet_pton과 inet_ntop 함수를 각각 포함하고 있다. 그리고 이들 함수는 주소 형식에 독립적인 높은 레벨의 추상화를 제공한다.

 

(8) 소켓 인터페이스를 위한 도움 함수들

getaddrinfo 함수와 소켓 인터페이스를 처음 배울 때는 이 함수들에 압도되곤 한다. 이러한 함수들을 더 높은 레벨의 도움 함수들로 감싸는 것이 편리할 때가 있다. 이 wrapping 함수들은 open_clientfdopen_listenfd라고 불린다. 클라이언트와 서버는 서로 통신하고 싶을 때마다 이 함수들을 이용할 수 있다.

open_clientfd 함수

클라이언트는 open_clientfd 함수를 호출함으로써 서버와의 통신을 수립한다.

open_clientfd 함수는 hostname에서 실행 중이고 포트 번호 port에서 (클라이언트의) 연결 요청을 listening하고 있는 서버와의 연결을 수립한다. 이 함수는 Unix I/O 함수를 이용하여 입출력이 준비된 open socket descriptor를 반환한다.

 

다음은 open_clientfd 함수의 코드이다.

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;
   }

 

addrinfo 구조체의 리스트를 반환하는 getaddrinfo를 호출한다. addrinfo 리스트의 각각의 addrinfo는 hostname에서 실행 중이고 port에서 listening 중인 서버와의 연결을 수립하는 데 적절한 소켓 주소를 가리키고 있다. 우리는 socket과 connect로의 호출이 성공할 때까지 이 리스트를 순회하면서 각각의 리스트 항목을 차례대로 시도해본다. 

connect 함수가 실패하면, 우리는 다음 항목을 시도하기 전에 조심스럽게 socket descriptor를 닫는다. 만약 connect 함수가 성공하면, 우리는 리스트 메모리를 free하고 클라이언트에게 socket descriptor를 반환한다. socket descriptor를 받은 클라이언트는 서버와 통신하기 위해 즉시 Unix I/O를 사용하기 시작한다.

코드에 특정 IP 버전에 대한 의존성이 전혀 없다는 것을 확인해라. socket과 connect 함수에 대한 인자들은 getaddrinfo 함수에 의해 자동으로 생성되는데 이는 우리의 코드를 깨끗하고 용이하게 만들어준다.

open_listenfd 함수

서버는 open_listfd 함수를 호출함으로써 연결 요청을 받을 준비가 된 listening descriptor를 생성한다.

open_listenfd 함수는 포트 번호 port에서 연결 요청을 받을 준비가 된 listening descriptor를 반환한다. 

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;
}

 

open_clientfd와 비슷하다. 우리는 getaddrinfo 함수를 호출하고, socket과 bind 함수가 성공할 때까지 반환된 리스트를 순회한다.

서버가 즉시 종료하고, 재시작하고, 연결 요청 수락을 시작할 수 있도록 서버를 구성하기 위하여 setsockopt 함수를 사용한다. 기본적으로 재시작된 서버는 대략 30초 간 클라이언트로부터의 연결 요청을 거부할 것이다. 이는 디버깅을 심각하게 저해한다.

마침내, 우리는 listenfd를 listening descriptor로 변환하기 위하여 listen 함수를 호출하고, 호출자에게로 돌아간다. 만약 listen 함수가 실패했다면, 우리는 호출자에게 돌아가기 전에 descriptor를 닫아서 메모리 누수를 방지한다.