2015. 2. 11.

[Windows Network Programming]소켓 주소 구조체(Socket address structures)와 바이트 정렬(Byte Ordering)

 윈속, 소켓에 대한 대략적인 소개를 했다. socket() 함수를 통해 프로토콜까지 지정하였다. 하지만 구체적으로 IP주소, 포트번호 등에 대해서는 아직 언급하지 않았다. 이에 대한 정보는 소켓 주소 구조체에 저장된다.


소켓 주소 구조체(Socket address structures)

 네트워크 프로그램에서 필요로 하는 주소 정보를 담고 있는 구조체이다.

  • 주소 체계: TCP/IP 프로토콜을 사용한다면, 이 값은 AF_INET이 된다. 주소 체계는 윈속과 소켓을 설명하면서 이미 언급한 내용이다.
  • 주소 정보: 해당 주소 체계에서 사용하는 주소 정보를 담고 있는 배열이다. TCP/IP 프로토콜를 사용한다면 IP 주소와 포트 번호가 저장된다.
 실제 프로그래밍에서는 SOCKADDR 구조체를 사용하기보다는 사용할 프로토콜에 맞는 소켓 주소 구조체를 사용한다. 다음은 그 예시를 보여준다.
  • 기반 소켓 주소 구조체: SOCKADDR
  • TCP/IP: SOCKADDR_IN
  • IrDA: SOCKADDR_IRDA
그럼 우리가 가장 자주 사용할 TCP/IP의 소켓 주소 구조체인 SOCKADDR_IN에 대해서 알아보자.


SOCKADDR_IN

 이 구조체는 위에서 설명했듯이 TCP/IP의 소켓 주소 구조체이다. 구조체의 정의를 코드로 살펴보자.

  • 주소 체계: 항상 AF_INET값을 사용한다.
  • 포트 번호
  • IP 주소: in_addr 구조체를 사용한다.
  • sin_zero[8]: 실제 사용하지 않아서 0으로 채운다.
이 내용을 그림으로 표현하면 다음과 같다.
 주소 체계, 포트 번호 sin_zero[8]는 주석 그대로 받아들이면 된다. 좀더 살펴볼 것은 in_addr 구조체이다. 이 구조체는 IP 주소를 저장하기 위해 정의되어 있다. 
 공용체(union)가 나왔다. 링크를 참조해서 이해하도록 하자. 공용체를 사용하면 동일한 메모리 영역을 in_addr의 정의에서처럼 4개의 unsigned char 변수로 구성된 구조체, 2개의 unsigned short 변수로 구성된 구조체, 1개의 unsigned long 변수로 구성된 구조체 단위로 접근할 수 있다. 보통은 unsigned long 단위로 접근한다. 그래서 매크로로 재정의해서 사용하기 변하게 해두었다.

실제 소켓 주소 구조체를 사용하는 방법은 다음과 같다.
 코드 중간에 inet_addr() 함수, htons() 함수는 조금 후에 설명할 것이다. 소켓 주소 구조체를 초기화하는 형태만 보고 넘어가자. sin_zero[8]은 zeromemory()  함수를 통해 0으로 초기화되었다.


바이트 정렬 함수

 바이트 정렬(Byte Ordering)이란 메모리에 데이터를 저장할 때의 바이트 순서를 가리키는 용어로, 빅 엔디안(Big-Endian)과 리틀 엔디안(Little Endian) 방식이 있다.

  • 빅 엔디안: 최상위 바이트(MSB, Most Significant Byte)부터 차례대로 저장하는 방식
  • 리틀 엔디안: 최하위 바이트(LSB, Least Significant Byte)부터 차례대로 저장하는 방식

  이 그림은 레지스터에 0A0B0C0D 값이 메모리에 어떻게 저장되어 있는지를 나타내고 있다. 이 값은 16진수로 표현된 4byte 크기의 값이다. 이 값의 MSB는 0A이고, LSB는 0D이다. MSB는 어떤 값이 가지는 값 중에 가장 최상위 바이트의 값을 말한다. 0A0B0C0D에서 최상위 바이트 값은 0A이다. LSB는 최하위 바이트 값으로 0D를 말한다. 빅 엔디안은 MSB부터 차례대로 메모리에 저장하는 방식이다. 그러므로 0A, 0B, 0C, 0D 순서로 메모리 영역에 차례대로 저장된다. 바이트 단위로 저장되는 것을 명심하도록 하자. 리틀 엔디안은 LSB부터 차례대로 메모리에 저장하는 방식이다. 그러므로 0D, 0D, 0B, 0A 순서로 메모리 영역에 저장된다.



 이 그림도 마찬가지 사실을 말해준다. 8byte 크기의 데이터 1122334455667788을 메모리에 저장하고자 할때, 빅 엔디안 방식과 리틀 엔디안 방식으로 각각 어떻게 처리하는지 보여준다. Byte 0 ~ Byte 7의 의미는 해당 바이트의 데이터가 얼마나 큰가를 나타낸다. 예를 들면 Byte 7에 해당하는 11이라는 값은 2^60 +  2^56의 값을 가지는 숫자이다. 반면 Byte 0에 해당하는 88이라는 값은 2^7 + 2^3의 값을 가지는 숫자이다. 그래서 Byte 7은 데이터 1122334455667788에서 가장 높은 자리 숫자의 바이트를 의미하며, Byte 0은 가장 낮은 자리 숫자의 바이트를 의미한다. 이는 다시 말해 해당 바이트가 원래 나타내는 숫자의 크기로 볼 수 있다. 바이트 단위로 표시된 숫자를 원래 데이터로 변환할 때 적용되는 가중치라고 생각할 수 있다. 다음 그림이 바로 MSB와 LSB의 가중치에 대한 예이다. MSB의 1은 2^7에 해당하는 bit이고, LSB의 1은 2^0에 해당하는 bit이다. 같은 1이라는 표현이라고 실제로 의미하는 숫자의 크기, 즉 가중치는 다르다.(이 그림에서는 MSB, LSB의 B가 bit를 의미하고 있다)

 장황한 MSB, LSB와 빅 엔디안, 리틀 엔디안에 대한 설명을 이 정도로만 하고, 이제 바이트 정렬이 네트워크와 무슨 상관이 있는지 알아보자.


바이트 정렬을 고려해야 하는 경우

 파일에 데이터를 저장하고 읽는 경우, 네트워크를 통해 데이터를 전송하는 경우 등에 바이트 정렬에 유의해야 한다. 각 시스템이 빅 엔디안 방식인지 리틀 엔디안 방식인지에 따라 데이터를 저장하고 해석하는 방식이 완전히 달리지기 때문이다. 만약 A라는 시스템은 빅 엔디안 방식을 사용하고, B라는 시스템은 리틀 엔디안 방식을 사용한다고 가정하자. A가 네트워크를 통해 B에게 0x11223344라는 데이터를 전송하려고 한다. A는 빅 엔디안을 따르기 때문에 자신의 데이터를 그대로 보낼 것이다. 하지만 이 데이터를 받은 B의 경우 리틀 엔디안을 따르기 때문에 0x44332211로 해석할 것이다. 동일한 데이터라도 바이트 정렬에 따라 전혀 다른 데이터가 될 수 있다. 그러므로 어떤 방식으로 바이트 정렬을 사용할지 정하고 고려해야 한다.

  • IP 주소, 포트 번호의 바이트 정렬 방식은 빅 엔디안 방식으로 통일하여 사용한다.
  • 네트워크에서는 빅 엔디안을 네트워크 바이트 정렬(Network Byte Ordering)이라고 부른다. 시스템이 사용하는 고유한 바이트 정렬 방식을 호스트 바이트 정렬(Host Byte Ordering)이다.

바이트 정렬을 위한 유틸리티 함수
 각 함수의 기능은 주석으로 요약해두었다. htons() 함수는 host-to-network-short라는 의미로 호스트 바이트 정렬로 저장된 값을 입력으로 받아서 네트워크 바이트 정렬로 변환한 값을 반환한다. ntohs() 함수는 network-to-host-short라는 의미로 네트워크 바이트 정렬로 저장된 값을 입력으로 받아서 호스트 바이트 정렬로 변환한 값을 반환한다.


 만약 호스트가 리틀 엔디안으로 바이트 정렬되어 있다면, ntohs() 함수를 통해서 바이트 정렬이 바뀐다. 네트워트 바이트 정렬은 빅 엔디안 방식을 다르고, 호스트가 리틀 엔디안을 따르기 때문에 변환이 이루어진다. htons() 함수를 호출하면 어떻게 되겠는가? 호스트의 리틀 엔디안 바이트 정렬이 네트워크 바이트 정렬인 빅 엔디안으로 변환될 것이다.

 호스트가 빅 엔디안일 경우를 생각해보자. 호스트도 빅 엔디안이고, 네트워크고 빅 엔디안이다. ntohs() 함수를 호출해도 빅 엔디안에서 빅 엔디안으로 변환되므로 바이트 정렬에 변화가 없다. htons() 함수를 호출해도 결과는 마찬가지이다.

 특이한 점은 SOCKADDR_IN 구조체는 호스트 바이트 정렬과 네트워크 바이트 정렬 모두 사용한다.
  • sin_family: 호스트 바이트 정렬
  • sin_port, sin_addr: 네트워크 바이트 정렬
그러므로 SOCKADDR_IN 구조체를 초기화할 때는 바이트 정렬 함수를 사용해서 초기화해주어야 한다.
 앞에 등장한 소켓 주소 구조체 초기화 코드이다. addr.sin_port 변수를 초기화할 때, htons() 함수를 호출하였음을 확인할 수 있다. sin_port는 네트워크 바이트 정렬을 따른다. 그러므로 호스트 바이트 정렬을 네트워크 바이트 정렬로 변환해주었다.


IP 주소 변환 함수
 inet_addr() 함수는 IPv4 방식의 IP 주소가 저장된 문자열을 IN_ADDR 구조체에 맞게 변환해준다. 그러므로 문자열을 인자로 전달하여 이 함수를 호출한 결과값을 소켓 주소 구조체에 사용한다.

 그렇다면 소켓 주소 구조체에 보관된 아이피 주소를 확인하기 위해서는 어떻게 해야할까? inet_addr() 함수에서 힌트를 얻어보자면 IN_ADDR 구조체에 저장된 IP 주소를 ASCII 문자열의 형태로 반환해서 출력하면 될 것 같다. 이런 기능을 제공하는 함수가 바로 inet_ntoa() 함수이다.
 inet_ntoa() 함수를 사용해서 IP 주소를 확인해보자.
 참고로 inet_addr() 함수는 이미 네트워크 바이트 정렬 된 IP 주소를 반환하므로 htonl() 함수를 적용해서는 안 된다. inet_addr() 함수에 의해 sin_addr 구조체는 이미 네트워크 바이트 정렬을 따르기 때문이다.


정리하면
  • 윈속, 소켓을 생성 및 초기화한 후, 소켓 주소 구조체에 주소 체계, 포트 번호, IP 주소를 저장한다.
  • 바이트 정렬에는 빅 엔디안과 리틀 엔디안이 있다.
  • 네트워크 프로그래밍에서 호스트와 네트워크의 바이트 정렬 방식 간의 변환은 중요하다. 이를 위해 바이트 정렬 함수, IP 주소 반환 함수 등을 활용할 수 있다.

댓글 없음:

댓글 쓰기