聯(lián)網(wǎng)檢測(cè):gethostbyname阻塞分析及使用lua完成DNS解析的判斷

背景:需要實(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)的原因之一。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、TCP狀態(tài)linux查看tcp的狀態(tài)命令:1)、netstat -nat 查看TCP各個(gè)狀態(tài)的數(shù)量2)、lso...
    北辰青閱讀 9,715評(píng)論 0 11
  • 最近在學(xué)習(xí)Python看了一篇文章寫得不錯(cuò),是在腳本之家里的,原文如下,很有幫助: 一、網(wǎng)絡(luò)知識(shí)的一些介紹 soc...
    qtruip閱讀 2,844評(píng)論 0 6
  • 網(wǎng)絡(luò) 理論模型,分為七層物理層數(shù)據(jù)鏈路層傳輸層會(huì)話層表示層應(yīng)用層 實(shí)際應(yīng)用,分為四層鏈路層網(wǎng)絡(luò)層傳輸層應(yīng)用層 IP...
    FlyingLittlePG閱讀 961評(píng)論 0 0
  • 網(wǎng)絡(luò)編程 一.楔子 你現(xiàn)在已經(jīng)學(xué)會(huì)了寫python代碼,假如你寫了兩個(gè)python文件a.py和b.py,分別去運(yùn)...
    go以恒閱讀 2,244評(píng)論 0 6
  • 個(gè)人認(rèn)為,Goodboy1881先生的TCP /IP 協(xié)議詳解學(xué)習(xí)博客系列博客是一部非常精彩的學(xué)習(xí)筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,193評(píng)論 0 8

友情鏈接更多精彩內(nèi)容