III : 소켓 주소 구조체 다루기
TCP/IP 윈도우 소켓 프로그래밍 을 읽고 정리한 문서입니다 ;)
소켓 주소 구조체
소켓 주소 구조체는 네트워크 프로그램에서 필요한 주소 정보를 담고 있는 구조체이다.
기본이 되는 것은 SOCKADDR
구조체이며, 정의는 다음과 같다.
typedef struct sockaddr {
u_short sa_family;
char sa_data[14];
} SOCKADDR;
sa_family
는 주소 체계를 나타내는 16비트 정수 값으로서, AF_INET
또는 AF_INET6
등이 될 수 있다.
**sa_data**
는 해당 주소 체계에서 사용할 주소 정보를 담게 된다. TCP/IP 환경이라면 IP주소와 포트를 담게 된다.
실제 프로그래밍에서는 별도의 소켓 구조체를 사용하게 된다. TCP/IP에서는 SOCKADDR_IN
또는 **SOCKADDR_IN6**
구조체를 사용한다.
typedef struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
} SOCKADDR_IN;
typedef struct sockaddr_in6 {
short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo;
struct in6_addr sin6_addr;
u_long sin6_scope_id;
} SOCKADDR_IN6;
typedef struct in_addr {
union {
struct { u_char s_b1, s_b2, s_b3, s_b4; } S_un_b;
struct { u_short s_w1, s_w2; } S_un_w;
u_long S_addr;
} S_un;
#define s_addr S_un.S_addr
} IN_ADDR;
소켓 주소 구조체는 크기가 크기 때문에 소켓 함수로 전달할 때에는 당연히 주소값을 사용해야 하며, 이 때 SOCKADDR *
형으로 변환하여야 한다. 또한, 프로토콜에 따라 구조체의 크기가 다르므로 구조체의 크기 또한 sizeof
연산자를 통해 같이 전달하게 된다.
바이트 정렬 함수
바이트 정렬은 메모리에 데이터를 저장할 때 바이트 순서를 나타내는 것으로, 빅 엔디안과 리틀 엔디안 두 종류가 존재한다.
빅 엔디안은 데이터를 최상위 바이트부터 차례로 저장하는 방식이며, 리틀 엔디안은 데이터를 최하위 바이트부터 차례로 저장하는 방식이다.
예를 들어, 16진수 0x12345678
을 저장한다 했을 때 다음과 같이 저장되게 된다.
정렬 방식 | 0x1000 | 0x1001 | 0x1002 | 0x1003 |
---|---|---|---|---|
빅엔디안 | 0x12 | 0x34 | 0x56 | 0x78 |
리틀엔디안 | 0x78 | 0x56 | 0x34 | 0x12 |
만일 네트워크 통신에 있어서 이 바이트 정렬을 고려하지 않으면 다음 문제가 발생하게 된다.
- IP주소, 포트 번호와 같이 프로토콜 구현을 위해 필요한 정보
- 호스트와 라우터가 서로 바이트 정렬 방식이 다르다면, IP 주소 해석이 다르게 되어 올바른 목적지로 향할 수 없다.
- 호스트와 호스트가 포트 해석 방식이 다르다면, 올바른 소켓 프로세스로 패킷이 전달하지 못할 것이다.
- 따라서, 네트워크의 IP 주소와 포트 번호의 바이트 정렬 방식은 빅 엔디안으로서, 이를 네트워크 바이트 정렬이라고 부르며 사용한다.
- 응용 프로그램이 주고받는 데이터
- 서버와 클라이언트 둘 다 개발하는 상황이라면, 빅 엔디안이나 리틀 엔디안으로 둘을 통일시키면 되지만, 일반적으로는 빅 엔디안을 사용한다.
- 클라이언트만 개발하는 상황이라면 서버가 사용하는 방식에 따라 적절한 변환을 거쳐야 한다.
이러한 바이트 변환을 편리하게 할 수 있도록 윈속에서는 함수를 제공하는데, 이들은 다음과 같다.
u_short htons(u_short hostshort); // host short to net short
u_long htonl(u_long hostlong); // host long to net long
u_short ntohs(u_short netshort); // net short to host short
u_long ntohl(u_long hostlong); // net long to host long
윈속 2.X 버전대에서는 바이트 정렬을 위한 확장 함수를 지원하는데, 이는 out 파라미터를 사용한다.
int WSAHtons(SOCKET s, u_short hostshort, u_short *lpnetshort);
int WSAHtonl(SOCKET s, u_long hostlong, u_long *lpnetlong);
int WSANtohs(SOCKET s, u_short netshort, u_short *lphostshort);
int WSANtohl(SOCKET s, u_long netlong, u_long *lphostlong);
IP주소 변환 함수
응용 프로그램은 IP 주소 문자열을 IP주소 체계로 변환해야할 상황이 생길 수 있다. 이 또한 윈속에서 제공하는 함수를 사용하면 쉽게 변환이 가능하다.
unsigned long inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
// 이 또한 WSA 를 접두어로 하는 주소 변환 함수가 있다.
int WSAStringToAddress(...);
int WSAAddressToString(...);
이들을 사용하는 실제 사용 예시를 살펴보면 다음과 같다.
SOCKADDR_IN addr;
ZeroMemory(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("123.123.123.123");
addr.sin_port = htons(9000);
SocketFunc(..., (SOCKADDR *)&addr, sizeof(addr), ...);
printf("IP 주소 = %s, 포트 번호 = %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
이 때 inet_addr
함수는 이미 네트워크 바이트 정렬된 IP 주소를 리턴한다는 것을 주의해야 한다.
도메인 이름 시스템과 이름 변환 함수
응용프로그램은 도메인을 이용해 통신하기 위해서는 제일 먼저 도메인 이름을 먼저 IP 주소로 변환해야 한다. 윈도우 소켓에는 DNS 리졸버가 존재하기 때문에 윈속은 도메인 IP 변환 함수를 제공한다.
struct hostent *gethostbyname(const char *name); // 도메인주소->IP
struct hostent *gethostbyaddr( // IP->도메인주소
const char *addr, // 네트워크 바이트 정렬된 IP주소
int len, // addr의 길이
int type // 주소체계 (AF_INET 등)
);
gethostbyname
함수나 gethostbyaddr
함수는 모두 hostent
형 구조체를 리턴한다.
typedef struct hostent {
char *h_name; // 공식 도메인 이름
char **h_aliases; // 도메인이 가지는 여러 별명 배열
short h_addrtype; // 주소 체계
short h_length; // IP주소의 길이
char **h_addr_list; // 호스트가 가지는 여러 IP 주소 배열
#define h_addr h_addr_list[0] // 첫 번째 IP 주소 접근 단축
} HOSTENT;
이를 이용해 실제로 도메인 이름과 IPv4 주소를 상호 변환하는 사용자 정의 함수를 작성하면 다음과 같다.
BOOL GetIpAddr(const char *domain, IN_ADDR *addr) { // 도메인 이름으로 IP 얻기
HOSTENT *hostEnt = gethostbyname(domain);
if (hostEnt == NULL) {
// handle error
return false;
}
if (hostEnt->h_addrtype != AF_INET) {
// handle error
return flase;
}
memcpy(addr, hostEnt->h_addr, hostEnt->h_length);
return true;
}
BOOL GetDomainName(IN_ADDR inAddr, char *name, int length) { // IP로 도메인 이름 얻기
HOSTENT *hostEnt = gethostbyaddr((char*)&inAddr, sizeof(inaddr), AF_INET);
if (hostEnt == NULL) {
// handle error
return false;
}
if (hostEnt->h_addrtype != AF_INET) {
// handle error
return false;
}
strncpy(name, hostEnt->h_name, length);
return true;
}
요약
- 소켓 주소 구조체
- 네트워크 프로그램에서 필요한 주소 정보를 담고 있는 구조체
- 다양한 소켓 함수의 인자로 사용된다.
SOCKADDR
,SOCKADDR_IN
,SOCKADDR_IN6
등 다양한 구조체 형식을 가진다.
- IP주소를 저장하기 위한 구조체
IN_ADDR
,IN_ADDR6
이 각각 IPv4, IPv6 주소를 저장한다.
- 바이트 정렬 방식
- 빅 엔디안 : 데이터를 최상위 비트부터 순서대로 저장한다. 네트워크의 기준이다.
- 리틀 엔디안 : 데이터를 최하위 비트부터 순서대로 저장한다.
- 바이트 정렬 변환 함수
htons()
,htonl()
,WSAHtons()
,WSAHtonl()
: 호스트 바이트 정렬 → 네트워크 바이트 정렬ntohs()
,ntohl()
,WSANtohs()
,WSANtohl()
: 네트워크 바이트 정렬 → 호스트 바이트 정렬
- IPv4 주소 변환 함수
inet_addr()
: 문자열에서 32비트 숫자로 변환한다.inet_ntoa()
: 32비트 숫자에서 문자열로 변환한다.WSAStringToAddress()
: 문자열에서 32비트 숫자로 변환하며, IPv6을 지원한다.WSAAddressToString()
: 32비트 숫자에서 문자열로 변환하며, IPv6을 지원한다.
- 도메인 이름과 IP체계 변환 함수
gethostbyname()
: 도메인 이름으로부터 IP 주소를 찾는다.gethostbyaddr()
: IP 주소로부터 도메인 이름을 찾는다.
'공부한 이야기 > 윈도우 소켓 프로그래밍' 카테고리의 다른 글
VI : 멀티스레드 (0) | 2023.04.29 |
---|---|
V : 데이터 전송하기 (0) | 2023.04.29 |
IV : TCP 서버-클라이언트 (0) | 2023.04.29 |
II : 윈도우 소켓 시작하기 (0) | 2023.04.29 |
I : 네트워크 소켓 프로그래밍 (0) | 2023.04.29 |