TCP IP 소켓 프로그래밍

04-2. 소켓 주소 구조체 다루기

장계탕 2023. 9. 2. 22:31

1. 바이트 정렬 함수 

바이트 정렬   ( Byte Order ) 메모리에 데이터를 저장할 때 바이트의 배치순서를 나타내는 용어
빅 엔디언      ( Big-endian )  = 네트워크 바이트 정렬  최상위 바이트 ( MSB : Most Significant Byte )부터 차례로 저장하는 방식
- 메모리의 앞에 주소 부터 쓰고 읽는다.

리틀 엔디언  ( Little-endian ) 최하위 바이트 ( LSB : Least Significant Byte ) 부터 차례로 저장하는 방식
- 데이터를 뒤에 주소 부터 쓰고 읽는다.

/* 
   +---------------------------------------------------------
   16진수 : 0x12345678 을 메모리 0x1000 번지에 저장할 때 두 방식
   ---------------------------------------------------------+
*/
----------------------------------------------------
 빅 엔디언    |  0x12  |  0x34  |  0x56  |  0x78  |  // 네트워크 바이트 정렬
----------------------------------------------------
              ↑ 0x1000 ↑ 0x1001 ↑ 0x1002 ↑ 0x1003
----------------------------------------------------
리틀 엔디언   |  0x78  |  0x56  |  0x34  |  0x12  |
----------------------------------------------------
              ↑ 0x1000 ↑ 0x1001 ↑ 0x1002 ↑ 0x1003

시스템에서 사용하는 바이트 정렬은 CPU 와 운영체제에 따라 다르다!. 

따라서 서로 다른 시스템 간의 데이터를 교환할 때 이를 고려해야한다!.

- 파일에 데이터를 저장하고 읽어오는 경우

- 네트워크를 통해 데이터를 송수신하는 경우

바이트 정렬에 유의해야 한다. 

 

만약 리틀 엔디언으로 파일에 저장하고 네트워크로 전송했는데 받는 쪽에서 빅 엔디언 방식으로 해당 데이터를 읽고 해석하면 전혀 다른 의미가 되기 때문이다.

 

※교수님 말씀 

 

리틀 엔디언과 빅엔디언이 서로 주고받을 때 문제다.

애플리케이션 계층 -> 데이터 입력

전송 계층 -> 포트번호 정보 + 데이터 

인터넷 계층 -> IP주소 + 포트번호 + 데이터 

네트워크 계층 -> IP주소 + 포트번호 + 데이터 + Max 어드레스 

----> 이렇게 된 패킷이 보내진다.

 

라우터 목적지까지 찾아가는 최적의 경로를 찾아 보낸다.  IP 주소로 ..

라우터가 읽어야 IP주소를 보고 어디로 보낼지 판단한다.

IP주소의 바이트 정렬 방식이 라우터와 호스트가 보낸 값이 같아야 한다.

 

CPU 마다 정렬방식이 다르다. 각자 쓸 때는 문제 없지만 네트워크가 발전하면서 문제가 됐다.

 

전세계에서는 빅엔디언 방식으로 정렬한다.

빅엔디언을 네트워크 바이트 정렬이라고도 한다. 

애플리케이션에서는 호스트 바이트 정렬

 

네트워크-> 빅엔디언 

호스트 바이트 정렬 -> 변수이다. 컴퓨터가 리틀엔디언/빅엔디언 방식을 적용한 CPU에 따라 다르다.

 

 

 

 

1-2. 네트워크 통신에서 바이트 정렬을 고려해야 하는 경우

_____                                         ______
호스트 <------> @ 라우터 --- @ 라우터 <------>  호스트
_____                                         ______

/* 패킷 */ 
[   Data   ][ Port Num ][ IP addr ]

1. IP 주소, 포트 번호와 같이 프로토콜 구현을 위해 필요한 정보 

IP주소 

호스트가 보낸 패킷의 IP헤더에 IP 주소가 포함되어 있다. 바이트 정렬을 약속하지 않으면 IP 주소 해석이 달라져 라우팅에 문제가 될 수 있다.

포트 번호 

바이트 정렬을 하지 않으면 두 호스트가 포트 번호 해석이 달라져 데이터가 잘못된 목적지 프로세스에 전달 될 수 있다. 

 

2. 응용 프로그램이 주고 받는 데이터 

데이터 

바이트 정렬을 약속하지 않으면 데이터 해석에 문제가 생길 수 있다. 

 

1-3. 응용 프로그램의 바이트 정렬 변환 소켓 함수

#include <winsock2.h> // window
#include <arpa/inet.h> // linux

// u_long   : 32 bit 
// u_short  : 16 bit 
/*
   hton*()
      h : 호스트 바이트 정렬로 저장된 값을 입력받아 
      n : 네트워크 바이트 정렬로 변환한 값을 리턴한다.
      
 [ Data ] ---> hton*( [Data] ) ---> [ 변환된 Data ] ---> 소켓 함수
*/
u_short htons(u_short hostshort); // host-to-network-short
u_long  htonl(u_long hostlong);   // host-to-network-long

/*
    ntoh*()
       n : 네트워크 바이트 정렬로 저장된 값을 입력받아 
       h : 호스트 바이트 정렬로 변환된 값을 리턴한다. 
       
       나의 컴퓨터 CPU 가 어떤 엔디언을 쓰던 이 함수를 쓰면 알아서 호스트 바이트 정렬을 한다.
 소켓 함수 ---> [ 소켓함수 결과 리턴 Data ] ---> ntoh*( 리턴 Data ) ---> [ Data ]
*/
u_short ntohs(u_short netshort);  // network-to-host-short
u_lonc  ntohl(u_long netlong);    // network-to-host-long


/*
   *s() 함수 : 16비트값을 입력으로 받는다. 
   *l() 함수 : 32비트값을 입력으로 받는다.
*/

 

TCP/IP 에서 사용할 주소 구조체 

struct sockaddr_in {
    short           sin_family;  // 호스트 바이트 정렬 
    unsigned short  sin_port;    // 네트워크 바이트 정렬
    struct in_addr  sin_addr;    // 네트워크 바이트 정렬
    char            sin_zero[8]; // 호스트 바이트 정렬
};
struct sockaddr_in6{
      short       sin6_family;    // 호스트 바이트 정렬 
      u_short     sin6_port;      // 네트워크 바이트 정렬 
      u_long      sin6_flowinfo   // 네트워크 바이트 정렬 
      struct      in6_addr;       // 네트워크 바이트 정렬  
      u_long      sin6_scope_id;  // 호스트 바이트 정렬 
};

 

2. IP 주소 변환 함수

네트워크 프로그램에서 IP 주소를 입력받을 때 cmd 창에서 ping 명령을 사용하거나 운영체제가 제공하는 입력용위젯 ( 컨트롤) 을 이용한다. 이때 응용 프로그램에서 IP 주소를 문자열 형태로 전달 받기때문에 네트워크 통신을 위해 32비트 ( IPv4 ) 또는 128비트 ( IPv6 ) 숫자로 변환해야 한다

 

2-1. IP 주소를 변환할 수 있도록 하는 소켓 함수 

// window 
#include <wc2tcpip.h> 
/*
   inet_pton() 
       IPv4/IPv6 주소를 입력받아 
       32/128비트 숫자(네트워크 바이트 정렬) 형태로 리턴 ( 2 진수 )
*/
int inet_pton(int af, const char *src, void *dst);

/*
   inet_ntop()
       32/128비트 숫자( 네트워크 바이트 정렬 ) 형태의 ( 2진수 ) 
       IPv4/IPv6 주소를 입력받아 문자열 형태로 리턴 
*/
const char *inet_ntop(int af, const void *src, char *dst, size_t size);

/* IPv4 주소만 지원하는 구형 함수 */
inet_Addr() , inet_ntoa()
/* IPv4/IPv6 주소를 지원하는 함수 */
WSAStringToAddress(), WSAAddressToString()



// Linux 
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

2-2. 바이트 정렬 함수와 IP 주소 변환 함수를 sockaddr_in 구조체에 사용하는 예 

앱 내 데이터를 네트워크 장비로 보낼 떄 10진수 IP 주소를 2진수로 바꿔준다. ( 송신 )

2진수를 10진수 IP주소로 바꿀 때는 수신 할 때 

// 소켓 주소 구조체를 초기화한다. 

struct sockaddr_in addr;                             // IPv4용 소켓 주소 구조체
memset(&addr, 0, sizeof(addr));                      // 0 으로 채운다.
addr.sin_family = AF_INET;                           // 주소 체계 : IPv4
inet_pton(AF_INET, "147.46.114.70", &addr.sin_addr); // IP 주소 : 문자열 -> 숫자
// inet_pton : 자동으로 네트워크 바이트 정렬로 바꿔준다. 

// 포트 번호 : 호스트 바이트 정렬 -> 네트워크 바이트 정렬
addr.sin_port = htons(9000);                         

// 소켓 함수를 호출한다. 
SocketFunc(..., (struct sockaddr *)&addr, sizeof(addr), ...); // 소켓 함수 호출
// 소켓 구조체를 입력으로 받아 내용을 채우면, 
// 응용 프로그램이 이를 출력 등의 목적으로 사용한다. 

// 소켓 함수를 호출한다.  
struct sockaddr_in addr;    // IPv4 용 소켓 주소 구조체 
int addrlen = sizeof(addr); // 리눅스에서는 int 대신 socklen_t 타입을 사용해야 한다.
SocketFunc(..., (struct sockaddr *)&addr, &addrlen, ...); // 소켓 함수 호출 

// 소켓 주소 구조체를 사용한다. 
// INET_ADDRSTRLEN : 문자열 형태의 IPv4 주소 최대 갈이
char ipaddr[INET_ADDRSTRLEN]; 
inet_ntop(AF_INET, &clientaddr.sin_addr, ipaddr, sizeof(ipaddr)); // IP주소 : 숫자 -> 문자열
printf("\n[TCP server] 클라이언트 접속 : IP 주소 = %s, 포트번호=%d\n",
        ipaddr, ntohs(clientaddr.sin_port));

 

3. DNS 와 이름 변환 함수 

윈도우 : 도메인 이름 -> IPv4 주소

DNS

Domain Name System 으로 IP 주소 와 마찬가지로 호스트나 라우터의 고유한 식별자이다. 사용하기 쉬운 장점으로 많은 네트워크 응용 프로그램에서 도메인 이름을 주소로 사용하는 기능을 제공한다. 

 

3-1. 도메인 이름과 IP 주소를 상호 변환할 수 있도록 제공되는 소켓 함수

#include <winsock2.h>
#include <netdb.h>

/* 도메인 이름 -> IP주소 ( 네트워크 바이트 정렬 ) */
struct hostent* gethostbyname(
                   const char* name ); // 도메인 이름 

/* IP 주소 ( 네트워크 바이트 정렬 ) -> 도메인 이름 */
struct hostent* gethostbyaddr(
                    const char* addr, // IP 주소 ( 네트워크 바이트 정렬 )
                    int         len,  // IP 주소의 길이 
                    int         type  // 주소 체계 ( AF_INET / AF_INET6 )
 );
 // 보안상 IP주소를 자주자주 바꾸는데 이게 업데이트가 잘 안되면 
 // gethostbyaddr 이 잘못된 것을 반환할 수도 있으니 주의하라...( 뻘짓함 )
 // - 해결방안은 없음
/*
   +------------------------
           hostent
   ------------------------+
*/

struct hostent {
     char   *h_name;           // 공식 도메인 이름 
     char  **h_aliases;        // 공식 도메인 이름 외 별명
     short   h_addrtype;       // 주소 체계 ( AF_INET / AF_INET6 )
     short   h_length;         // IP 주소 길이 ( 바이트 ) 4(IPv4)/16(IPv6)
     char  **h_addr_list;      // IP주소 ( 네트워크 바이트 정렬 ) 
     // -> 특정 IP가 여러개의 주소값을 가질 수 있기 떄문에 **이다.
     
     
#define h_Addr h_addr_list[0] // h_addr 맨 앞의 주소 하나만 읽겠다.  ( 대표 아이피 주소 )
};
________________________________
 hostent {}
 [ h_name      ]        
 [ h_aliases   ] -----> [ 별명1 주소   ] -> [ 별명 #1\0 ]
 [ h_addrtype  ]        [ 별명2 주소   ] -> [ 별명 #2\0 ]
 [ h_length    ]        [     NULL     ]
 [ h_addr_list ] -----> [      ] -> [ ( in_addr/in6_addr) IP주소 #1 ]
                        [      ] -> [ ( in_addr/in6_addr) IP주소 #2 ]
                        [ NULL ]

return 되는 h_addr_list 를 4바이트 씩 읽으면 아이피 주소를 얻을 수 있음 

 

3-2. 도메인 이름과 IPv4 주소를 상호 변환하는 사용자 정의 함수

// 도메인 이름 -> IPv4 주소
bool GetIPAddr(const char *name, struct in_addr *addr)
{
	struct hostent *ptr = gethostbyname(name);
	if (ptr == NULL) {
		err_display("gethostbyname()");
		return false;
	}
	if (ptr->h_addrtype != AF_INET)
		return false;
	memcpy(addr, ptr->h_addr, ptr->h_length);
	return true;
}
// IPv4 주소 -> 도메인 이름
bool GetDomainName(struct in_addr addr, char *name, int namelen)
{
	struct hostent *ptr = gethostbyaddr((const char *)&addr,
		sizeof(addr), AF_INET);
	if (ptr == NULL) {
		err_display("gethostbyaddr()");
		return false;
	}
	if (ptr->h_addrtype != AF_INET)
		return false;
	strncpy(name, ptr->h_name, namelen);
	return true;
}