in_addr 구조체
2025.03.24 - [삽질/델파이] - 아이피 구하기 에서 윈도우 소켓 을 이용해서 로컬 아이피를 구하는 방법을 알아보았는데 이데 대해서 조금 더 알아본다.
gethostbyname 은 PHostEnt 라는 구조체의 포인터를 넘겨주는데 이 구조체는 다음과 같다. ( 원형과 라자루스, 델파이로 포팅된것은 버전별로 약간 상이할 수도 있는데 기본적으로는 같으니 차이점은 알아서 잘... 특별히 언급하지 않으면 라자루스 4.0 rc2 - fpc 3.2.2를 기준으로..)
https://learn.microsoft.com/ko-kr/windows/win32/api/winsock/ns-winsock-hostent
HOSTENT(winsock.h) - Win32 apps
HOSTENT(winsock.h) 구조체는 함수에서 호스트 이름, IPv4 주소 등과 같은 지정된 호스트에 대한 정보를 저장하는 데 사용됩니다.
learn.microsoft.com
type
hostent = record
{ official name of host }
h_name: pchar;
{ alias list }
h_aliases: ^pchar;
{ host address type }
h_addrtype: SmallInt;
{ length of address }
h_length: SmallInt;
{ list of addresses }
case byte of
0: (h_addr_list: ^pchar);
1: (h_addr: ^pchar)
end;
THostEnt = hostent;
PHostEnt = ^THostEnt;
h_addrtype 은 프로토콜 종류 로 AF_ 로 시작되는 값들이 있는데 AF_INET (2) 은 ipv4, AF_INET6 (23)는 ipv6 그리고 AF_UNSPEC (0) 정도만 기억해 두고 다른 값들은 winsock2.pas 에 선언되어 있으니 참고하도록.
마지막 가변으로 선언되어져 있는 h_addr_list에 주소 데이터가 들어가져 있다. 여러 개의 주소들이 있을 수 있다. h_addr 은 h_addr_list [0]으로 접근되어 사용할 수 있다고 하는데 예전 버전의 호환성 때문에 유지되고 있는 것 같다. 자세한 건 모르겠고 그냥 그렇다고 한다.
h_addr_list 가 ^pchar 로 포인터로 되어 있는데 in_addr 구조체로 되어 있다.
type
SunB = packed record
s_b1,s_b2,s_b3,s_b4 : u_char;
end;
SunW = packed record
s_w1,s_w2 : u_short;
end;
in_addr = record
case integer of
0 : (S_un_b : SunB);
1 : (S_un_w : SunW);
2 : (S_addr : u_long);
end;
TInAddr = in_addr;
PInAddr = ^TInAddr;
주소값은 하나지만 바이트(S_un_b) , 워드(S_un_W) , 롱(S_addr) 타입으로 각각 상황에 따라 적당한 것을 골라 쓸 수 있다.
S_addr 은 u_long 은 부호없는 정수 Cardinal이다. 8바이트니깐 SunW는 4바이트 2개, SunB는 1바이트 4개로 이루어져 있다. Sun 뒤에 W는 워드, B는 바이트를 뜻하는 걸 알 수 있다.
SubB는 1바이트 4개인데 우리가 흔히 보는 192.168.0.1과 같은 닷 데시멀 노테이션 ( Dot-decimal notation ) 이라는 4개의 옥텟 ( octet ) 으로 사용될 수 있다. s_b1, s_b2, s_b3, s_b4 가 각 옥텟에 1:1 대응을 한다. 1~255 사이의 값이 들어가 있어서 그냥 쓰면 된다.
다음은 간단한 예이다.
var
aInAddr4: TInAddr;
apInAddr4: PInAddr;
cIP: string;
cStr: string;
nB1, nB2, nB3, nB4: char;
nW1, nW2: Word;
nL1: ULONG;
begin
cIP:= '192.168.0.108';
aInAddr4.s_addr:= inet_addr( PChar(cIP) );
apInAddr4:= @aInAddr4; //굳이 포인터형으로 쓰지 않아도 되지만 실제로 사용할때는 포인터로 접근하는경우가 많아서...
Log('IP : '+cIP+#13#10);
nB1:= apInAddr4^.S_un_b.s_b1;
nB2:= apInAddr4^.S_un_b.s_b2;
nB3:= apInAddr4^.S_un_b.s_b3;
nB4:= apInAddr4^.S_un_b.s_b4;
cStr:= '';
cStr:= cStr + Format('S_un_b.s_b1 : %.2x : %d', [Ord( nB1 ), Ord( nB1 ) ]) +#13#10;
cStr:= cStr + Format('S_un_b.s_b1 : %.2x : %d', [Ord( nB2 ), Ord( nB2 ) ]) +#13#10;
cStr:= cStr + Format('S_un_b.s_b2 : %.2x : %d', [Ord( nB3 ), Ord( nB3 ) ]) +#13#10;
cStr:= cStr + Format('S_un_b.s_b3 : %.2x : %d', [Ord( nB4 ), Ord( nB4 ) ]);
Log( cStr );
//ord 대신 byte( value ) 나 integer( value )로 형변환 해도 된다.
end;
{* 출력
IP : 192.168.0.108
S_un_b.s_b1 : C0 : 192
S_un_b.s_b1 : A8 : 168
S_un_b.s_b2 : 00 : 0
S_un_b.s_b3 : 6C : 108
*}
SubW는 2개의 워드로 이루어져 있는데 우선 값을 한번 살펴보자
nW1:= apInAddr4^.S_un_w.s_w1;
nW2:= apInAddr4^.S_un_w.s_w2;
cStr:= '';
cStr:= cStr + Format('S_un_w1 : %.4x : %d', [ nW1, nW1 ])+#13#10;
cStr:= cStr + Format('S_un_w2 : %.4x : %d', [ nW2, nW2 ]);
Log( cStr );
{* 출력예
S_un_w1 : A8C0 : 43200
S_un_w2 : 6C00 : 27648
*}
워드니 2바이트씩 A8C0 와 6C00 으로 이루어져 있다. 그런데 C0A8 이 아니고 A8C0 이다. 이는 엔디안 ( Endianness ) 또는 바이트 정렬(순서) 이라고 하는데 간단히 알아보자면 빅 엔디안 은 최상위(MSB) 부터, 리틀 엔디안 은 최하위(LSB) 로 부터 저장한다. 이는 cpu 와 운영체제마다 다르다.
방식이 통일되지 않다 보니 다른 곳과 통신을 하다보니 아이피 주소, 포트번호 와 같은 프로토콜 정보 와 응용 프로그램의 데이터 의 교환에 심각한 문제가 발생한다.
현재 통신쪽은 빅 엔디안 방식으로 흔히 네트워크 바이트 정렬 이라고 한다. 자세한것은 더 찾아보길 바라며 참고로 인텔 시퓨(x86, x64 ), AMD 는 리틀 엔디안 방식을 사용하고 있다.
그러면 어떻게 하나? 이미 winsock 에 준비된 것이 있다.
호스트 정렬을 네트워크 정렬로 ( hton - host to network ) , 네트워크 정렬을 호스트로 ( ntoh - network to host ) 변경해주고 s 와 l 은 각각 short ( 워드 ), long ( 롱) 을 뜻한다. 이 밖에 d, f, ll 같은 함수가 더 생긴것 같으니 msdn 을 참고하고... 위 함수는 WSAStartup 으로 초기화를 하지 않아도 사용할수 있고 반드시 WSAStartup 으로 초기화 한 에 사용할수 있는 WSAhtons 와 같은 함수군이 존재한다. ( winsock2 )
다음은 htons 함수로 사용해본 예이다.
cStr:= '';
cStr:= cStr + Format('S_un_w1 : %.4x : %.4x', [ nW1, htons(nW1) ])+#13#10;
cStr:= cStr + Format('S_un_w2 : %.4x : %.4x', [ nW2, htons(nW2) ]);
Log( cStr );
{* 출력예
S_un_w1 : A8C0 : C0A8
S_un_w2 : 6C00 : 006C
*}
보면 앞뒤가 변경이 된걸 확인할수 있다. 이를 각 바이트별로 나눠서 출력해보면...
nW1:= htons(nW1);
nW2:= htons(nW2);
cStr:= cStr + Format('%d.%d.%d.%d', [nW1 shr 8, nW1 and $ff, nW2 shr 8, nW2 and $ff]);
Log( cStr );
{*
192.168.0.108
*}
마지막으로 long 타입은 S_Addr 를 살펴보자.
var
cStr: string;
nL1: Cardinal;
begin
nL1:= APInaddr4^.S_addr;
cStr:= '';
cStr:= cStr + Format('S_Addr : %.8x : %d', [ nL1, nL1 ])+#13#10;
Log( cStr );
{*
S_Addr : 6C00A8C0 : 1811982528
*}
역시 2개의 워드 가 서로 바뀌여져 있는걸 알수가 있다. 위에서 변환함수 중에 htonl 이 있어 이걸 한번 사용해볼까?
cStr:= cStr + Format('S_Addr : %.8x : %.8x', [ nL1, htonl(nL1) ])+#13#10;
Log( cStr );
{*
S_Addr : 6C00A8C0 : C0A8006C
*}
예상했던 결과인가? 다시 2개의 워드로 나눠서 위의 SunW 에서 했듯이 각 바이트별로 출력을 해보도록 하자.
nW1:= nL1 and $ffff;
nW2:= nL1 shr 16;
nW1:= htons( nW1 );
nW2:= htons( nW2 );
cStr:= '';
cStr:= cStr + Format('S_Addr : %.4x : %.4x', [ nW1, nW2 ] )+#13#10;
cStr:= cStr + Format('%d.%d.%d.%.d', [nW1 shr 8, nW1 and $ff, nW2 shr 8, nW2 and $ff]);
Log(cStr);
{*
S_Addr : C0A8 : 006C
192.168.0.108
*}
비트연산은 간결하긴 하지만 이 역시 엔디안 방식에 따라서 결과값이 달라질수 있다. 비트연산 말고도 자료형을 쪼개는 방법은 많이 있으니 한번 해보도록 하자