웹 서버 구현기 2-1 TCP/IP: 서버 소켓에 주소와 포트 할당하기

Plato·2023년 6월 7일
0

웹서버 구현기

목록 보기
7/11

서론

저번 글을 통해 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 구조체를 사용한다.

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_INETIPv4 인터넷 프로토콜
AF_INET6IPv6 인터넷 프로토콜
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가 반환하는 문자열을 복사해서 다른 곳에 저장하여 쓰는 것을 추천한다.

0개의 댓글