3-2. 주소정보를 담는 구조체 sockaddr_in [TCP/IP][C][LINUX]
앞서 우리는 클라이언트와 서버를 간략하게 짜보면서 sockaddr_in이라는 구조체를 사용했습니다. 이는 주소정보를 담기 위한 구조체입니다.
이 구조체는 다음과 같이 정의되어 있습니다.
struct sockaddr_in{
sa_family_t sin_family; //주소 체계
uint16_t sin_port; //16비트 TCP/UDP PORT번호
struct in_addr sin_addr; //32비트 IP주소
char sin_zero[8]; //사용되지 않음(단, 0으로 채워줘야 함)
}
//위에서 사용된 struct in_addr은 다음과 같습니다.
struct in_addr{
in_addr_t s_addr; //32비트 IPv4 인터넷 주소
}
sin_family는 주소체계를 담습니다. 프로토콜 체계마다 적용되는 주소체계가 다르기 때문인데, IPv4는 AF_INET, IPv6은 AF_INET6 을 저장하면 됩니다.
sin_port는 16비트의 PORT번호를 저장합니다. 저장할 때 네트워크 바이트 순서로 저장해야하는데, CPU에 따라 바이트 저장 순서가 다르기 때문입니다. 상위 바이트의 값을 작은 번지수에 저장하는 방식을 빅 엔디안, 큰 번지수에 저장하는 방식을 리틀 엔디안이라 합니다.
예를 들어, 4바이트 자료형에 0x12345678을 저장할 때, 빅엔디안은 (0x12,0x34,0x56,0x78) 리틀엔디안은 그 역순으로 저장하게 됩니다. 위와 같은 CPU의 데이터 저장방식을 호스트 바이트 순서라고 합니다.
네트워크 바이트 순서는 위 두 방식의 충돌을 방지하기 위한 수단으로, 바이트 저장 순서를 빅엔디안으로 통일합니다. C에서는 네트워크 바이트 순서 - 호스트 바이트 순서를 변환하는 함수를 제공합니다.
unsigned short htons(unsigned short);
// host to network (PORT) unsigned short ntohs(unsigned short); // network to host (PORT) unsigned long htonl(unsigned long); // host to network (IP) unsigned long ntohl(unsigned long); // network to host (IP)
sin_addr은 32비트 IP주소정보를 저장합니다. 이 또한 네트워크 바이트 순서로 저장해야 합니다.
문자열로 받아온 ***.***.***.*** 형태의 IP를 32비트의 정수형태로 바꿔줘야 하는데, 이 또한 string을 32bit 정수형으로 바꿔주고 동시에 네트워크 바이트 순서로 바꿔주기까지 하는 함수가 있습니다. 또한 유효하지 못한 IP주소에 대한 오류검출을 해줍니다.
#include<arpa/inet.h>
in_addr_t inet_addr(const char * string);
//success: 32bit 정수 fail: INADDR_NONE
이어서 소개할 함수는 inet_addr와 기능은 같지만 더불어 in_addr에 저장까지 시켜줍니다.
#include<arpa/inet.h>
int inet_aton(const char * string, struct in_addr * addr)
//success: 1 fail: 0
// string: IP주소 addr: 변환된 정보를 저장할 in_addr 구조체 변수의 주소값
아래는 네트워크 바이트 순서로 정렬된 32bit 정수형 IP주소를 문자열로 바꿔줍니다. 다만 char형 포인터를 반환하게 되는데 이는 이미 문자열의 주소값이 메모리공간에 저장되었다는 뜻이고 따로 메모리를 할당하지 않으므로 호출시마다 해당 주소값의 문자열이 바뀌게 됩니다. 따라서 strcpy를 통해 따로 할당을 해줘야합니다.
#include<arpa/inet.h>
char * inet_ntoa(struct in_addr adr);
// success: 문자열의 주소값 fail: -1
sin_zero는 sockaddr_in의 크기를 sockaddr과 일치시키기 위해 삽입된 멤버입니다.(형변환을 위함) 0으로 채워져있어야만 합니다.
sockaddr 구조체 형태는 다음과 같습니다.
struct sockaddr{
sa_family_t sin_family; //주소체계
char sa_data[14]; //주소정보
}
14바이트의 sa_data에는 IP주소와 PORT번호가 포함되고 남는 공간은 0으로 채워져야 합니다. 이는 주소정보를 담기엔 불편한 요구이므로, sockaddr_in을 채운 후 sockaddr으로 형변환을 하는 방법으로 사용하게 됩니다.
sockaddr_in의 sin_family를 제외한 크기는 14바이트입니다. sin_port가 2바이트, sin_addr의 크기가 4바이트, 그리고 sin_zero의 크기가 8바이트기 때문입니다.
sockaddr 또한 sin_family를 제외한 크기는 14바이트입니다. sockaddr_in의 sin_zero는 0으로 채워져 있으므로, 형변환을 하게 되면 sockaddr의 sa_data에 port와 IP주소가 먼저 채워지고 나머지가 0으로 채워지게 됩니다.
INADDR_ANY는 서버 소켓 생성에서 많이 사용하는 상수로, 자신의 IP주소를 갖고 있습니다. 컴퓨터 내에 IP가 2개 이상일 때, INADDR_ANY를 사용하면 PORT번호만 같다면 갖고있는 모든 IP에서 수신할 수 있습니다.
소켓에 인터넷 주소를 할당할 때는 bind 함수를 사용하게 되는데, 함수의 구조는 다음과 같습니다.
#include<sys/socket.h>
int bind(int sockfd, strucvt sockaddr *myaddr, socklen_t addrlen);
// success:0 fail:-1 // sockfd : 주소정보를 할당할 소켓의 파일 디스크럽터
// myaddr : 할당하고자 하는 주소 정보를 지닌 구조체 변수의 주소 값
// addrlen : 두 번째 인자로 전달된 구조체 변수의 길이정보
위에서 다룬 내용을 토대로 서버 프로그램에서 흔히 등장하는 서버 소켓 초기화 과정을 정리하고 마칩니다.
int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port = "9190";
// TCP 소켓을 만듭니다.
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
// serv_addr를 0으로 초기화합니다. (이로써 sin_zero는 0으로 초기화되었습니다.)
memset(&serv_addr, 0, sizeof(serv_addr));
// 주소체계는 IPv4를 따릅니다.
serv_addr.sin_family=AF_INET;
// INADDR_ANY로 자신의 32bit ip정보를 가져오고
//htonl 함수를 통해 네트워크 바이트 순서로 바꿔줍니다.
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//htons 함수를 통해 port를 네트워크 바이트 순서로 바꿔줍니다. (atoi는 char*를 int로 바꿔줍니다)
serv_addr.sin_port=htons(atoi(serv_port));
// bind의 두 번째 인자는 sockaddr*를 요구하므로, serv_addr의 주소값을 형변환 시켜줍니다.
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));