나의 브을로오그으

#8. 도메인 이름과 인터넷 주소 본문

네트워크/열혈 TCP_IP 소켓프로그래밍

#8. 도메인 이름과 인터넷 주소

__jhp_+ 2022. 8. 5. 09:19

#1. Domain Name System

DNS(Domain Name System) : IP주소와 도메인 이름 사이에서의 변환을 수행하는 시스템

 

도메인 이름이란?

IP주소를 대신하여 도메인 이름이라는 것을 부여한 것.

 

DNS 서버

인터넷 브라우저 주소 창에 네이버의 IP주소인 223.130.200.107를 직접 입력하면 네이버의 메인 페이지를 볼 수 있다.

(주소가 변경될 수 있기 때문에 cmd -> nslookup www.naver.com을 을 입력)

사실 IP 주소를 직접 입력하나 도메인 이름을 입력하나 똑같이 네이버 메인 페이지를 볼 수 있다. 두 접속의 결과는 같지만 방법은 다르다. 도메인 네임은 서버에 부여된 별명(가상의 주소)이기 때문에 실제주소가 아니다. 따라서 도메인 네임을 실제 주소로 변환하는 과정을 거쳐야 한다. 그리고 바로 이런 변환을 담당하는 것이 DNS 서버이다.

 

모든 컴퓨터에는 디폴트 DNS 서버의 주소가 등록되어 있는데, 바로 이 디폴트 DNS 서버를 통해서 도메인 이름에 대한 IP주소 정보를 얻게 된다. 즉, 우리가 웹 브라우저 주소 주소 창에 도메인 이름을 입력하면 브라우저는 해당 도메인 이름의 IP주소를 디폴트 DNS 서버를 통해 얻게 된다. 그다음에 비로소 서버로 요청을 하게 된다.

 

 

ping & nslookup

서버의 도메인 이름은 사업적인 목적이 아니라면 변경하지 않는다. 그러나 서버의 IP주소는 상대적으로 쉽게 바뀔 수 있다. 만약에 도메인 이름에 해당하는 IP주소를 확인하고 싶다면 콘솔상에 다음과 같이 입력하라.

ping www.naver.com  

그러면 해당 도메인의 IP주소 확인이 가능하다. 참고로 ping은 목적지에 IP 데이터그램이 수신되는지 확인할 때 사용하는 명령어이다. 그런데 ping은 IP주소로의 변환과정을 거치면서 해당 서버의 IP주소를 함께 보이기 때문에 ping을 통해서 IP주소를 확인할 수 있다.  그리고 컴퓨터에 등록되어 있는 디폴트 DNS 서버의 주소를 알고 싶다면 콘솔상에 다음과 같이 입력한다.

nslookup

리눅스의 경우 위의 명령어가 입력되면 추가 입력을 요구하는데, 이때 server라고 입력해서 디폴트 DNS 서버의 IP주소를 확인 할 수 있다.

 

물론 우리의 컴퓨터에 설정되어 있는 디폴트 DNS 서버가 모든 도메인 IP주소를 알고 있지는 않다. 그러나 디폴트 DNS서버는 우리가 찾고자 하는 도메인의 IP주소를 못찾는다면 다른 DNS Server에 요청하여 결과를 알려준다.

 

호스트 Domain name의 IP 주소를 Default DNS server로 요청 -> 디폴트 DNS 서버가 모르면 상위 DNS 서버에게 요청 -> 이 작업을 반복하다가 최상위(Root) DNS 서버에게까지 질의가 전달된다. Root DNS 서버는 해당 질의를 누구에게 응답해야 하는지 알고 있다. 그래서 자신보다 하위에 있는 DNS 서버에게 다시 질의를 던져서 결국은 IP 주소를 얻어내며, 그 결과는 질의가 진행된 반대 방향으로 전달이 되어 결국에는 질의를 시작한 호스트에게 IP주소가 전달된다.

이렇듯 DNS는 계층적으로 관리되는 일종의 분산 데이터베이스 시스템이다.

 

#2. IP주소와 도메인 이름 사이의 변환

#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
-> 성공 시 hostnet 구조체 변수의 주소 값, 실패 시 NULL 포인터 반환

struct hostent
{
  char* h_name;          // official name
  char** h_aliases;      // alias list
  int h_addrtype;        // host address type
  int h_length;          // address length
  char** h_addr_list;    // address list
}

이 함수는 변환하고자 하는 도메인 이름을 인자로 주면 IP 주소정보가 반환된다.

물론 struct hostnet 구조체를 보면 여러 정보들도 받아오지만, IP 주소정보만 필요하다면 h_addr_list만 신경 쓰면 된다.

 

h_name

이 멤버에는 '공식 도메인 이름(Official domain name)'이라는 것이 저장된다. 공식 도메인 이름은 해당 홈페이지를 대표하는 도메인 이름이라는 의미를 담고 있지만, 실제 우리에게 잘 알려진 유명 회사의 도메인 이름이 공식 도메인 이름으로 등록되지 않은 경우가 많다.

 

h_aliases

같은 메인 페이지인데도 다른 도메인 이름으로 접속할 수 있는 경우도 있다. 이런 경우는 하나의 IP에 두 개이상의 도메인 이름을 지정을 하는 것이 가능하다. 이 도메인 이름들은 h_aliases를 통해 얻을 수 있다.

 

h_addrtype

gethostbyname 함수는 IPv4뿐만 아니라 IPv6까지 지원한다. 때문에 h_addr_list로 반환된 IP주소의 주소체계에 대한 정보를 이 멤버를 통해 반환한다. IPv4의 경우 이 멤버에는 AF_INET이 저장된다.

 

h_addr_list

이것이 가장 중요한 멤버이다. 이 멤버를 통해서 도메인 이름에 대한 IP주소가 정수의 형태로 반환된다. 참고로 접속자수가 많은 서버는 하나의 도메인 이름에 대응하는 IP를 여러 개 둬서, 둘 이상의 서버로 부하를 분산시킬 수 있는데, 이러한 경우에도 이 멤버를 통해서 모든 IP의 주소정보를 얻을 수 있다.

 

 

[gethostbyname.c]

/* linux */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
void error_handling(char* message);

int main(int argc, char* argv[])
{
	int i;
	struct hostent* host;
	if (argc != 2)
	{
		printf("Usage : %s <addr> \n", argv[0]);
		exit(1);
	}

	host = gethostbyname(argv[1]);
	if (!host)
		error_handling("gethost... error");

	printf("Official name: %s \n", host->h_name);
	for (i = 0; i < host->h_aliases[i]; ++i)
		printf("Aliases %d: %s \n", i + 1, host->h_aliases[i]);
	printf("Address type: %s \n", host->h_addrtype == AF_INET ? "AF_INET" : "AF_INET6");
	for (i = 0; i < host->h_addr_list[i]; ++i)
		printf("IP addr %d: %s \n", i + 1, inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));

	return 0;
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

여기서 눈여겨 볼것은 h_addr_list의 IP주소를 inet_ntoa함수를 호출하여 network 주소를 10진수 주소로 변환하는 함수이며, 이때 인자로 h_addr_list[i]에 해당하는 IP주소를 in_addr* 형식으로 형변환하여 전달하였다.

 

[실행결과]

root@my_linux:/tcpip# gcc gethostbyname.c -o hostname
root@my_linux:/tcpip# ./hostname www.naver.com
Official name: www.naver.com
Aliases 1: www.naver.com
Address type: AF_INET
IP addr 1: 202.131.29.80
IP addr 2: 222.122.195.6

 

h_addr_list는 문자열 포인터 배열을 참조하고 있다.

h_addr_list -> char*, char*, char* .... 

각각의 char* 문자열 포인터는

char* -> IP addr #1 (in_addr 구조체 변수의 주소값이다.)

 

[왜 in_addr*이 아닌 char* 일까?]

구조체 hostnet의 멤버 h_addr_list가 가리키는 배열의 구조체 in_addr의 포인터 배열이 아닌, char형 포인터 배열인 이유가 궁금할 것이다. 그런데 우리가 이미 예상했을 것이라 생각한다.  구조체 hostnet는 IPv4만을 위해 정의된 구조체가 아니다. 

h_addr_list가 가리키는 배열에는 IPv6 기반의 주소 정보가 저장될 수도 있다. 때문에 일반화를 위해서 char형 포인터 배열로 선언한 것이다. (void형 포인터 배열로 선언했으면, 의미가 조금 더 잘 통하지 않았을까?)

이렇게 void*로 표현했다면 이해하기 쉬웠겠지만, 안타깝게도 구조체 hostnet 뿐만아닌 대부분의 소켓 함수들은 void형 포인터가 표준화되기 이전에 정의되었고, 당시에는 참조의 대상이 일정하지 않은 경우 char형 포인터 변수를 활용하였다.

 

 

IP주소를 이용해서 도메인 정보 얻어오기

#include <netdb.h>
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);
-> 성공 시 hostnet 구조체 변수의 주소 값, 실패 시 NULL 포인터 반환

addr : IP주소를 지나는 in_addr 구조체 변수의 포인터 전달, IPv4 이외의 다양한 정보를 전달받을 수 있도록 일반화하기 위해서 매개변수를 char형 포인터로 선언.

len : 첫 번째 인자로 전달된 주소정보의 길이, IPv4의 경우 4, IPv6의 경우 16 전달

family : 주소체계 정보 전달. IPv4의 경우 AF_INET, IPv6의 경우 AF_INET6 전달.

 

/* linux */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
void error_handling(char* message);

int main(int argc, char* argv[])
{
	int i;
	struct hostent* host;
	struct sockaddr_in addr;
	if (argc != 2)
	{
		printf("Usage: %s <IP> \n", argv[0]);
		exit(1);
	}

	memset(&addr, 0, sizeof(addr));
	addr.sin_addr.s_addr = inet_addr(argv[1]);
	host = gethostbyaddr((const char*)&addr.sin_addr, 4, AF_INET);
	if (!host)
		error_handling("gethost... error");

	printf("Official name: %s \n", host->h_name);
	for (i = 0; i < h_aliases[i]; ++i)
		printf("Aliases %d: %s \n", i + 1, host->h_aliases[i]);
	printf("Address type: %s \n", host->h_addrtype == AF_INET ? "AF_INET" : "AF_INET6");
	for(i = 0; i < h_addr_list[i]; ++i)
		printf("IP addr %d: %s \n", i + 1,
			inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));
	return 0;
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

[실행화면]

root@my_linux:/tcpip# gcc gethostbyaddr.c -o hostaddr
root@my_linux:/tpcip# ./hostaddr 74.125.19.106
Official name: nuq04s01-in-f106 google.com
Address type: AF_INET
IP addr 1: 74.126.19.106

 

 

#3. 윈도우 기반으로 구현하기

#include <WinSock2.h>

struct hostent* gethostbyname(const char* name);
-> 성공 시 hostnet 구조체 변수의 주소 값, 실패 시 NULL 포인터 반환

struct hostent* gethostbyaddr(const char* addr, int len, int type);
-> 성공 시 hostnet 구조체 변수의 주소 값, 실패 시 NULL 포인터 반환

 

[gethostbyname_win.c]

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")
void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	int i;
	struct hostent* host;
	if (argc != 2)
	{
		printf("Usage: %s <addr> \n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	host = gethostbyname(argv[1]);
	if (!host)
		ErrorHandling("gethost...error");

	printf("Official name: %s \n", host->h_name);
	for (i = 0; host->h_aliases[i]; ++i)
		printf("Aliases %d: %s \n", i + 1, host->h_aliases[i]);
	printf("Address type: %s \n", 
		(host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
	for(i = 0; host->h_addr_list[i]; ++i)
		printf("IP addr %d: %s \n", i + 1,
			inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

[gethostbyaddr_win.c]

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")
void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	int i;
	struct hostent* host;
	SOCKADDR_IN addr;
	if (argc != 2)
	{
		printf("Usage: %s <IP> \n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	memset(&addr, 0, sizeof(addr));
	addr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
	host = gethostbyaddr((const char*)&addr.sin_addr, 4, AF_INET);
	if (!host)
		ErrorHandling("gethost...error");

	printf("Official name: %s \n", host->h_name);
	for (i = 0; host->h_aliases[i]; ++i)
		printf("Aliases %d: %s \n", i + 1, host->h_aliases[i]);
	printf("Address type: %s \n",
		(host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
	for (i = 0; host->h_addr_list[i]; ++i)
		printf("IP addr %d: %s \n", i + 1,
			inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

 

[내용 확인문제]

01. 다음 중 DNS(Domain Name System)에 대한 성명으로 잘못된 것을 모두 고르면?

a. DNS가 존재하기 때문에 IP를 대신해서 도메인 이름을 사용할 수 있다.

b. DNS 서버는 사실 라우터를 의미한다. 라우터가 도메인 이름정보를 참조하여 데이터의 진로를 결정하기 때문이다.

c. 하나의 DNS 서버에 모든 도메인 정보가 다 등록되어 있지는 않다. 그러나 등록되지 않은 도메인의 IP주소도 얻어올 수 있다.

d. DNS 서버는 운영체제에 따라서 구분이 된다. 즉, 윈도우 운영체제용 DNS서버와 리눅스용 DNS 서버는 구분이 된다.

답)

b. DNS 서버는 도메인 이름을 IP 주소로 변경하는 역할이다.

d. 윈도우, 리눅스 등 서버를 구분되지 않는다.

 

02. 아래의 대화를 읽고 동수가 제안한 해결책으로 문제가 해결될 수 있는지 말해보자.

정수 : 동수냐? 야! 우리학교 네트워크 망에 연결되어 있는 디폴트 DNS 서버가 다운되어서 이력서를 넣어야 할 회사들의 홈페이지에 접속이 안돼! 뭔가 방법이 없을까? 

 

동수 : 인터넷과 연결은 되어있는데 DNS 서버만 다운된거야?

 

정수 : 응! 해결책 뭐 없을까?  그냥 주변에 있는 PC방으로 달려가야 하나?

 

동수 : 그렇게까지 할 필요가 뭐 있냐? 내가 우리학교 DNS 서버 IP주소를 불러줄 테니까 네가 사용하는 컴퓨터의 디폴트 DNS 서버 주소를 변경해!

 

정수 : 그런다고 되냐? 디폴트 DNS 서버는 반드시 로컬 네트워크상에 연결되어 있어야 한다고!

 

동수 : 아냐! 지난번에 우리학교도 디폴트 DNS 서버가 죽으니까, 네트워크 관리자가 다른 DNS 서버의 IP주소를 알려주던데?

 

정수 : 그건 너네 학교에 DNS 서버가 여러 대 있으니까 가능했던 거야!

 

동수 : 그런가? 네 말이 맞는 것 같기도 하다. 그럼 얼른 PC방으로 달려가봐라!

답)

해결책이 될 수 있다. DNS 서버의 주소를 변경하여 사용하면 된다.

 

03. 웹 브라우저의 주소 창에 www.orentec.co.kr를  를 입력해서 메인 페이지가 보이기까지의 과정을 정리해 보자. 단, 웹 브라우저가 접속한 디폴트 DNS 서버에는 www.orentec.co.kr에  에 대한 IP 주소가 등록되어 있지 않다고 가정하자.

답)

이런 경우 디폴트 DNS 서버에서 찾지 못한 IP 주소는 상위 DNS서버에 요청하여 찾게 된다. 찾게되면 계층적으로 하위 DNS서버에게 응답하여 최종적으로 내 컴퓨터 디폴트 DNS 서버에 전달되어 해당 IP주소 장버가 웹 브라우저에 전달된다. 이후 웹브라우저가 HTTP 페이지 요청 메시지를 만들어 전달받은 IP주소로 보내면 

1. 디폴트 DNS서버에서 IP주소를 찾지 못한다.

2. 상위 DNS서버에 IP주소를 요청한다. 요청받은 상위 DNS서버에서 해당 IP주소를 찾으면 계층적으로 하위 DNS서버에게 전달한다.

3. 최종적으로 전달받은 IP주소를 호스트에서 웹브라우저에게 전달하여, 메시지를 만들고 해당 페이지를 요청

4. 요청받은 문서를 웹브라우저에서 랜더링하면 메인 페이지가 보인다.