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

3 minute read

03 주소체계와 데이터 정렬

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

IPv4 주소체계(책 64페이지)

  • class A : 네트워크 ID 1BYTE 호스트 ID 3BYTE : 첫 번째 바이트 범위 [0,127]
  • class B : 네트워크 ID 2BYTE 호스트 ID 2BYTE : 첫 번째 바이트 범위 [128,191]
  • class C : 네트워크 ID 3BYTE 호스트 ID 1BYTE : 첫 번째 바이트 범위 [192,223]
  • class D : 네트워크 ID 4BYTE 호스트 ID 0BYTE
  • IP 주소 203.211.172.103의 경우 203만 보고 네트워크 주소가 3BYTE임을 알 수 있다.

  • 네트워크 주소 : 네트워크의 구분을 위한 IP 주소의 일부. 4바이트 IP 주소 중 일부만 참조해서 일단 특정 도메인의 네트워크로 데이터를 전송
  • 호스트 주소 : 특정 도메인으로 데이터를 보낸 뒤, 호스트 주소를 참조해서 개인에게 데이터를 전송
  • 네트워크 주소를 따라서 데이터가 전송되는 것은 네트워크를 구성하는 라우터 또는 스위치로 데이터가 전송되는 것을 의미한다.

  • 동영상을 보면서 브라우저를 키고 있으면, 동영상과 브라우저를 위한 소켓이 각각 최소 1개씩 필요하다.
  • 컴퓨터에는 NIC(네트워크 인터페이스 카드)라고 불리는 데이터 송수신 장치가 하나씩 달렸다.
  • IP는 데이터를 NIC를 통해서 컴퓨터 내부로 전송하는데 사용된다.
  • NIC를 통해서 수신된 데이터 안에는 PORT 번호가 새겨져 있다.
  • 운영체제는 이 포트 정보를 참조해서 일치하는 포트번호의 소켓에 데이터를 연결한다.
  • 포트 번호는 중복이 불가능하지만 TCP 소켓과 UDP 소켓은 포트번호를 공유하지 않는다. TCP소켓에 9190를 할당해도 UDP소켓에 9190을 할당할 수 있다.

03-2 주소 정보의 표현

주소 정보를 담을 때는 어떤 주소체계를 사용하는지 / IP 주소가 무엇인지 / port 번호가 무엇인지를 알아야 한다. 아래의 구조체는 bind 하무에 주소 정보를 전달해서 소켓에 주소 정보를 전달하는 연락을 한다.

struct sockaddr_in{
    sa_family_t     sin_family; //주소체계(Address Family) e.g) AF_INET
    uint16_t        sin_port;   // 16비트 TCP/UDP PORT 번호. 단 네트워크 바이트 순서대로 저장해야 함
    struct in_addr  sin_addr;   // 32비트 주소
    char            sin_zero[8]; //사용되지 않음. sockaddr과 struct 크기를 일치시키기 위함
}

sockaddr struct는 아래와 같이 되어 있다.

struct sockaddr{
    sa_family_t     sin_family;
    char            sa_data[14];
}

sa_data에 저장되는 주소정보에는 IP 주소와 PORT 번호가 포함되어야 한다. 그리고 이 두가지 정보를 담고 남은 부분은 0으로 채워져야하는 규칙이 있다. 이를 쉽게 만족시키기 위해서 sockaddr_in struct를 만들고 위에서 설명한 방식대로 진행한다.

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

CPU에 따라서 4바이트 정수 1을 메모리공간에 저장하는 방식이 달라진다. 4바이트 정수 1을 2진수로 표현하면 00000000 00000000 00000000 00000001이다. 이 순서를 메모리에 저장하는 방식에 따라서 빅엔디안리틀엔디안이 나눠진다.

  • 빅 엔디안 : 상위 바이트 값을 작은 번지수에 저장하는 방식
    • 메모리 주소 : 0x20 0x21 0x22 0x23
    • 저장할 데이터 : 0x12345678(0x12가 최상위 바이트)
    • 저장된 데이터 : 0x12 0x34 0x56 0x78
  • 리틀 엔디안 : 상위 바이트의 값을 큰 번지수에 저장하는 방식
    • 메모리 주소 : 0x20 0x21 0x22 0x23
    • 저장할 데이터 : 0x12345678
    • 저장된 데이터 : 0x78 0x56 0x34 0x12

CPU의 데이터 저장방식은 CPU 종류에 따라서 다르며 호스트 바이트 순서라고 부른다. 데이터를 주고 받는 두 호스트의 호스트 바이트 오더가 다르면 전송받은 데이터를 어떤 엔디안에 따라서 해석해야할지 모른다. 따라서 네트워트를 통해서 바이트를 전송할 때는 네트워크 바이트 오더를 정해서 이를 따른다. 네트워크 바이트 오더빅 엔디안의 기준을 따른다.

그래서 htos(), ntohs(), htonl(), ntohl()의 함수를 사용해서 네트워크 바이트 오더로 변환을 한다. 리눅스에서 long 타입은 4바이트라는 점을 기억해야 한다.(short는 2바이트) 예를 들어서 htons는 “short 형 데이터를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하라”라는 뜻이다.

일반적으로 인텔이나 AMD CPU는 리틀엔디안을 사용하기 때문에 htons()를 사용하는 것이 필수적이다.

하지만 데이터를 전송하기 전에 다 바꿔줘야 하는 것은 아니다. 데이터를 보내면 자동으로 네트워크 바이트 오더로 바꿔주고, 수신하면 호스트 바이트 오더로 바꿔준다. 이는 sockaddr_in의 구조체에 변수 데이터를 채울 때는 제외하고는 신경을 쓰지 않아도 된다.

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

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

sockaddr_in 구조체에서 주소 정보를 저장하기 위해서는 32비트 정수형으로 정의된 struct in_addr sin_addr를 채워야한다. 우리가 생각하는 IP 주소는 211.214.107.99과 같은 “점이 찍힌 십진수 표현방식”이지만 이를 정수형으로 바꿔서 표현하는데는 어려움이 있다. 그래서 문자열 IP주소를 정수형으로 바꿔주는 함수 inet_addr()를 사용한다. 물론 이때 반환되는 정수는 네트워크 바이트 순서로 정렬되어 있다. 다시 얘기하지만 우리는 sockaddr_in에 데이터를 채울 때 네트워트 바이트 오더로 바꿔서 채워야한다.

inet_aton()함수는 inet_addr()과 같은 기능을 하지만 전달인자가 in_addr()이라는 점에서 차이점을 가진다. inet_ntoa()는 정수형 IP 주소 정보를 익숙한 형태의 IP주소로 바꿔주는 기능을 한다. 그런데 inet_ntoa()의 반환형은 char* 이기 때문에 inet_ntoa()를 여러 번 호출해야하는 경우 따로 값을 저장해두지 않으면 값이 덮어씌워지는 문제가 발생할 수 있다. strcpy()를 통해서 변환된 문자열 형태를 복사해두면 안전하다.

지금까지의 내용을 이해했다면 아래의 내용을 이해할 수 있다.

struct sockaddr_in addr;
char *serv_ip = "211.217.18.13"; //argc argv로 입력될 때는 char*형으로 생각된다. 
char *serv_port= "9190"; //argc argv로 입력될 때는 char*형으로 생각된다. 
memset(%addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr= inet_addr(serve_ip);
addr.sin_port=htons(atoi(serve_port));

매번 서버의 IP주소를 입력하는 것이 번거롭다면 아래와 같이 하면 된다.

addr.sin_addr.s_addr= inet_addr(serve_ip);

위와 같은 방식의 장점은 컴퓨터에서 다수의 IP주소를 할당해서 사용하는 경우(일반적으로 라우터가 이런 특성을 갖는다.), 할당 받은 IP 중 어떤 주소를 토애서 데이터가 들어오더라고 PORT 번호만 일치하면 수신을 할 수 있게 된다. IP는 컴퓨터에 저장되어 있는 NIC(네트워크 인터페이스 카드, 랜카드)의 갯수만큼 부여받을 수 있다.

지금까지 설명한 내용을 바탕으로 서버 프로그램에서 흔히 등장하는 서버 소켓 초기화의 과정을 정리하면 다음과 같다.

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

serv_sock = socket(PF_INET, SOCK_STREAM, 0);

memset(%addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr= inet_addr(serve_ip);
addr.sin_port=htons(atoi(serve_port));

bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr));