Chapter 03 - 주소체계와 데이터 정렬

03-1 : 소켓에 할당되는 IP주소와 PORT번호

IPv4(Internet Protocol version 4) : 4바이트 주소체계

  • 네트워크 주소 / 호스트 주소로 나뉨
  • 주소 형태에 따라 A, B, C, D, E 클래스로 분류
  • 16바이트 주소체계인 IPv6는 아직 범용적으로 사용되지 않음
  • 데이터 전송시 먼저 네트워크 주소를 참조하여 라우터(Router) 혹은 스위치(Switch)를 사용해 해당 네트워크로 데이터를 전송, 이후 호스트 주소를 참조하여 호스트에 데이터를 전송
    • 라우터 : 외부로부터 수신된 데이터를 호스트에 전달하고 호스트가 전달하는 데이터를 외부로 송신해주는 물리적 장치
    • 스위치 : 라우터보다 기능적으로 작은 것, 사실상 라우터와 같은 의미로 사용

IP주소의 첫번째 바이트에 따라 클래스를 구별

  • 0-127 : 클래스 A / 128-191 : 클래스 B / 192-223 : 클래스 C / 224-255 : 클래스 D


PORT번호 : 하나의 운영체제 내에서 소켓을 구분하는 목적으로 사용

  • 16비트로 표현, 범위 0-65535
  • 0-1023번은 ‘잘 알려진 PORT(Well-known PORT)’로 특정 프로그램에 할당이 예약되어있음
  • 기본적으로 중복은 불가능하나, TCP / UDP 소켓은 각각의 포트번호를 가지고있기 때문에 중복이 가능함

03-2 : 주소정보의 표현

IPv4기반 주소표현을 위한 구조체로 sockaddr_in을 사용

struct sockaddr_in
{
	sa_family_t		sin_family;		// 주소체계(Address Family)
	uint16_t		sin_port;		// 16비트 TCP/UDP 포트번호
	struct			in_addr;		// 32비트 IP주소
	char			sin_zero[8];	// 남는 바이트 채우는 용도
}

struct in_addr
{
	in_addr_t		s_addr;			// 32비트 IPv4 인터넷 주소
}
  • sin_family에는 AF_INET 등의 주소체계를 저장
  • sin_port에는 포트번호를 네트워크 바이트 순서로 저장해야함
  • sin_addr에는 32비트 IP 주소정보를 네트워크 바이트 순서로 저장해야하며, 이 때 사용되는 in_addr 구조체의 멤버가 uint32_t형이므로 32비트 정수형으로 해석
  • sin_zero는 구조체 sockaddr_in의 크기를 구조체 sockaddr와 일치시키기위해 삽입되며, 모두 0으로 채워저야함


bind() 함수의 두번째 인자는 sockaddr 구조체 변수의 주소 값을 요구

struct sockaddr
{
	sa_family_t		sin_family
	char			sa_data[14];	// 주소정보
}

그러나 주소정보를 나타내는 sa_data를 직접 설정하는데는 불편함이 따르기 때문에 간편하게 설정할 수 있는 sockaddr_in 구조체를 아래와같이 형변환하여 사용함

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

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

CPU가 데이터를 메모리에 저장하는 방식인 ‘호스트 바이트 순서(Host Byte Order)’은 두가지로 나뉨

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

따라서 네트워크를 통해 데이터 전송시에는 통일된 기준인 ‘네트워크 바이트 순서(Network Byte Order)’를 기준으로 사용하기로 약속
이 때 네트워크 바이트 순서는 빅 엔디안 방식을 사용함


바이트 순서의 변환을 돕는 함수들이 존재

unsigned short htons(unsinged short);
unsigned short ntohs(unsinged short);
unsigned long htonl(unsinged long);
unsigned long ntohl(unsinged long);
  • h는 호스트 바이트 순서, n은 네트워크 바이트 순서, s는 short, l은 long을 의미
  • s가 붙으면 2바이트 short형이므로 포트번호 변환에 사용
  • l이 붙으면 4바이트 long형이므로 IP주소 변환에 사용

바이트 변환 예제

// endian_conv.cpp

#include <iostream>
#include <arpa/inet.h>

using namespace std;

int main(int argc, char *argv[])
{
	unsigned short	host_port = 0x1234;
	unsigned short	net_port;
	unsigned long	host_addr = 0x12345678;
	unsigned long	net_addr;

	net_port = htons(host_port);
	net_addr = htonl(host_addr);

	cout << hex;
	cout << "Host ordered port : " << host_port << endl;
	cout << "Network ordered port : " << net_port << endl;
	cout << "Host ordered address : " << host_addr << endl;
	cout << "Network ordered address : " << net_addr << endl;

	return 0;
}

03-4 : 인터넷 주소의 초기화와 할당

문자열로 표현된 IP주소를 32비트 정수형으로 변환해주는 함수가 존재

#include <arpa/inet.h>

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

inet_addr() 함수를 사용한 예제

// inet_addr.cpp

#include <iostream>
#include <arpa/inet.h>

using namespace std;

int main(int argc, char *argv[])
{
	char* addr1 = "1.2.3.4";
	char* addr2 = "1.2.3.256";

	unsigned long conv_addr = inet_addr(addr1);
	if (conv_addr == INADDR_NONE)
		cout << "Error occured!\n";
	else
		cout << "Network ordered integer addr : " << hex << conv_addr << endl;

	conv_addr = inet_addr(addr2);
	if (conv_addr == INADDR_NONE)
		cout << "Error occureded\n";
	else
		cout << "Network ordered integer addr : " << conv_addr << endl;

	return 0;
}

유효하지 않은 IP주소에 대한 오류검출도 가능


문자열 형태의 IP주소를 32비트 네트워크 바이트 순서로 정렬하는 함수가 존재

#include <arpa/inet.h>

int inet_aton(const char* string, struct in_addr* addr);
// 성공시 true, 실패시 false 반환

inet_aton() 함수를 사용한 예제

// inet_aton.cpp

#include <iostream>
#include <arpa/inet.h>

using namespace std;

void error_handling(char* message);

int main(int argc, char *argv[])
{
	char* addr = "127.232.124.79";
	struct sockaddr_in	addr_inet;

	if (!inet_aton(addr, &addr_inet.sin_addr))
		error_handling("Conversion error");
	else
		cout << "Network ordered integer addr : " << hex << addr_inet.sin_addr.s_addr << endl;

	return 0;
}

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

inet_aton() 함수는 inet_addr() 함수와는 달리 변환된 IP주소 정보를 sockaddr_inin_addr 구조체 변수에 대입하는 과정을 추가로 거칠 필요가 없음


네트워크 바이트 순서로 정렬된 정수형 IP주소를 문자열로 변환하는 함수가 존재

#include <arpa/inet.h>

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

반환된 문자열은 함수 내부적으로 할당된 메모리공간게 저장되기 때문에 inet_ntoa() 함수가 재호출될 경우 이전에 저장된 문자열 정보가 지워질 수 있음

inet_ntoa() 함수를 사용한 예제

// inet_ntoa.cpp

#include <iostream>
#include <arpa/inet.h>

using namespace std;

int main(int argc, char *argv[])
{
	struct sockaddr_in	addr1, addr2;
	char*	str_ptr;
	char	str_arr[20];

	addr1.sin_addr.s_addr = htonl(0x1020304);
	addr2.sin_addr.s_addr = htonl(0x1010101);

	str_ptr = inet_ntoa(addr1.sin_addr);
	strcpy(str_arr, str_ptr);
	cout << "Dotted-Decimal notation1 : " << str_ptr << endl;

	inet_ntoa(addr2.sin_addr);
	cout << "Dotted-Decimal notation2 : " << str_ptr << endl;
	cout << "Dotted-Decimal notation3 : " << str_arr << endl;

	return 0;
}


소켓 생성시 주로 아래와 같이 인터넷 주소정보를 초기화함

struct sockaddr_in addr;
char* serv_ip = "211.217.168.13";			// IP주소 문자열 선언
char* serv_port = "9190";					// 포트번호 문자열 선언
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));		// 문자열 기반의 포트번호 초기화

sockaddr_insin_zero를 0으로 초기화하기 위해 memset() 함수를 사용


서버에서는 bind() 함수, 클라이언트에서는 connect() 함수를 통해 IP와 포트번호를 할당

  • 서버에서는 sockaddr_in 구조체 변수를 선언하여 서버 소켓이 동작하는 컴퓨터의 IP 및 부여할 포트번호로 초기화한 후 bind() 함수를 호출
  • 클라이언트에서는 sockaddr_in 구조체 변수를 선언하여 서버 소켓의 IP 및 포트번호로 초기화 후 connect() 함수 호출


매번 서버의 IP주소를 입력하는 대신, INADDR_ANY 상수를 사용할 수 있음

struct sockaddr_in addr;
char* serv_port = "9190";
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));

INDOOR_ANY 사용시 소켓이 동작하는 컴퓨터의 IP주소가 자동으로 할당됨
컴퓨터 내에 두 개 이상의 IP를 할당받아 사용하는 경우, 포트 번호만 일치하면 수신할 수 있음
단, 클라이언트에서는 서버의 기능을 일부 포함하는 것이 아니라면 잘 사용되지 않음


03-5 : 윈도우 기반으로 구현하기

sockaddr_in 구조체와 변환함수들은 윈도우에도 동일한 이름으로 존재하며, 사용방법 역시 동일
단, inet_aton() 함수는 윈도우에 존재하지 않음

윈도우에 종속적인 변환함수로, 주소정보를 나타내는 문자열으로 주소정보 구조체 변수를 채워넣는 함수가 존재

#include <winsock2.h>

INT WSAStringToAddress(LPTSTR AddressString, INT AddressFamily, LPWSAPROTOCOL_INFO lpProtocolInfo, LPSOCKADDR lpAddress, LPINT lpAddressLength);
// 성공시 0, 실패시 SOCKET_ERROR 반환
  • AddressString : IP와 포트번호를 담고있는 문자열의 주소값
  • AddressFamily : 첫번째 인자로 전달된 주소정보가 속하는 주소체계
  • IpProtocolInfo : 프로토콜 프로바이더(Provider), 일반적으로 NULL
  • IpAddress : 주소정보를 담을 구조체 변수의 주소값
  • IpAddressLength : 네번째 인자로 전달된 주소값의 변수 크기를 담고 있는 변수의 주소값

WSAStringToAddress와 반대 기능을 하는 WSAAddressToString 함수도 존재

#include <winsock2.h>

INT WSAAddressToString(LPSOCKADDR lpsaAddress, DWORD dwAddressLength, LPWSAPROTOCOL_INFO lpProtocolInfo, LPTSTR lpszAddressString, LPDWORD lpdwAddressStringLength);
// 성공시 0, 실패시 SOCKET_ERROR 반환

윈도우 종속 변환함수를 사용한 예제

// conv_addr_win.cpp

#undef UNICODE
#undef _UNICODE
#include <iostream>
#include <arpa/inet.h>

using namespace std;

int main(int argc, char *argv[])
{
	char* strAddr = "203.211.218.102:9190";

	char		strAddrBuf[50];
	SOCKADDR_IN	servAddr;
	int			size;

	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	size = sizeof(servAddr);
	WSAStringToAddress(strAddr, AF_INET, NULL, (SOCKADDR*)&servAddr, &size);
	
	size = sizeof(strAddrBuf);
	WSAAddressToString((SOCKADDR*)&servAddr, sizeof(servAddr), NULL, strAddrBuf, &size);

	cout << "Second conv result : " << strAddrBuf << endl;
	WSACleanup();
	return 0;
}

프로젝트의 환경에 따라 매크로가 정의되어있을 수 있으므로 #undef UNICODE, #undef _UNICODE를 통해 기존에 정의된 매크로를 해제
typedef struct sockaddr_in SOCKADDR_IN이 선언되어있으므로 sockaddr_in 구조체를 SOCKADDR_IN으로도 사용 가능



내용 확인문제

  1. IP주소 체계인 IPv4와 IPv6의 차이점은 무엇인가? 그리고 IPv6의 등장배경은 어떻게 되는가?

    IPv4는 4비트, IPv6는 16비트 주소체계
    IPv4 체계 하에서 IP주소가 고갈될 것을 염려하여 IPv6 표준이 등장함

  2. 회사의 로컬 네트워크에 연결되어있는 개인 컴퓨터에 데이터가 전송되는 과정을, IPv4의 네트워크 ID와 호스트 ID, 그리고 라우터의 관계를 기준으로 설명하여라.

    IPv4의 네트워크 ID에 따라 회사의 로컬 네트워크로 전송됨
    이후 라우터를 통해 호스트 ID에 따라 데이터를 개인 컴퓨터로 전송

  3. 소켓의 주소는 IP와 PORT번호 두가지로 구성된다. 그렇다면 IP가 필요한 이유는 무엇이고, PORT번호가 필요한 이유는 또 무엇인가? 다시말해서, IP를 통해서 구분되는 대상은 무엇이고, PORT번호를 통해서 구분되는 대상은 또 무엇인가?

    IP는 특정 네트워크의 컴퓨터를 구분하기 위해, 포트번호는 하나의 운영체제 내에서 소켓을 구분하기 위해 필요함

  4. IP주소의 클래스를 결정하는 방법을 설명하고, 이를 근거로 다음 IP주소들이 속하는 클래스를 판단해보자.

    클래스는 IP주소의 첫번째 바이트의 값에 따라 결정됨

    • 214.121.212.102 : 클래스 C
    • 120.101.122.89 : 클래스 A
    • 129.78.102.211 : 클래스 B
  5. 컴퓨터는 라우터 또는 스위치라 불리는 물리적인 장치를 통해서 인터넷과 연결된다. 그렇다면 라우터 또는 스위치의 역할이 무엇인지 설명해보자.

    외부로부터 수신된 데이터를 호스트에 전달, 호스트가 전달하는 데이터를 외부로 송신하는 역할

  6. ‘잘 알려진 PORT(Well-known PORT)’는 무엇이며, 그 값의 범위는 어덯게 되는가? 그리고 잘 알려진 PORT 중에서 대표적인 HTTP와 FTP의 PORT번호가 어떻게 되는지 조사해보자.

    0-1023의 범위를 가지고있으며 HTTP는 80번, FTP는 20번(데이터 전송) / 21번(FTP 제어) 포트번호를 가지고있음

  7. 소켓에 주소를 할당하는 bind 함수의 프로토타입은 다음과 같다.
    int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
    그런데 호출은 다음의 형태로 이루어진다.
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    여기서 serv_addr은 구조체 sockaddr_in의 변수이다. 그렇다면 bind 함수의 프로토타입과 달리 구조체 sockaddr_in의 변수를 사용하는 이유는 무엇인지 간단히 설명해보자.

    bind() 함수의 프로토타입에서 요구하는 sockaddr 구조체는 주소정보를 설정하는데 불편함이 있기 때문에 손쉽게 정보를 초기화할 수 있는 sockaddr_in 구조체를 사용

  8. 빅 엔디안과 리틀 엔디안에 대해서 설명하고, 네트워크 바이트 순서가 무엇인지, 그리고 이것이 필요한 이유는 또 무엇인지 설명해보자.

    빅 엔디안은 상위 바이트의 값을 작은 번지수에, 리틀 엔디안은 상위 바이트의 값을 큰 번지수에 저장하는 CPU의 데이터 저장 방식
    네트워크 바이트 순서는 네트워크를 통한 데이터 전송시 통일된 기준을 사용해야하기 때문에 필요하며, 빅 엔디안을 사용함

  9. 빅 엔디안을 사용하는 컴퓨터에서 4바이트 정수 12를 리틀 엔디안을 사용하는 컴퓨터에게 전송하려한다. 이 때 데이터의 전송과정에서 발생하는 엔디안의 변환과정을 설명해보자.

    빅 엔디안으로 저장된 4바이트 정수를 리틀 엔디안 형식에 맞도록 상위 바이트와 하위 바이트의 순서를 바꾸어 변환

  10. ‘루프백 주소(loopback address)’는 어떻게 표현되며, 의미하는 바는 무엇인가? 그리고 루프백 주소를 대상으로 데이터를 전송하면 어떠한 일이 벌어지는가?

    루프백 주소는 127.0.0.1의 고정된 IP주소를 가지고있으며, 자기자신의 IP주소를 의미함
    루프백 주소를 대상으로 데이터 전송시 데이터는 데이터를 전송한 컴퓨터로 수신됨