출처: TCP/IP 윈도우 소켓 프로그래밍(https://product.kyobobook.co.kr/detail/S000001636201)
1. 소켓 주소 구조체
소켓 주소 구조체는 소켓 프로그램에서 필수로 사용해야 하는 자료구조이다. 네트워크 프로그램에서 필요한 주소 정보를 담고 있으며, 다양한 소켓 함수의 인자로 사용한다. 프로토콜 체계에 따라 주소 지정 방식이 다르므로 다양한 소켓 주소 구조체가 존재한다.
가장 기본이 되는 구조체는 SOCKADDR
구조체로 ws2def.h
파일에 다음과 같이 정의되어 있다.
typedef struct sockaddr {
u_short sa_family;
char sa_data[14];
} SOCKADDR;
sa_family
: 주소 체계를 나타내는 16비트 정수 값이다. TCP/IP를 사용한다면AF_INET
또는AF_INET6
값이다.sa_data[14]
: 해당 주소 체계에서 사용할 주소 정보를 담는다. 주소 체계에 따라 필요한 정보가 다르므로 가장 형태인 바이트 배열로 선언되어 있다. TCP/IP 프로토콜을 사용한다면 IP 주소와 포트 번호가 입력된다.
SOCKADDR
은 가장 기본적인 정보만 포함하고 있는 구조체로, 실제로 소켓을 사용할 때에는 애플리케이션에서 사용할 프로토콜의 종류에 맞는 별도의 소켓 주소 구조체를 사용한다.
// IPv4 소켓 주소 구조체
typedef struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[0]; // 0으로 설정
} SOCKADDR_IN;
// IPv6 소켓 주소 구조체
typedef struct sockaddr_in6 {
short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo; // 대부분 0으로 설정
struct in6_addr sin6_addr;
char sin_zero[0];
u_long sin6_scope_id; // 대부분 0으로 설정
} SOCKADDR_IN6;
sin_family
,sin6_family
: 주소 체계를 의미한다.sin_port
,sin6_port
: 포트 번호를 의미한다.sin_addr
,sin6_addr
: IP 주소를 의미하며, 각각 32비트in_addr
구조체와 128비트in6_addr
구조체를 사용한다.
in_addr
과 in6_addr
구조체는 각각 inaddr.h
파일과 in6addr.h
파일에 다음과 같이 정의되어 있다.
//
// IPv4 Internet address
// This is an 'on-wire' format structure.
//
typedef struct in_addr {
union {
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { USHORT s_w1,s_w2; } S_un_w;
ULONG S_addr;
} S_un;
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
//
// IPv6 Internet address (RFC 2553)
// This is an 'on-wire' format structure.
//
typedef struct in6_addr {
union {
UCHAR Byte[16];
USHORT Word[8];
} u;
} IN6_ADDR, *PIN6_ADDR, FAR *LPIN6_ADDR;
in_addr
구조체는 각 멤버들이 공용체를 사용하여 메모리를 공유하게 설계되어 있다.in_addr
구조체는 총 32비트의 크기를 가지고 있으며 8바이트씩 접근이 필요하면S_un_b
를 사용하고, 16비트씩 접근이 필요하면S_un_w
, 32비트 접근이 필요하면S_addr
을 사용하면 된다.UCHAR = unsinged char
USHORT = unsigned short
ULONG = unsigned long
in6_addr
구조체는 단순한 바이트, 워드의 배열로 되어 있다.
소켓 주소 구조체를 사용할 때 주의사항은 다음과 같다.
- 소켓 주소 구조체는 크기가 크기 때문에 소켓 함수 인자로 전달할 때는 항상 주소 값을 사용하고,
SOCKADDR*
형식으로 캐스팅해야 한다. - 프로토콜에 따라 소켓 주소 구조체의 크기가 달라지므로
sizeof
연산자를 사용하여 크기 정보를 같이 전달해야 한다.
2. 바이트 정렬 함수
바이트 정렬은 메모리에 데이터를 저장할 때 바이트 순서를 나타내는 용어이다. 빅 엔디안 방식과 리틀 엔디안 방식이 있는데 시스템에서 사용하는 정렬 방식은 CPU와 운영체제마다 다르다.
따라서 네트워크 통신에서 각각의 호스트가 서로 다른 바이트 정렬을 사용하고 있으면 정확한 데이터 전송이 불가능하다.
네트워크 통신에서는 IP 주소와 포트 번호의 바이트 정렬 방식은 빅 엔디안으로 통일해 사용하며 네트워크 바이트 정렬(network byte ordering)
이라고 부른다.
응용 프로그램 단계에서도 바이트 정렬이 다를 수 있다. 네트워크 통신에서 빅 엔디안으로 통일하여 데이터를 전달 받아도, 패킷의 데이터의 바이트 정렬 방식이 다르다면 문제가 될 수 있기 때문이다. 응용 프로그램도 대개 네트워크 바이트 정렬과 동일한 정렬 방식을 사용한다.
윈도우 소켓에서는 응용 프로그램이 바이트 정렬 방식을 편리하게 변환할 수 있도록 다음과 같은 함수가 제공된다.
u_short htons(u_short hostshort);
u_long htonl(u_long hostlong);
u_short ntohs(u_short netshort);
u_long ntohl(u_long netlong);
- 호스트에서 네트워크, 혹은 네트워크에서 호스트로 바이트 정렬 방식을 변환해준다.
윈도우 소켓 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 *lphostshort);
int WSANtohl(SOCKET s, u_long netlong, u_long *lphostlong);
- 소켓 디스크립터를 사용하고 세 번째 인자로 변환된 결과를 리턴한다.
TCP/IP에서 사용하는
SOCKADDR_IN
,SOCKADDR_IN6
소켓 주소 구조체에서 네트워크 통신에 필요한 정보(IP 주소, 포트 번호 등)를 나타내는 멤버들은 네트워크 바이트 정렬 방식을 따른다.
3. IP 주소 변환 함수
애플리케이션에서 IP 주소를 편리하게 변환할 수 있도록 윈도우 소켓 함수를 제공한다.
// IPv4 주소 변환
// 문자열 -> 숫자
unsigned long inet_addr(const char* cp);
// 숫자 문자열
char* inet_ntoa(struct in_addr in);
이 함수들은 IPv4 주소를 문자열에서 숫자로 변환하거나, 숫자를 문자열로 변환해주는 함수이다. IPv6 주소 변환을 사용하고 싶다면 IPv4와 IPv6를 모두 지원하는 함수를 사용하면 된다.
// IPv4 or IPv6 주소 변환
// 문자열 -> 숫자
int WSAStringToAddress (
LPSTR AddressString, // 문자열 형식의 IP 주소
INT AddressFamily, // AF_INET or 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
구조체에 사용하는 방법이다.
// 소켓 주소 구조체 초기화
SOCKADDR_IN addr;
ZeroMemory(&addr, sizeof(addr));
// 프로토콜 지정
addr.sin_family = AF_INET;
// IP 주소 지정(문자열 -> 숫자 변환 함수 이용)
// #define s_addr S_un.S_addr(inaddr.h)
addr.sin_addr.s_addr = inet_addr("147.46.114.70");
// 포트 번호 지정(네트워크 바이트 정렬)
addr.sin_port = htons(9000);
// 소켓 함수 호출
SocketFunc(..., (SOCKADDR*)&addr, sizeof(addr), ...);
IP 주소 변환 함수를 사용할 때 주의 사항은 다음과 같다.
// 1. inet_addr 함수가 리턴하는 IP 주소는 네트워크 바이트 정렬된 주소이므로, 해당 주소에 다시 바이트 정렬 변환 함수를 사용하면 안 된다.
htonl(inet_addr("127.0.0.1")); // X
// 2. 마찬가지로 SOCKADDR_IN.sin_addr에 네트워크 바이트 정렬된 주소값이 들어있기 때문에, 해당 값에 다시 바이트 정렬 변환 함수를 사용하면 안 된다.
ntohl(addr.sin_addr); // X
4. DNS와 이름 변환 함수
TCP/IP 프로토콜은 내부적으로 숫자 형태의 IP 주소를 기반으로 동작하므로 사용자가 입력한 도메인 이름을 반드시 IP 주소로 변환해야 한다.
도메인 이름과 IP 주소 변환은 인터넷에 존재하는 여러 도메인 이름 서버(DNS)가 관리하며, 어느 한 DNS가 모든 정보를 갖고 있지 않다는 점에서 분산 데이터베이스라고 할 수 있다.
응용 프로그램이 도메인 이름과 IP 주소를 상호 변환할 수 있도록 다음과 같은 윈도우 소켓 함수가 제공된다.
// 도메인 이름 -> IP 주소(네트워크 바이트 정렬)
struct hostent* gethostbyname(
const char* name // 도메인 이름
);
// IP 주소(네트워크 바이트 정렬) -> 도메인 이름
struct hostent* gethostbyaddr(
const char* addr, // IP 주소
int len, // IP 주소 길이
int type // 주소 체계(IF_INET, IF_INET6)
)
두 함수 모두 hostent
구조체 포인터를 리턴한다. 이 구조체의 정보는 다음과 같다.
typedef struct hostent {
char* h_name;
char** h_aliases;
short h_addrtype;
short h_length;
char** h_addr_list;
#define h_addr h_addr_list[0]
} HOSTENT;
h_name
: 공식 도메인 이름이다.h_aliases
: 한 호스트가 공식 도메인 이름 외에 다른 이름을 여러 개 가질 수 있는데, 이를 별칭이라 한다. 호스트가 여러 별칭을 가진 경우, 이 포인터를 따라가면 모든 별명을 얻을 수 있다.h_addrtype
: 주소 체계를 나타내는 값이다.(AF_INET, AF_INET6)h_length
: IP 주소의 길이(바이트 단위)다.h_addr_list
: 네트워크 바이트 정렬된 IP 주소이다. 한 호스트가 여러 IP 주소를 가진 경우, 이 포인터를 따라가면 모든 IP 주소를 얻을 수 있다. 특정 호스트에 접속할 때는 대개 첫 번째 IP 주소만 사용하므로h_addr_list[0]
에 접근하는데, 매크로를 통해 쉽게 접근할 수 있다.
'네트워크 > 윈도우 소켓 프로그래밍' 카테고리의 다른 글
TCP 클라이언트 함수 (0) | 2024.04.05 |
---|---|
TCP 서버 함수 (0) | 2024.04.05 |
소켓의 생성과 닫기 (0) | 2024.04.01 |
윈도우 소켓 애플리케이션 초기화 및 종료 (0) | 2024.04.01 |
일반적인 윈도우 소켓 함수의 오류 처리 (0) | 2024.04.01 |