저번 글을 통해 TCP 소켓을 생성하는 방법에 대해 알아봤다. 이번 글에서는 소켓에 주소와 포트를 할당하는 방법을 알아보자.
서버는 TCP 소켓을 생성한 뒤에, 해당 소켓에 ip주소와 포트를 할당해야한다. 이는 특정 ip 주소와 포트를 목적지로 하는 패킷이 들어오면, 이 소켓을 통해 받겠다는 선언을 하는 것이다. 일반적으로 하나의 컴퓨터는 하나의 ip 주소를 가질텐데 왜 서버의 소켓에 ip주소를 할당해야할까? 라우터와 같은 컴퓨터는 여러 개의 ip 주소를 갖고 일반적인 컴퓨터라도 랜카드를 여러 개 부착하면 여러 개의 ip 주소를 가질 수 있다. 그렇기에 어느 ip주소를 목적지로 하는 패킷이, 어느 소켓으로 가야할지 결정지을 필요가 있는 것이다.
bind 함수 호출을 통해 주소와 포트를 할당할 수 있다. bind의 프로토타입은 아래와 같다.
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
sockfd는 주소와 포트를 할당할 소켓의 파일 디스크립터이다. 유닉스에서 소켓은 파일이기에 파일 디스크립터를 통해 소켓을 나타낸다. struct sockaddr가 주소와 포트를 담는 구조체이고 addrlen은 두 번째 인자로 전달된 구조체의 크기를 나타낸다.
sockaddr의 구조체를 살펴보자
struct sockaddr
{
sa_family_t sin_family; // 주소체계(Address Family)
char sa_data[14]; // 주소정보
}
주소 정보를 나타내는 sa_data 문자열에 ip 주소와 포트를 담고 남은 비트를 0으로 채워야한다. ip 주소와 포트를 담을 수 있을만큼의 공간보다 sa_data가 더 큰 공간을 갖는 이유는, 소켓은 IPv4 뿐만이 아니라 다른 주소체계도 사용할 수 있도록 만들어져있기에 다양한 주소체계를 지원할 수 있을만큼 큰 공간을 가져야하기 때문이다. sockaddr를 사용해서 TCP 소켓에 ip 주소와 포트를 할당하는 건 직관적이지 않기에 일반적으로 sockaddr_in 구조체를 사용한다.
struct sockaddr_in
{
sa_family_t sin_family; // 주소체계(Address Family)
uint16_t sin_port; // 16비트 TCP/UDP PORT번호
struct in_addr sin_addr; // 32비트 IP주소
char sin_zero[8]; // 사용되지 않음
}
주소체계(Address Family) | 의미 |
---|---|
AF_INET | IPv4 인터넷 프로토콜 |
AF_INET6 | IPv6 인터넷 프로토콜 |
AF_LOCAL | 로컬 통신을 위한 유닉스 프로토콜 |
우리는 IPv4 프로토콜을 사용할 것이기 때문에, sin_family에는 AF_INET을 넣으면 된다.
sin_port와 sin_addr에는 각각 소켓에 할당할 포트와 주소를 담아야하는데, 주의할 점은 '네트워크 바이트 순서'로 저장해야 한다는 것이다. '네트워크 바이트 순서'는 빅 엔디안을 의미한다. 이렇게 '네트워크 바이트 순서'가 존재하는 이유는 통신하는 시스템에 따라서 바이트 순서가 다를 수 있고 이로 인해 송수신자가 서로 데이터를 다르게 해석할 수 있기 때문이다. 빅 엔디안으로 통신하면, 송수신자의 바이트 순서가 다름에 따라 생기는 오류를 방지할 수 있다.
sin_zero는 구조체 sockaddr와 sockaddr_in이 동일한 크기를 갖도록 만들기위해 존재하는 멤버다. 0으로 채워넣어주면 된다.
sockaddr_in 구조체를 bind 함수의 두 번째 인자에 전달해야하는데, bind 함수는 sockaddr 구조체를 가리키는 포인터를 받기 때문에, 이를 아래와 같이 형변환 해주면 된다.
bind(sock_fd, (struct sockaddr *)(&my_sockaddr_in), sizeof(my_sockaddr_in));
다음의 함수를 사용해서 숫자를 빅 엔디안으로 바꿀 수 있다.
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
위에서 n은 네트워크를 h는 호스트를 s와 l은 각각 short와 long을 나타낸다. htons는 호스트의 바이트 순서를 따르는 short 자료형의 데이터를 네트워크 바이트 순서를 따르도록 바꾸는 함수이다. 나머지는 쉽게 추측할 수 있다.
문자열 주소를 정수 주소로 그리고 정수 주소를 문자열 주소로 바꾸는 함수는 아래와 같다.
int inet_aton(const char *string, struct in_addr *addr);
char *inet_ntoa(struct in_addr *addr);
inet은 인터넷, a는 array 그리고 n은 number를 의미한다. inet_ntoa 함수의 경우 char *를 반환한다는 것에 주목하자. 사용자가 동적할당을 할 것을 요구하지 않는다. inet_ntoa가 반환하는 문자열을 inet_ntoa를 추가적으로 호출한 뒤에 접근하면 오류가 발생할 수 있다. 기존의 문자열이 덮어쓰여지기 때문이다. 즉 inet_ntoa는 매 호출 때 마다 같은 공간에 데이터를 작성한다. 그렇기에 inet_ntoa가 반환하는 문자열을 복사해서 다른 곳에 저장하여 쓰는 것을 추천한다.