DNS解析的做用是把域名解析成相應的IP地址,由於在廣域網上路由器須要知道IP地址才知道把報文發給誰。DNS是Domain Name System域名系統的縮寫,它是一個協議,在RFC 1035具體描述了這個協議。具體過程以下圖所示:html
這個過程看似簡單,可是有幾個問題:linux
(1)瀏覽器是怎麼知道DNS解析服務器,如上圖的8.8.8.8這臺?chrome
(2)一個域名能夠解析成多個IP地址嗎,若是隻有一個IP地址,在併發量很大的狀況下,那臺服務器可能會爆?瀏覽器
(3)把域名綁了host以後,是否是就不用域名解析了直接用的本地host指定的IP地址?緩存
(4)域名解析的有效時間爲多長,即過了多久後同一個域名須要再次進行解析?bash
(5)什麼是域名解析的A記錄、AAAA記錄、CNAME記錄?服務器
其實域名解析和Chrome沒有直接關係,即便是最簡單的curl命令也須要進行域名解析,可是咱們能夠經過Chrome源碼來看一下這個過程是怎麼樣的,而且回答上面的問題。網絡
首先第一個問題,瀏覽器是怎麼知道DNS解析服務器的,在本機的網絡設置裏面能夠看到當前的DNS服務器IP,如我電腦的:session
這兩個DNS Server是我家接的某正寬帶提供的:併發
通常寬帶服務商都會提供DNS服務器,谷歌還爲公衆提供了兩個免費的DNS服務,分別爲8.8.8.8和8.8.4.4,取這兩個IP地址是爲了容易記住,當你的DNS服務很差用的時候,能夠嘗試改爲這兩個。
入網的設備是怎麼獲取到這些IP地址的呢?是經過動態主機配置協議(DHCP),當一臺設備連到路由器以後,路由器經過DHCP給它分配一個IP地址,並告訴它DNS服務器,以下路由器的DHCP設置:
經過wireshark抓包能夠觀察到這個過程:
當個人電腦連上wifi的時候,會發一個DHCP Request的廣播,路由器收到這個廣播後就會向個人電腦分配一個IP地址並告知DNS服務器。
這個時候系統就有DNS服務器了,Chrome是調res_ninit這個系統函數(Linux)去獲取系統的DNS服務器,這個函數是經過讀取/etc/resolver.conf這個文件獲取DNS:
#
# Mac OS X Notice
#
# This file is not used by the host name and address resolution
# or the DNS query routing mechanisms used by most processes on
# this Mac OS X system.
#
# This file is automatically generated.
#
search DHCP HOST
nameserver 59.108.61.61
nameserver 219.232.48.61複製代碼
search選項的做用是當一個域名不可解析時,就會嘗試在後面添加相應的後綴,如ping hello,沒法解析就會分別ping hello.DHCP/hello.HOST,結果最後都沒法解析。
Chrome在啓動的時候根據不一樣的操做系統去獲取DNS服務器配置,而後把它放到DNSConfig的nameservers:
// List of name server addresses.
std::vector<IPEndPoint> nameservers;複製代碼
Chrome還會監聽網絡變化同步改變配置。
而後用這個nameservers列表去初始化一個socket pool即套接字池,套接字是用來發請求的。在須要作域名解析的時候會從套接字池裏面取出一個socket,並傳遞想要用的server_index,初始化的時候是0,即取第一個DNS服務IP地址,一旦解析請求兩次都失敗了,則server_index + 1使用下一個DNS服務。
unsigned server_index =
(first_server_index_ + attempt_number) % config.nameservers.size();
// Skip over known failed servers.
// 最大attempts數爲2,在構造DnsConfig設定的
server_index = session_->NextGoodServerIndex(server_index);複製代碼
若是全部的nameserver都失敗了,那麼它會取最先失敗的nameserver.
Chrome在啓動的時候除了會讀取DNS server以外,還會去取讀取和解析hosts文件,放到DNSConfig的hosts屬性裏面,它是一個哈希map:
// Parsed results of a Hosts file.
//
// Although Hosts files map IP address to a list of domain names, for name
// resolution the desired mapping direction is: domain name to IP address.
// When parsing Hosts, we apply the "first hit" rule as Windows and glibc do.
// With a Hosts file of:
// 300.300.300.300 localhost # bad ip
// 127.0.0.1 localhost
// 10.0.0.1 localhost
// The expected resolution of localhost is 127.0.0.1.
using DnsHosts = std::unordered_map<DnsHostsKey, IPAddress, DnsHostsKeyHash>;複製代碼
hosts文件在linux系統上是在/etc/hosts:
const base::FilePath::CharType kFilePathHosts[] =
FILE_PATH_LITERAL("/etc/hosts");複製代碼
讀取這個文件沒有什麼技巧,須要一行行地去處理,並作一些非法狀況的判斷,如上面代碼的註釋。
這樣DNSConfig裏面就有兩個配置了,一個是hosts,另外一個是nameservers,DNSConfig是組合到DNSSession,它們的組合關係以下圖所示:
resolver是負責解析的驅動類,它組合了一個client,client建立一個session,session層有一個很大的做用是用來管理server_index和socket pool如分配socket等,session初始化config,config用來讀取本地綁的hosts和nameservers兩個配置。這幾層各有各的職責。
resolver有一個重要的功能,它組合了一個job,用來建立任務隊列。resolver還組合了一個Hostcache,它是放解析結果的緩存,若是緩存緩存命中的話,就不用去解析了,這個過程是這樣的,外部調rosolver提供的HostResolverImpl::Resolve接口,這個接口會先判斷在本地是否能處理:
int net_error = ERR_UNEXPECTED;
if (ServeFromCache(*key, info, &net_error, addresses, allow_stale,
stale_info)) {
source_net_log.AddEvent(NetLogEventType::HOST_RESOLVER_IMPL_CACHE_HIT,
addresses->CreateNetLogCallback());
// |ServeFromCache()| will set |*stale_info| as needed.
return net_error;
}
// TODO(szym): Do not do this if nsswitch.conf instructs not to.
// http://crbug.com/117655
if (ServeFromHosts(*key, info, addresses)) {
source_net_log.AddEvent(NetLogEventType::HOST_RESOLVER_IMPL_HOSTS_HIT,
addresses->CreateNetLogCallback());
MakeNotStale(stale_info);
return OK;
}
return ERR_DNS_CACHE_MISS;複製代碼
上面代碼先調serveFromCache去cache裏面看有沒有,若是cache命中的話則返回,不然看hosts是否命中,若是都不命中則返回CACHE_MISS的標誌位。若是返回值不等於CACHE_MISS,則直接返回:
if (rv != ERR_DNS_CACHE_MISS) {
LogFinishRequest(source_net_log, info, rv);
RecordTotalTime(info.is_speculative(), true, base::TimeDelta());
return rv;
}複製代碼
不然建立一個job,並看是否能馬上執行,若是job隊列太多了,則添加到job隊列後面,並傳遞一個成功的回調處理函數。
因此這裏和咱們的認知基本上是同樣的,先看下cache有沒有,而後再看hosts有沒有,若是沒有的話再進行查詢。在cache查詢的時候若是這個cache已通過時了即staled,也會返回null,而判斷是否stale的標準以下:
bool is_stale() const {
return network_changes > 0 || expired_by >= base::TimeDelta();
}複製代碼
即網絡發生了變化,或者expired_by大於0,則認爲是過期的cache。這個時間差是用當前時間減掉當前cache的過時時間:
stale.expired_by = now - expires_;複製代碼
而過時時間是在初始化的時候使用now + ttl的值,而這個ttl是使用上一次請求解析的時候返回的ttl:
uint32_t ttl_sec = std::numeric_limits<uint32_t>::max();
ttl_sec = std::min(ttl_sec, record.ttl);
*ttl = base::TimeDelta::FromSeconds(ttl_sec);複製代碼
上面代碼作了一個防溢出處理。在wireshark的dns response能夠直觀地看到這個ttl:
當前域名的TTL值爲600s即10分鐘。這個能夠在買域名的提供商進行設置:
另外能夠看到這個記錄類型是A的,什麼是A呢,以下圖所示:
在添加解析的時候能夠看到,A就是把域名解析到一個IPv4地址,而AAAA是解析到IPv6地址,CNAME是解析到另一個域名。使用CNAME的好處是當不少其它域名指向一個CNAME時,當須要改變IP地址時,只要改變這個CNAME的地址,那麼其它的也跟着生效了,可是得作二次解析。
若是域名在本地不能解析的話,Chrome就會去發請求了。操做系統提供了一個叫getaddrinfo的系統函數用來作域名解析,可是Chrome並無使用,而是本身實現了一個DNS客戶端,包括封裝DNS request報文以及解析DNS response報文。這樣多是由於靈活度會更大一點,例如Chrome能夠自行決定怎麼用nameservers,順序以及失敗嘗試的次數等。
在resolver的startJob裏面啓動解析。取到下一個queryId,而後構建一個query,再構建一個DnsUDPAttempt,再執行它的start,由於DNS客戶端查詢使用的是UDP報文(輔域名服務器向主域名服務器查詢是用的TCP):
uint16_t id = session_->NextQueryId();
std::unique_ptr<DnsQuery> query;
query.reset(new DnsQuery(id, qnames_.front(), qtype_, opt_rdata_));
DnsUDPAttempt* attempt =
new DnsUDPAttempt(server_index, std::move(lease), std::move(query));
int rv = attempt->Start(
base::Bind(&DnsTransactionImpl::OnUdpAttemptComplete,
base::Unretained(this), attempt_number,
base::TimeTicks::Now()));複製代碼
具體解析的過程拆成了幾步,這個代碼組織是這樣的,經過一個state決定執行順序:
int rv = result;
do {
// 最開始的state爲STATE_SEND_QUERY
State state = next_state_;
next_state_ = STATE_NONE;
switch (state) {
case STATE_SEND_QUERY:
rv = DoSendQuery();
break;
case STATE_SEND_QUERY_COMPLETE:
rv = DoSendQueryComplete(rv);
break;
case STATE_READ_RESPONSE:
rv = DoReadResponse();
break;
case STATE_READ_RESPONSE_COMPLETE:
rv = DoReadResponseComplete(rv);
break;
default:
NOTREACHED();
break;
}
} while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);複製代碼
state從第一個case執行完以後變成第二個case的state,在第二個case的執行函數裏面又把它改爲第三個,這樣依次下來,直到變成while循環裏面的STATE_DONE,或者是ERR狀態結束當前transaction事務。因此這個代碼組織仍是比較有趣的。
最後解析成功以後,會把結果放到cache裏面:
if (did_complete) {
resolver_->CacheResult(key_, entry, ttl);
RecordJobHistograms(entry.error());
}複製代碼
而後生成一個addressList,傳遞給相應的callback,由於DNS解析可能會返回多個結果,以下面這個:
這裏咱們沒用Chrome打印結果了,都是直接看的wireshark的輸出,由於添加打印函數比較麻煩,直接看wireshark的輸出比較直觀,節省時間。
本文簡單地介紹了DNS解析的過程以及DNS的一些相關概念,相信到這裏,應該能夠回答上面提出的幾個問題了。總地來講,客戶端向域名解析服務器發起查詢,而後服務器返回響應。DNS服務器nameservers是在設備接入網絡的時候路由器經過DHCP發給設備的,chrome會按照nameservers的順序發起查詢,並將結果緩存,有效時間根據ttl,有效期內兩次查詢直接使用cache。DNS解析的結果有幾種類型,最多見的是A記錄和CNAME記錄,A記錄表示結果是一個IP地址,CNAME表示結果是另一個域名。
本文沒有很深刻詳細地介紹,可是核心的概念和邏輯過程應該是都有涉及了。