제임스딘딘의
Tech & Life

개발자의 기록 노트/C

[네트워크/C] sockaddr, sockaddr_in, sockaddr_un 구조체 - 소켓 주소 정보를 나타낸다

제임스-딘딘 2017. 6. 14. 01:56

기본 지식

'Socket Address Structures' 에 관해서 설명하는 글이다.

리눅스/유닉스 시스템에서는 소켓(socket)의 통신 대상을 지정하기 위해 '주소(address)'를 사용한다.
이 '주소' 라는 것을 저장하거나 표현하는데 사용하는 구조체가 바로, 본 포스팅에서 설명하고자 하는 'struct sockaddr' 이다.
bind( ), connect( ) 와 같은 함수들이 2번째 매개변수로써 바로 이 'struct sockaddr' 을 받는다.

이 struct sockaddr은 기본 형태이고, 주소체계(Address family)값에 따라서 구조체를 형변환 해서 사용하면 편리하다.
즉, 일반적으로 개발을 하다보면 struct sockaddr_in, struct sockaddr_un, struct sockaddr_in6 등의 구조체와 상호 형변환을 해서 사용하게 된다.

주소체계 유형은 아래와 같은 것들이 있다

AF_INET, AF_INET6, AF_UNIX, AF_LOCAL, AF_LINK, AF_PACKET


관련 헤더

다음 세가지 헤더파일이 관련있다.

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */



sockaddr 구조체

sockaddr 구조체는 소켓의 주소를 담는 기본 구조체 역할을 한다.

속된말로, '소켓 주소의 와꾸[각주:1]를 잡는 녀석' 쯤으로 이해하면 쉽다.

아래와 같이 정의되어 있다.

struct sockaddr {
	u_short    sa_family;     // address family, 2 bytes
	char    sa_data[14];     // IP address + Port number, 14 bytes
};


구성은 정말 간단하다. 2개의 멤버변수를 가진 구조체이다.

 sa_family : 주소체계를 구분하기 위한 변수이며, 2 bytes 이다. 참고로, u_short는 unsigned short를 말한다.

 sa_data : 실제 주소를 저장하기 위한 변수다. 14 bytes 이다.

즉, 이 구조체는 16 bytes 의 와꾸(틀, 크기)를 잡아주는 녀석이다.


sockaddr_in 구조체 : AF_INET인 경우

위에서 정의된 sockaddr 구조체에서 sa_family가 AF_INET인 경우에 사용하는 구조체이다.

sockaddr을 그대로 사용할 경우, sa_data에 IP주소와 Port번호가 조합되어 있어 쓰거나 읽기 불편하다.

그래서 sockaddr_in을 사용한다. 이 구조체에서 사용하는 IP주소는 IPv4 주소체계를 사용한다.


struct sockaddr_in {
	short    sin_family;          // 주소 체계: AF_INET
	u_short  sin_port;            // 16 비트 포트 번호, network byte order
	struct   in_addr  sin_addr;   // 32 비트 IP 주소
	char     sin_zero[8];         // 전체 크기를 16 비트로 맞추기 위한 dummy
};

struct  in_addr {
	u_long  s_addr;     // 32비트 IP 주소를 저장 할 구조체, network byte order
};


sin_family : 항상 AF_INET을 설정한다. 이것은 필수다.

sin_port : 포트번호를 가진다. 2bytes 이다. 즉, 포트번호는 0~65535 의 범위를 갖는 숫자 값이다. 이 변수에 저장되는 값은 network byte order이어야 한다. 

참고로, 1024이하의 포트 번호는 'privileged port' 혹은 'reserved port' 라고 불리우는 예약된 포트이다. 따라서, 권한을 가진 프로세스만이 1024 이하의 소켓주소에 대해서 바인딩 (bind) 할 수 있다. 여기서 '권한을 가진 프로세스' 라 함은, 리눅스에서는 'CAP_NET_BIND_SERVICE' 라는 capability를 가지고 있는 사용자 영역(user space)의 프로세스를 지칭한다.

두번째 참고로, raw socket (socket type을 'SOCK_RAW' 로 생성한 소켓)은 포트 번호 라는 개념을 가지지 않는다. 포트번호는 TCP 혹은 UDP에서만 구현하는 개념이다.

sin_addr : 호스트 IP주소이다.
이 변수에는 INADDR_ 로 시작하는 값, 예를 들면 'INADDR_ANY' 와 같은 것이 저장되어야 한다. 혹은 inet_aton( ), inet_addr( ), inet_makeaddr( ) 과 같은 라이브러리가 제공하는 함수의 반환값이 저장되어야 한다. 혹은 name resolver를 통해 직접 설정도 가능하다. 이 방법은 gethostbyname( )을 알아보면 된다.

sin_zero : 8 bytes dummy data이다. 반드시 모두 0으로 채워져 있어야 한다. sockaddr_in가 sin_zero를 제외한 크기(sin_family + sin_port + sin_addr)가 8 bytes이므로, 총합이 16 bytes이다. struct sockaddr구조체와 크기를 일치시키려는 목적인 걸 눈치 챌 수 있을 것이다.

여백을 채워서 16 bytes로 만든다는 뉘앙스로 padding bytes 혹은 padding data 라고도 한다.


#struct sockaddr_in 사용 예제

아래는 소켓을 열고 원격지의 호스트 (주소 127.0.0.1, 포트 80)에 연결하는 방법을 보여준다.

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <inttypes.h>
#include <strings.h>

int main(void)
{
	int sock;
	struct sockaddr_in in;

	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		perror("socket");
		return (1);
	}

	bzero(&in, sizeof (struct sockaddr_in));
	in.sin_family = AF_INET;
	in.sin_port = htons(80);
	if (inet_pton(AF_INET, "127.0.0.1", &in.sin_addr) != 1) {
		perror("inet_pton");
		return (1);
	}

	if (connect(sock, (struct sockaddr *)&in,
				sizeof (struct sockaddr_in)) != 0) {
		perror("connect");
		return (1);
	}

	/* use socket */

	return (0);
}



sockaddr_in6 구조체 : AF_INET6인 경우

IPv6 주소체계의 소켓주소를 사용하는 구조체이다.

IPv4에 사용되는 struct sockaddr_in과는 달리, struct sockaddr_in6는 bzero( ) 또는 memset( )과 같은 함수를 통해 0으로 초기화 해주어야 하는 몇가지 멤버변수들이 추가되어 있다. 만약 strcut sockaddr_in6을 사용하기 전에 모두 0으로 초기화 하지 않으면, 프로그램은 정의되지 않은 동작을 야기할 수 있으므로 주의 해야 한다.

아래와 같은 멤버변수를 가지고 있다.

#include <netinet/in.h>

struct sockaddr_in6 {
	sa_family_t     sin6_family;   /* Address family, AF_INET6 */
	in_port_t       sin6_port;     /* port number */
	uint32_t        sin6_flowinfo; /* IPv6 flow information and traffic class */
	struct in6_addr sin6_addr;     /* IPv6 address */
	uint32_t        sin6_scope_id; /* Interface Scope ID (new in 2.4) */
};

struct in6_addr {
	unsigned char   s6_addr[16];   /* IPv6 address */
}; 

sin6_family : 항상 AF_INET6 이어야만 한다.

sin6_port : IPv6 포트를 저장하는 변수이다.
이 값은 직접 조작하기보다는, ntohs( ) htons( ) 로 조작하는 같이 좋다. 혹은 앞의 2개 함수와 동등한 수준으로 직접 조작하거나 말이다.

sin6_flowinfo : IPv6 헤더와 연관된 트래픽 클래스와 플로우 레이블을 포함한다. 

sin6_addr : 16bytes (128bits)의 IPv6주소를 저장하는 변수이다.
inet_pton( ) 함수 외에도, netinet/in.h 헤더파일에는 IPv6 주소를 조작하고 테스트하기 위한 많은 매크로를 정의하고 있다. 이들에 대한 설명은, 추후 IPv6에 대해 설명하는 포스팅에서 따로 자세히 다뤄보도록 하겠다.

sin6_scope_id : sin6_addr의 주소범위에 따라 달라지는 식별자를 포함할 수 있다. 프로그램은 sin6_scope_id를 초기화 할 필요는 없다. 다양한 라이브러리 함수 호출의 결과로, 운영체제에 의해 채워지는 값이다.


#struct sockaddr_in6 사용 예제

아래 예제는 소켓을 열고 포트 12345의 로컬 IPv6 주소 :: 1 포트에 바인드하도록 준비하는 방법을 보여준다.
예제는 libsocket과 libnsl 을 링크해야 한다. (컴파일시 링크옵션 추가 필요함 : -lsocket -lnsl)

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <inttypes.h>
#include <strings.h>

int
main(void)
{
	int sock6;
	struct sockaddr_in6 in6;

	if ((sock6 = socket(AF_INET6, SOCK_STREAM, 0)) < 0) {
		perror("socket");
		return (1);
	}

	bzero(&in6, sizeof (struct sockaddr_in6));
	in6.sin6_family = AF_INET6;
	in6.sin6_port = htons(12345);
	if (inet_pton(AF_INET6, "::1", &in6.sin6_addr) != 1) {
		perror("inet_pton");
		return (1);
	}

	if (bind(sock6, (struct sockaddr *)&in6,
				sizeof (struct sockaddr_in6)) != 0) {
		perror("bind");
		return (1);
	}

	/* use server socket */

	return (0);
}




sockaddr_un 구조체 : AF_UNIX 또는 AF_LOCAL인 경우

struct sockaddr_un은 하나의 시스템에서 서로다른 프로세스 사이의 통신에 사용되는 소켓의 주소를 지정하는데 사용하는 구조체이다.

'Unix Domain Socket' 이라고도 한다. 이 Unix domain 소켓은 파일 시스템의 경로에 의해 식별된다.

struct sockaddr_un은 아래와 같은 멤버를 가지고 있다.


#include <sys/un.h>

#define UNIX_PATH_MAX    108

struct sockaddr_un {
    sa_family_t sun_family;               /* AF_UNIX */
    char        sun_path[UNIX_PATH_MAX];  /* pathname */
};

sun_family : 항상 AF_UNIX 값을 가져야 한다.

sun_path : 파일 시스템 경로를 지정한다. NUL 로 끝나는 문자열(C string)이어야 한다. 경로의 최대 길이는 NUL terminator를 포함해서 108 bytes이다.


주의 

응용프로그램이 자신의 local address를 표현할 때도 사용하며, 원격지의 상대방 주소를 표현할 때도 소켓 주소(socket address)로 표현한다.


참고 자료

https://illumos.org/man/3socket/sockaddr_in

  1. 일본어로 테두리 또는 범위라는 뜻이다. 자세한 내용은 다음 주소를 찾아가보자. https://namu.wiki/w/%EC%99%80%EA%BE%B8 [본문으로]