Prev | Contents | Next

3 IP 주소, 구조체들, 데이터 처리(Munging)

분위기를 전환해서 코드에 대해 이야기하는 부분이 되었습니다.

그러나 코드가 아닌 이야기를 조금 더 하겠습니다. 먼저 IP 주소와 포트에 대해 이해하고 넘어가겠습니다. 그 다음 소켓 API가 IP주소와 다른 정보를 저장하고 조작하는 방법에 대해 이야기하겠습니다.

3.1 IP 주소, 4판과 6판(버전4와 버전6)

벤 케노비(역자 주 : 스타워즈의 케릭터)를 아직 오비-완 케노비라고 부르던 좋은 옛 시절에는 인터넷 프로토콜 제4판 또는 IPv4라고 부르던 좋은 네트워크 라우팅 체계가 있었습니다. 그것은 4개의 바이트(또는 네 개의 “옥텟” (역자 주 : 8비트로 이루어진 1바이트를 명시적으로 지칭하는 표현. 컴퓨터의 초기에는 7비트로 이루어진 바이트도 있었습니다.))로 이루어지고 보통 192.0.2.111 같은 “점과 숫자” 형태로 기록되던 주소를 가졌습니다.

여러분은 아마도 그것을 주변에서 보셨을 것입니다.

사실 이 글을 적는 시점에는 인터넷의 거의 모든 사이트가 IPv4를 사용합니다.

오비-완을 비롯해 모두가 행복했습니다. 모든 것이 훌륭했습니다. Vint Cerf 같은 부정적인 사람이 IPv4주소가 고갈되기 직전이라고 모두에게 경고하기 전까지 말입니다.

(다가오는 IPv4의 종말과 어둠을 모두에게 경고한 것 외에도 Vint Cerf13는 인터넷의 아버지로 아주 잘 알려져 있습니다. 그래서 저는 그의 판단에 반기를 들 수가 없습니다.)

주소가 고갈된다니 이게 가능한 일일까요? 32비트 IPv4 주소는 수십억개인데 정말로 가능한지 의문일겁니다. 정말로 세상에 수십억 개의 컴퓨터가 있을까요?

있습니다.

여기에 더해서, 컴퓨터가 정말 적던 초기에는 모두가 수십억은 불가능할 정도로 큰 수라고 생각했습니다. 그래서 일부 큰 조직에 관대하게도 수백만 개의 아이피 주소를 할당해 주었습니다. (그런 식으로 많은 IP 주소를 할당받은 조직의 이름을 몇 개 대자면 Xerox, MIT, Ford, HP, IBM, GE, AT&T, 그리고 애플(Apple)이라고 하는 작은 회사도 있었습니다.)

사실 몇몇 임시방편이 없었다면 아이피는 예전에 다 떨어졌을 것입니다.

그러나 우리는 모든 사람, 모든 컴퓨터, 모든 계산기, 모든 전화기, 모든 주차 정산기, 모든 강아지(강아지들에게도 아이피주소가 필요하겠지요)에게 IP주소가 있는 시대를 살고 있습니다.

그리고 그렇게 해서 IPv6이 태어났습니다. 그리고 Vint Cerf는 아마도 불사의 존재이겠지만(그의 물리적 형체가 사라져야 한다면 그는 이미 Internet2의 깊은 곳에서 일종의 초인공지능 ELIZA14 프로그램 같은 모습으로 존재하고 있을 것입니다.), 다시 한 번 다음 버전의 인터넷 프로토콜에서 주소가 부족해서 그가 “내가 말했지”라는 식으로 이야기하는 것을 듣고 싶어할 사람은 없습니다.

이게 무슨 의미냐고요?

우리에게 아주 많은 주소가 필요하다는 뜻입니다. 두 배도 아니고, 십억배도 아니고, 천조배도 아니고, 7양9천자(역자 주 : 양은 10의 28승을 의미하는 수단위)배가 필요합니다.

“Beej, 그게 사실인가요? 저에게는 큰 수를 신뢰하지 않을 많은 이유가 있습니다.”라고 말씀하실 것입니다. 사실 32비트와 128비트 사이에는 그다지 큰 차이가 없어보일지도 모르겠습니다. 96비트가 더 있을 뿐이니까요. 하지만 우린 지금 거듭제곱에 대해서 생각해야 합니다. 32비트가 대략 40억개의 숫자들을 의미합니다. 128비트는 대략 11업 (역자 주 : 1업은 10의 76승을 의미)입니다. (정확히는 2의 128승입니다) 그것은 이 우주의 모든 별에 대해서 IPv4인터넷이 백만 개씩 있는 것과 비슷합니다.

IPv4의 점과 숫자 표기는 잊어버리세요. 이제 우리에겐 콜론으로 구별하는 2바이트 조각으로 구성되는 16진수 표기법이 있습니다. 예시는 아래와 같습니다.

2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551

그게 다가 아닙니다! 대부분의 경우에 여러분은 0이 아주 많이 들어있는 주소를 가지게 될 것입니다. 그리고 그 0들을 두 개의 콜론 사이에 압축해서 넣을 수 있습니다. 또한 각각의 바이트 쌍의 앞에 오는 0은 생략할 수 있습니다. 예를 들어 아래의 각 주소쌍은 동일한 의미입니다.

2001:0db8:c9d2:0012:0000:0000:0000:0051
2001:db8:c9d2:12::51

2001:0db8:ab00:0000:0000:0000:0000:0000
2001:db8:ab00::

0000:0000:0000:0000:0000:0000:0000:0001
::1

::1주소는 재귀 주소(루프백 주소) 입니다. 그것은 언제나 “내가 실행중인 이 기계”를 의미합니다. IPv4에서 루프백 주소는 127.0.0.1입니다.

마지막으로 IPv6주소에는 여러분이 보실지도 모르는 IPv4호환 모드가 있습니다. 예시를 원하신다면 이렇습니다. 192.0.2.33을 IPv6주소로 표기한다면 아래와 같습니다. “::ffff:192.0.2.33”.

이건 아주 재미있는 일입니다.

사실 너무 재미있다고 할 수 있는데, IPv6의 제작자들이 수자(역자 주 : 10의 24승) 개의 주소를 기사도적으로 잘라내어 예약된 주소로 지정했기 때문입니다. 그러나 우리에게는 그러고도 너무 많은 주소가 남아있어서 남은 주소를 세기도 귀찮을 정도입니다. 아직도 은하계의 모든 남녀노소, 강아지, 주차정산기에 배정할 주소가 남아있습니다. 은하계의 모든 행성에 주차정산기가 있다는 것은 확실합니다. 저를 믿으세요.

3.1.1 Subnets(서브넷 또는 부분망)

관리적인 측면에서 때때로 “아이피 주소의 이 비트까지의 첫 부분은 네트워크 부분 이고 나머지는 호스트 부분” 이라고 칭하는 것이 편할 때가 있습니다.

예를 들어 IPv4주소에서 192.0.2.12를 가지고 있다면 우리는 첫 3바이트가 네트워크 주소이고 마지막 바이트가 호스트 주소라고 말할 수 있습니다. 조금 다르게 말하면 192.0.2.0 네트워크에 있는 호스트 12에 대해서 말한다고 할 수 있습니다.(네트워크 부분에서 우리가 호스트 바이트를 0으로 바꾼 것에 주목하세요.)

더 오래된 정보를 알려드리겠습니다! 고대에는 이 서브넷에 “클래스”가 있었습니다. 각각 주소의 첫 번째, 두 번째, 세 번째 바이트까지가 네트워크 부분임을 의미했습니다. 여러분이 네트워크 부분에 1바이트를 받고 호스트 부분에 3바이트를 받을 정도로 운이 좋다면 여러분의 네트워크에 24비트 규모(대략 천육백만)의 호스트를 가질 수 있었습니다. 이것이 “클래스 A”네트워크입니다. 반대쪽 끝을 “클래스 C”라고 했습니다. 여기에서는 3바이트가 네트워크 부분이고 1바이트가 호스트입니다.(256개의 호스트이고, 예약된 주소때문에 몇 개를 더 빼야 합니다.)

그래서 보시다시피, A클래스는 몇 개 없고, C클래스는 엄청나게 많았으며, B클래스는 중간정도였습니다.

IP 주소의 네트워크 부분은 넷마스크라는 것으로 표시되는데, IP주소에서 네트워크 번호를 얻어내기 위해서 넷마스크와 비트단위 AND연산을 하게 되어있습니다. 넷마스크는 대체로 255.255.255.0처럼 보입니다. (예를 들어 넷마스크가 저렇고 여러분의 IP가 192.0.2.12라면 네트워크는 192.0.2.12 AND 255.255.255.0이고 결과는 192.0.2.0입니다.)

불행히도 이것은 인터넷의 결과적인 필요에 비해 섬세하지 못했던 것으로 드러났습니다. C 클래스 네트워크는 꽤 빠르게 고갈되고 있었고 A클래스는 이미 다 소진되었으니 물어볼 것도 없습니다. 이 상황을 타개하기 위해서 8이나 16, 24개의 비트 뿐 아니라 임의의 비트를 넷마스크로 쓸 수 있는 능력이 필요했습니다. (예를 들어 255.255.255.252 같은 넷마스크로 30비트의 네트워크 부분과 2비트의 호스트(4개의 호스트)를 허락할 필요가 있었습니다.) (넷마스크는 언제나 여러 개의 비트 1 뒤에 뒤따라오는 여러 개의 비트 0임을 기억하세요.)

그러나 255.192.0.0같은 긴 문자열을 넷마스크로 쓰는 것은 불편했습니다. 첫 번째로 사람들이 보기에 그것이 몇 비트인지 알기가 힘들었고 두 번째로 너무 길었습니다. 그래서 새로운 방식이 등장했고 훨씬 좋았습니다. IP 주소 뒤에 빗금(역자 주 : 슬래시 또는 /)을 적고 네트워크 비트의 수를 십진수로 적는 것입니다. 예시는 이렇습니다: 192.0.2.12/30

IPv6을 위해서는 이렇게 할 것입니다: 2001:db8::/32 또는 2001:db8:5413:4028::9db9/64

3.1.2 포트 번호

친절하게도 아직 기억해주신다면, 계층화 네트워크 모델에서 호스트 대 호스트 전송 계층(TCP and UDP)과 분리된 인터넷 계층 (IP)이 있음을 아실 것입니다. 다음 문단으로 가기 전에 한 번 더 살펴보셔도 좋습니다.

(IP 계층이 사용하는) IP주소 말고도 TCP (stream sockets)와 UDP (datagram sockets)는 우연히도 한 가지 주소를 더 사용하는 것을 알 수 있습니다. 그것은 바로 포트 번호입니다. 이것은 16비트 숫자이며 연결을 위한 로컬 주소같은 것입니다.

IP 주소를 호텔의 도로명 주소라고 생각하고, 포트 번호를 방 번호라고 생각하세요. 이것은 꽤 적절한 비유입니다. 어쩌면 나중에 자동차 산업과 관련된 다른 비유를 떠올릴 수 있을지도 모르겠습니다.

여러분이 이메일 수신과 웹서비스를 처리하는 컴퓨터를 가지고 있다고 해봅시다. 하나의 IP주소로 어떻게 그 두 일을 구분할 수 있을까요?

인터넷의 서로 다른 서비스들은 서로 다른 “잘 알려진” 포트번호를 가지고 있습니다. 전체 목록은 거대한 IANA 포트 목록15 또는 여러분이 유닉스 장치를 사용하신다면 /etc/services파일에서 볼 수 있습니다. HTTP(웹)는 80번 포트를 사용하고, telnet은 23번을, SMTP는 25를 쓰고 게임인 DOOM16은 666번(역자 주 : 둠은 지옥의 악마와 싸우는 내용의 게임이며, 666은 기독교에서 악마의 숫자로 알려져 있습니다) 포트를 사용했습니다. 1024번 아래의 포트는 흔히 특별한 것으로 취급되어, 사용하기 위해서는 보통 운영체제 특권이 필요합니다.

포트 번호에 대해서는 이정도로 하겠습니다.

3.2 바이트 순서

왕국의 명령으로, 앞으로는 “엉터리”와 “큰 것 먼저”인 두 개의 바이트 순서가 있을 것입니다!

농담입니다. 그러나 한 쪽이 다른 쪽보다 더 좋습니다. :-)

사실 이것에 대해 딱 잘라 말할 방법은 없습니다. 그러니 허풍을 좀 떨겠습니다: 여러분의 컴퓨터는 뒤에서 바이트들을 여러분이 생각하는 것과 반대되는 순서로 저장하고 있었을 수도 있습니다. 아무도 이것을 여러분에게 알려주고 싶어하지 않았을 것입니다.

중요한 것은 인터넷 세계의 모든 사람들이 b34f같은 16진수 2바이트 수를 표현하고자 할 때 b3이 앞에 오고 4f이 뒤에 오는 연속된 바이트로 저장하는 것에 대체로 동의했다는 사실입니다. 이것은 말이 되기도 하고, Wilford Brimley17 라면 이것에 대해서 “마땅히 해야 할 일”이라고 할 것입니다. 큰 쪽이 앞에 저장되는 이 방식을 Big-Endian(빅엔디언)이라고 합니다.

안타깝게도 전 세계 이곳저곳에 흩어진 일부 컴퓨터들, 그러니까 인텔 혹은 인텔 호환 프로세서 컴퓨터들은 바이트를 반대 순서로 저장합니다. 그래서 b34f4f뒤에 b3이 있는 순차적 바이트들로 저장됩니다. 이런 저장법을 Little-Endian(리틀 엔디언)이라고 합니다.

아직 용어 해설이 조금 남았습니다! 둘 중에서 좀 더 상식적으로 보이는 빅엔디언은 네트워크 바이트 순서(Network Byte Order)라고도 합니다. 네트워크에서 우리가 그렇게 바이트를 전송하기 때문입니다.

여러분의 컴퓨터는 숫자를 호스트 바이트 순서(Host Byte Order)로 저장합니다. 인텔 80x86이라면 그것은 리틀엔디언입니다. 모토롤라 68k라면 호스트 바이트 순서는 빅엔디언입니다. PowerPC라면 호스트 바이트 순서는.. 상황에 따라 다릅니다! (역자 주 : 현재 널리 쓰이는 x86-64 프로세서들은 리틀 엔디언이며, 이것은 흔히 쓰이는 ARM프로세서와 애플실리콘 등의 ARM변종에서도 동일합니다. 리틀/빅엔디언 여부에 관계 없이 한 바이트 내에서는 무조건 MSB가 앞에 온다는 것도 기억해야 합니다. 결론적으로 대부분의 컴퓨터들의 호스트 바이트 오더가 네트워크 바이트 오더와 다릅니다.)

패킷을 만들거나 자료 구조를 채워넣는 많은 경우에 여러분은 여러분의 2바이트 또는 4바이트 숫자들이 네트워크 바이트 순서로 확실히 기록되도록 해야합니다. 하지만 원시 호스트 바이트 순서를 모른다면 어떻게 이런 작업을 할 수 있을까요?

좋은 소식입니다! 그냥 호스트 바이트 순서가 늘 틀렸다고 가정하고, 그것을 네트워크 바이트 순서로 재정렬하는 함수에 넣으세요. 그 함수가 필요하다면 마법의 변환과정을 처리하고, 여러분의 코드는 서로 다른 바이트 정렬 방식을 가진 기계 사이에서 호환성을 가질 것입니다.

여러분이 변환할 수 있는 숫자에는 두 가지 종류가 있습니다: short(2바이트) 과 long(4바이트)입니다. 위에서 말한 처리함수들은 부호 없는 종류에도 쓰일 수 있습니다. 호스트 바이트 순서로 기록된 short을 네트워크 바이트 순서로 변환하고 싶다면, “host”의 “h”로 시작하고 “to”를 이어서 적고 “network”의 “n”을 적고 “short”의 “s”를 적으세요. 다 붙이면 htons()이 됩니다. (읽는 법 : Host to Network Short)

정말 쉽지요…

“n”과 “h”, “s”, “l”의 모든 조합을 원하는대로 쓸 수 있습니다. 정말로 바보같은 것만 제외하고 말입니다. 예를 들어 stolh() 그러니까 “Short to Long Host”는 없습니다. 대신 이런 것들이 있습니다:

함수설명
htons()host to network short
htonl()host to network long
ntohs()network to host short
ntohl()network to host long

간단히 말하자면 숫자가 랜선을 타고 나가기 전에 네트워크 바이트 순서로 변환해야 하며 랜선에서 들어올 때에 호스트 바이트 순서로 변환해야 합니다.

64비트 종류에 대해서는 저는 잘 모릅니다. 그리고 만약 부동소수점에 대한 것을 원하신다면 한참 아래에 있는 직렬화 절을 참조하세요.

달리 말하지 않는 이상 이 문서의 숫자들은 호스트 바이트 순서라고 생각하세요.

3.3 struct

마침내 여기까지 왔습니다. 프로그래밍에 대해서 말할 차례입니다. 이 절에서는 소켓 인터페이스가 사용하는 다양한 데이터 형식에 대해서 논할 것이며 그 중 몇몇은 정말로 어렵습니다.

쉬은 것 부터 시작하겠습니다: 소켓 설명자입니다. 소켓 설명자는 아래의 형식입니다.

int

그냥 평범한 int입니다.

여기서부터 이상해집니다. 저와 함께 꾹 참고 따라오세요.

나의 첫 번째 구조체™—struct addrinfo. 이 구조체는 꽤나 최근의 발명품입니다. 이것은 이후의 사용을 위한 소켓 주소 구조체를 준비하기 위해 사용됩니다. 또한 호스트 이름 찾기나 서비스 이름 찾기 에도 사용됩니다. 나중에 실제 사용 예시를 보시면 이해가 되겠지만 지금은 연결을 만들 때 맨 처음 호출하는 것들 중 하나라고 알아두세요.

struct addrinfo {
    int              ai_flags;     // AI_PASSIVE, AI_CANONNAME 등.
    int              ai_family;    // AF_INET, AF_INET6, AF_UNSPEC
    int              ai_socktype;  // SOCK_STREAM, SOCK_DGRAM
    int              ai_protocol;  // "미지정"을 위해서 0을 쓰세요
    size_t           ai_addrlen;   // ai_addr의 바이트 단위 크기
    struct sockaddr *ai_addr;      // sockaddr_in 또는 _in6 구조체
    char            *ai_canonname; // 완전한 정규화된 호스트이름

    struct addrinfo *ai_next;      // linked list, next node
};

이 구조체에 내용을 조금 채워넣은 후에 getaddrinfo() 을 호출할 것입니다. 이 함수는 여러분에게 필요한 모든 것들로 채워진, 이 구조체의 링크드 리스트를 반환할 것입니다.

ai_family필드에서 IPv4나 IPv6을 강제할 수 있고 무엇이든 상관없다면 AF_UNSPEC 으로 둘 수 있습니다. 이것은 여러분의 코드가 IP버전에 무관해지도록 해주는 좋은 기능입니다.

이것이 링크드 리스트임을 기억하세요: ai_next는 다음 요소를 가리킵니다. 여러분이 고를 수 있는 여러 개의 결과가 돌아올 수 있다는 의미입니다. 저라면 쓸 수 있는 첫 번째 것을 쓰겠습니다. 그러나 여러분은 다른 비즈니스 요구사항이 있을지도 모릅니다. 저는 알 수 없는 일입니다.

struct addrinfo안의 ai_addrstruct sockaddr 에 대한 포인터임을 보실 수 있습니다. 여기서부터 IP 주소 구조체의 내부를 살펴볼 때에 지저분해지기 시작하는 곳입니다.

여러분은 대체로 이 구조체들에 쓰기 작업을 할 일이 없을 것입니다. 대체로 여러분의 struct addrinfo을 채우기 위해서 getaddrinfo()을 호출하는 것이 여러분이 해야 할 일의 전부입니다. 그러나 그 안에서 값을 꺼내오기 위해서는 그 안을 들여다봐야만 하므로 이제부터 설명하겠습니다.

(struct addrinfo가 발명되기 전의 코드에서는 모든 정보를 손으로 채워넣어야 했습니다. 저 거친 야생에는 정확히 그런 일을 하는 IPv4코드를 많이 보실 수 있습니다. 이 안내서의 오래된 버전을 포함한 여러 곳에서 말입니다.)

몇몇 struct는 IPv4용이고, 어떤 것은 IPv6용이며 어떤 것은 양쪽 모두에 필요합니다. 뭐가 무엇인지도 적어두겠습니다.

어쨌든 struct sockaddr은 여러 종류의 소켓을 위한 소켓 주소 정보를 저장합니다.

struct sockaddr {
    unsigned short    sa_family;    // 주소 계열, AF_xxx
    char              sa_data[14];  // 14 바이트의 프로토콜 주소
};

sa_family는 몇 가지 것들 중 하나가 될 수 있는데, 우리가 이 문서에서 하는 모들 일에 대해서는 AF_INET (IPv4) 이나 AF_INET6 (IPv6)가 될 것입니다. sa_data는 소켓을 위한 목적지 주소와 포트 번호가 담겨 있습니다. 이것에 주소를 직접 적어넣는 일은 지루하고 불편합니다.

struct sockaddr을 상대하기 위해서 프로그래머들은 IPv4를 위한 병렬적인 구조체인 struct sockaddr_in (“Internet”의 “in”)을 만들었습니다.

그리고 이것이 중요한 부분입니다: struct sockaddr_in에 대한 포인터는 struct sockaddr에 대한 포인터로 형변환 될 수 있고 그 반대도 가능합니다. 그래서 connect()struct sockaddr*을 원하더라도 struct sockaddr_in을 사용할 수 있고 마지막에 형변환만 하면 되는 것입니다!

// (IPv4 only--see struct sockaddr_in6 for IPv6)

struct sockaddr_in {
    short int          sin_family;  // 주소 계열, AF_INET
    unsigned short int sin_port;    // 포트 번호
    struct in_addr     sin_addr;    // 인터넷 주소
    unsigned char      sin_zero[8]; // sockaddr 구조체와 같은 크기
};

이 구조체는 소켓 주소의 요소들을 참조하는 일을 쉽게 해 줍니다. sin_zero (struct sockaddr과 길이를 맞추기 위해서 덧대진 것)은 memset()을 이용해서 0으로 설정되어야 함을 기억하세요. 또한 sin_familystruct sockaddrsa_family에 대응되며 “AF_INET”으로 설정되어야 함을 기억하세요. 마지막으로 sin_port은 반드시 네트워크 바이트 순서 로 기록해야 함을 기억하세요.( htons()을 써야한다는 의미입니다.)

더 깊게 파고들어가봅시다. struct in_addr에는 sin_addr필드가 있습니다. 저것이 무엇일까요? 지나치게 과장할 필요는 없지만 저것은 지금껏 있었던 가장 무서운 공용체 (역자 주 : 하나의 메모리 구역을 서로 다른 데이터타입처럼 읽고 쓸 수 있게 해 주는 C언어의 기능) 중 하나입니다.

// (IPv4 전용--IPv6를 위해서는 in6_addr 구조체를 참조하세요)

// 인터넷 주소 (역사적인 이유로 존재하는 구조체)
struct in_addr {
    uint32_t s_addr; // 32비트 정수 (4 바이트)
};

와! 사실 이것은 공용체 였습니다. 그러나 이제 그 시절은 지났습니다. 잘된 일입니다. 그러니까 만약 여러분이 struct sockaddr_in 형으로 ina를 선언했다면 ina.sin_addr.s_addr이 (네트워크 바이트 순서로 적힌) 4바이트의 IP주소를 가리킬 것입니다. 여러분의 시스템이 struct in_addr에 대해서 여전히 형편없는 공용체를 사용해도 여러분은 제가 위에서 한 것과 동일한 방식으로 4바이트 IP주소를 참조할 수 있습니다.(이것은 #define 덕분입니다.)

IPv6에 대해서도 유사한 struct가 있습니다:

// (IPv6 전용--IPv4를 위해서는 sockaddr_in 구조체와 in_addr 구조체를 참조하세요)

struct sockaddr_in6 {
    u_int16_t       sin6_family;   // 주소 계열, AF_INET6
    u_int16_t       sin6_port;     // 포트 번호, 네트워크 바이트 순서
    u_int32_t       sin6_flowinfo; // IPv6 흐름 정보
    struct in6_addr sin6_addr;     // IPv6 주소
    u_int32_t       sin6_scope_id; // 스코프 아이디
};

struct in6_addr {
    unsigned char   s6_addr[16];   // IPv6 주소
};

IPv4용 구조체가 그렇듯이 IPv6 구조체도 IPv6주소와 포트번호를 가진 것에 주목하세요.

지금은 IPv6의 흐름 정보나 Scope ID 필드에 관한 내용은 다루지 않을 것입니다. 이것은 초보자용 안내서이기 때문입니다. :-)

마지막으로 중요한 것은, 여기에 IPv4와 IPv6의 모든 구조체를 담기에 충분히 크게 설계된 struct sockaddr_storage라는 또 하나의 단순한 구조체가 있다는 사실입니다. 때때로 어떤 함수 호출에 대해서 여러분은 그 함수가 여러분의 struct sockaddr를 IPv4주소로 채울지 아니면 IPv6주소로 채울지 알 수 없는 경우가 있습니다. 그러므로 이 병렬 구조체를 넘기면 됩니다. 이것은 좀 더 크다는 점을 제외하면 struct sockaddr과 아주 비슷하며, 여러분은 이것을 여러분이 원하는 형식으로 형변환 할 수 있습니다.

struct sockaddr_storage {
    sa_family_t  ss_family;     // 주소 계열

    // 이것들은 모두 패딩이고 구현에 특정적인 내용입니다. 무시하세요.
    char      __ss_pad1[_SS_PAD1SIZE];
    int64_t   __ss_align;
    char      __ss_pad2[_SS_PAD2SIZE];
};

중요한 것은 여러분이 ss_family 필드에서 주소 계통을 볼 수 있다는 사실입니다. 그것이 AF_INET또는 AF_INET6인지 확인하세요(IPv4또는 IPv6인지 확인하기 위해서). 그 뒤 필요하다면 struct sockaddr_in 또는 struct sockaddr_in6으로 형변환할 수 있을 것입니다.

3.4 IP 주소, 파트 2

여러분에게는 다행스럽게도, IP 주소를 다룰 수 있게 해주는 많은 함수가 있습니다. 손으로 직접 종류를 알아내고 long<<연산자로 값을 채워넣을 필요가 없습니다.(역자 주 : <<은 비트 옮기기 연산자이며 큰 메모리 영역에 작은 값을 집어넣고자 할 때 흔히 사용합니다.)

우선 여러분이 struct sockaddr_in ina를 가지고 있다고 합시다. 그리고 저장하고 싶은 두개의 주소, “10.12.110.57”와 “2001:db8:63b3:1::3490” 가 있다고 합시다. 여러분이 사용해야 하는 함수는 inet_pton() 입니다. 이것은 숫자와 점 표기법으로 적힌 IP주소를 여러분이 AF_INET또는 AF_INET6 를 지정하는 것에 따라서 struct in_addr또는 struct in6_addr으로 변환합니다. (“pton”는 “presentation to network”(역자 주 : 표현에서 네트워크로)의 약어이며 쉽게 기억하고 싶다면 “printable to network”라고 해도 됩니다.) 변환은 아래와 같이 이루어집니다:

struct sockaddr_in sa; // IPv4
struct sockaddr_in6 sa6; // IPv6

inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr)); // IPv4
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6

(짧은 노트 : 이 일을 하는 예전 방식은 inet_addr()이나 inet_aton()함수를 사용했습니다. 이것들은 이제 구형이고 IPv6과는 동작하지 않습니다.)

위의 코드 예제는 그다지 견고하지 않은데 오류 확인이 없기 때문입니다. inet_pton() 은 오류가 발생하면 -1을 돌려주고 주소가 엉망이면 0을 돌려줍니다. 그러니 결과물을 사용하기 전에 복귀값이 0보다 큰지 확인하세요.

좋습니다. 이제 여러분은 IP주소 문자열을 그것의 이진 표현으로 바꿀 수 있습니다. 반대로는 어떻게 하는지 궁금하신가요? struct in_addr구조체를 가지고 있고 그것의 숫자와 점 표기법을 출력하길 원하신다면 어떻게 해야할까요? (또는 struct in6_addr을 16진수와 콜론 표기법으로 출력한다면 어떻게 해야할까요?) 이 경우 여러분은 inet_ntop() 을 사용해야 합니다. (“ntop”는 “network to presentation”을 의미하며 쉽게 기억하려면 “network to printable”이라고 부르셔도 됩니다.) 예제는 아래와 같습니다:

// IPv4:

char ip4[INET_ADDRSTRLEN];  // IPv4 문자열을 담아둘 공간
struct sockaddr_in sa;      // 이곳에 무엇인가 담겨있다고 가정합니다

inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);

printf("The IPv4 address is: %s\n", ip4);


// IPv6:

char ip6[INET6_ADDRSTRLEN]; // IPv6 문자열을 담아둘 공간
struct sockaddr_in6 sa6;    // 이곳에 무엇인가 담겨있다고 가정합니다

inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);

printf("The address is: %s\n", ip6);

이 함수를 호출하려면 주소 종류(IPv4 또는 IPv6)와 주소, 결과를 담을 문자열에 대한 포인터, 그 문자열의 최대 길이를 넘겨줘야 합니다. (두 개의 매크로인 INET_ADDRSTRLENINET6_ADDRSTRLEN이 편리하게도 가장 긴 IPv4또는 IPv6문자열을 담아둘 문자열의 크기를 가지고 있습니다.)

(이 일을 하는 오래된 방식에 대한 다른 이야기 : 이 변환 작업을 하는 역사적인 함수는 inet_ntoa()입니다. 마찬가지로 구식이고 IPv6에는 작동하지 않습니다.)

마지막으로 이 함수들은 숫자 형태의 IP 주소에만 사용할 수 있습니다. 이 함수들은 “www.example.com”같은 호스트이름에 대한 네임서버 DNS 탐색을 하지 않습니다. 그 작업을 위해서는 다음에 보실 getaddrinfo()을 써야합니다.

3.4.1 사설(또는 분리된) 망

많은 곳들이 보호를 목적으로 네트워크를 외부로부터 숨기는 방화벽을 가지고 있습니다. 그리고 흔히 이 방화벽들은 Network Address Translation또는 NAT이라는 절차를 통해서 “내부” IP 주소를 (세상의 다른 사람들이 아는)“외부” IP주소로 변환합니다.

벌써 긴장되나요? “이 이상한 것들로 뭘 하려는 생각이죠?”라고 하시는 듯 합니다.

진정하고 무알콜(아니면 알콜이 있는)음료수를 준비하세요. NAT은 여러분을 위해서 투명하게 처리되므로(역자 주 : 알 필요가 없게, 보이지 않게) 초보자는 NAT에 대해서 신경쓸 필요도 없습니다. 그러나 저는 여러분이 보는 네트워크 숫자로 인해 헷갈릴 일이 없도록 방화벽 뒤의 네트워크에 대해서 이야기하고 싶었습니다.

예를 들어 제 집에는 방화벽이 있습니다. 저에게는 디지털 가입자 회선(DSL) 회사가 저에게 배정해 준 두 개의 정적 IPv4주소가 있습니다. 그런데 제 네트워크에 있는 컴퓨터는 일곱 대 입니다. 이것이 어떻게 가능하냐고요? 두 개의 컴퓨터가 같은 아이피를 쓸 수는 없습니다. 그렇게 되면 데이터가 어디로 가야할지 알 수 없게 됩니다.

답은 이렇습니다. 컴퓨터들은 아이피 주소를 공유하지 않습니다. 그것들은 2천4백만개의 아이피 주소가 할당된 사설 네트워크에 속해 있습니다. 그것들 전체가 저만을 위한 것입니다. 최소한 저에게는 그렇습니다. 원리는 이렇습니다:

제가 원격 컴퓨터에 로그인하면, 그것은 제가 192.0.2.33에서 로그인했다고 말해줍니다. 그 주소는 제 인터넷 서비스 제공자가 저에게 준 공용 아이피 주소입니다. 그러나 제가 로컬 컴퓨터에게 저의 주소를 물어보면 그것은 10.0.0.5라고 대답합니다. 누가 IP주소를 번역해주는 것일까요? 그렇습니다. 바로 방화벽입니다. 그것이 NAT을 수행하는 것입니다.

10.x.x.x는 완전히 외부와 차단된 네트워크 또는 방화벽 뒤의 네트워크만이 사용하도록 예약된 몇 개의 네트워크 대역 중 하나입니다. 어떤 사설 네트워크 숫자가 사용 가능한지는 RFC 191818에 제시되어 있습니다. 그러나 여러분이 보실 일반적인 것들은 10.x.x.x 또는 192.168.x.x입니다. x에는 보통 0에서 255사이의 수가 들어갑니다. 좀 덜 일반적인 것으로 172.y.x.x가 있습니다. y에는 16부터 31사이의 수가 옵니다.

NAT동작을 수행하는 방화벽 뒤에 있는 망(네트워크)이 이런 사설 네트워크 중 하나 여야만 하는 것은 아닙니다. 그러나 보통 그런 종류입니다.

(재미있는 사실! 제 외부 아이피 주소가 실제로 192.0.2.33인 것은 아닙니다. 192.0.2.x 네트워크는 문서에 “진짜” 아이피 주소가 쓰였다고 믿게 하기 위해서 예약된 대역입니다. 바로 이 안내서에 쓰인 것 처럼 말입니다! 우와아아아!)

IPv6도 어떤 의미로는 사설망을 가지고 있습니다. 그것들은 fdXX:으로 시작합니다. (미래에는 fcXX:으로 시작할 수도 있습니다.) RFC 419319 문서에 따르자면 그렇습니다. 그러나 NAT과 IPv6은 일반적으로 같이 쓰이지 않습니다. (IPv6 to IPv4 게이트웨이(gateway)같은 것을 만들지 않는다면 말입니다. 그리고 그것은 이 문서의 범위를 넘어섭니다.) 아무튼 이론적으로는 여러분에게는 너무나 많은 주소가 있어서 더이상 NAT을 쓸 필요가 없을 것입니다. 그럼에도 여러분이 외부와 통하지 않는 주소를 할당하고 싶다면, 위에 적은 대역을 쓸 수 있다는 의미입니다.


Prev | Contents | Next