sockaddr 구조체
소켓 주소를 표현하는 구조체다. 아래 정의를 보면 주소 체계와 주소 두가지 정보만 갖고 있는 단순한 구조로 되어있다. 원래 소켓 자체가 TCP/IP만을 목적으로 만들어진 것이 아니어서, 다양한 주소 체계에 맞게 범용 목적으로 사용하기 위해 이런 구조를 가지고 있다.
struct sockaddr{
sa_family_t sa_family; // 소켓의 주소체계. PF_INET= IPv4 주소체계.
char sa_data[14]; // 해당 주소체계에서 사용하는 주소 정보.
}
sockaddr_in 구조체
IPv4 주소체계에서 사용하는 구조체다. 소켓 프로그램은 범용 주소 구조체로 sockaddr을 사용하지만, 주소체계의 종류에 따라 별도의 전용 구조체를 만들어 사용하는게 아무래도 편리할 것이다. 참고로, 기타 다른 주소체계 중 Local Unix 주소 체계는 sockaddr_un 구조체를 사용한다.
소켓라이브러리는 sockaddr을 사용하므로 라이브러리에 주소 정보를 넘길 때는 sockaddr로 형변환을 하여 넘긴다. 그러므로 당연히 구조체의 크기는 동일하다. 아래 세부 구조를 살펴보자.
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과 같은 크기를 유지하기 위해 필요한 패딩(padding) 공간. 항상 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:주소포트번호) *------------* 클라이언트 소켓 주소(IP:주소포트번호)
만약 포트를 사용하지 않고 그냥 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 바이트 형식을 가진다. 예전에 아무 생각없이 htonl(inet_addr("ip"))로 사용했다가 반나절을 삽질한 적이 있다. 이런 오류는 화면상에 표시도 되지 않으므로 디버거를 통해야한 확인이 가능하다. 몇 가지 안되는 소켓관련 함수는 사용법을 꼭 주지해야 한다.