[열혈TCP/IP] 03 주소체계와 데이터 정렬
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));