從Chrome源碼看DNS解析過(guò)程
DNS解析的作用是把域名解析成相應(yīng)的IP地址,因?yàn)樵趶V域網(wǎng)上路由器需要知道IP地址才知道把報(bào)文發(fā)給誰(shuí)。DNS是Domain Name System域名系統(tǒng)的縮寫,它是一個(gè)協(xié)議,在RFC 1035具體描述了這個(gè)協(xié)議。具體過(guò)程如下圖所示:
這個(gè)過(guò)程看似簡(jiǎn)單,但是有幾個(gè)問(wèn)題:
(1)瀏覽器是怎么知道DNS解析服務(wù)器,如上圖的8.8.8.8這臺(tái)?
(2)一個(gè)域名可以解析成多個(gè)IP地址嗎,如果只有一個(gè)IP地址,在并發(fā)量很大的情況下,那臺(tái)服務(wù)器可能會(huì)爆?
(3)把域名綁了host之后,是不是就不用域名解析了直接用的本地host指定的IP地址?
(4)域名解析的有效時(shí)間為多長(zhǎng),即過(guò)了多久后同一個(gè)域名需要再次進(jìn)行解析?
(5)什么是域名解析的A記錄、AAAA記錄、CNAME記錄?
其實(shí)域名解析和Chrome沒(méi)有直接關(guān)系,即使是最簡(jiǎn)單的curl命令也需要進(jìn)行域名解析,但是我們可以通過(guò)Chrome源碼來(lái)看一下這個(gè)過(guò)程是怎么樣的,并且回答上面的問(wèn)題。
首先***個(gè)問(wèn)題,瀏覽器是怎么知道DNS解析服務(wù)器的,在本機(jī)的網(wǎng)絡(luò)設(shè)置里面可以看到當(dāng)前的DNS服務(wù)器IP,如我電腦的:
這兩個(gè)DNS Server是我家接的某正寬帶提供的:
一般寬帶服務(wù)商都會(huì)提供DNS服務(wù)器,谷歌還為公眾提供了兩個(gè)免費(fèi)的DNS服務(wù),分別為8.8.8.8和8.8.4.4,取這兩個(gè)IP地址是為了容易記住,當(dāng)你的DNS服務(wù)不好用的時(shí)候,可以嘗試改成這兩個(gè)。
入網(wǎng)的設(shè)備是怎么獲取到這些IP地址的呢?是通過(guò)動(dòng)態(tài)主機(jī)配置協(xié)議(DHCP),當(dāng)一臺(tái)設(shè)備連到路由器之后,路由器通過(guò)DHCP給它分配一個(gè)IP地址,并告訴它DNS服務(wù)器,如下路由器的DHCP設(shè)置:
通過(guò)wireshark抓包可以觀察到這個(gè)過(guò)程:
當(dāng)我的電腦連上wifi的時(shí)候,會(huì)發(fā)一個(gè)DHCP Request的廣播,路由器收到這個(gè)廣播后就會(huì)向我的電腦分配一個(gè)IP地址并告知DNS服務(wù)器。
這個(gè)時(shí)候系統(tǒng)就有DNS服務(wù)器了,Chrome是調(diào)res_ninit這個(gè)系統(tǒng)函數(shù)(Linux)去獲取系統(tǒng)的DNS服務(wù)器,這個(gè)函數(shù)是通過(guò)讀取/etc/resolver.conf這個(gè)文件獲取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選項(xiàng)的作用是當(dāng)一個(gè)域名不可解析時(shí),就會(huì)嘗試在后面添加相應(yīng)的后綴,如ping hello,無(wú)法解析就會(huì)分別ping hello.DHCP/hello.HOST,結(jié)果***都無(wú)法解析。
Chrome在啟動(dòng)的時(shí)候根據(jù)不同的操作系統(tǒng)去獲取DNS服務(wù)器配置,然后把它放到DNSConfig的nameservers:
- // List of name server addresses.
- std::vector<IPEndPoint> nameservers;
Chrome還會(huì)監(jiān)聽(tīng)網(wǎng)絡(luò)變化同步改變配置。
然后用這個(gè)nameservers列表去初始化一個(gè)socket pool即套接字池,套接字是用來(lái)發(fā)請(qǐng)求的。在需要做域名解析的時(shí)候會(huì)從套接字池里面取出一個(gè)socket,并傳遞想要用的server_index,初始化的時(shí)候是0,即取***個(gè)DNS服務(wù)IP地址,一旦解析請(qǐng)求兩次都失敗了,則server_index + 1使用下一個(gè)DNS服務(wù)。
- unsigned server_index =
- (first_server_index_ + attempt_number) % config.nameservers.size();
- // Skip over known failed servers.
- // ***attempts數(shù)為2,在構(gòu)造DnsConfig設(shè)定的
- server_index = session_->NextGoodServerIndex(server_index);
如果所有的nameserver都失敗了,那么它會(huì)取最早失敗的nameserver.
Chrome在啟動(dòng)的時(shí)候除了會(huì)讀取DNS server之外,還會(huì)去取讀取和解析hosts文件,放到DNSConfig的hosts屬性里面,它是一個(gè)哈希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系統(tǒng)上是在/etc/hosts:
- const base::FilePath::CharType kFilePathHosts[] =
- FILE_PATH_LITERAL("/etc/hosts");
讀取這個(gè)文件沒(méi)有什么技巧,需要一行行地去處理,并做一些非法情況的判斷,如上面代碼的注釋。
這樣DNSConfig里面就有兩個(gè)配置了,一個(gè)是hosts,另一個(gè)是nameservers,DNSConfig是組合到DNSSession,它們的組合關(guān)系如下圖所示:
resolver是負(fù)責(zé)解析的驅(qū)動(dòng)類,它組合了一個(gè)client,client創(chuàng)建一個(gè)session,session層有一個(gè)很大的作用是用來(lái)管理server_index和socket pool如分配socket等,session初始化config,config用來(lái)讀取本地綁的hosts和nameservers兩個(gè)配置。這幾層各有各的職責(zé)。
resolver有一個(gè)重要的功能,它組合了一個(gè)job,用來(lái)創(chuàng)建任務(wù)隊(duì)列。resolver還組合了一個(gè)Hostcache,它是放解析結(jié)果的緩存,如果緩存緩存***的話,就不用去解析了,這個(gè)過(guò)程是這樣的,外部調(diào)rosolver提供的HostResolverImpl::Resolve接口,這個(gè)接口會(huì)先判斷在本地是否能處理:
- 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;
上面代碼先調(diào)serveFromCache去cache里面看有沒(méi)有,如果cache***的話則返回,否則看hosts是否***,如果都不***則返回CACHE_MISS的標(biāo)志位。如果返回值不等于CACHE_MISS,則直接返回:
- if (rv != ERR_DNS_CACHE_MISS) {
- LogFinishRequest(source_net_log, info, rv);
- RecordTotalTime(info.is_speculative(), true, base::TimeDelta());
- return rv;
- }
否則創(chuàng)建一個(gè)job,并看是否能立刻執(zhí)行,如果job隊(duì)列太多了,則添加到j(luò)ob隊(duì)列后面,并傳遞一個(gè)成功的回調(diào)處理函數(shù)。
所以這里和我們的認(rèn)知基本上是一樣的,先看下cache有沒(méi)有,然后再看hosts有沒(méi)有,如果沒(méi)有的話再進(jìn)行查詢。在cache查詢的時(shí)候如果這個(gè)cache已經(jīng)過(guò)時(shí)了即staled,也會(huì)返回null,而判斷是否stale的標(biāo)準(zhǔn)如下:
- bool is_stale() const {
- return network_changes > 0 || expired_by >= base::TimeDelta();
- }
即網(wǎng)絡(luò)發(fā)生了變化,或者expired_by大于0,則認(rèn)為是過(guò)時(shí)的cache。這個(gè)時(shí)間差是用當(dāng)前時(shí)間減掉當(dāng)前cache的過(guò)期時(shí)間:
- stale.expired_by = now - expires_;
而過(guò)期時(shí)間是在初始化的時(shí)候使用now + ttl的值,而這個(gè)ttl是使用上一次請(qǐng)求解析的時(shí)候返回的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);
上面代碼做了一個(gè)防溢出處理。在wireshark的dns response可以直觀地看到這個(gè)ttl:
當(dāng)前域名的TTL值為600s即10分鐘。這個(gè)可以在買域名的提供商進(jìn)行設(shè)置:
另外可以看到這個(gè)記錄類型是A的,什么是A呢,如下圖所示:
在添加解析的時(shí)候可以看到,A就是把域名解析到一個(gè)IPv4地址,而AAAA是解析到IPv6地址,CNAME是解析到另外一個(gè)域名。使用CNAME的好處是當(dāng)很多其它域名指向一個(gè)CNAME時(shí),當(dāng)需要改變IP地址時(shí),只要改變這個(gè)CNAME的地址,那么其它的也跟著生效了,但是得做二次解析。
如果域名在本地不能解析的話,Chrome就會(huì)去發(fā)請(qǐng)求了。操作系統(tǒng)提供了一個(gè)叫g(shù)etaddrinfo的系統(tǒng)函數(shù)用來(lái)做域名解析,但是Chrome并沒(méi)有使用,而是自己實(shí)現(xiàn)了一個(gè)DNS客戶端,包括封裝DNS request報(bào)文以及解析DNS response報(bào)文。這樣可能是因?yàn)殪`活度會(huì)更大一點(diǎn),例如Chrome可以自行決定怎么用nameservers,順序以及失敗嘗試的次數(shù)等。
在resolver的startJob里面啟動(dòng)解析。取到下一個(gè)queryId,然后構(gòu)建一個(gè)query,再構(gòu)建一個(gè)DnsUDPAttempt,再執(zhí)行它的start,因?yàn)镈NS客戶端查詢使用的是UDP報(bào)文(輔域名服務(wù)器向主域名服務(wù)器查詢是用的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()));
具體解析的過(guò)程拆成了幾步,這個(gè)代碼組織是這樣的,通過(guò)一個(gè)state決定執(zhí)行順序:
- int rv = result;
- do {
- // 最開(kāi)始的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從***個(gè)case執(zhí)行完之后變成第二個(gè)case的state,在第二個(gè)case的執(zhí)行函數(shù)里面又把它改成第三個(gè),這樣依次下來(lái),直到變成while循環(huán)里面的STATE_DONE,或者是ERR狀態(tài)結(jié)束當(dāng)前transaction事務(wù)。所以這個(gè)代碼組織還是比較有趣的。
***解析成功之后,會(huì)把結(jié)果放到cache里面:
- if (did_complete) {
- resolver_->CacheResult(key_, entry, ttl);
- RecordJobHistograms(entry.error());
- }
然后生成一個(gè)addressList,傳遞給相應(yīng)的callback,因?yàn)镈NS解析可能會(huì)返回多個(gè)結(jié)果,如下面這個(gè):
這里我們沒(méi)用Chrome打印結(jié)果了,都是直接看的wireshark的輸出,因?yàn)樘砑哟蛴『瘮?shù)比較麻煩,直接看wireshark的輸出比較直觀,節(jié)省時(shí)間。
本文簡(jiǎn)單地介紹了DNS解析的過(guò)程以及DNS的一些相關(guān)概念,相信到這里,應(yīng)該可以回答上面提出的幾個(gè)問(wèn)題了??偟貋?lái)說(shuō),客戶端向域名解析服務(wù)器發(fā)起查詢,然后服務(wù)器返回響應(yīng)。DNS服務(wù)器nameservers是在設(shè)備接入網(wǎng)絡(luò)的時(shí)候路由器通過(guò)DHCP發(fā)給設(shè)備的,chrome會(huì)按照nameservers的順序發(fā)起查詢,并將結(jié)果緩存,有效時(shí)間根據(jù)ttl,有效期內(nèi)兩次查詢直接使用cache。DNS解析的結(jié)果有幾種類型,最常見(jiàn)的是A記錄和CNAME記錄,A記錄表示結(jié)果是一個(gè)IP地址,CNAME表示結(jié)果是另外一個(gè)域名。
本文沒(méi)有很深入詳細(xì)地介紹,但是核心的概念和邏輯過(guò)程應(yīng)該是都有涉及了。
【本文是51CTO專欄作者“人人網(wǎng)FED”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)51CTO聯(lián)系原作者獲取授權(quán)】