在瀏覽器中輸入一個地址,點擊回車以後發生了什麼?這是一個面試中常見的問題 ,這個看似常見簡單的操做,其中卻隱藏了大量複雜的互聯網技術。本篇博客,咱們就聊一聊網上衝浪的第一步: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