https://techlog.gurucat.net/292
'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
다음 세가지 헤더파일이 관련있다.
1
2
3
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
sockaddr 구조체는 소켓의 주소를 담는 기본 구조체 역할을 한다.
속된말로, '소켓 주소의 와꾸1를 잡는 녀석' 쯤으로 이해하면 쉽다.
아래와 같이 정의되어 있다.
1
2
3
4
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 구조체에서 sa_family가 AF_INET인 경우에 사용하는 구조체이다.
sockaddr을 그대로 사용할 경우, sa_data에 IP주소와 Port번호가 조합되어 있어 쓰거나 읽기 불편하다.
그래서 sockaddr_in을 사용한다. 이 구조체에서 사용하는 IP주소는 IPv4 주소체계를 사용한다.
1
2
3
4
5
6
7
8
9
10
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)에 연결하는 방법을 보여준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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);
}
IPv6 주소체계의 소켓주소를 사용하는 구조체이다.
IPv4에 사용되는 struct sockaddr_in과는 달리, struct sockaddr_in6는 bzero( ) 또는 memset( )과 같은 함수를 통해 0으로 초기화 해주어야 하는 몇가지 멤버변수들이 추가되어 있다. 만약 strcut sockaddr_in6을 사용하기 전에 모두 0으로 초기화 하지 않으면, 프로그램은 정의되지 않은 동작을 야기할 수 있으므로 주의 해야 한다.
아래와 같은 멤버변수를 가지고 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
#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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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);
}
struct sockaddr_un은 하나의 시스템에서 서로다른 프로세스 사이의 통신에 사용되는 소켓의 주소를 지정하는데 사용하는 구조체이다.
'Unix Domain Socket' 이라고도 한다. 이 Unix domain 소켓은 파일 시스템의 경로에 의해 식별된다.
struct sockaddr_un은 아래와 같은 멤버를 가지고 있다.
1
2
3
4
5
6
7
8
#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://techlog.gurucat.net/292 [하얀쿠아의 이것저것 만들기 Blog]