본문 바로가기
전공 수업/컴퓨터 통신(Computer Communication)

[5주 차] - Windows 소켓 주소 구조체 (1)

by TwoJun 2022. 9. 27.

    과목명 : 컴퓨터 통신(Computer communication)

수업일자 : 2022년 09월 26일 (월)

 

 

 

1. 소켓 주소 구조체

1-1. 정의 

- 네트워크 프로그램에서 필요한 주소 정보를 담고 있는 구조체를 의미합니다.

-  프로토콜 체계에 따라 주소 지정 방식이 다르므로 다양한 소켓 주소 구조체가 존재합니다.

- 본이 되는 것은 SOCKADDR 구조체로 ws2def.h 헤더 파일(winsock2.h에 포함)에 아래와 같이 정의되어 있습니다.

typedef struct sockaddr {
	u_short sa_family;
        char    sa_data[14];
}SOCKADDR;

(1) sa_family 

- 주소 체계를 나타내는 16비트 정수 값이며 예를 들어, TCP/IP 프로토콜을 사용한다면 AF_INET 또는  AF_INET6입니다.

(2) sa_data[14]

- 주소 체계에서 사용할 주소 정보를 가지고 있고 일반적으로 배열의 형태를 가지며 TCP/IP 프로토콜을 사용한다면 IP 주소와 포트 번호가 저장됩니다.

 

- 실제 프로그래밍에서는 응용 프로그램이 사용할 프로토콜의 종류에 맞는 별도의 소켓 구조체를 이용합니다. 예를 들어 TCP/IP 프로토콜에서는 SOCKADDR_IN 또는 SOCKADDR_IN6,

IrDA(Infrared Data Association)에서는 SOCKADDR_IRDA를 사용합니다. 

 

 

 

 

 

1-2. IPv4, IPv6의 소켓 주소 구조체 필드

/* IPv4 소켓 주소 구조체 */
typedef struct sockaddr_in {
    short          sin_family;
    u_short        sin_port;
    struct in_addr sin_addr;
    char           sin_zero[8];    // 나머지 8바이트는 0으로 할당됨. 
}SOCKADDR_IN;


/* IPv6 소켓 주소 구조체 */
typedef struct sockaddr_in6 {
    short            sin6_family;
    u_short          sin6_port;
    u_long           sin6_flowinfo;    // 대부분 0으로 설정
    struct int6_addr sin6_addr;
    u_long           sin6_scope_id;    // 대부분 0으로 설정
}SOCKADDR_IN6;

자주 사용되는 주소 구조체 필드의 기능은 아래와 같습니다.

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

 

 

 

 

- IP 주소를 저장하기 위한 in_addr, in6_addr 구조체는 각각 inaddr.h(winsock2.h 파일에 포함)와 in6addr.h(ws2tcpip.h 파일에 포함) 파일에 다음과 같이 정의되어 있습니다.

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;

 

 

 

 

- 현재까지 살펴본 SOCKADDR과 SOCKADDR_IN, SOCKADDR_IN6 구조체의 크기는 아래의 표와 같습니다.

구조체 이름 전체 크기(바이트)
SOCKADDR 16
SOCKADDR_IN 16
SOCKADDR_IN6 28
SOCKADDR_IRDA 32
SOCKADDR_BTH 30

- 여기서 주의할 점은, 소켓 주소 구조체는 크기가 큰 편이기에 소켓 함수 인자로 전달할 때는 항상 주소 값을 사용하며 반드시 SOCKADDR 포인터형으로 형 변환(Type casting)을 해야 합니다.

 

- 위의 표처럼 사용할 프로토콜에 따라 소켓 주소 구조체의 크기가 달라지므로 sizeof 연산자를 이용하여 얻은 구조체의 크기 정보를 같이 전달해 주어야 합니다.

 

 

 

 

 

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

- 단, SocketFunc() 함수는 임의의 소켓 함수를 나타낸다.

 

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

SocketFunc(..., (SOCKADDR *)&addr, sizeof(addr), ...);

 

- SOCKADDR : 형 변환(Type casting)

- &addr : 주소

- sizeof : addr 변수의 크기

 

 

 

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

 

SOCKADDR_IN addr;

SocketFunc(..., (SOCKADDR *)&addr, sizeof(addr), ...);

// 소켓 구조체를 사용한다.

...

 

 

 

 

 

 

2. 바이트 정렬(Byte ordering) 함수

2-1. 정의

- 바이트 정렬은 메모리에 데이터를 저장할 때 바이트 순서를 나타내는 용어입니다.

 

 

2-2. 빅 엔디안(Big-endian) 방식과 리틀 엔디안(Little-endian) 방식

- 빅 엔디안(Big-endian) :  최상위 바이트(MSB)부터 차례대로 저장하는 방식을 의미합니다.

- 리틀 엔디안(Little-endian) : 최하위 바이트(LSB)부터 차례대로 저장하는 방식을 의미합니다.

 - 시스템에서 사용하는 바이트 정렬 방식은 CPU와 운영체제(OS)에 따라 달라집니다.

16진수 0x012345678을 메모리 0x1000번지에 저장할 때 빅 엔디안 방식과 리틀 엔디안 방식의 비교

 

 

 

 

 

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

 

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

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

- (a)의 케이스처럼, 호스트의 라우터가 IP 주소 정렬 방식을 약속하지 않으면 IP 주소 해석이 달라져 라우팅에 문제가 발생할 수 있습니다. 

 

- (b)의 케이스처럼, 두 호스트가 포트 번호의 바이트 정렬 방식을 약속하지 않으면 포트 번호 해석이 달라져 데이터가 잘못된 프로세스로 전달될 수 있습니다.

 

- 이러한 문제는 시스템이 사용하는 바이트 정렬 방식(호스트 바이트 정렬이라고도 함)이 통일되어 있지 않아 발생합니다.

- 이러한 문제를 해결하기 위해 IP 주소와 포트 번호의 정렬 방식은 빅 엔디안(Big-endian)으로 통일해서 사용합니다.

- 빅 엔디안(Big-endian) 방식을 네트워크 바이트 정렬(Network byte ordering)이라고도 부릅니다.

 

 

 

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

- (c)의 케이스는 데이터의 바이트 정렬 방식에 따른 문제점입니다. 두 호스트가 주고받는 데이터의 정렬 방식을 약속하지 않으면 데이터 해석에 대하여 문제가 발생할 수 있습니다.

 

- 서버와 클라이언트 같이 구현한다면 바이트 정렬 방식을 통일해서 구현합니다. 일반적으로는 네트워크 바이트 정렬(빅 엔디안) 방식을 사용하며, 클라이언트만 구현한다면 기존 서버가 정한 바이트 정렬 방식을 따라서 구현합니다.

 

 

 

 

 

2.4 바이트 정렬 방식을 편하게 변환할 수 있는 함수 제공 : hton*(), ntoh() 함수

(1) hton*() 함수 

- 호스트 바이트 정렬로, 값을 입력받아 네트워크 바이트 정렬로 변환한 값을 리턴합니다.

- 응용 프로그램이 소켓 함수에 데이터를 넘겨주기 전에 호출합니다.

 

(2) ntoh*() 함수

- 네트워크 바이트 정렬로 저장된 값을 입력받아 호스트 바이트 정렬로 변환한 값을 리턴합니다. 

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

 

(3) *s() 함수는 16비트 값을 *l() 함수는 32비트 값을 입력받으므로 데이터의 크기에 따라 적합한 버전을 선택합니다.

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-5. 윈속 2.x 이상부터 지원되는 바이트 정렬을 위한 확장 함수

- 첫 번째 인자로 소켓 디스크립터를 사용, 변환된 결과를 리턴값이 아닌 세 번째 인자로 전달한다는 특징을 가집니다.

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 *lpnetshort);
int WSANtohl(SOCKET s, u_long netlong, u_long *lpnetlong);

 

- 추가적으로 TCP/IP에서 사용할 SOCKADDR_IN, SOCKADDR_IN6 구조체는 다음과 같은 바이트 정렬 방식을 따릅니다. SOCKADDR_IN / SOCKADDR_IN6 구조체를 초기화하는 방법은 IP 주소 변환 함수에서 더 자세히 알아보겠습니다.

SOCKADDR_IN, SOCKADDR_IN6 구조체의 바이트 정렬 방식(Byte ordering)

 

 

 

 

2-6. 바이트 정렬 함수 실습

// ByteOrdering.cpp : Defines the entry point for the console application.
//

#pragma commnet(lib, "ws2_32")
#include "stdafx.h"
#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));


	// 잘못된 사용 example
    printf("0x%x -> -0x%x\n", x1 = htonl(x1));

	WSACleanup();
	return 0;
}


	/* 실행결과
	[호스트 바이트 -> 네트워크 바이트]
	0x1234 -> 0x3412
	0x12345678 -> 0x78563412

	[네트워크 바이트 -> 네트워크 바이트]
	0x3412 -> 0x1234
	0x78563412 -> 0x12345678

	0x1234 -> -0x43120000
    */

 

 

 

 

 

 

3. IP 주소 변환 함수

- 응용 프로그램에서 IP 주소를 편리하게 변환할 수 있도록 다음과 같은 윈속 함수가 제공됩니다.

 

- inet_addr() 함수는 문자열 형태로 IPv4 주소를 입력받아 32비트 숫자(네트워크 바이트 정렬)로 리턴합니다.

 

- inet_ntoa() 함수는 32비트 숫자(네트워크 바이트 정렬)로 IPv4 주소를 입력받아 문자열 형태로 리턴합니다.

/* IPv4 주소 변환 */
// 문자열 -> 숫자
unsigned long inet_addr(const char *cp);

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

 

 

 

- inet_addr(), inet_ntoa() 함수와 달리 IPv4, IPv6 주소 변환을 모두 지원하는 WSAStringToAddress() /  WSAAdressToString() 함수도 존재합니다.

/* 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 WSAAdressToString (
    LPSOCKADDR lpsaAddress,     // 숫자 형식의 IP 주소; SOCKADDR_IN 또는 SOCKADDR_IN6
    DWORD dwAddressLength,      // 주소 구조체의 길이
    LPWSAPROTOCOL_INFO lpProtocolInfo    // NULL
    LPTSTR lpszAddressString,    // IP 주소(문자열)을 저장할 버퍼
    LPDWORD lpdwAddressStringLength       // 버퍼의 길이
};

 

 

 

 

아래는 바이트 정렬 함수와 IP 주소 변환 함수를 SOCKADDR_IN 구조체에 사용하는 예시입니다.

 

(1) 응용 프로그램이 소켓 구조체를 초기화하고 소켓 함수에 넘겨준다.(SocketFunc() 함수는 임의의 소켓 함수를 나타낸다.)

 

// 소켓 주소 구조체를 초기화
SOCKADDR_IN addr;          // 소켓 주소 얻기
ZeroMemory(&addr, sizeof(addr));     /* 0으로 채운다 */
addr.sin_family = AF_INET;

addr.sin_addr.s_addr = inet_addr("147.46.114.70");
addr.sin_port = htons(9000);     // Port number

// 소켓 함수 호출
SocketFunc(..., (SOCKADDR *)&addr, sizeof(addr), ...);

 

 

 

(2) 소켓 함수가 소켓 주소 구조체를 입력으로 받아 내용을 채우면 응용 프로그램이 이를 출력 등 여러 가지 목적으로 사용하게 됩니다.

 

// 소켓 함수를 호출

SOCKADDR_IN addr;

int addrlen = sizeof(addr);

SocketFunc(..., (SOCKADDR *)&addr, &addrlen, ...);

 

// 소켓 구조체 사용

printf("IP 주소 = %s,  포트 번호 = %d\n", inet_ntoa(addr.sin_addr), ntoh(addr.sin_port));

 

 

 

 

 

3-1. IP 주소 변환 함수 실습

#pragma comment(lib, "ws2_32")
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    WSADATA wsa;
    if(WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
        return 1;
    
    /*---------------*/
    /* IPv4 변환 실습
    /*---------------*/
    // 기존 IPv4 주소 출력
    char *ip4test = "147.46.114.70";
    printf("IPv4 주소(변환 전) = %s\n", ipv4test);
    
    // inet_addr() 함수 
    printf("IPv4 주소(변환 후) = 0x%x\n", inet_addr(ipv4test));
    
    // inet_ntoa() 함수
    IN_ADDR ipv4num;
    ipv4num.s_addr = inet_addr(ipv4test);
    printf("IPv4 주소(재변환 후) = %s\n", inet_ntoa(ipv4num));
    
    printf("\n");
    
    /*---------------*/
    /* IPv6 변환 실습
    /*---------------*/
    // 기존 IPv6 주소 출력
    char *ipv6test = "2001:0230:abcd:ffab:0023:eb00:ffff:1111");
    printf("IPv6 주소(변환 전) = %s\n", ipv6test);
    
    // WSAStringToAddress() 함수 
    SOCKADDR_IN6 = ipv6num;
    int addrlen = sizeof(ipv6num);
    WSAStringToAddress(ipv6test, AF_INET6, NULL,
        (SOCKADDR *)&ipv6num, &addrlen);
    printf("IPv6 주소(변환 후) = 0x");
    
    for (int i = 0; i < 16; i++) { printf("02x", ipv6num.sin6_addr.u.Byte[i]);
    printf("\n");
    
    
    // WSAAddressToString() 함수
    char ipaddr[50];
    DWORD ipaddrlen = sizeof(ipaddr);
    WSAAddressToString((SOCKADDR *)&ipv6num, sizeof(ipv6num), 
        NULL, ipaddr, &ipaddrlen);
    printf("IPv6 주소(재변환 후) = %s\n", ipaddr);
    
    WSACleanup();
    
    return 0;
}


/* 실행 결과
IPv4 주소(변환 전) = 147.46.114.70
IPv4 주소(변환 후) = 0x46722e93
IPv4 주소(재변환 후) = 147.46.114.70

IPv4 주소(변환 전) = 2001:0230:abcd:ffab:0023:eb00:ffff:1111
IPv4 주소(변환 전) = 0x20010230abcdffab0023eb00ffff1111
IPv4 주소(변환 전) = 2001:0230:abcd:ffab:0023:eb00:ffff:1111
*/

 

 

 

- 학부에서 수강했던 전공 수업 내용을 정리하는 포스팅입니다.

- 내용 중에서 오타 또는 잘못된 내용이 있을 시 지적해 주시기 바랍니다.

댓글