ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TCP/IP 소켓 프로그래밍 - 3장 : 주소체계와 데이터 정렬
    네트워크 2024. 6. 16. 20:19

     

    3장

    IP 주소 체계

    1. IPv4 ( 4바이트 주소 체계 )
    2. IPv6 ( 16바이트 주소 체계 ) 로 나뉜다.

    IPv4에서 4바이트의 주소는 네트워크 주소호스트 주소로 나뉜다.

    네트워크 주소를 찾아 먼저 데이터가 전송되고 이후 데이터를 전송받은 라우터 또는 스위치

    다시 호스트 주소로 데이터를 전송한다.

    IP는 데이터를 NIC (네트워크 인터페이스 카드) 을 통해 컴퓨터 내부로 전송하는 데 사용된다.

     

    컴퓨터 내부로 전송된 데이터를 소켓에 분배해주는 작업은 운영체제가 담당한다.

    이때 운영체제는, port 번호를 이용소켓에 데이터를 분배하는 작업을 진행한다.

     

    따라서, port 번호는 소켓을 구분하기 위해 사용되므로 동일한 port 번호를 서로 다른 소켓에게 할당할 수 없다!

     

    port 번호의 범위는 0이상 65535이하이지만,

    0부터 1023까지는 well-known port로 특정 프로그램에 예약되어 있기 때문에, 이를 제외하고 할당해야 한다.

     

    데이터 전송의 목적지 주소에는 IP 주소 뿐만아니라 Port 번호도 포함이 되어야 한다.


    03-2 주소정보의 표현

    application 레벨에서 IP 주소와 Port 번호를 표현하기 위한 구조체가 정의되어 있다.

     

    struct sockaddr_in{
        sa_family_t    sin_family; //주소체계
        uint16_t       sin_port;   //16비트 TCP/UDP 포트번호
        struct in_addr sin_addr;   //32비트 IP주소
        char           sin_zero[8];//사용 안됨.
    }

    위 구조체는 bind 함수에 주소 정보를 전달하는 용도로 사용된다.

    1. sin_family : 주소 체계 정보 저장 ( AF_INET : IPv4 인터넷 프로토콜에 적용하는 주소체계 )
    2. sin_port : 16비트 port 번호 저장. ( ** 네트워크 바이트 순서로 저장 )
    3. sin_addr : 32비트 IP 주소 정보 저장. ( ** 네트워크 바이트 순서로 저장 )
    4. sin_zero : 구조체 sockaddr_in의 크기를 구조체 sockaddr과 일치시키기 위한 멤버. (별 의미 없다.)

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

    위에서 두번째 인자는 sockaddr 구조체를 사용하지만 sockaddr 구조체는 주소 정보를 담기에 다소 불편하게 정의되어 있다. (아래 참조)


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

    형변환을 통해서 sockaddr에 bind함수가 요구하는 데이터를 채운 효과를 볼 수 있다.


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

    cpu에 따라서 4바이트 정수 1을 메모리에 저장하는 방식이 달라질 수 있다.


    00000000 00000000 00000000 00000001 //빅엔디안
    00000001 00000000 00000000 00000000 //리틀엔디안

    CPU가 데이터를 메모리에 저장하는 방식에는 2가지가 있다.

    1. 빅 엔디안 : 상위 바이트의 값을 작은 번지수에 저장하는 방식
    2. 리틀 엔디안 : 상위 바이트의 값을 큰 번지수에 저장하는 방식

    네트워크 상에서 데이터를 전송할 때엔 빅 엔디안 기준으로 변경해서 송수신해야한다.

     

    unsigned long htonl(unsigned long);은 h, to, n, l로 나누어서

    long 형 데이터를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하라는 의미가 된다.

     

    unsigned short ntohs(unsigned short); 는 n, to, h, s로 나누어서

    short 형 데이터를 네트워크 바이트 순서에서 호스트 바이트 순서로 변환하라는 의미가 된다.

     

    데이터를 전송하기 전에 일일이 다 네트워크 바이트 순서로 바꿔줄 필요는 없다.

    이는 자동으로 해준다.

    주의할 점은 sockaddr_in 구조체에 변수를 채울 때만큼은 직접 바꿔줘야 한다.


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

     

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

    “187.232.182.42”같은 점이 찍힌 10진수의 문자열을 전달하면

    네트워크 바이트 순서로 정렬된 ( 빅 엔디안 ) 32비트 정수를 리턴한다.

     

    inet_aton은 inet_addr과 유사하지만 , 구조체 변수(in_addr)을 이용하면서 활용도가 훨씬 더 높다.

    inet_addr은 변환된 주소정보를 다시 in_addr에 넣는 별도의 작업과정이 필요하지만,

    inet_aton은 인자로 in_addr의 주소 값을 전달하면 자동으로 구조체에 저장이 된다.


    #include <arpa/inet.h>
    
    int inet_aton(const char* string, struct in_addr * addr);
    
    // 성공 시 1(true ), 실패 시 0(false ) 반환

    네트워크 바이트 순서로 정렬된 정수형 IP 주소를 우리가 쉽게 볼 수 있는 문자열로 바꾸는 함수는 아래와 같다.


    #include <arpa/inet.h>
    
    char * inet_ntoa(struct in_addr adr);
    //성공 시 변환된 문자열의 주소 값, 실패 시 -1  반환 

    **주의할 점은 위 함수는 프로그래머에게 별도의 메모리 공간의 할당 요구 없이

    함수 내부적으로 메모리 공간을 할당해 변환된 메모리 정보를 저장하고 있다는 것이다.

     

    따라서, 함수 호출 후에 되도록이면 문자열 정보를 따로 저장해 놔야 한다.


    서버의 주소정보 초기화


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

    위처럼 구현하면, 다른 컴퓨터에서 실행할 때마다 코드를 변경해줘야 하는 경우가 생긴다.

    그래서 프로그램 실행시 main 함수에 IP와 Port번호를 전달하도록 할 수 있다.

    서버에서는 일일이 IP주소를 입력하지 않아도 다음과 같이 초기화를 진행할 수 있다.


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

    위처럼 짜게 되면,

    1. IP주소를 직접 입력할 필요가 없고,
    2. Multi-homed 컴퓨터에서 port번호만 일치하면 수신이 가능해진다.

    바꿔 말하면, 서버 소켓 생성 시 하나의 컴퓨터에 여러개의 IP주소 속에서 어느 IP주소로 들어오는 데이터를 수신할 지 결정할 수 있기도 하다는 뜻이다.

    그 때문에, 서버 소켓 생성시에도 IP 주소를 초기화 해주어야 한다.

    소켓에 인터넷 주소 할당해주기!

    위에서는 sockaddr_in 구조체의 변수 초기화 방법을 정리해보았다.

    초기화된 주소 정보를 소켓에 할당하는 역할은 bind 함수가 맡는다.


    #include <sys/socket.h>
    
    int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
    
    // 성공 시 0, 실패 시 -1 반환
    // myaddr : 구조체의 주소 값
    // addrlen : 2번째 인자로 구조체 변수의 길이정보

    총정리하면 아래와 같은 코드가 된다.

     

    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));  // 주소 정보 할당

     

Designed by Tistory.