Public/tip & tech

sockaddr, sockaddr_in 구조체

quantapia 2009. 8. 17. 10:29

sockaddr 구조체

  소켓 주소를 표현하는 구조체다. 아래 정의를 보면 주소 체계와 주소 두가지 정보만 갖고 있는 단순한 구조로 되어있다. 원래 소켓 자체가 TCP/IP만을 목적으로 만들어진 것이 아니어서, 다양한 주소 체계에 맞게 범용 목적으로 사용하기 위해 이런 구조를 가지고 있다.

 

struct sockaddr{

    sa_family_t sa_family;  // 소켓의 주소체계. PF_INET= IPv4 주소체계.

    char sa_data[14];      // 해당 주소체계에서 사용하는 주소 정보.

}

 

sockaddr_in 구조체

  IPv4 주소체계에서 사용하는 구조체다. 소켓 프로그램은 범용 주소 구조체로 sockaddr을 사용하지만, 주소체계의 종류에 따라 별도의 전용 구조체를 만들어 사용하는게 아무래도 편리할 것이다. 소켓라이브러리는 sockaddr을 사용하므로 라이브러리에 주소 정보를 넘길 때는 sockaddr로 형변환을 하여 넘긴다. 그러므로 당연히 구조체의 크기는 동일하다. 기타 다른 주소체계 중 Local Unix 주소 체계는 sockaddr_un 구조체를 사용한다.

 

struct sockaddr_in{

    sin_family_t sin_family;   // IPv4 주소체계에서 사용하므로 항상 AF_INET으로 설정

    unist16_t sin_port;         // 포트 번호

    struct in_addr sin_addr; // IP주소를 나타내는 32비트 정수 타입 구조체

    char sin_zero[8];         // sockaddr과 같은 크기를 유지하기 위해 필요한 공간. 항상 0.

}

  솔라리스에서 PF_INET과 AF_INET의 정의를 찾아보니 아래 결과가 나온다. 즉 둘은 같은 값이다.

 # grep PF_INET *.h
socket.h:#define        PF_INET         AF_INET
socket.h:#define        PF_INET6        AF_INET6
socket.h:#define        PF_INET_OFFLOAD AF_INET_OFFLOAD /* Sun private; do not use */

 

  위의 sin_zero는 항상 0이어야 하는데 이를 어기면 간혹 IP 주소를 터무니 없는 값으로 인식하는 경우가 생긴다. 그래서 일반적으로 memset (유닉스)이나 ZeroMemory (윈도우즈) 등으로 초기화한 후 사용한다.

 

  위 두 구조체(sin_addr까지 포함하면 3개)의 구조는 각 운영체제마다 사용하는 타입 이름이 다르므로 약간씩 차이가 있다.

 

소켓 페어 (socket pair)

 

  TCP/IP 프로토콜 체계에서 사용하는 주소 체계는 아래의 주소 형식을 가진다. 이는 프로그램상의 규칙이 아니고, IP 프로토콜의 정의라는 것을 기억하자.

소켓 주소: [트랜트포트 포트 번호, IP 주소]

  이 주소를 클라이언트 측과 서버 측의 주소로 결합해서 하나의 소켓 페어(socket pair)라고 부르며 이 소켓페어가 하나의 가상의 통신회선이 된다. 만약 포트를 사용하지 않고 그냥 IP주소로만 소켓 페어를 구성한다고 생각해보자. 그러면 서버와 클라이언트 사이의 가상 회선은 오직 1개만 연결되는 셈이다. 그럴 경우 지금 내가 글을 쓰고 있는 이 네이버의 스마트 에디터 화면에서 네이버 검색을 이용하려면 이 창을 닫아야만 가능하다는 얘기다(새로운 창으로 네이버를 접속할 수 없으니 말이다). 더군다나 웹브라우저는 웹서버를 통해 텍스트, 이미지, 동영상등 무수히 많은 파일을 여러 포트를 통해 다운받는데 이 회선이 모두 사라지면 그 느려 터질 속도 또한 짐작이 가지 않는가?

 

  BSD기반의 소켓 및 윈속 라이브러리는 클라이언트에서 서버로 연결할 때 connect 함수를 사용하는데 어디에도 클라이언트의 IP 주소 및 포트를 설정하는 부분이 없다. 이는 서버 연결시 자동으로 클라이언트의 IP주소와 임의로 생성한 포트 번호를 사용하여 접속하기 때문이다. 그러므로 서버와 연결하는 클라이언트 프로그램을 내 컴퓨터에서 2번 이상 실행하더라도 클라이언트측 포트 번호가 무작위로 설정되어 충돌없는 소켓 페어(가상 회선)를 자동으로 생성되므로 오류없이 실행할 수 있다.

 

  참고로 .NET에서는 소켓 주소 표현으로 EndPoint라는 클래스를 사용하는데, 클라이언트 측 포트 번호를 임의로 지정할 수 있다(0을 입력하면 라이브러리에서 중복되지 않게 자동으로 할당한다). 이럴 경우 클라이언트 프로그램을 2번 실행하면 어떤 일이 벌어지겠는가? 이미 연결된 포트로 바인딩할 수 없다는 에러가  발생할 것이다. 꼭 한번 테스트해 보시라.

 

표준 예제

/* 서버용 */

struct sockaddr_in server_address;


memset(&server_address,0,sizeof(server_address));
server_address.sin_family=AF_INET;
server_address.sin_addr.s_addr=htons(INADDR_ANY);

server_address.sin_port=htons(PORT); 

 

  INADDR_ANY는 서버의 IP주소를 자동으로 찾아서 대입해주는 함수이다(복잡한 #define문으로 정의되어 있다. long형값 0). INADDR_ANY를 지정할 경우 2가지 이점이 있다. 서버는 NIC을 2개 이상 가지고 있는 경우가 많은데 만일 특정 NIC의 IP주소를 sin_addr.s_addr에 지정하면 다른 NIC에서 요청한 연결은 서비스 할 수 없게 된다. 이때 INADDR_ANY를 사용하면 두 NIC을 모두 바인딩해주므로 어느 IP를 통해 접속하더라도 정상적인 서비스가 가능하다. 또 다른 이점은 이식성인데, 특정 IP를 지정했을 경우 서버가 바뀐 곳에 프로그램이 설치된다면 주소값을 변경해야 하지만 INADDR_ANY를 사용하면 곧바로 컴파일할 수 있는 장점이 생긴다.

 

  서버의 주소와 포트 번호는 IP헤더에 저장되어 네트웍으로 전송되는데 이를 중계하는 라우터들은 항상 네트워크 바이트 방식, 즉 빅 엔디언으로 처리한다. 그러므로, 소켓에서도 빅 엔디언 방식으로 정렬되어 있어야 한다. 그래서 htons(host to network short) 함수로 주소와 포트번호를 변환해서 사용해야 한다.

 

/* 클라이언트용 */

struct sockaddr_in client_address;


memset(&client_address,0,sizeof(client_address));
client_address.sin_family=AF_INET;
client_address.sin_addr.s_addr= inet_addr("192.168.56.1"); // 서버 주소

client_address.sin_port=htons(PORT); 

 

  p.s) 위 예제 중 inet_addr 함수는 문자열을 받아 long형 값을 돌려주는데 Network 바이트 형식을 가진다. 예전에 아무 생각없이 ntonl(inet_addr("ip"))로 사용했다가 반나절을 삽질한 적이 있다. 이런 오류는 화면상에 표시도 되지 않을 뿐더러 디버거를 통해야한 확인이 가능하다. 몇 가지 되지도 않는 소켓관련 함수는 사용법을 꼭 주지해야 한다.