在浏览器中输入一个地址,点击回车以后发生了什么?这是一个面试中常见的问题 ,这个看似常见简单的操做,其中却隐藏了大量复杂的互联网技术。本篇博客,咱们就聊一聊网上冲浪的第一步:DNS解析。面试
DNS解析是一种服务,其又被称为域名解析。它的做用是将域名解析到具体的网络IP地址,以便进行后续的网络链接操做。DNS解析提及来也简单,从表面上看,就是经过一个查询服务,将域名映射成IP地址,能够往深处推敲,你就会发现其实并无那么简单,世界上有无数的终端接入互联网,DNS服务是如何从浩如烟海的数据中找到目标数据的,DNS服务是如何保证搜索性能的等等都是值得讨论的话题。编程
在深刻了解DNS解析以前,首先须要对咱们当下使用的网络系统有大体的了解。关于网络系统的分层,流行的有OSI的7层模型与TCP/IP的五层模型,其中关系以下图所示:浏览器
以OSI的7层网络模型为例,其中每一层都负责对应的协议,两台终端在进行交互时,同层之间进行交互。咱们从上到下来看:缓存
应用层:顾名思义应用层的主要做用是搭建应用,其负责实现应用层面的协议,例如文件传输FTP协议,还有咱们最经常使用的HTTP协议以及邮箱相关协议等。服务器
表示层:表示层用来对应用层的数据进行封装处理,如压缩与解压。网络
会话层:会话层位于传输层之上,其用来管理一对会话,即会话的链接开始,同步,中断等等。框架
传输层:传输层述责数据的分段传输与接收重组,这一层有TCP、UDP等传输协议。socket
网络层:网路层负责根据IP来将数据传递到指定的目的主机,其会肯定传输路由等问题。性能
数据链路层:将数据进行MAC地址的封装与解封等。网站
物理层:定义物理媒介的协议,以二进制的方式传输数据,定义数据的传输速率等参数。
当数据真正的经过7层网络模型传输以前,首要肯定的是数据要传输到哪里,咱们知道经过IP地址肯定数据要到达的目的终端,然而IP地址是有一串数字(字母)与点符号构成的,可读性不好且难于记忆,所以采用别名的方式来代替直接使用IP进行地址肯定,这个别名就是域名,将域名解析为IP的过程就是域名解析。
既然须要将域名解析成为IP地址,则就须要有一个服务器提供这样的解析功能,这个服务器须要维护这一张域名与IP地址映射的表,在客户提出解析需求时,从表中查询出域名对应的IP地址返回给客户,以下图所示:
然而在实际应用中,上图中的设计结构明显是不切实际的,由一台服务器来维护全部的域名与IP映射关系明显是不现实的。首先,世界上的域名和IP数量很是庞大,而且更新也很是频繁,维护成本不少。另外一方面,大量的客户同时进行域名解析请求,也会使解析的效率和速度出现瓶颈。现实中的DNS解析是采用层层递进,多级缓存,递归查询的结构组织而成的,下图很好的描述了这一过程:
从图中能够看到首先当客户端发起DNS解析时,会从本机NDS缓存中进行查找,一样也会查找本机的Hosts文件中是否有指定对应的解析规则,因为本机的Hosts文件具备最高的优先级,所以咱们想在本机将某个域名强制指向一个固定的IP,则能够采用修改Hosts文件的方式,在Mac系统中,此Hosts文件在etc文件夹下。
当本机缓存中没有解析出此域名的信息且Hosts文件中也没有指定时,会想本地DNS服务器发起查询,本地DNS服务器也会维护一张缓存表用来提升查询效率,若是本地DNS服务器没有查到,会向根DNS服务器发起请求,根DNS服务器会采用递归迭代的方式进行搜索,全球有13台根DNS服务器,根DNS服务器会根据域名后缀返回对应的顶级域名服务器,顶级域名服务器会再次根据域名分类将指定的主DNS地址返回,如此迭代直到解析到对应的IP地址再一步步返回给客户机。
根域名服务器是域名解析系统中最高级别的域名服务器,其复杂返回顶级域名服务器,他们是互联网的基础。目前,全球有13个根域名服务器地址(并不是实际的服务器数量),能够在以下网站查找到这些根域名服务器的信息。
顶级域名服务器用于某个顶级域名下的DNS解析,例若有专门负责.com后缀的顶级域名服务器,负责.edu后缀的顶级域名服务器等,顶级域名服务器将查询到的主域名服务器返回。
主域名服务器负责某个区域的域名解析。一样,主域名服务器会配套辅助域名服务器进行备份与分担负载。
一次完整的HTTP请求首先要作的就是DNS解析(若是是经过域名进行请求)。平时在开发中,咱们不多关注是由于发起HTTP的网络请求层帮咱们封装好了这一部分逻辑。有时候,为了提升请求的效率或防止DNS劫持,咱们也能够本身进行DNS解析。
以iOS中的编程为例,能够直接使用CoreFoundation框架中的接口进行NDS解析:
Boolean result; CFHostRef hostRef; CFArrayRef addresses = NULL; CFArrayRef names = NULL; NSMutableArray * ipsArr = [[NSMutableArray alloc] init]; CFStringRef hostNameRef = CFStringCreateWithCString(kCFAllocatorDefault, "www.baidu.com", kCFStringEncodingASCII); hostRef = CFHostCreateWithName(kCFAllocatorDefault, hostNameRef); CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); result = CFHostStartInfoResolution(hostRef, kCFHostAddresses, NULL); if (result == true) { addresses = CFHostGetAddressing(hostRef, &result); names = CFHostGetNames(hostRef, &result); } if(result) { struct sockaddr_in* remoteAddr; for(int i = 0; i < CFArrayGetCount(addresses); i++) { CFDataRef saData = (CFDataRef)CFArrayGetValueAtIndex(addresses, i); remoteAddr = (struct sockaddr_in*)CFDataGetBytePtr(saData); if(remoteAddr != NULL) { //获取IP地址 char ip[16]; strcpy(ip, inet_ntoa(remoteAddr->sin_addr)); NSString * ipStr = [NSString stringWithCString:ip encoding:NSUTF8StringEncoding]; [ipsArr addObject:ipStr]; } } } CFAbsoluteTime end = CFAbsoluteTimeGetCurrent(); NSLog(@"name:%@", names); NSLog(@"IP:%@ \n time cost: %0.3fs", ipsArr,end - start); CFRelease(hostNameRef); CFRelease(hostRef);
运行上面代码,便可将指定域名的IP地址解析出来,其中CFHostCreateWithName方法根据域名建立一个主机引用对象,CFHostStartInfoResolution方法用来进行主机信息的解析,若是解析成功,CFHostGetAddressing方法用来获取具体的IP地址数据。
上面进行NDS解析的方法比较上层,还有一种方式能够获取到更多的信息,示例代码以下:
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); char *ptr, **pptr; struct hostent *hptr; char str[32]; ptr = "www.baidu.com"; NSMutableArray * ips = [NSMutableArray array]; NSMutableArray * alis = [NSMutableArray array]; if((hptr = gethostbyname(ptr)) == NULL) { return 0; } for(pptr=hptr->h_addr_list; *pptr!=NULL; pptr++) { // inet_ntop方法是将二进制数据转换成IP字符串 NSString * ipStr = [NSString stringWithCString:inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)) encoding:NSUTF8StringEncoding]; [ips addObject:ipStr?:@""]; } // 获取主机别名 for(pptr=hptr->h_aliases; *pptr!=NULL; pptr++) { NSString * ali = [NSString stringWithCString:*pptr encoding:NSUTF8StringEncoding]; [alis addObject:ali?:@""]; } CFAbsoluteTime end = CFAbsoluteTimeGetCurrent(); // 获取主机名 NSLog(@"%s", hptr->h_name); NSLog(@"alias:%@", alis); NSLog(@"ip:%@\ntime cost: %0.3fs", ips,end - start);
gethostbyname方法能够方便的获取指定域名的主机信息,其只支持IPV4,若支持解析IPV6,须要使用gethostbyname2方法,这两个方法都会返回hostent结构体,其中封装了主机的信息。
除了上面介绍的两种方便的方式外,也能够直接经过C语言的Socket接口来进行DNS解析,DNS实际上也是一种协议,只须要向域名解析服务器的指定端口发送指定的请求数据便可获取到解析的结果,代码以下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #define DNS_SVR "198.41.0.4" #define DNS_HOST 0x01 #define DNS_CNAME 0x05 int socketfd; struct sockaddr_in dest; static void send_dns_request(const char *dns_name); static void parse_dns_response(); /** * Generate DNS question chunk */ static void generate_question(const char *dns_name , unsigned char *buf , int *len); static int is_pointer(int in); static void parse_dns_name(unsigned char *chunk , unsigned char *ptr , char *out , int *len); int main(int argc , char *argv[]){ socketfd = socket(AF_INET , SOCK_DGRAM , 0); if(socketfd < 0){ perror("create socket failed"); exit(-1); } bzero(&dest , sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(53); dest.sin_addr.s_addr = inet_addr(DNS_SVR); send_dns_request("www.baidu.com"); parse_dns_response(); return 0; } static void parse_dns_response(){ unsigned char buf[1024]; unsigned char *ptr = buf; struct sockaddr_in addr; char *src_ip; int n , i , flag , querys , answers; int type , ttl , datalen , len; char cname[128] , aname[128] , ip[20] , *cname_ptr; unsigned char netip[4]; size_t addr_len = sizeof(struct sockaddr_in); n = recvfrom(socketfd , buf , sizeof(buf) , 0 , (struct sockaddr*)&addr , &addr_len); ptr += 4; /* move ptr to Questions */ querys = ntohs(*((unsigned short*)ptr)); ptr += 2; /* move ptr to Answer RRs */ answers = ntohs(*((unsigned short*)ptr)); ptr += 6; /* move ptr to Querys */ /* move over Querys */ for(i= 0 ; i < querys ; i ++){ for(;;){ flag = (int)ptr[0]; ptr += (flag + 1); if(flag == 0) break; } ptr += 4; } printf("-------------------------------\n"); /* now ptr points to Answers */ for(i = 0 ; i < answers ; i ++){ bzero(aname , sizeof(aname)); len = 0; parse_dns_name(buf , ptr , aname , &len); ptr += 2; /* move ptr to Type*/ type = htons(*((unsigned short*)ptr)); ptr += 4; /* move ptr to Time to live */ ttl = htonl(*((unsigned int*)ptr)); ptr += 4; /* move ptr to Data lenth */ datalen = ntohs(*((unsigned short*)ptr)); ptr += 2; /* move ptr to Data*/ if(type == DNS_CNAME){ bzero(cname , sizeof(cname)); len = 0; parse_dns_name(buf , ptr , cname , &len); printf("%s is an alias for %s\n" , aname , cname); ptr += datalen; } if(type == DNS_HOST){ bzero(ip , sizeof(ip)); if(datalen == 4){ memcpy(netip , ptr , datalen); inet_ntop(AF_INET , netip , ip , sizeof(struct sockaddr)); printf("%s has address %s\n" , aname , ip); printf("\tTime to live: %d minutes , %d seconds\n" , ttl / 60 , ttl % 60); } ptr += datalen; } } ptr += 2; } static void parse_dns_name(unsigned char *chunk , unsigned char *ptr , char *out , int *len){ int n , alen , flag; char *pos = out + (*len); for(;;){ flag = (int)ptr[0]; if(flag == 0) break; if(is_pointer(flag)){ n = (int)ptr[1]; ptr = chunk + n; parse_dns_name(chunk , ptr , out , len); break; }else{ ptr ++; memcpy(pos , ptr , flag); pos += flag; ptr += flag; *len += flag; if((int)ptr[0] != 0){ memcpy(pos , "." , 1); pos += 1; (*len) += 1; } } } } static int is_pointer(int in){ return ((in & 0xc0) == 0xc0); } static void send_dns_request(const char *dns_name){ unsigned char request[256]; unsigned char *ptr = request; unsigned char question[128]; int question_len; generate_question(dns_name , question , &question_len); *((unsigned short*)ptr) = htons(0xff00); ptr += 2; *((unsigned short*)ptr) = htons(0x0100); ptr += 2; *((unsigned short*)ptr) = htons(1); ptr += 2; *((unsigned short*)ptr) = 0; ptr += 2; *((unsigned short*)ptr) = 0; ptr += 2; *((unsigned short*)ptr) = 0; ptr += 2; memcpy(ptr , question , question_len); ptr += question_len; sendto(socketfd , request , question_len + 12 , 0 , (struct sockaddr*)&dest , sizeof(struct sockaddr)); } static void generate_question(const char *dns_name , unsigned char *buf , int *len){ char *pos; unsigned char *ptr; int n; *len = 0; ptr = buf; pos = (char*)dns_name; for(;;){ n = strlen(pos) - (strstr(pos , ".") ? strlen(strstr(pos , ".")) : 0); *ptr ++ = (unsigned char)n; memcpy(ptr , pos , n); *len += n + 1; ptr += n; if(!strstr(pos , ".")){ *ptr = (unsigned char)0; ptr ++; *len += 1; break; } pos += n + 1; } *((unsigned short*)ptr) = htons(1); *len += 2; ptr += 2; *((unsigned short*)ptr) = htons(1); *len += 2; }
其中198.41.0.4是任意选择的一个根域名服务器的地址,若是解析成功,将在控制台看到以下的打印信息:
------------------------------- www.baidu.com is an alias for www.a.shifen.com www.a.shifen.com has address 112.80.248.76 Time to live: 1 minutes , 7 seconds www.a.shifen.com has address 112.80.248.75 Time to live: 1 minutes , 7 seconds