背景:需要實(shí)現(xiàn)一個(gè)判斷設(shè)備是否能夠上網(wǎng)的功能,可能的實(shí)現(xiàn)方案有ping、DNS解析、http get等之一或者是幾個(gè)結(jié)合起來一起判斷,我們這里講其中的一步,DNS解析。解析DNS可以作為判斷設(shè)備能夠上網(wǎng)的前提條件使用,如果沒有這一步,像ping、http get等操作就不具備實(shí)施條件,除非你說你用ip作為參數(shù),即使ip是通的但解析不了DNS那么上網(wǎng)檢測(cè)的結(jié)果就失去了其意義。
在Linux系統(tǒng)上,使用C語言還是什么其他的什么語言去做DNS解析工作,基本都會(huì)調(diào)用到系統(tǒng)庫(kù)函數(shù)gethostbyname,而這個(gè)函數(shù)是阻塞的,當(dāng)DNS服務(wù)器不可達(dá)時(shí),阻塞的時(shí)間是15秒鐘。你的程序是否能夠忍受這15秒的時(shí)間呢,用戶是否能夠容忍15秒后才告訴他答案,或者測(cè)試構(gòu)造了一個(gè)DNS服務(wù)器不可達(dá)的環(huán)境發(fā)現(xiàn)你的檢測(cè)進(jìn)程阻塞在那里而導(dǎo)致依賴于這個(gè)檢測(cè)結(jié)果的相關(guān)功能出現(xiàn)異常的時(shí)候,你要不要去解這個(gè)問題呢?我想大多數(shù)程序沒有考慮到這個(gè)問題,因?yàn)間ethostbyname用于太多的開源代碼中(只要我們?cè)诿钚谢蚺渲脜?shù)中配置的服務(wù)器等地址使用的是域名),而大部分場(chǎng)景都是正常工作的,我們都假定一個(gè)前提,網(wǎng)絡(luò)是好的,而網(wǎng)絡(luò)是異常時(shí)這個(gè)程序工作異常也是正常情況。
而我們做一個(gè)商用產(chǎn)品的價(jià)值之處在于如何更好地提升用戶的體驗(yàn),而不僅僅是可用。產(chǎn)品經(jīng)理會(huì)想盡辦法讓用戶感受到產(chǎn)品的價(jià)值,尤其是在出現(xiàn)異常用戶不知所措的時(shí)候告訴用戶,甚至是精準(zhǔn)告訴用戶,是哪里哪一步出現(xiàn)問題了,而不只是反映網(wǎng)絡(luò)連接斷開,請(qǐng)檢查你的網(wǎng)絡(luò)。程序員為了滿足這種價(jià)值將竭盡全力在各種條件和邏輯間作處理及轉(zhuǎn)換,此所謂業(yè)務(wù)流程、邏輯、場(chǎng)景而非協(xié)議、模塊等比較原子的東西。所以你是否可以理解一個(gè)做了N年產(chǎn)品的程序員他的積累或價(jià)值往往不是他能徒手寫某個(gè)算法或者跟你說他印象最深刻或最有成就感的一個(gè)案例,也許你試圖引導(dǎo)他深入一步步去剖析這個(gè)案例以探尋到他的深度。但你可能會(huì)失望,話說這些邊緣的設(shè)備一些邊緣的細(xì)節(jié)(雖然現(xiàn)在換了個(gè)名字叫邊緣計(jì)算),可能比起大數(shù)據(jù)、虛擬化、AI之類的你也不感興趣。
gethostbyname函數(shù)的阻塞情況分析:
1)socket(udp)
2)connect
使用DNS服務(wù)器地址和端口進(jìn)行連接,udp的connect作用和本文相關(guān)的大概兩個(gè):
1、connect會(huì)做路由查詢,如果同網(wǎng)段則走接口路由,返回成功。如果不同網(wǎng)段,則為該套接口查找下一跳路由,一般是默認(rèn)路由,如果沒有找到下一跳路由,則connect階段就會(huì)出錯(cuò),返回網(wǎng)絡(luò)可不達(dá),跳到第6)步,也可以理解為這是網(wǎng)絡(luò)異常的時(shí)候第一次快速退出的地方。反之,不同網(wǎng)段而有下一跳路由時(shí),返回成功。
2、udp connect后,當(dāng)send發(fā)送數(shù)據(jù)時(shí),如果對(duì)端存在異常時(shí),可以收到對(duì)端異步返回的錯(cuò)誤信息,比如感知到ICMP報(bào)文的端口不可達(dá)信息,而非connect對(duì)這個(gè)異步信息是無感知的,這樣的一個(gè)好處是有網(wǎng)絡(luò)錯(cuò)誤也可以收到并退出,不用一直阻塞recv。
3)send
由于udp無連接機(jī)制,此處都是成功的,而如果非connect情況下,此處應(yīng)該調(diào)用sendto,sendto其實(shí)會(huì)隱式調(diào)用如connect一樣的查找路由過程,如上述2)1、中一樣處理。
4)poll(wait 5 seconds)
5)recv
到了這一步,說明網(wǎng)絡(luò)是可達(dá)的,這里的可達(dá)有兩種情況:
1、端到端可達(dá),就是設(shè)備和DNS服務(wù)器是可達(dá)的,這個(gè)時(shí)候的異常也有兩種:端口不可達(dá),比如DNS服務(wù)器主機(jī)并沒有運(yùn)行DNS服務(wù)器,所以,此時(shí)將會(huì)返回端口不可達(dá)信息,而connect情況下,poll會(huì)收到該信息并置于錯(cuò)誤句柄集合中,recv將返回-1,該錯(cuò)誤信息一般是ECONNREFUSED (Connection refused);DNS代理服務(wù)器工作正常,但是DNS代理的上級(jí)不可達(dá),無法完成正常的DNS查詢,但此時(shí)DNS代理服務(wù)器收到下級(jí)的DNS查詢后,如果他知道無法完成轉(zhuǎn)發(fā)查詢,則一般會(huì)返回RCODE=5(Refused)報(bào)文,此時(shí)recv也能正常收到該返回報(bào)文,因?yàn)闆]對(duì)域名解析成功,gethostbyname會(huì)設(shè)置h_errno為HOST_NOT_FOUND。這兩種情況都不會(huì)造成阻塞。
2、下一級(jí)路由不可達(dá),此時(shí)必然阻塞,因?yàn)榇藭r(shí)設(shè)備不會(huì)收到任何控制和數(shù)據(jù)報(bào)文,需要等待epoll超時(shí)才能退出,epoll超時(shí)會(huì)直接調(diào)到第6步。
3、下一級(jí)就是目標(biāo)DNS服務(wù)器主機(jī),但不可達(dá),此時(shí)必然阻塞,原因同上。
4、下一級(jí)路由可達(dá)但是DNS服務(wù)器主機(jī)不可達(dá),可能數(shù)據(jù)報(bào)文到達(dá)某一跳時(shí),會(huì)返回ICMP Network is unreachable錯(cuò)誤,但是該錯(cuò)誤無法到達(dá)recv接口,即使是connect情況下,因?yàn)樵揑CMP的IP元組信息和connect保存的IP元組信息無法對(duì)應(yīng)(因?yàn)镈NS服務(wù)器主機(jī)不可達(dá),所以ICMP的錯(cuò)誤信息是由另外一臺(tái)機(jī)子返回,而非DNS服務(wù)器主機(jī)返回),所以此時(shí)也必然阻塞。
6)close
7)goto 1)2次,加初始時(shí)一次,一起執(zhí)行3次,最長(zhǎng)阻塞約15秒鐘。
鑒于有上述異常,為了不讓這個(gè)問題產(chǎn)生在用戶環(huán)境,只能放棄gethostbyname,自己實(shí)現(xiàn)一個(gè)gethostbyname用于解決聯(lián)網(wǎng)檢測(cè)需求。下面是lua 寫的Demo代碼,上面想清楚了,寫代碼并不耗什么時(shí)間(但是在發(fā)送DNS報(bào)文的處理上還是頗費(fèi)周章,最終想到使用文件來處理二進(jìn)制的方法,對(duì)lua還需要更多了解)。
--[[dns_data_bin是dns報(bào)文的文件名
dns_host是dns服務(wù)器主機(jī)的ip
dns_port是dns服務(wù)器的端口
超時(shí)時(shí)間設(shè)置為2,最長(zhǎng)2秒鐘返回檢測(cè)結(jié)果
]]
local function check_dns(dns_data_bin, dns_host, dns_port)
? local recv_data, ret, err
? --[[lua操作二進(jìn)制組裝DNS報(bào)文比較困難,所以
? 將DNS報(bào)文的二進(jìn)制文件保存于文件,由lua讀出到變
? 量]]
local dns_file = io.open(dns_data_bin,"rb")
local len = dns_file:seek("end")
dns_file:seek("set",0)
local dns_data = dns_file:read("*a")
dns_file:close()
local socket = require("socket")
local sock = assert(socket.udp())
--[[設(shè)置超時(shí)時(shí)間,只對(duì)receive有效,其實(shí)改gethostbyname就是為了改超時(shí)時(shí)間]]
sock:settimeout(2)
ret, err = sock:setpeername(dns_host, dns_port)
if(ret == nil) then
--[[setpeername做的事情就是connect,ret為nil時(shí),表示connect失敗]]
print(dns_host, dns_port, err)
--[[返回失敗]]
return nil
end
--[[通過send發(fā)出DNS報(bào)文]]
sock:send(dns_data, len)
recv_data, err = sock:receive()
sock:close()
? --[[檢查是否有數(shù)據(jù)返回]]
if(recv_data == nil) then
--[[沒有數(shù)據(jù)返回,此處即超時(shí)返回]]
print(dns_host, dns_port, dns_data_bin, err)
return nil
else
--[[有數(shù)據(jù)返回,但是此處需要解析返回?cái)?shù)據(jù),由于lua操作二進(jìn)制不便,這里簡(jiǎn)單以長(zhǎng)度做判斷]]
len = string.len(recv_data)
print(dns_host, dns_port, dns_data_bin, len)
if(len < 40) then
--[[此處即為返回Refused的情況]]
return nil
else
--[[返回成功]]
return 1
end
end
end
也說下另一個(gè)常見的方法:siglongjmp,這里不詳述,該方法有一個(gè)問題,siglongjmp跳過當(dāng)前的gethostbyname后,無法將gethostbyname中打開的socket關(guān)閉,如果使用頻繁,將會(huì)消耗系統(tǒng)的句柄資源,出現(xiàn)資源泄露,這個(gè)可以使用netstat命令查看便知。這也是這個(gè)檢測(cè)需求最終沒有使用c語言實(shí)現(xiàn)的原因之一。