공부중

[TCP/IP]주소체계와 데이터 정렬 본문

Programing/TCP IP 소켓

[TCP/IP]주소체계와 데이터 정렬

곤란 2013. 1. 8. 22:42
반응형

IP는 Internet Protocol의 약자로 인터넷 상에서 데이터를 송수신할 목적으로 컴퓨터에게 부여하는 값이다.

반면 PORT번호는 컴퓨터에게 부여하는 값이 아닌, 프로그램상에서 생성되는 소켓을 구분하기 위해 소켓에 부여되는 번호를 뜻한다.

 

∙인터넷 주소(Internet Address)

인터넷에 컴퓨터를 연결해서 데이터를 주고받기 위해서는 IP주소를 부여 받아야 한다.

IP주소 체계는 두 종류로 나누어진다.

  • IPv4(Internet Protocol version 4)

4바이트 주소체계

  • IPv6(internet Protocol version 6)

16바이트 주소체계

네트워크 주소(네트워크 ID)란 네트워크의 구분을 위한 IP주소의 일부를 말한다.

IPv4 기준의 4바이트 IP주소는 네트워크 주소와 호스트(컴퓨터) 주소로 나뉘며 주소의 형태에 따라서 A,B,C,D,E 클래스로 분류가 된다.

클래스 A

1Byte

1Byte

1Byte

1Byte

 

네트워크 ID

호스트 ID

     

클래스 B

1Byte

1Byte

1Byte

1Byte

 

네트워크 ID

호스트 ID

   

클래스 C

1Byte

1Byte

1Byte

1Byte

 

네트워크 ID

호스트 ID

   

클래스 D

1Byte

1Byte

1Byte

1Byte

 

멀티캐스트 IP주소

 

 

⊙라우터와 스위치

네트워크를 구성하기 위해 외부로부터 수신된 데이터를 호스트에 전달하고 호스트가 전달하는 데이터를 외부로 송신해주는 장치를 라우터 또는 스위치라 한다.

 

∙클래스 별 네트워크 주소와 호스트 주소의 경계

IP주소의 첫 번째 바이트만 가지고 네트워크 주소가 몇 바이트인지 판단이 가능하다.

  • 클래스 A의 첫 번째 바이트 범위

0 ~ 127

  • 클래스 B의 첫 번째 바이트 범위

128 ~ 191

  • 클래스 C의 첫 번째 바이트 범위

192 ~ 223

 

아래는 위와 같은 표현이다.

  • 클래스 A의 첫 번째 비트는 항상 0으로 시작
  • 클래스 B의 첫 두 비트는 항상 10으로 시작
  • 클래스 C의 첫 세 비트는 항상 110으로 시작

 

이러한 기준이 정해져 있기 때문에 소켓을 통해서 데이터를 송수신할 때, 별도로 신경쓰지 않아도

네트워크로 데이터가 이동하고 호스트로 데이터가 전송되는 것이다.

 

∙소켓의 구분에 활용되는 PORT번호

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

하나의 운영체제 내에서 동일한 PORT번호를 둘 이상의 소켓에 할당할 수 없다.

PORT번호는 16비트로 표현된다. 때문에 잘 할당할 수 있는 PORT번호의 범위는 0 이상 65535이하이다.

그러나 0부터 1023번까지는 '잘 알려진 PORT(Well-known PORT)'라 해서

특정 프로그램에 할당하기로 예약이 되어있기 때문에 이 범위를 제외한 다른 값을 주어야 한다.

 

PORT번호는 중복이 불가능 하나 TCP소켓과 UDP소켓은 PORT번호를 공유하지 않기 때문에 중복 되어도 상관없다.

TCP소켓을 생성시 9190 PORT번호를 할당했을때 다른 TCP소켓에는 9190 PORT번호를 할당이 불가능 하나 UDP소켓에는 가능하다.

 

데이터 전송의 목적지 주소에는 IP주소 뿐만이 아닌 PORT번호도 포함이 되어야 해당하는 응용프로그램 소켓까지 데이터를 전달 할 수 있기 때문이다.

 

∙IPv4 기반의 주소표현을 위한 구조체

 

struct sockaddr_in

{

    sa_family_t    sin_family;        //주소체계(Adress Family)

    uint16_t    sin_port;        //16bit TCP/UDP PORT번호

    struct in_addr    sin_addr;        //32bit IP 주소

    char        sin_zero[8];        //사용되지 않음

};

 

위에 언급된 in_addr은 다음과 같다.

struct in_addr

{

    in_addr_t    s_addr;        //32bit IPv4 인터넷 주소

};

 

uint16_t,in_addr_t와 같은 자료형은 POSIX(Portable Operating System Interface)에서 찾을 수 있다.

POSIX란 유닉스 계열의 운영체제에 적용하기 위한 표준을 의미 한다.

자료형 이름

자료형에 담길 정보

선언된 헤더파일

int8_t

signed 8-bit int

sys/types.h

uint8_t

unsigned 8-bit int(unsigned char)

int16_t

signed 16-bit int

uint16_t

unsigned 16-bit int(unsigned short)

int32_t

signed 32-bit int

uint32_t

unsigned 32bit- int(unsigned long)

sa_family_t

주소체계(address family)

sys/socket.h

socklen_t

길이정보(length of struct)

in_addr_t

IP주소정보, uint32_t로 정의되어있음

netinet/in.h

in_port_t

PORT번호정보, uint16_t로 정의되어있음

 

POSIX에서 정의하고 있는 자료형이다.

 

이렇게 자료형을 정의한 이유는 확장성을 고려했기 때문에 그렇다.

ex) int32_t를 사용하면 int가 기본으로 64bit으로 바뀌어도 4바이트로 고정이 된다.

 

∙구조체 sockaddr_in의 멤버 분석

⊙멤버 sin_family

주소체계(Adress Family)

의 미

AF_INET

IPv4 인터넷 프로토콜에 적용하는 주소체계

AF_INET6

IPv6 인터넷 프로토콜에 적용하는 주소체계

AF_LOCAL

로컬 통신을 위한 유닉스 프로토콜의 주소체계

프로토콜 체계마다 적용하는 주소체계가 다르므로 sin_family에 적용할 주소체계 정보를 저장하면 된다.

 

⊙멤버 sin_port

16bit PORT번호를 저장한다.

PORT번호를 저장한다는 사실보다 '네트워크 바이트 순서'로 저장해야 한다는 사실이 더 중요하다.

 

⊙멤버 sin_addr

32bit IP주소정보를 저장한다. 이것도 ''네트워크 바이트 순서'로 저장해야 한다

이 멤버를 정확히 파악하기 위해서는 구조체 in_addr도 함께 살펴야 한다.

그러나 in_addr의 유일한 멤버가 uint32_t로 선언되어 있어서 32bit 정수자료형 으로 인식해도 된다.

 

⊙멤버 sin_zero

특별한 의미는 없고 단순히 구조체 sockaddr_in의 크기를 구조체 sockaddr와 일치시키기 위해 삽입된 멤버이다.

하지만 반드시 0으로 채워야 하고 만일 0으로 채우지 않으면 원하는 결과를 얻지 못한다.

 

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

CPU가 데이터를 메모리에 저장하는 방식은 다음과 같이 두 가지로 나뉜다.

이는 CPU가 데이터를 해석하는 방식도 두 가지로 나뉜다는 뜻이다.

 

4byte정수 1을 2진수로 표현하면

00000000 00000000 00000000 00000001

위의 순서 그대로 메모리에 저장하는 CPU가 있는 반면

00000001 00000000 00000000 00000000

이렇게 거꾸로 저장하는 CPU도 있다.

 

이 때문에 이러한 부분을 고려하지 않고 데이터를 송수신하면 문제가 발생할 수 있다.

저장순서가 다르다는 것은 전송되어온 데이터의 해석 순서가 다르기 때문이다.

 

 

  • 빅 엔디안(Big Endian)

상위 바이트의 값을 작은 번지수에 저장하는 방식

  • 리틀 엔디안(Little Endian)

상위 바이트의 값을 큰 번지수에 저장하는 방식

ex) 0x20번지를 시작으로 4byte형 int형 정수 0x12345678을 저장한다고 했을시

0x20번지

0x21번지

0x22번지

0x23번지

0x12 

0x34

0x56 

0x78 

    

정수 0x12345678

→빅 엔디안 바이트 표현

 

정수 0x12345678에서 0x12가 최상위, 0x78이 최하위 바이트이므로 빅 엔디안 방식에서 최상위 바이트인 0x12부터 저장된다.

 

0x20번지

0x21번지

0x22번지

0x23번지

0x78

0x56

0x34

0x12

    

정수 0x12345678

→리틀 엔디안 바이트 표현

 

이렇게 데이터 저장방식은 CPU마다 다르다

CPU의 데이터 저장방식을 의미하는 '호스트 바이트 순서(Host Byte Order)'는 CPU에 따라서 차이가 난다.

Intel계열 CPU는 리틀 엔디안 방식으로 데이터를 저장한다.

 

빅 엔디안 시스템에서 0x12, 0x34의 조합으로 만들어지는 값은 리틀 엔디안 시스템에서 0x34, 0x12의 조합으로 만들어지는 값과 같다.

바이트 순서에 대한 문제를 고려하지 않고 0x12, 0x34순으로 데이터를 전송하면 리틀 엔디안 시스템에서는 0x1234가 아닌 0x3412가 되어버리므로

네트워크를 통해서 데이터를 전송할 때에는 통일된 기준으로 데이터를 전송하기로 약속 하였다.

이 약속을 '네트워크 바이트 순서(Network Byte Order)'라고 한다

 

이 네트워크 바이트 순서의 약속은 네트워크 상으로 데이터를 전송할 때 데이터의 배열을 빅 엔디안 기준으로 변경해서 송수신하기로 약속하였다.

리틀 엔디안 시스템에서는 데이터를 전송하기 앞서 빅 엔디안 정렬방식으로 데이터를 재 정렬해야 한다.

 

∙바이트 순서의 변환(Endian Conversions)

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

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

 

htons에서의 h는 호스트(hose) 바이트 순서, n은 네트워크(network) 바이트 순서를 의미한다.

 

htons는 다음과 같다.

short형 데이터를 호스트 바이트 순서에서 네트워크바이트 순서로 변환해라

 

ntohs는 다음과 같다

short형 데이터를 네트워크 바이트 순서에서 호스트바이트 순서로 변환해라

 

⊙데이터를 전송하기 전에 직접 네트워크 바이트 순서로 데이터를 변경해야하고 수신된 데이터도 호스트 바이트 순서로 변경해서 저장해야 한다고 생각할 수 있지만

이러한 변환의 과정은 자동으로 이루어진다. 때문에 sockaddr_in 구조체 변수에 데이터를 채울 때 이외에는 바이트 순서를 신경쓰지 않아도 된다.

 

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

sockaddr_in 안에서 주소 정보를 저장하기 위해 선언된 멤버는 32bit 정수형으로 정의 되어있다.

IP주소 정보의 할당을 위해서 32bit 정수형태로 IP주소를 표현할 수 있어야 하는데

문자열로 표현된 IP주소를 32bit 정수형으로 변환하고 이 과정에서 네트워크 바이트 순서로의 변환도 동시에 진행되는 함수가 있다.

#include <arpa/inet.h>

in_addr_t inet_addr(const char* string);

성공 시 빅 엔디안으로 변환된 32bit 정수값, 실패시 INADDR_NONE반환

예로 함수 인자로 "210.122.72.168"같이 점이 찍힌 10진수로 표현된 문자열을 전달하면 IP주소 정보를 32bit 정수형으로 반환한다

이때 반환되는 정수는 네트워크 바이트 순서로 정렬 되어있다.

그리고 유효하지 못한 IP주소에 대한 오류검출 능력도 있다.

 

#include <arpa/inet.h>

int iner_aton(const char* string, struct in_addr* addr);

성공 시1(true), 실패 시 0(flase) 반환

  • string

변환할 IP주소 정보를 담고 있는 문자열 주소 값 전달

  • addr

변환된 정보를 저장할 in_addr구조체 변수의 주소 값 전달

기능상으로는 inet_addr 함수와 동일하나 구조체 변수 in_addr를 이용하는 형태라는 차이점이 있다.

 

inet_addr 함수를 사용할시 변환된 IP주소 정보를 구조체 sockaddr_in에 선언되어있는 in_addr구조체 변수에 대입하는 과정을 추가로 거쳐야 하나

inet_aton을 사용할 시 별도의 대입과정 없이 인자로 in_addr구조체 변수의 주소값을 전달하면 변환된 값이 자동으로 in_addr구조체 변수에 저장되기 때문이다.

 

#include <arpa/inet.h>

char *inet_ntoa(struct in_addr adr);

성공 시 변환된 문자열의 주소 값, 실패 시 -1반환

inet_ntoa 함수는 정수형태의 IP정보를 참조하여 문자열 형태의 IP정보로 변환해서 변환된 무자열의 주소 값을 반환한다

 

∙인터넷 주소의 초기화

 

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번호 초기화

 

memset함수는 동일한 값으로 바이트 단위 초기화를 할 때 호출 하는 함수 이다.

addr을 전부 0으로 초기화 하는 이유는, 0으로 초기화 해야 하는 sockaddr_in구조체 멤버 sin_zero를 0으로 초기화 하기 위해서 이다.

atoi함수는 문자열로 표현되어 있는 값을 정수로 변환해서 반환한다.

 

위 코드에서는 문자열로 표현된 IP주소와 PORT 번호를 기반으로 하는 sockaddr_in 구조체 변수의 초기화 과정을 보인 것이다.

 

∙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));

앞에서 언급한 방식과 큰 차이점은 INADDR_ANY라는 이름의 상수를 통해 서버의 IP주소를 할당하고 있다.

소켓의 IP주소를 이렇게 초기화 할 경우 소켓이 동작하는 컴퓨터의 IP주소가 자동으로 할당되기 때문에

IP주소를 직접 입력하는 수고를 덜 수 있다.

 

컴퓨터 내에 두 개 이상의 IP를 할당 받아서 사용하는경우 (multi-homed 컴퓨터 ex. 라우터)

할당 받은 IP중 어떤 주소를 통해서 데이터가 들어와도 PORT번호만 일치하면 수신할 수 있다.

 

⊙서버 소켓 생성시 IP주소가 필요한 이유

IP주소는 컴퓨터에 장작되어있는 NIC(랜카드)의 개수만큼 부여가 가능하고

이러한 경우 서버 소켓이라 할지라도

어느 IP주소로 들어오는(어느 NIC으로 들어오는) 데이터를 수신할지 결정해야 한다.

이 때문에 서버 소켓의 초기화 과정에서 IP주소를 요구하는 것이다

그러나 NIC가 하나뿐인 컴퓨터라면 INADDR_ANY를 이용해 초기화 하는 것이 편리하다

 

∙소켓에 인터넷 주소 할당하기

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

성공 시 0, 실패 시 -1 반환

  • sockfd

주소정보를(IP와 PORT) 할당할 소켓의 파일 디스크립터

  • myaddr

할당하고자 하는 주소정보를 지니는 구조체 변수의 주소 값

  • addrlen

두 번째 인자로 전달된 구조체 변수의 길이정보

bind함수는 초기화된 주소정보를 소켓에 할당한다.

 

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

이 두 함수는 앞서 설명한 리눅스의 경우와 차이가 없다.

 

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

inet_addr과 inet_ntoa은 리눅스와 같으나 윈도우에는 inet_aton함수가 존재하지 않는다.

 

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

윈도우에서 소켓에 인터넷 주소를 할당하는 과정은 리눅스와 동일하다

bind 함수의 의미와 매개변수 및 반환형 모두 동일하다

리눅스 기반의 소켓 초기화 및 주소 할당의 과정과 차이가 없다.

 

∙WSAStringToAddress & WSAAddressToString

이 둘은 inet_ntoa와 inet_addr함수와 기능은 같으나 다양한 프로토콜에 적용이 가능하다는 장점이 있다.

IPv4뿐만이 아닌 IPv6에서도 사용이 가능하나 이 두 함수를 사용하면 윈도우에 종속적인 코드가 만들어 지므로 이식성이 떨어진다.

 

#include <winsock2.h>

INT WSAStringToAddress(     

    LPTSTR addressstring, INT AddressFamily,

    LPWSAPROTOCOL_INFO lpProtocolInfo, LPSOCKADDR lpAddress, LPINT lpAddressLength

);

성공 시 0, 실패 시 SOCKET_ERROR반환

  • AddressString

IP와 PORT번호를 담고 있는 문자열의 주소 값 전달

  • AddressFamily

첫 번째 인자로 전달된 주소 정보가 속하는 주소체계 정보전달

  • lpProtocolInfo

프로토콜 프로바이더(Provider) 설정, 일반적으로 NULL 전달

  • lpAddress

주소정보를 담을 구조체 변수의 주소 값 전달

  • lpAddressLength

네 번째 인자로 전달된 주소 값의 변수 크기를 담고 있는 변수의 주소 값 전달.

이 함수는 주소정보를 나타내는 문자열을 가지고 주소정보 구조체 변수를 적절히 채워 넣을 때 호출하는 함수이다.

 

#include <winsock2.h>

INT WSAAddressToString(     

    LPSOCKADDR lpsaAddress, DWORD dwAddressLength,

    LPWSAPROTOCOL_INFO lpProtocolInfo, LPTSTR lpszAddressString,

    LPDWORD lpdwAddressStringLength

);

성공 시 0, 실패 시 SOCKET_ERROR반환

  • lpsaAddress

문자열로 변환할 주소정보를 지니는 구조체 변수의 주소 값 전달.

  • dwAddressLength

첫번째 인자로 전달된 구조체 변수의 크기 전달.

  • lpProtocolInfo

프로토콜 프로바이더(Provider) 설정, 일반적으로 NULL 전달

  • lpszAddressString

문자열로 변환된 결과를 저장할 배열의 주소 값 전달.

  • lpdwAddressStringLength

네 번째 인자로 전달된 주소 값의 배열 크기를 담고 있는 변수의 주소 값 전달

WSAAddressToString함수는 WSAStringToAddress 함수와 반대의 기능이다

구조체 변수에 저장된 데이터를 참조하여 주소정보를 담고 있는 문자열을 만들어서 반환한다.

 

반응형