TCP-IP 윈도우 소켓 프로그래밍

[TCP/IP] 3장. 소켓 주소 구조체 다루

민돌v 2021. 4. 9. 16:00

소켓 주소 구조체

1. 소켓 주소 구조체

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

1) 기본형은 SOCKADDR 구조체이다.

typedef struct sockaddr {
    u_short sa_family;
    char sa_data[14];
} SOCKADDR;
  • sa_family : 주소 체계를 나타내는 16비트 정수 값
    • 예) TCP/IP 프로토콜 → AF_INET 또는 AF_INET6
  • sa_data : 해당 주소 체계에서 사용할 주소 정보
    • 예) TCP/IP 프로토콜 → IP 주소와 포트 번호

2) 프로토콜 체계에 따라 다양한 형태가 존재

    예) TCP/IP → SOCKADDR_IN{ } 또는 SOCKADDR_IN6{ }

         IrDA → SOCKADDR_IRDA{ }

실제 프로그래밍에서는 응용 프로그램이 사용할 프로토콜의 종류에 맞는 별도의 소켓 주소 구조체를 사용한다.

3) 소켓 구조체

  1. SOCKADDR_IN 구조체 - IPv4 전용
typedef struct sockaddr_in {
    short sin_family; // AF_INET
    u_short sin_port; // 포트 번호
    struct in_addr sin_addr; // IPv4 주소
    char sin_zero[8]; // 0으로 설정
} SOCKADDR_IN;

   2. SOCKADDR_IN6 구조체 - IPv6 전용

typedef struct sockaddr_in6 {
    short sin6_family; // AF_INET6
    u_short sin6_port; // 포트 번호
    u_long sin6_flowinfo; // 대부분 0으로 설정
    struct in6_addr sin6_addr; // IPv6 주소
    u_long sin6_scope_id;// 대부분 0으로 설정
} SOCKADDR_IN;

 

  • sin_family, sin6_family : 주소 체계를 의미하며, 각각 AF_INET과 AF_INET6값을 사용한다.
  • sin_port, sin6_port : 포트 번호를 의미하며, 부호 없는 16비트 정수 값을 사용한다.
  • sin_addr, sin6_addr : IP주소를 의미하며, 각각 32비트 in_addr 구조체와 128비트 in6_addr구조체를 사용한다.

 

4) In_addr(IP주소를 저장하기 위한 구조체)

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;

typedef struct in6_addr {
    union {
        u_char Byte[16];
        u_short Word[8];
    } u;
} IN6_ADDR;

- in_addr 구조체와 달리 IPv6 주소를 담는 in6_addr구조체는 단순한 바이트 또는 워드 배열로 정의되어 있다.

 

 

 


2. 소켓 주소 구조체 사용 예

1) 주소 값 전달

: 소켓 주소 구조체는 크기가 크기 때문에 소켓 함수 인자로 전달할 때는 항상 주소 값을 사용하여,

반드시 SOCKADDR포인터형으로 변환해야 한다.(type casting)

 

- 또한 위의 그림과 같이 사용할 프로토콜에 따라 소켓 주소 구조체의 크기가 달라지므로 sizeof 연산자를 사용해 얻은 크기 정보를 같이 전달해야 한다.

 

2) 소켓 구조체 사용 예

 

가. 응용 프로그램이 소켓 주소 구조체를 초기화하고 소켓 함수에 넘겨주는 경우

     (SocketFunc()는 임의의 소켓 함수를 나타냄)

// 소켓 주소 구조체를 초기화한다.
SOCKADDR_IN addr;
...
SocketFunc(..., (SOCKADDR *)&addr, sizeof(addr), ...);

 

나. 소켓 함수가 소켓 주소 구조체를 이볅으로 받아 내용을 채우면, 응용 프로그램이 이를 출력 등의 목적으로 사용하는 경우

     (SocketFunc()는 임의의 소켓 함수를 나타냄)

SOCKADDR_IN addr;
SocketFunc(..., (SOCKADDR *)&addr, sizeof(addr), ...);
// 소켓 주소 구조체를 사용한다.
...

 


바이트 정렬 함수

1. 바이트 정렬

바이트 정렬은 메모리에 데이터를 저장할 때 바이트 순서를 나타내는 용어로, 빅 에디안과 리틀 에디안 방식이 있다.

1) 빅 에디안(big-endian)

  • 최상위 비트로 부터 차례로 저장하는 방식

2) 리틀 에디안 (little-endian)

  • 최하위 바이트부터 차례로 저장하는 방식

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

 

 

 

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

파일에 데이터를 저장하고 읽어오는 경우나 네트워크를 통해 데이터를 송수신하는 경우 바이트 정렬방식에 유의해야한다.

ex) 리틀 에디안 방식 저장한 데이터 → 빅 에디안으로 읽어는 경우 (전혀 다른 데이터로 인식)

 

(네트워크 통신에서 바이트 정렬 방식을 고려해야하는 2가지 관점)

 

1) 프로토콜 구현을 위해 필요한 정보

  1. 호스트와 라우터가 ip주소의 바이트 정렬방식을 약속하지 않을 때
  2. 포트 번호 바이트 정렬 방식이 다를때
  3. 위의 1,2번의 경우의 시스템이 사용하는 바이트 정렬방식(Host Byte Ordering)이 통일되어 있지 않아서 발생한다.

해결

  1. IP주소와 포트 번호의 바이트 정렬 방식은 빅 에디안으로 통일해 사용한다.
  2. 네트워크 용어로는 빅 에디안을 네트워크 바이트 정렬이라 부른다.

 

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

  1. 데이터의 바이트 정렬방식을 약속하지 않으면 데이터 해석 문제가 발생할 수 있다.
  2. 대개는 네트워크 바이트 정렬(빅 에디안) 방식을 사용한다.
  3. 클라이언트만 제작한다면 기존 서버가 정한 바이트 정렬 방식을 따르면 된다.

 

 

3. 바이트 정렬 함수

1) hton() & ntoh()

  • hton() : host to network
  • ntoh() : network to host

데이터 크기에 따라 적합한 버전을 선택해서 사용한다.

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

 

※ 일반적으로 hton()함수는 응용프로그램이 소켓 함수에 데이터를 넘겨주기 전에 호출

※ ntoh()함수는 소켓 함수가 결과로 리턴한 데이터를 응용 프로그램이 출력 등의 목적으로 사용하기 전에 호출한다.

 

 

2) 원속 확장 바이트 정렬 함수

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);

 

3) SOCKADDR_IN/SOCKADDR_IN6 구조체의 바이트 정렬 방식

 


IP 주소 변환 함수

1. ip 주소 변환 함수

윈도우 응용 프로그램에서는 IP주소를 문자열 형태로 전달 받으므로, 네트워크 통신을 위해 이를 32비트 또는 128비트의 숫자로 변환해야 한다.

1) ip 주소 컨트롤

  • 그림과 같이 윈도우의 ip주소 컨트롤을 이용하면 ip주소를 입력받아 문자열 또는 32비트 숫자를 얻을 수 있다.
  • 하지만 IPv6에는 사용할 수 없다는 단점과, 콘솔 응용 프로그램에는 사용할 수 없으므로 범용적이지 못하다.

 


2) 윈속 함수

  • 응용 프로그램에서 IP주소를 편리하게 변환할 수 있도록 윈속 함수가 제공된다.
  • inet_addr()함수는 문자열 형태로 IPv4주소를 입력바아 32비트 숫자(네트워크 바이트 정렬)로 리턴한다.
  • inet_ntoa()함수는 32비트 숫자(네트워크 바이트 정렬)로 IPv4 주소를 입력바아 문자열 형태로 리턴한다.
/* IPv4 주소변환 */
// 문자열 -> 숫자
unsigned long inet_addr (const char *cp);

// 숫자 -> 문자열
char *inet_ntoa (struct in_addr in);

 

  • 위의 함수와 달리 IPv4/IPv6 모두 지원하는 함수도 있다.
/* IPv4 또는 IPv6 주소 변환 */

//문자열 -> 숫자
int WSAStringToAddress (
    LPTSTR AddressString,	    	// 문자열 형식의 IP 주소
    INT AddressFamily,			// AF_INET 또는 AF_INET6
    LPWSAPROTOCOL_INFO lpProtocolInfo,  // NULL
    LPSOCKADDR lpAddress,   		 // IP 주소(숫자)를 저장할 구조체; SOCKADDR_IN 또는 SOCKADDR_IN6
    LPINT lpAddressLength  		// 주소 구조체의 길이
);


//숫자 -> 문자열
int WSAAddressToString (
    LPSOCKADDR lpsaAddress,  		 	 // 숫자 형식의 IP 주소; SOCKADDR_IN 또는 SOCKADDR_IN6
    DWORD dwAddressLength,   			 // 주소 구조체의 길이
    LPWSAPROTOCOL_INFO lpProtocolInfo,    	// NULL
    LPTSTR lpszAddressString,    		// IP 주소(문자열)를 저장할 버퍼
    LPDWORD lpdwAddressStringLength   	 // 버퍼의 길이
);

도메인 이름 시스템과 이름 변환 함수

1. 도메인 이름

도메인 이름은 IP주소와 마찬가지로 호스탄 라우터의 고유한 식별자이다.

1) 도메인 이름

  • 'www.naver.com' 처럼 숫자보다는 문자 위주 구성
  • IP주소보다 기억하기 쉽다
  • TCP/IP 프로토콜은 내부적으로 숫자 형태의 IP주소를 기반으로 동ㅈ가하므로 사용자가 입력한 도메인 이름을 반드시 IP주소로 변환해야 한다.
  • 상호 변환할 수 있는 윈속 함수가 제공된다.
/* 도메인 이름  IP 주소(네트워크 바이트 정렬) */
struct hostent *gethostbyname (
      const char *name // 도메인 이름
);

/* IP 주소(네트워크 바이트 정렬)  도메인 이름 */
struct hostent *gethostbyaddr (
      const char *addr, // IP 주소(네트워크 바이트 정렬)
      int len, // IP 주소의 길이
      int type // 주소 체계(AF_INET 또는AF_INET6)
);

 

2) hostent 구조체형 포인터

  • gethostbyname() / gethostbyaddr() 두 함수 모두 hostent 구조체형 포인터를 리터한다.
typedef struct hostent {
      char* h_name; // official name of host
      char** h_aliases; // alias list
      short h_addrtype; // host address type
      short h_length; // length of address
      char** h_addr_list; // list of addresses
      #define h_addr h_addr_list[0] // address, for backward compatibility
} HOSTENT;
  1. h_name : 도메인 이름
  2. h_aliases : 한 호스트가 공식 도메인 이름 외에 다른 이름을 여러 개 가질 수 있는데, 이를 별명(aliases)라 한다. 호스트가 여러 별명을 가진 경우, 이 포인터를 따라가면 모든 별명을 얻을 수 있다.
  3. h_addrtype : 주소 체계
  4. h_length : 주소 길이
  5. h_addr_list : 네트워크 바이트 정렬된 ip 주소

 

3) hostent를 사용한 사용자 정의 함수

// 도메인 이름 -> IPv4 주소
BOOL GetIPAddr(char *name, IN_ADDR *addr)
{
      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(IN_ADDR addr, char *name, int namelen)
{
    HOSTENT *ptr = gethostbyaddr((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;
}