주소 체계와 데이터 정렬

주소정보의 표현

struct sockaddr_in
{
   sa_family_t     sin_family;  // 주소 체계(Address Family)
   uint16_t        sin_port;    // 16비트 TCP/UDP PORT 번호
   struct in_addr  sin_addr;    // 32비트 IP 주소
   char            sin_zero[8]; // 사용되지 않음
}

이 구조체는 bind함수에 구조체를 전달하는 용도로 사용된다.

struct in_addr
{
   in_addr_t      s_addr;  // 32비트 IPv4 인터넷 주소
}

POSIX

sockaddr_in이나 내부의 in_addr 구조체에는 생소한 자료형이 있다. 이들은 POSIX 표준에 정의되어 있다. (확장성을 고려)

     자료형 이름                          자료형에 담길 정보                     선언된 헤더파일
int8_t
uint8_t
int16_t
uint16_t
int32_t
uint32_t
signed 8-bit int
unsigned 8-bit int (unsigned char)
signed 16-bit int
unsigned 16-bit int (unsigned short)
signed 32-bit int
unsigned 32-bit int (unsigned long)


sys/types.h
sa_family_t
socklen_t
주소체계(address family)
길이정보(length of struct)
sys/socket.h
in_addr_t
in_port_t
IP주소정보, uint32_t로 정의되어 있음
PORT번호정보, uint16_t로 정의되어 있음
netinet/in.h

구조체 sockaddr_in의 멤버에 대한 분석

맴버 sin_family
프로토콜 체계마다 적용하는 주소체계가 다르다.

주소체계(Address Family) 의 미
AF_INET IPv4 인터넷 프로토콜에 적용하는 주소체계
AF_INET6 IPv6 인터넷 프로토콜에 적용하는 주소체계
AF_LOCAL 로컬 통신을 위한 유닉스 프로토콜의 주소체계

맴버 sin_port
16비트 PORT 번호를 저장한다. 단, ‘네트워크 바이트 순서’로 저장해야 하는데, 이에 대해서는 잠시 후에 별도로 설명하겠다. 이 멤버에 관해서는 PORT번호를 저장한다는 사실보다 네트워크 바이트 순서로 저장해야 한다는 사실이 더 중요하다.

맴버 sin_addr
32비트 IP주소정보를 저장한다. 이 역시 ‘네트워크 바이트 순서’로 저장해야 한다. 이 멤버를 정확히 파악하기 위해서는 구조체 in_addr도 함께 살펴봐야 한다. 그런데 구조체 in_addr의 유일한 멤버가 uint32_t로 선언되어 있으니, 간단히 32비트 정수자료형으로 인식해도 괜찮다.

맴버 sin_zero
특별한 의미를 지니지 않는 멤버이다. 단순히 구조체 sockaddr_in의 크기를 구조체 sockaddr와 일치시키기 위해 삽입된 멤버이다. 그러나 반드시 0으로 채워야 한다. 만약에 0으로 채우지 않으면 원하는 결과를 얻지 못한다.

인자로써의 sockaddr_in

sockaddr_in 구조체 변수의 주소 값은 bind 함수의 인자로 다음과 같이 전달된다. bind 함수에 대한 자세한 설명은 잠시 후에 진행이 되니, 일단은 인자전달과 형변환 위주로만 코드를 살펴보자

struct sockaddr_in serv_addr;
. . . .
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
    error_handling("bind() error");

여기서 중요한 것은 두 번째 전달인자이다. 사실 bind 함수는 sockaddr 구조체 변수의 주소 값을 요구한다. 앞서 설명한 주소체계, PORT번호, IP주소정보를 담고 있는 sockaddr 구조체 변수의 주소 값을 요구하는 것이다. 그런데 아래 보이듯이 구조체 sockaddr은 이들 정보를 담기에 다소 불편하게 정의되어 있다.

struct sockaddr
{
    sa_family_t  sin_family; // 주소체계(Address Family)
    char         sa_data[14] // 주소정보
}

위의 구조체 멤버 sa_data에 저장되는 주소정보에는 IP주소와 PORT번호가 포함되어야 하고, 이 두 가지 정보를 담고 남은 부분은 0으로 채울 것을 bind 함수는 요구하고 있다. 그런데 이는 주소정보를 담기에 매우 불편한 요구사항이다.

그래서 구조체 sockaddr_in이 등장한 것이다. sockaddr_in 구조체 멤버를 앞서 설명한대로 채우면, 이 때 형성되는 구조체 변수의 바이트 열이 bind 함수가 요구하는 바이트열이 된다. 결국 인자전달을 위한 형변환을 통해서 sockaddr 구조체 변수에 bind 함수가 요구하는 바대로 데이터를 채워 넣은 효과를 볼 수 있다.


네트워크 바이트 순서와 인터넷 주소 변환

Big Endian, Little-Endian을 고려하지 않고 데이터를 송수신하면 문제가 발생할 수 있다. 저장 순서가 다르다는 것은 전송되어온 데이터의 해석순서가 다름을 뜻하기 때문이다.

바이트 순서(Order)와 네트워크 바이트 순서

  • 빅 엔디안(Big Endian) : 상위 바이트의 값을 작은 번지수에 저장하는 방식
  • 리틀 엔디안(Little Endian) : 상위 바이트의 값을 큰 번지수에 저장하는 방식

0x12345678을 저장한다고 해보자

big.png little.png

위 그림처럼 빅 엔디안와 리틀 엔디안은 서로 데이터의 저장 방식이 다르기 때문에 네트워크를 통해 받는 데이터가 어떤 엔디안 방식을 쓰느냐에 따라 다르게 해석될 여지가 있다. 따라서 네트워크를 통해 데이터를 전송할 때는 통일된 기준으로 데이터를 전송하기로 약속하였으며, 이 약속을 ‘네트워크 바이트 순서’라고 한다.

네트워크 바이트 순서는 빅 엔디안 방식으로 통일하는 것이다.

바이트 순서의 변환

이제 sockaddr_in 구조체 변수에 값을 채우기 앞서 네트워크 바이트 순서로 변환해서 저장해야 하는 이유를 알았을 것이다. 그럼 이번에는 바이트 순서의 변환을 돕는 함수를 소개하겠다.

  • unsigned short htons(unsigned short);
  • unsigned short ntohs(unsigned short);
  • unsigned long htonl(unsigned long);
  • unsigned long ntohl(unsigned long);

htons에서의 h는 호스트(host) 바이트 순서를 의미한다. htons에서의 n은 네트워크(network) 바이트 순서를 의미한다. s는 short, l은 long을 의미한다.(linux에서 long은 4바이트이다.)

s는 2바이트 short를 의미하므로 PORT번호의 변환에 사용되고, l은 4바이트 long을 의미하므로 IPv4의 변환에 사용된다.


인터넷 주소의 초기화와 할당

문자열 정보를 네트워크 바이트 순서의 정수로 변환하기

sockaddr_in 안에서 주소 정보를 저장하기 위해 선언된 멤버는 32비트 정수형으로 정의되어 있다. 따라서 우리는 IP주소 정보의 할당을 위해서 32비트 정수형태로 IP주소를 표현할 수 있어야 한다. 그러나 문자열 정보에 익숙한 우리들에게 이는 만만치 않은 일이다.

그래서 문자열 IP주소를 32비트 전수형으로 변환해주는 inet_addr함수가 있다. 이 함수는 네트워크 바이트 순서로의 변환도 동시에 진행한다.

#include <arpa/inet.h>

in_addr_t inet_addr(const char * string);
// 성공 시 빅 엔디안으로 변환된 32비트 정수 값, 실패 시 INADDR_NONE 반환

이어서 소개하는 inet_aton함수도 기능상으로는 inet_addr함수와 동일하다. 다만 구조체 변수 in_addr를 이용하는 형태라는 점에서 차이를 보인다. 활용도는 inet_aton이 더 높다.

#include <arpa/inet.h>

int inet_aton(const char * string, struct, in_addr * addr);
// 성공 시 1(true), 실패 시 0(false) 반환
  • string: 변환할 IP주소 정보를 담고 있는 문자열의 주소 값 전달.
  • addr: 변환된 정보를 저장할 in_addr 구조체 변수의 주소 값 전달.

위 함수를 사용하면 구조체 sockaddr_in의 in_addr 구조체 변수에 대입하는 과정을 추가로 거치지 않아도 된다.


inet_aton의 반대 기능을 제공하는 함수도 있다. inet_ntoa함수는 네트워크 바이트 순서로 정렬된 정수형 IP 주소를 문자열의 형태로 변환해준다.

#include <arpa/inet.h>

char * inet_ntoa(struct in_addr adr);
// 성공 시 변환된 문자열의 주소 값, 실패 시 -1 반환

위 함수는 문자열을 char 형 포인터로 반환하기 때문에, 반환된 문자열 정보를 다음과 같이 다른 메모리 공간에 복사해 두는 것이 좋다.

str_ptr = inet_ntoa(addr1.sin_addr);
strcpy(str_arr, str_ptr);

인터넷 주소의 초기화 정리

지금까지 살펴본 내용을 기반으로 소켓생성과정에서 흔히 등장하는 인터넷 주소정보의 초기화 방법을 보이겠다.

struct sockaddr_in addr;
char *serv_ip = "211.217.168.13";   // IP주소 문자열 선언
char *serv_port = "9190";           // PORT번호 문자열 선언
memset(&addr, 0, sizeof(addr));     // 구조체 변수 addr의 모든 멤버 0으로 초기화
addr.sin_family = AF_INET;          // 주소체계 지정
addr.sin_addr.s_addr = inet_addr(serv_ip);  // 문자열 기반의 IP주소 초기화
addr.sin_port = htons(atoi(serv_port));     // 문자열 기반의 PORT번호 초기화

클라이언트의 주소정보 초기화

서버프로그램은 bind를 통해, 클라이언트 프로그램은 connect를 통해 외친다.
서로 다른 함수를 통해 외치기 때문에, 준비해야 할 주소 값의 유형도 다르다.

서버 프로그램에서는 sockaddr_in 구조체 변수를 하나 선언해서, 이를 서버 소켓이 동작하는 컴퓨터의 IP와 소켓에 부여할 PORT 번호로 초기화한 다음에 bind 함수를 호출한다.

반면에 클라이언트 프로그램에서는 sockaddr_in 구조체 변수를 하나 선언해서, 이를 연결할 서버 소켓의 IP와 PORT 번호로 초기화한 다음에 connect함수를 호출한다.

INADDR_ANY

서버 소켓의 생성과정에서 매번 서버의 IP주소를 입력하는 것은 귀찮은 일이 될 수 있다. 그래서 다음과 같이 주소정보를 초기화 해도 된다.

addr_sin_addr.s_addr = htonl(INADDR_ANY);
  • 다만 이 경우, inet_addr이나 inet_aton과 같이 호스트 순서를 네트워크 바이트 순서로 자동 변환해주지 않기 때문에 htonl로 네트워크 바이트 주소로 변경해 줘야 한다.

소켓에 인터넷 주소 할당하기(bind)

지금까지 sockaddr_in의 초기화 방법에 대해 살펴보았으니, 이제는 초기화된 주소정보를 소켓에 할당하는 일만 남았다. bind 함수가 바로 이런 역할을 담당한다.

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
// 성공 시 0, 실패 시 -1 반환
  • sockfd: 주소정보를(IP와 PORT를) 할당할 소켓의 파일 디스크립터.
  • myaddr: 할당하고자 하는 주소정보를 지니는 구조체 변수의 주소 값.
  • addrlen: 두 번째 인자로 전달된 구조체 변수의 길이정보.

위의 함수호출이 성공하면, 첫 번째 인자에 해당하는 소켓에 두 번째 인자로 전달된 주소정보가 할당된다.

최종 정리

int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port="9190";

/* 서버 소켓(리스닝 소켓) 생성 */
serv_sock=socket(PF_INET, SOCK_STREAM, 0);

/* 주소정보 초기화 */
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(serv_port));

/* 주소정보 할당 */
bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr));
. . . . .

클라이언트 프로그램이 아닌, 서버 프로그램이라면 위의 코드구성을 기본적으로 갖추게 된다. 물론 위에서 ㅇ보이지 않ㅇㄴ 오류처리에 대한 코드는 추가로 포함이 된다.


윈도우 기반으로 구현하기

함수 htons, htonl의 윈도우 기반 사용 예

#include <stdio.h>
#include <winsock2.h>
void ErrorHandling(char* message);

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    unsigned short host_port = 0x1234;
    unsigned short net_port;
    unsigned long host_addr = 0x12345678;
    unsigned long net_addr;
    
    if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");
    
    net_port = htons(host_port);
    net_addr = htonl(host_addr);
    
    printf("Host ordered port: %#x \n", host_port);
    printf("Network ordered port: %#x \n", net_port);
    printf("Host ordered address: %#lx \n", host_addr);
    printf("Network ordered address: %#lx \n", net_addr);
    WSAClenup();
    return 0;
}

void ErrorHandling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

리눅스와 비교하여 라이브러리 초기화에 필요한 WSAStartup 함수의 호출과 헤더파일 winsock2.h에 대한 #include문의 추가 이외에는 달라진 점이 없음에 주목하기 바란다.

함수 inet_addr, inet_ntoa의 윈도우 기반 사용 예

윈도우에는 inet_aton 함수가 존재하지 않는다.

#include <stdio.h>
#include <string.h>
#include <winsock2.h>
void ErrorHandling(char* message);

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");
        
    /* inet_addr 함수의 호출 예 */
    {
        char *addr = "127.212.124.78";
        unsigned long conv_addr = inet_addr(addr);
        if(conv_addr == INADDR_NONE)
            printf("Error occured! \n");
        else
            printf("Network ordered integer addr: %#lx \n", conv_addr);
    }
    
    /* inet_ntoa 함수의 호출 예 */
    {
        struct sockaddr_in addr;
        char *strPtr;
        char strArr[20];
        
        addr.sin_addr.s_addr = htonl(0x1020304);
        strPtr = inet_ntoa(addr.sin_addr);
        strcpy(strArr, strPtr);
        printf("Dotted-Decimal notation3 %s \n", strArr);
    }
    
    WSACleanup();
    return 0;
}

void ErrorHandling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

윈도우에서 소켓에 인터넷 주소 할당하기

bind 함수의 의미와 매개변수 및 반환형의 형태가 완전히 동일하다.

SOCKET servSock;
struct sockaddr_in servAddr;
char *servPort = "9190";

/* 서버 소켓 생성 */
servSock = socket(PF_INET, SOCK_STREAM, 0);

/* 주소정보 초기화 */
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(servPort));

/* 주소정보 할당 */
bind(servSock, (struct sockaddr*) &servAddr, sizeof(servAddr));