ch03. 소켓 주소 구조체 다루기

해담·2024년 10월 13일

소켓 주소 구조체

소켓 주소 구조체

소켓 주소 구조체는 네트워크 프로그램에 필요한 주소 정보를 담는 구조체로, 다양한 소켓 함수의 인수로 사용됩니다. 이를 통해 IP 주소와 포트 번호 같은 정보를 소켓에 전달할 수 있습니다. 프로토콜 체계에 따라 주소 지정 방식이 다르므로 다양한 소켓 주소 구조체가 존재합니다. 기본이 되는 것은 sockaddr 구조체로, 다음과 같이 정의되어 있습니다.

#include <winsock2.h>  // 윈도우
#include <sys/socket.h>  // 리눅스
struct sockaddr {
    unsigned short  sa_family;
    char            sa_data[14];
};

sa_family는 주소 체계를 나타내는 16비트 정숫값입니다. TCP/IP 프로토콜을 사용한다면 IP 버전에 따라 AF_INET 또는 AF_INET6값을 가집니다.
sa_data는 해당 주소 체계에서 사용할 주소 정보로, 주소 체계에 따라 필요한 정보가 다르므로 가장 일반적인 형태인 바이트 배열로 선언되어 있습니다. TCP/IP 프로토콜을 사용한다면 IP 주소와 포트 번호를 저장합니다.

하지만, sockaddr 구조체는 대표 타입이며, 실제로는 이를 확장한 구조체를 사용합니다. TCP/IP에서는 IP 버전에 따라 sockaddr_in 또는 sockaddr_in6, 블루투스에서는 sockaddr_bth를 사용합니다.

#include <winsock2.h>  // 윈도우
#include <sys/socket.h>  // 리눅스
struct sockaddr_in {
    short           sin_family; // 주소 체계 (AF_INET: IPv4)
    unsigned short  sin_port;   // 부호 없는 16비트 포트 번호
    struct in_addr  sin_addr;   // 32비트 IPv4 주소
    char            sin_zero[8]; // 항상 0으로 설정
};
#include <winsock2.h>  // 윈도우
#include <sys/socket.h>  // 리눅스
struct sockaddr_in6 {
    short            sin6_family;   // 주소 체계 (AF_INET6: IPv6)
    unsigned short   sin6_port;     // 부호 없는 16비트 포트 번호 
    unsigned long    sin6_flowinfo; // 대부분 0으로 설정
    struct in6_addr  sin6_addr;     // 128비트 IPv6 주소
    unsigned long    sin6_scope_id; // 대부분 0으로 설정
};

sin_family과 sin6_family는 주소 체계를 의미하며, 각각 AF_INET과 AF_INET6 값을 사용합니다. (호스트 바이트 정렬)
sin_port와 sin6_port는 포트 번호를 의미하며, 부호 없는 16비트 정숫값을 사용합니다. (네트워크 바이트 정렬)
sin_addr과 sin6_addr는 IP 주소를 의미하며, 각각 32비트 in_addr 구조체와 128비트 in6_addr 구조체 타입입니다. (네트워크 바이트 정렬)

아래는 소켓 구조체의 크기입니다. sockaddr_in를 제외한 나머지 소켓 주소 구조체는 대표 타입인 sockaddr 구조체와 크기가 다르다는 것을 알 수 있습니다.

소켓 주소 구조체 전체 크기(바이트 단위)
sockaddr 16
sockaddr_in 16
sockaddr_in6 28
sockaddr_bth 30

IP 주소 구조체

sockaddr_in은 소켓 주소 전체를 다루며, IP 주소와 포트 번호를 모두 포함하고 있습니다. 반면 in_addr는 IP 주소만을 다루는 구조체로, sockaddr_in 내부에서 IP 주소를 저장하는 용도로 사용됩니다.

IP 주소를 저장하기 위한 in_addr 구조체와 in6_addr 구조체의 정의에서, IPv4 주소를 담는 in_addr 구조체는 32비트 주소를 담는 s_addr 필드를 가집니다. 그리고 IPv6 주소는 128비트이기 때문에, 이를 저장하기 위해 운영체제에 따라 공용체/배열로 구성됩니다. (코드는 교재 66쪽 참고)

응용 프로그램에서 소켓 주소 구조체를 사용하는 상황 2가지

여기서 주의할 사항은 소켓 주소 구조체는 크기가 크므로 소켓 함수 인수로 전달할 때는 항상 주솟값을 사용하며, 반드시 sockaddr 포인터 타입으로 변환해야 한다는 점입니다. 또한 sizeof 연산자를 사용하여 계산한 크기 정보를 같이 전달해야 하며, SocketFunc() 함수는 임의의 소켓 함수를 나타냅니다.

  1. 응용 프로그램이 소켓 주소 구조체를 초기화하고 소켓 함수에 넘겨줍니다. (클라이언트가 서버에 연결할 때처럼 응용 프로그램이 직접 소켓 주소를 설정하고, 이를 소켓 함수에 전달하여 네트워크 작업을 수행하는 상황)
  struct socketaddr_in addr;
  SocketFunc(..., (struct sockaddr *)&addr, sizeof(addr), ...);
  1. 소켓 함수가 소켓 주소 구조체를 입력으로 받아 내용을 채우면, 응용 프로그램이 이를 출력 등의 목적으로 사용합니다. (서버가 클라이언트의 요청을 수락할 때, 소켓 함수가 클라이언트의 IP 주소와 포트 번호를 자동으로 채우는 상황)
    둘째 줄에서 소켓 주소 구조체의 크기를 계산하고, 셋째 줄에서 값이 아닌 주소 형태로 전달하는 것에 유의합니다.
  struct socketaddr_in addr;
  int addrlen = sizeof(addr);
  SocketFunc(..., (struct sockaddr *)&addr, &addrlen, ...);

바이트 정렬 함수

바이트 정렬은 메모리에 데이터를 저장할 때 바이트의 배치 순서를 나타내는 용어로, 빅 엔디언(big-endian)과 리틀 엔디언(little-endian) 방식이 있습니다. 빅 엔디언은 최상위 바이트(MSB; Most Significant Byte)부터 저장하는 방식이며, 리틀 엔디언은 최하위 바이트(LSB; Least Significant Byte)부터 차례로 저장하는 방식입니다.

파일에 데이터를 저장하고 읽어오는 경우나 네트워크를 통해 데이터를 송수신하는 경우에는 바이트 정렬에 유의해야 합니다. 가령, 한쪽에서 데이터를 리틀 엔디언 방식을 사용하고, 다른 쪽에서 빅 엔디언 방식으로 해당 데이터를 읽어서 해석하면 전혀 다른 데이터로 인식되는 문제가 있습니다.

네트워크 통신에서 바이트 정렬을 고려해야 하는 경우를 두 가지로 나누어 살펴 보겠습니다.

  1. IP 주소, 포트 번호와 같이 프로토콜 구현을 위해 필요한 정보
    호스트가 보낸 패킷의 IP 헤더에는 IP 주소가 포함되어 있습니다. 이 패킷을 라우터가 받으면 IP 주소를 참조하여 다음 위치의 라우터로 보냅니다. 이때 호스트와 라우터가 IP 주소의 바이트 정렬을 약속하지 않으면 IP 주소 해석이 달라져 라우팅에 문제가 생길 수 있습니다.
    또한 두 호스트가 포트 번호의 바이트 정렬을 약속하지 않으면 포트 번호 해석이 달라져 데이터가 잘못된 목적지 프로세스에 전달될 수 있습니다.
    -> 이를 해결하고자 IP 주소와 포트 번호의 바이트 정렬을 빅 엔디언으로 통일하여 사용합니다. 빅 엔디언을 네트워크 용어로 네트워크 바이트 정렬이라고 부릅니다.

  2. 응용 프로그램이 주고받는 데이터
    호스트가 주고받는 데이터에 대해 바이트 정렬을 약속하지 않으면 데이터 해석의 문제가 생길 수 있습니다. 따라서 서버와 클라이언트를 같이 제작하는 경우라면 빅/리틀 엔디언 중 하나로 통일, 클라이언트만 제작한다면 기존 서버가 정한 바이트 정렬 방식을 따르면 됩니다.

응용 프로그램이 바이트 정렬 방식을 편리하게 변환할 수 있도록 다음과 같은 소켓 함수가 제공됩니다. hton*() 함수(host-to-network)는 호스트 바이트 정렬로 저장된 값을 입력받아 네트워크 바이트 정렬로 변환된 값을 리턴합니다. ntoh*() 함수(network-to-host)는 네트워크 바이트 정렬로 저장된 값을 입력받아 호스트 바이트 정렬로 변환한 값을 리턴합니다. *s() 함수(short)는 16비트 값을, *l() 함수(long)는 32비트 값을 입력으로 받으므로 데이터 크기에 따라 적절한 버전을 선택하면 됩니다. 즉, htons, htonl, ntohs, ntohl 함수가 존재합니다.

또한 일반적으로 응용 프로그램과 데이터, 소켓 함수가 존재한다고 할 때, 응용 프로그램과 데이터 사이에서 호출합니다. 즉, hton() 함수는 응용 프로그램이 소켓 함수에 데이터를 넘겨주기 전에 호출하고, ntoh() 함수는 소켓 함수가 결과로 리턴한 데이터를 응용 프로그램이 출력 등의 목적으로 사용하기 전에 호출합니다.

72쪽 필요한가??? -> 여기부터 다시 보기

IP 주소 변환 함수

IP 주소를 입력받는 방법에는 두 가지가 있습니다. 첫 번째는 명령행 인수를 사용하거나 운영체제가
제공하는 입력용 위젯을 이용하는 방법입니다. 하지만 이는 IP 주소를 32비트(IPv4) 또는 128비트
(IPv6) 숫자로 변환해야 한다는 단점이 있습니다. 두 번째는 IP 주소 전용 위젯을 사용하면 IP 주소를 입력받아 문자열 또는 32비트 숫자(네트워크 바이트 정렬)를 얻을 수 있습니다. 하지만 IPv6에는 사용할 수 없고, 콘솔 응용 프로그램에서 사용하기에는 부적절하여 범용적이지 못하다는 단점이 있습니다.

이를 해결하기 위해 응용 프로그램에서 IP 주소를 편리하게 변환할 수 있도록 소켓 함수가 제공됩니다. inet_pton()는 문자열 형태의 IPv4/IPv6 주소를 입력받아 32/128비트 숫자(네트워크 바이트 정렬) 형태로 리턴합니다. inet_ntop()는 32/128비트 숫자(네트워크 바이트 정렬) 형태의 IPv4/IPv6 주소를 입력받아 문자열 형태로 리턴합니다.

// 윈도우
#include <ws2tcpip.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const char *src, char *dst, size_t size);
// 리눅스
#include <ws2tcpip.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const char *src, char *dst, socklen_t size);

유의할 점은 inet_ntop()의 함수에서 마지막 인수의 타입이 조금 다릅니다. 기본적으로 모두 정수형이지만, 리눅스에서는 정확히 socklen_t 타입 또는 그와 동등한 타입이 아니면 컴파일 오류가 발생할 수 있습니다.

밑은 바이트 정렬 함수와 IP 주소 변환 함수를 sockaddr_in 구조체에 사용하는 예시 두 가지입니다.
1. 응용 프로그램이 소켓 주소 구조체를 초기화하고 소켓 함수에 넘겨준다. 단, SocketFunc() 함수는 임의의 소켓 함수를 나타낸다.

// 소켓 주소 구조체를 초기화한다.
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 주소: 문자열 -> 숫자
addr.sin_port = htons(9000);  // 포트 번호: 호스트 바이트 정렬 -> 네트워크 바이트 정렬

// 소켓 함수를 호출한다.
SocketFunc(..., (struct sockaddr *)&addr, sizeof(addr), ...);
  1. 소켓 함수가 소켓 주소 구조체를 입력으로 받아 내용을 채우면, 응용 프로그램이 이를 출력 등의 목적으로 사용한다. 단, SocketFunc() 함수는 임의의 소켓 함수를 나타낸다.
// 소켓 함수를 호출한다
struct sockaddr_in addr;       // IPv4용 소켓 주소 구조체
int addrlen = sizeof(addr);    // 리눅스에서는 int 대신 socklen_t 타입을 사용해야 한다.
SocketFunc(..., (struct sockaddr *)&addr, &addrlen, ...);  // 소켓 함수 호출

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

DNS와 이름 변환 함수

도메인 이름은 IP 주소와 마찬가지로 호스트나 라우터의 고유한 식별자입니다. IP 주소보다 기억하고 사용하기 쉬우나, TCP/IP 프로토콜은 내부적으로 숫자 형태의 IP 주소를 기반으로 동작하기 때문에 반드시 숫자 형태의 IP 주소로 변환해야 합니다.

응용 프로그램이 도메인 이름과 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)
);

gethostbyname() 함수와 gethostbyaddr() 함수는 모두 hostent 구조체형 포인터를 리턴합니다.

struct hostent {
	char  *h_name;       // 공식 도메인 이름
    chat **h_aliases;    // 한 호스트가 공식 도메인 이름 외에 다른 이름을 가질 수 있는데, 이를 별명이라고 합니다. 호스트가 여러 별명을 가진 경우, 이 포인터를 따라가면 모든 별명을 얻을 수 있습니다.
    short  h_addrtype;   // 주소 체계: AF_INET 또는 AF_INET6
    short  h_length;     // IP 주소의 길이로, 바이트 단위입니다. 4(IPv4) 또는 16(IPv6)
    char **h_addr_list;  // 네트워크 바이트 정렬된 IP 주소입니다. 한 호스트가 IP 주소를 여러 개 가진 경우, 이 포인터를 따라가면 모든 IP 주소를 얻을 수 있습니다. 특정 호스트에 접속할 때는 대개 첫 번째 IP 주소만 사용하므로 h_addr_list[0]에 접근하는데, 매크로를 통해 정의된 h_addr를 사용하면 편합니다.
#define h_addr h_addr_list[0]
};

ping www.google.com

ping 결과에서 보듯이 PING을 구현하는 TCP/IP 프로토콜은 www.google.com Domain Nmae을 TCP/IP 로 동작되는 숫자 형태 (142.250.206.228) 의 IP 주소로 변환해서 사용합니다.

nslookup www.google.com

nslookup 명령어는 Domain Name Server를 조회할 때 사용하는 명령어입니다.

tracert www.google.com

tracert 명령어는 end-to-end route에 있는 router IP Address를 조회하는 명령어입니다.

profile
해담이를먹여살리기위한..

0개의 댓글