본문 바로가기

별걸다하는 IT/네트워크_소켓_통신

[네트워크/소켓 C언어 프로그래밍] 주소 체계 저장 방법 sockaddr, sockaddr_in, sockaddr_in6 구조체 알아보기, 설명과 사용예제.

반응형

안녕하세요!!

저번시간에는 가장 기초적인 서버/클라이언트 프로그램을 작성해보았어요.

전체적인 흐름을 잡는데는 이해가 되었을 거 같으나, 내부적으로 코드 한줄 한줄이 궁금한 사람이 있을 것 같아서~~

네트워크 프로그래밍 중 목적지를 찾는데 중요한 '주소 저장하기' 관련 구조체들과 사용법을 알아보려고 합니다. 

SOCKET ADDRESS STRUCTURES 소켓 주소 체계 알아보기

주소 구조체가 정의되어 있는 이유!

저번 포스팅에서 말했다시피 소켓주소는 'IP+포트'입니다. 즉 목적지에 찾아가려면 IP주소만 있어서는 안되고 포트 정보까지 필요해요! 

그리고 IP주소도 IPv4이냐 IPv6이냐 등에 따라 IP길이가 다르죠?? IPv4는 32비트 주소체계를 사용하지만 IPv6는 128비트 주소체계를 사용하잖아요. 즉 우리가 어떤 프로토콜을 사용하느냐에 따라 주소 정보를 나타내는 데이터 타입이 달라지는데 이를 사용하기 편하게 주소구조체로 만들어놓았답니다. 주소 프로토콜 체계에 따라 사용하기 쉽게 그에 맞는 틀을 형성해주는 것이죠.

 

관련 헤더파일

#include <netinet/in.h> 

#include <arpa/inet.h> -> 이 안에 netinet/in.h 포함되어있음 둘 중 하나만 넣어줘도 됩니다.

주소 구조체 종류

크게 주소를 담는 구조체는 sockaddr, sockaddr_in, sockaddr_in6, sockaddr_un 등등.. 있습니다.

 

주소체계에 따른 소켓 구조체는 아래와 같습니다. (기본 틀 제외)

Socket Structure Address Family
struct sockaddr_dl AF_LINK
struct sockaddr_in AF_INET
struct sockadrr_in6 AF_INET6
struct sockaddr_ll AF_PACKET
struct sockaddr_un AF_UNIX

각각의 구조체가 어떤 필드로 구성되어 있고 또 네트워크 프로그래밍을 하기 위해서는 구조체를 어떻게 채워줘야 하는지 알아볼거예요. 이 전체를 한 포스팅에 담긴 무리가 있으니 오늘은 우리가 가장 많이 사용하는 IPv4와 IPv6에 해당하는 주소구조체에 대해 다뤄보도록 하겠습니다.

1. sockaddr

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

그래서 보통 connect(연결요청)과 같은 함수들이 인자 타입으로 sockaddr을 받아요

sockaddr_in으로 했던 sockaddr_un으로 했던 결국 sockaddr타입으로 형변환한 값이 매개변수로 들어가게 됩니당

저번시간에 작성했었던 코드인데요 빨간색 부분 보면 선언은 sockaddr_in으로 해줬지만, 인자로 넣을 때에는 sockaddr*타입으로 형변환을 해주죠?! (서버 프로그램에서 bind 함수에서도 마찬가지예요)

좀 더 코드를 유연하게 만들기 위해 C/C++의 다형성 특징(Polymorphism)을 사용했다고 생각하면 이해가 쉬워요 :)

나는 모든것을 받아주는 generic 틀이노라!!

 

결론: sockaddr 구조체는 일반적인 (범용적으로 사용 가능한) 구조체이다. 

 

근데 void* 타입을 사용하면 되지 sockaddr*을 사용해야만 하는가?

여러 종류 주소 정보 구조체 포인터를 받기 위해 sockaddr*을 사용해야만 하는가? void*을 쓰면 되는거 아닌가?

함수의 매개변수 데이터 타입을 void 포인터(void*)로 선언하는 경우, 어떤 변수의 포인터이든지 인자 값으로 받을 수 있기 때문에 sockaddr*을 선언하는 것보다 의미전달이 명확해진다. 따라서 이러한 생각을 할 수도 있으나 다음과 같은 역사적인 이유가 존재한다.

4.2BSD가 1983년에 발표되었고, ANSI 표준 C는 1989년 12월 14일 ANSI X3.159-1989라는 공식 명칭으로 첫 번째 표준이 완성되었다. 즉 BSD 계열 소켓 API가 ANSI-C보다도 약 7년이나 앞서서 발표되었던 것. 그런데 그 당시에는 void 포인터가 표준으로 존재하지 않았다. 결국 후에 발표된 ANSI 표준C에서 void 포인터를 정의하고 있음에도 불구하고 여전히 이전에 발표한 표준을 고수하고 있다. 따라서 sockaddr의 포인터를 인자 타입으로 사용하고 있는 것은 과거의 잔재라고 볼 수 있다. 그러나 지금까지 사용해왔고 앞으로도 그럴 것이다. <from 윤성우 TCP/IP 소켓 프로그래밍 발췌>

 

책에서는 이러한 이유로 void*대신 sockaddr*을 사용하고 있다고 말하고 있습니다. 이러한 이유도 맞는데 저는 어느정도, void*보다는 sockaddr*을 사용하는게 의미전달에도 맞지 않나 싶어요. void*를 쓰면 정말아무거나 올 수 있는거라 어떤 계열의 타입이 와야하는지 파악하기 쉽지 않고, 또 역참조가 불가능하다는 단점도 있는 것 같습니다.

보면 sockaddr 계열의 구조체가 앞에 4바이트를 주소체계를 저장하는데, 역참조 안때리고 sockaddr 참조 때리면 바로 접근이 가능하니까요~!

 

결론! 현재는 주소체계 인자가 필요할 경우 sockaddr*로 형변환을 맞춰서 넣어주게끔 되어있다!

 

sockaddr 구조체 구성

sockaddr 구조체는 아래와 같이 이루어져 있습니다.

struct sockaddr {
     u_short sa_family;
     char sa_data[14];
};

구성은 정말 간단합니다. 멤버변수가 2개밖에 없어요.

[멤버변수]

첫번째 필드 sa_family : 주소체계 

두번째 필드 sa_data : 해당 주소체계에서 사용하는 주소 정보 (IP정보+포트정보)

앞에서 설명한 이유로, 실제 프로그래밍에서는 응용 프로그램에 사용할 프로토콜의 종류에 맞는 별도의 소켓 주소 구조체를 사용합니다. 

2. sockaddr_in

sockaddr_in구조체는 IPv4 주소를 저장하는 구조체입니다. 현재 가장 많이 쓰이는 주소 체계인만큼 흔하게 쓰여요!

바로 구조체 구성 정보를 살펴볼까요?

 

sockaddr_in 구조체 구성 

struct sockaddr_in (
	sa_family_t	sin_family;
	uint16_t	sin_port;
	struct in_addr	sin_addr;
	char		sin_zero[8];
};

[멤버변수]

첫번째 필드 sin_family : 

주소체계를 저장하는 필드입니다. sa_family랑 변수명만 다를뿐 똑같아요. sockaddr_in은 Ipv4를 위한 주소체계이니까 AF_INET을 넣어주면 됩니다.

두번째 필드 sin_port:

포트 정보를 저장합니다.

세번째 필드 sin_addr:

IPv4 주소를 저장합니다. 타입은 in_addr 구조체예요.

struct in_addr {
	uint32_t	s_addr; //32비트 IPv4 주소
}

간단하게 이런 구조를 가지고 있습니다.

네번째 필드 sin_zero:

사용하지 않는 필드입니다. 0으로 채워줘야 합니다.

 

sin_family가 필요한 이유?

여기서 궁금한점! 어차피 sockaddr_in은 IPv4를 의미하는데 왜 첫번째 필드에 굳이 IPv4를 의미하는 AF_INET을 넣어줄까요?

 


과연 이러한 정보를 넣어줄 필요가 있어? 어차피 sockaddr_in이 IPv4의 인터넷 프로토콜 체계를 의미하고 있잖아!

맞는 말이다. 어차피 sockaddr_in이 IPv4의 인터넷 프로토콜 체계를 의미하고 그 안에서 쓰고 있는 주소 체계도 한 종류이므로 굳이 sin_family를 설정할 필요는 없어 보인다. 그러나 다음과 같은 역사정 배경을 뒤로 하고 있다.

 

과거 BSD 소켓이 개발되던 과정에서 전문가들은 "앞으로 하나의 프로토콜 체계 안에서 여러 주소 체계가 사용될 수도 있을 것이다"라고 예상을 하였다. 따라서 sockaddr_in 데이터 타입을 정의할 때, 주소 체계를 독립적으로 설정할 수 있도록 sin_family라는 이름의 멤버를 넣어 두었다.

결과적으로 오늘날 프로토콜 체계안에서 존재하는 주소체계는 하나이다. 그래서 sin_family의 설정은 불필요해 보인다. 그래도 반드시 넣어주어야 한다. 

<from 윤성우 TCP/IP 소켓 프로그래밍 발췌>


sin_zero 사용하지 않는데 있는 이유에 대해서는 아래 포스팅을 참조해주세요!

sockaddr_in 사용 예시 

sockaddr_in 구조체에다가 사용하려는 주소 정보를 할당해볼게요.

struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(3030);

세번째 라인: sockaddr_in이니까 AF_INET을 할당해줍시다.

네번째 라인: inet_addr은 추후 관련 함수들을 모두 다루겠지만 문자열+점으로 이루어진 이 IP주소를 32bit 주소체계로 변환해줄뿐만 아니라 네트워크바이트오더에 맞게끔 변환도 같이 해줍니다.

다섯번째 라인: htons는 host 바이트 순서에서 네트워크 바이트 순서로 변환해주는 함수입니다.

▼ 네트워크 바이트 오더 개념이 궁금하신 분은 아래 포스팅을 참조해주세요!

 

[Byte Order 바이트 오더] 빅엔디안(Big Endian)과 리틀엔디안(little endian) - 1편, 몰랑이말랑이블로그

안녕하세요~~!! 오늘도 시작되는 말랑이몰랑이 블로그 포스팅입니다~ ㅎㅎ 오늘은 네트워크나 통신쪽을 공부한다면 알고 있어야 할 Byte Order 의 빅엔디안과 리틀엔디안에 대한 개념을 완전하게 잡아보는 시간을..

jhnyang.tistory.com

3. sockaddr_in6

sockaddr_in6 구조체는 눈치채셨겠지만 IPv6 주소체계를 저장하기 위한 구조체입니다.

 

sockaddr_in6 구조체 구성 [4.3BSD]

4.4BSD버전에는 맨 앞에 길이를 담는 멤버변수가 하나 추가되어 있습니다. sockaddr_in6구조체는 sockaddr_in과 달리 더 많은 필드들을 가지고 있어요. 이러한 필드들은 자주 사용되지 않는 IPv6의 여러 기능들도 고려한 것이며 잘 사용하지 않는 것들도 있다는거~!

typedef struct sockaddr_in6 {
	sa_family_t	sin6_family;
 	in_port_t	sin6_port;
 	uint32_t	sin6_flowinfo;
 	struct in6_addr	sin6_addr;
 	uint32_t	sin6_scope_id;
}

[멤버변수]

첫번째 필드 sin_family : 

주소체계를 저장하는 필드입니다. sockaddr_in6은 IPv6를 위한 주소체계이니까 AF_INET6를 넣어주면 됩니다.

두번째 필드 sin6_port:

마찬가지로 포트 정보를 저장하는 필드!

세번째 필드 sin6_flowinfo:

출처: https://tools.ietf.org/id/draft-ietf-ipngwg-rfc2553bis-04.txt

32비트 필드로, IPv6 헤더 정보에서 찾을 수 있는 traffic class와 flow label(플로우 레이블)를 포함하고 있습니다. 20bits전까지는 flow label, 그리고 20~27까지는 traffic class를 의미합니다. 대게 이 필드는 0입니다. (자세한건 IP헤더 정보 공부하기!) 

네번째 필드 sin6_addr:

IPv6 주소를 여기다 저장!

struct sin6_addr {
	uint8_t s6_addr[16];
};

 IPv6는 128bit인거 아시죠? 8bit짜리 16개 해서 총 128bit를 저장합니다. 

다섯번째 필드 sin6_scope_id:

인터페이스 scope identifier.

출처: https://networkengineering.stackexchange.com/questions/46653/what-is-the-use-of-the-ipv6-scope-id

link-local 주소체계에서는 모든 인터페이스가 같은 네트워크를 가지고 있습니다. 그래서 어떤 인터페이스인지 구분할 필요가 있는데 그 값을 여기에 넣어주는 겁니다. 특정 조건에서 사용되는 것이므로 대게 이 필드 값은 0입니다.

 

sockaddr_in6 사용 예시 

sockaddr_in6 구조체에다가 사용하려는 주소 정보를 할당해볼게요.

struct sockaddr_in6 serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin6_family = AF_INET6;
serv_addr.sin6_flowinfo = 0;
serv_addr.sin6_addr =  in6addr_any;
serv_addr.sin6_port = htons(3500);

 

오늘은 여기까지입니다! 오늘도 찾아주신 방문자분들 감사드려요. 포스팅이 도움이 되셨다면 공감/댓글 또는 광고보답 중 하나는 어떤가요? 정보를 널리 전파하는데 큰 즐거움이 됩니다.

 

반응형