서버들은 IP로 구분된다. 하지만 IP는 기억하기도 쉽지 않고 표현하기도 힘들다. 그래서 일종의 별명을 IP주소마다 부여했는데 이것을 도메인이라고 한다. 그리고 IP주소와 도메인 사이의 변환을 수행하는 시스템을 가리켜 DNS(Domain Name System)이라고 부른다.
실제로 우리가 특정 웹사이트에 접속할때는 주로 도메인을 이용해서 접속한다. 예를 들어서 검색창에 www.naver.com을 입력했다고 하자. 이때 DNS로 부터 해당 도메인의 IP정보를 얻어오는 과정은 아래와 같다.
1. 로컬 DNS 서버에 입력한 도메인을 IP주소로 변경해줄 것을 요청해서 접근한다. 모든 호스트에는 로컬 DNS 서버가 등록되어 있다.
2. 만약 이 로컬 DNS 서버에 내가 원하는 도메인의 IP주소가 없으면 Root DNS 서버에 요청해서 어떤 DNS 서버가 내가 원하는 도메인의 IP를 갖고 있는지 알아오게 한다(Local DNS 서버에는 Root DNS 서버의 IP가 등록되어 있다)
3. Root 도메인 서버는 어떤 DNS 서버가 IP를 갖고 있을만한지 알고 있기에 그런 DNS서버를 Local DNS서버에 소개해준다(아래 예시에서는 com DNS를 소개해줬다).
4. 만약 Root DNS서버가 소개해준 DNS서버가 갖고 있지 않다면 그 서버가 또 다른 서버를 소개해준다.
5. 해당 도메인의 IP주소를 갖고 있는 DNS서버를 찾을때까지 계속하고 결국 찾으면 Local DNS에 해당 정보를 넘겨준다.
6. Local DNS에 캐싱을 한 후에 PC에 전달한다.
프로그래밍에서 도메인을 왜 사용해야 하는가?
예를 들어 어떤 도메인을 운영하고 있다고 가정해보자. 이 서비스를 이용하기 위해서는 도메인의 IP주소에 접속해서 서비스를 받도록 설계 되어야 한다. 그런데 코드안에 IP주소를 써야할 곳에 직접 IP주소를 써서 코딩을 하게 된다면 IP주소가 바뀔때 마다(IP주소 변경은 언제든지 일어날 수 있다) 소스코드를 수정해서 다시 배포해야하고, 사용자들은 이전 버전의 클라이언트 프로그램을 지우고 새로 컴파일해서 사용하도록 해야한다. 이런 경우는 말도 안되므로 대신 상대적으로 바뀔 가능성이 낮은 도메인 이름을 등록하고 도메인을 근거로 IP주소를 얻어와서 서버에 접속한다면 IP주소가 바뀔때마다 위에서 언급한것처럼 코드를 매번 수정할 일은 없을 것이다. 따라서 도메인을 사용해야 하는 것이다.
다음 함수를 이용하면 문자열 형태의 도메인으로부터 IP주소 정보를 얻을 수 있다.
#include <winsock2.h>
// 성공 시 hostent 구조체 변수의 주소값, 실패시 NULL반환
struct hostent* gethostbyname(const char* name);
여기서 반환되는 구조체 hostent에 대해 알아볼 필요가 있다.
struct hostent
{
char* h_name; // official name
char** h_aliases; // alias list
int h_addrtype; // host address type
int h_lengthl; // address length
char** h_addr_list; // address list
}
- h_name : 공식 도메인 이름을 저장한다. 해당 홈페이지를 대표하는 도메인 이름을 의미한다
- h_aliases : 똑같은 홈페이지를 여러 도메인으로 접속할 수 있는 경우가 있는데 이는 한 IP에 다른 도메인 이름을 지정하는 것이 가능해서 그렇다. 공식 도메인 이름 이외에 해당 페이지에 접속할 수 있는 또 다른 도메인 이름 지정이 가능한데 그 이름을 h_aliases에 저장한다
- h_addrtype : IP주소체계(ex. AF_INET)
- h_length : 함수 호출 결과로 반환된 IP주소의 크기 정보(IPv4의 경우 4, IPv6의 경우 16이 저장)
- h_addr_list : 도메인 이름에 대한 IP주소가 정수 형태로 반환된다. IP주소가 여러개일 때 이 멤버를 통해 모든 IP주소 정보를 얻을 수 있다.
실제 코드에서 gethostbyname()을 이용하면 h_addr_list의 타입에 의문점이 드는 부분이 있다.
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
void ErrorHandling(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]));
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
h_addr_list는 위에서 char** 타입이라고 했다. 즉 char*의 배열이라는 의미인데 i번째 인덱스에 접근한 것은 char*일텐데 이것을 in_addr*로 캐스팅하고 있다. 사실 h_addr_list는 원래 in_addr* 타입의 배열이다. 그런데 char*의 배열로 표기되어 있는 이유는 IPv4, IPv6 모두를 포괄하는 타입으로 만들기 위해서이다(in_addr구조체는 in_addr_t 타입의 s_addr이라는 멤버 하나만을 갖고 있는걸 기억하자. in_addr_t는 uint32_t로 정의되어있다. 즉 4바이트이므로 IPv4에만 사용가능하다).
Reference
https://www.netmanias.com/ko/post/blog/5353/dns/dns-basic-operation
'공부 > Server' 카테고리의 다른 글
소켓 옵션 정리 (0) | 2021.08.01 |
---|---|
TCP 기반의 소켓의 우아한 연결 종료(Half-close) (0) | 2021.08.01 |
TCP/UDP에 대한 이해 (0) | 2021.08.01 |
윈도우 기반 소켓 프로그래밍 필수 함수 (0) | 2021.07.31 |
윈도우 기반 소켓 프로그래밍 시작하기(socket, protocol 개념 및 기본적인 함수 정리) (0) | 2021.07.27 |