바이트 정렬(byte ordering): 메모리에 데이터를 저장할 때 바이트 순서를 나타내는 용어로 빅 엔디안과 리틀 엔디안 방식이 있다. 이는 CPU와 운영체제에 따라 다르다.
아래 그림은 16진수 0x12345678을 메모리 0x1000번지에 저장할 때 두 바이트 정렬 방식의 차이를 보여준다. 빅 엔디안은 최상위 바이트로부터, 리틀 엔디안은 최하위 바이트부터 차례로 저장함을 할 수 있다.
(3-1 그림)
파일에 데이터를 저장하고 읽어오는 경우나 네트워크를 통해 데이터를 송신하고 수신하는 경우에는 바이트 정렬 방식에 유의해야 한다.
프로토콜 구현과 응용 프로그램 데이터라는 두 가지 관점에서 바이트 정렬 방식을 고려해야하는 경우를 살펴볼 것이다.
(3-2 (a) 그림)
위 그림은 IP 주소의 바이트 정렬 방식에 따른 문제점을 보인다. 호스트가 보낸 패킷의 IP 헤더에는 IP 주소가 포함되어 있다. 이 패킷을 라우터가 받으면 IP 주소를 참조하여 다음 위치에 있는 라우터에 보낸다. 이때 호스트와 라우터가 IP 주소의 바이트 정렬 방식을 약속하지 않으면 IP 주소 해석이 달라져 라우팅에 문제가 생길 수 있다.
(3-2 (b) 그림)
다음은 포트 번호의 바이트 정렬 방식에 따른 문제점을 보인다. 두 호스트가 포트 번호의 바이트 정렬 방식을 약속하지 않으면 포트 번호 해성이 달라져 데이터가 잘못된 목적지 프로세스에 전달될 수 있다.
이런 문제는 시스템이 사용하는 바이트 정렬 방식(호스트 바이트 정렬(host byre ordering))이 통일 되어 있지 않아서 발생한다. 이를 해결하기 위해 IP 주소와 포트 번호의 바이트 정렬 방식을 빅 엔디안으로 통일해 사용한다. 네트워크 용어로는 빅 엔디안을 네트워크 바이트 정렬(network byte ordering)이라고 한다.
(3-2 (c) 그림)
그림은 데이터의 바이트 정렬 방식에 따른 문제점을 보인다. 두 호스트가 주고받는 데이터에 대한 바이트 정렬 방식을 약속하지 않으면 데이터 해석 문제가 발생할 수 있다. 서버와 클라이언트를 같이 제작하는 경우라면 바이트 정렬 방식을 둘 중 하나로 통일해서 구현하면 되는데, 대개는 네트워크 바이트 정렬(빅 에디안) 방식을 사용한다. 클라이언트만 제작한다면 기존 서버가 정한 바이트 정렬 방식을 따르면 된다.
응용 프로그램이 바이트 정렬 방식을 편하게 변환할 수 있도록 다음과 같은 윈속 함수가 제공된다.
u_short htons(u_short hostshort); // host-to-network-short
u_long htonl(u_long hostlong); // host-to-network-long
u_short ntohs(u_short netshort); // network-to-host-short
u_long ntohl(u_long netlong); // network-to-host-long
(3-3, 3-4 그림)
일반적으로 htons()
, htonls()
함수는 응용 프로그램이 소켓 함수에 데이터를 넘겨주기 전에 호출하며, ntohs()
, ntohl()
함수는 소켓 함수가 결과로 리턴한 데이터를 으용 프로그램이 출력 등의 목적으로 사용하기 전에 호출된다.
윈속 2.x 에서는 바이트 정렬을 위해 다음과 같은 확장 함수도 지원한다. 첫 번째 인자로 소켓 디스크립터를 사용하고, 변환된 결과를 리턴 값이 아닌 세 번째 인자로 전달하는 차이가 있다.
int WSAHtons(SOCKET s, u_short hostshort, u_short *lpnetshort);
int WSAHtonl(SOCKET s, u_long hostshort, u_long *lpnetlong);
int WSANtohs(SOCKET s, u_short netshort, u_short *lphostshort);
int WSANtohl(SOCKET s, u_long netlong, u_short *lphostlong);
많이 사용되는 TCP/IP에서 사용할 SOCKADDR_IN
/SOCKADDR_IN6
구조체의 경우 아래와 같은 바이트 정렬 방식을 따른다.
(3-5 그림)
바이트 정렬 함수를 사용하는 간단한 예제를 아래 소개할 것이다.
#pargma comment(lib, "ws2_32")
#include <winsock2.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return 1;
u_short x1 = 0x1234;
u_long y1 = 0x12345678;
u_short x2;
u_long y2;
// 호스트 바이트 → 네트워크 바이트
printf("[호스트 바이트 -> 네트워크 바이트]\n");
printf("0x%x -> 0x%x\n", x1, x2 = htons(x1));
printf("0x%x -> 0x%x\n", y1, y2 = htonl(y1));
// 네트워크 바이트 → 호스트 바이트
printf("[네트워크 바이트 -> 호스트 바이트]\n");
printf("0x%x -> 0x%x\n", x2, ntohs(x2));
printf("0x%x -> 0x%x\n", y2, ntohl(y2));
// 잘못된 사용 예
printf("[잘못된 사용 예]\n");
printf("0x%x -> 0x%x\n", x1, htonl(x1));
WSACleanup()
return 0;
}
실행 결과는 아래와 같다.
참고 자료
김성우 저, "TCP/IP 윈도우 소켓 프로그래밍", 한빛아카데미, 2018