天天看點

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

自上而下的了解網絡(1)——DNS篇

一.引言

現代生活中,網絡可謂是無處不在,購物需要網絡,付款需要網絡,各種生活繳費需要網絡,在各行各業的工作中,更是離不開網絡。說到底,網絡的作用無非是支援計算機間進行資料交換。世界各地有着不計其數的網絡裝置,這些網絡裝置是如何有序正常的進行資料交流的呢?網絡以及各種協定的工作原理又是怎樣的呢?本系列部落格,我們将嘗試自上而下的對網路的工作原理進行介紹,從應用層開始,逐層向下,詳細的幫助你了解網絡的核心工作原理。當然,網絡協定多如牛毛,在網絡分層中每一層的知識也是非常浩渺,希望這些部落格可以起到抛磚引玉的作用,能夠使你對于天天使用的網際網路網絡在宏觀上有認識,在微觀上也有了解。

二.通路網站的第一步是什麼?

說到網絡,對于普通使用者來說,使用最多的可能就是浏覽各種網站了,雖然現在移動裝置上的App基本代替了傳統的PC應用和網站,但是這些App裡提供的資料本質上網站中提供的資料并無不同,使用的網絡技術并無不同。

我們知道,不論是通路網站還是App内進行接口請求,這些資料都是存儲在“伺服器”這種特殊的遠端裝置上的,要向伺服器擷取資料,首先我們需要找到伺服器的位置,這很好了解,隻有找到它,我們才能和它産生資料交流。網際網路無論多大,本質上依然是通過電纜、光纖或各種無線裝置這類連接配接媒體連接配接在一起的,如果一台裝置沒有硬體上連接配接入網際網路,那麼說破天我們也無法和它産生資料互動。要找到一台網際網路裝置,實際上是通過其實體Mac位址來找到的,這就像現實中的門牌号一樣,每家的門牌号都不同,說到這,我們要再老生常談一下,抛出網絡分層模型給你看:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

關于這個網絡分層模型,它在我們後面部落格中的出境還少不了,現在你可以先不用管它,你隻需要先知道實體層是負責裝置實體媒介相關的協定,資料鍊路層通過硬體的Mac位址找到具體要網絡裝置,網絡層通過IP協定來封裝真實的Mac位址,傳輸層是對網絡層的一種封裝,TCP,UDP等傳輸協定在這一層工作,而最上層的應用層就是我們常說的網絡應用協定,如DNS,HTTP,HTTPS和FPT協定工作在這一層。

關于網絡分層模型,我們先把多說了,我們的宗旨是自上而下的了解網絡,那麼還是回到第一步來。我們在通路網站時,都會現在浏覽器輸入網站的位址,這通常是一個域名,例如我要通路自己的技術部落格網站,我會在浏覽器輸入如下的位址:

https://huishao.cc/

huishao.cc就是一個域名,首先隻通過域名我們是找不到要通路的對方伺服器的,這就好像現實中我要去小王家,可以我隻知道小王的名字“王某某”是無法找到他的家的,我需要有一個住址簿,告訴我小王究竟住在哪了,這樣我才能找到他。當然,此住址可能也不是真正的實體位置,可能是一個社群,比如小王住在“光明社群”,具體光明社群在哪,我們可以再通過檢視地圖擷取。對應到網際網路中,域名就是一個名字,它友善我們對網站進行記憶,IP位址則是要通路的對方在邏輯上的位址,這友善網際網路的網絡管理,最終的硬體位址則是真正的對方位置。IP位址到硬體位址的映射,等我們讨論到了再細聊,本篇部落格我們就說域名到IP位址映射這一過程。

三. DNS伺服器

現在你應該已經明确,要通過域名找到某個裝置,第一步是先得到此域名對應的IP位址,那麼此IP位址是怎麼得到的呢?首先,一定有一個地方維護了域名與IP位址的映射關系,如果你有過建站的經曆,那麼你一定進行過域名綁定操作,一個網站建成後,理論上就已經可以使用IP的方式來進行通路,但是為了易記和動态變動IP,通常會對其進行域名綁定。由域名擷取到IP的這一過程,我們稱之為域名解析。

域名解析是一種服務,提供域名解析服務的伺服器即是DNS伺服器,下圖可以很形象的表示域名伺服器的工作方式:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

可以發現,映射表中記錄了域名與IP間的映射關系,在實際的應用中,上圖中描述的場景看似可行,實際卻并非如此,世界上的域名與IP總數是一個非常龐大的數字,由一台伺服器來維護所有域名IP資訊幾乎不可能,而且對于域名解析服務,請求量是巨大的,會有大量的使用者頻繁的進行域名解析請求,單伺服器明顯是不能滿足需求的。是以,實際生産環境中的DNS解析是采用層層遞進,多級緩存,遞歸查詢的方式進行的。再看下圖:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

上圖看似複雜,實際上隻是描述了三個關鍵詞:層層遞進,多級緩存,遞歸查詢。

四.DNS解析過程

下面我們來解釋域名要解析成正确的IP位址,要經過的幾個重要過程。

1. 本機hosts檔案

本機hosts檔案是優先級最高的域名IP映射表,對于Mac作業系統,這個檔案在根目錄的etc檔案夾下,我們可以直接将域名與對應的IP寫在這個檔案中,在進行域名解析時,首先會從這個檔案中找。廣播IP和本機IP對應的域名實際上就定義在這裡,如下:

127.0.0.1       localhost
255.255.255.255 broadcasthost

           

你也可以在其中新增任意映射,例如将huishao.cc的域名映射到127.0.0.1的本機IP,儲存後,

在浏覽器再輸入huishao.cc

,你将無法再通路到珲少的部落格網站,如下圖所示:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

更多時候,hosts的正确用法是開發應用程式時,測試環境和正式環境可以将域名配置到不同的IP,這樣無需應用程式代碼中做邏輯,隻需要切換hosts檔案即可實作環境的切換。

2. 本機應用緩存

本機應用緩存是多級緩存中的第一級,例如當我們在浏覽器中通路過某個域名後,其解析的結果會被浏覽器緩存下來,當我們再次通路這個域名時,其首先會檢查浏覽器緩存,如果緩存能夠命中此域名,則直接使用,緩存的有效時間會受TTL配置影響(我們後面會介紹)。

3. 本機系統緩存

與本機應用緩存類似,作業系統中也會有一份域名解析的緩存,如果本機應用緩存中沒有命中,會從作業系統緩存中檢查是否之前有過此域名的解析記錄。如果能夠命中則會直接使用。

4. 路由器域名解析緩存

如果本機系統緩存依然沒有命中,而你的裝置又是通過路由器接入的公網,此時你的域名解析服務很大可能是路由器提供的,可以打開網絡設定的DNS一欄,觀察DNS伺服器的位址,如果是192.168.x.x類型内網位址,則說明是由路由器來完成DNS解析了。如下圖所示:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

路由器内,實際上也會緩存一張DNS解析表,會從其中尋找是否有可以命中的緩存,如果存在并且未過期,則直接使用。有時候,你會發現電腦可以直接使用IP通路網站但是無法使用域名進行通路,很大可能是路由器的DNS服務出問題了,最簡單的解決方式就是将配置的DNS伺服器IP位址改成公共的。

5. 通路本地域名伺服器

如果以上的緩存都沒有命中,那麼邏輯上我們就需要通過外網的DNS服務來進行解析了,首先本地伺服器(LDNS)來解析域名,這裡的本地伺服器是指城市或區域的DNS伺服器,一般就有營運商部署在當地,距離近,性能好,并且也有緩存機制,幾乎可以覆寫大多數的域名解析請求。

6. 轉發與遞歸

如果你通路的域名比較冷門,本地伺服器依然無法解析,則會進行轉發,将此請求轉發到更進階的營運商DNS伺服器或者根DNS伺服器,根DNS伺服器會根據域名來傳回頂級的域名伺服器位址,本地伺服器可以繼續向頂級域名伺服器請求解析。如此遞歸進行,直到解析成功,再将IP位址依次傳回到我們的裝置,并逐層做緩存,以便我們下次通路時可以快速得到響應。

上面過程中,我們有提到根域名伺服器,其是最進階别的域名伺服器,它負責傳回頂級域名伺服器,目前全球有13個根域名伺服器站。頂級域名伺服器用來針對某個頂級域名進行解析,例如.com頂級域名,.edu頂級域名,.cc頂級域名和.cn頂級域名等。頂級域名伺服器在解析時會将查詢到的主域名伺服器傳回。主域名伺服器負責某個區域的域名解析,同樣,主域名伺服器會配套輔助域名伺服器進行備份與分擔負載。

五.DNS協定

前面說了這麼多,都是宏觀上的認識。現在,我們要讨論一些更深入的東西了。雖然對于DNS是幹什麼的,解析的過程是怎樣的我們有了一些了解。但是DNS協定究竟是怎麼操作的呢?IP資料是怎麼得到的?我們可以手動來進行DNS解析麼?要了解這些問題,首先需要對DNS協定本身做個了解。

DNS協定是工作在應用層的一種協定,全稱Domain Name System。DNS協定是基于UDP之上實作的,前面說過UDP是工作在傳輸層的一種網絡協定,等我們說到它的時候再深入探讨。現在你隻需要知道,基于UDP任何人都可以實作一個DNS解析服務。DNS解析分為兩步,首先需要用戶端向伺服器發送一個DNS請求封包,伺服器收到封包,解析完成後再傳回一個DNS封包給用戶端,此封包中就包含解析的資料。

DNS協定規定其請求封包與響應封包的結構是一緻的,都包含Header,Question,Answer,Authority,Additional這5個部分。

1. Header部分

Header部分的長度是一定的,固定為12個位元組。DNS協定文檔中有一張圖,很好的描述了Header的資料結構:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

ID:ID占了兩個位元組,它是一個辨別符,由用戶端請求的時候填充,DNS伺服器解析後,會将此ID傳回,用來讓用戶端将響應與請求對應起來。

配置字段:上圖中第2行的都是配置字段,其占了兩個位元組。

QR占1為,設定為0表示目前是DNS請求封包,設定為1表示目前為DNS響應封包。

Opcode占4位,此值由請求封包設定,并且被複制到響應封包傳回。其用來設定查詢的類型,設定為0表示标準查詢,即由域名解析出IP,設定為1表示反向查詢,即由IP反查出域名,設定為2用來查詢伺服器的狀态,3-15為保留字段,以待後續使用。

AA字段占1位,隻在傳回的響應封包中有,0表示傳回資料的伺服器不是權威伺服器,1表示傳回資料的伺服器是權威伺服器。需要注意,傳回的響應封包中可能有多個應答,此字段表明的是第一個應答的伺服器類型。

TC字段占1位,表示此封包是否由于資料的傳輸大小而被截斷,當此字段的為1時,資料不可信。

RD字段占1位,該值需要在請求封包中設定,響應封包會直接複制該值。此值表示是否希望伺服器進行遞歸查詢。

RA字段占1位,其在響應封包中設定,表示服務端是否支援遞歸查詢。

Z字段占3位,是保留字段。

rcode字段占4位,是響應封包的響應碼,0表示沒有錯誤;1表示請求格式有誤,服務端無法解析;2表示伺服器出錯;3表示請求的域名不存在;4表示伺服器不支援這類請求;5表示伺服器拒絕此次請求;6-15是保留參數。

QDCOUNT:占16位,表明Question部分包含的執行個體個數,是無符号數。

ANCOUNT:占16位,表明Answer部分包含的回答個數,是無符号數。

NSCOUNT:占16位,表明Authority部分包含的授權伺服器數量,是無符号整數。

ARCOUNT:占16位,表明Additional部分中包含的資源記錄數量,是無符号整數。

2. Question部分

這個部分用來定義查詢的問題,問題的個數在QDCOUNT指明,通常隻會攜帶一個問題。每個問題的格式定義如下:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

QNAME:此部分位元組數不定,描述要查詢的域名。在解析的時候,這部分以0x00結尾。需要注意,域名通常由符号“.”進行分割,每段的長度不定,QNAME每段的開頭會先指明此段的長度,以huishao.cc域名為例,其構造出的QNAME部分如下:

0x07 0x68 0x75 0x69 0x73 0x68 0x61 0x6f 0x02 0x63 0x63 0x00

其中最後一個位元組0x00标記了QNAME部分的結束,0x07表示第一段的長度為7個位元組,即0x68 0x75 0x69 0x73 0x68 0x61 0x6f是第一段,通過查詢ascii碼對照表可知,這段資料就是huishao,同理,之後的一個位元組為0x02,表示第二段的長度為2個位元組,0x63對應ascii表中的字母c,

最終可以解析為huishao.cc

QTYPE:占兩個位元組,對應查詢的類型,定義如下:

Type:意義 對應的值
A:iPv4主機位址 1
NS:權威域名伺服器 2
MD:郵箱位址(棄用,使用MX) 3
MF:轉發郵箱(棄用,使用MX) 4
CNAME:規範的别名 5
SOA:标記權威區域開始 6
MB:郵箱域名 7
MG:郵箱成員 8
MR:郵箱重命名域名 9
NULL:空的類型 10
WKS:服務描述 11
PTR:域名指針 12
HINFO:主機資訊 13
MINFO:郵箱或者郵件清單資訊 14
MX:郵件交換 15
TXT:字元串 16
AAAA: IPv6域名 28

上面列舉的查詢類型中,有兩個我們需要額外關注,A和CNAME,A類型即是我們查詢域名IP所要使用的,CNAME别名技術也很常用,後面會介紹。

QCLASS:占兩個位元組,表明查詢的類别,定義如下:

CLASS:意義
IN:Internet查詢
CS:棄用,RFC查詢
CH:the CHOAS class
HS:Hesiod

進行DNS解析時,隻需要設定成IN類即可。

3. Answer部分

這部分是響應的傳回資料,可能包含多條資源記錄,其格式如下:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

NAME:此記錄所屬的域名,長度不定,需要注意,這一部分存放的可能是真正的域名(格式和QNAME一緻),也可能是指針,指向真正存放域名的位元組位置,甚至可以是一部分是域名,一部分是指針。這樣做的好處是可以節省響應封包的資料空間,當檢查到某個位元組的高兩位為11時,則此位元組及之後一個位元組就是一個指針。例如對于huishao.cc域名的解析,其響應的完整的DNS封包如下(16進制):

b3 a4 81 80 00 01 00 01 00 00 00 00 07 68 75 69

73 68 61 6f 02 63 63 00 00 01 00 01 c0 0c 00 01

00 01 00 00 02 58 00 04 b9 c7 6d 99

其中開頭的12個位元組為Header部,随後的16個位元組為Question部,後面的即為Answer部,Answer部分開頭的c0位元組高兩位為11,表明其是一個指針,占兩個位元組,c0,0c兩個位元組将前兩位的1去掉後為十進制數12,表明NAME的真實值在第12個位元組處開始,即複用了QNAME的資料。

TYPE:占兩個位元組,與QTYPE定義一緻。

CLASS:占兩個位元組,與QCLASS定義一緻。

TTL:占4個位元組,此字段非常重要,标記了緩存的有效時長,機關是秒。順便分析一下上面的資料,此DNS解析資料的緩存有效期為0x0258,即600秒,10分鐘。

RDLENGTH:占兩個位元組,表明RDATA字段的位元組數。

RDATA:真正的解析資料,與TYPE有關,如果是IPv4域名解析,此處為解析的結果。

4. Authority,Additional

這兩部分的資料結構與Answer部分完全一緻,解析方式也完全一緻。

六.紙上得來終覺淺,絕知此事要躬行

通過前面的介紹,DNS協定的工作原理應該是明了了,如果需要更深入的了解細節,可以閱讀其官方的文檔:

https://datatracker.ietf.org/doc/html/rfc1035

當然,如果你還是感覺雲裡霧裡也沒有關系,我們通過實踐來驗證理論。

1.抓個活物來看看

Wireshark是一個網絡封包分析軟體,能夠截取網絡封包,對于網絡傳輸的資料包進行分析十分友善。我們打開此軟體後,找一個域名進行通路,即可抓取到對應的DNS資料包,以huishao.cc為例,如下圖所示:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

可以看到,Wireshark可以分析出此次網絡互動的時間,發起方IP,目标方IP,協定類型,資料長度和相信資訊。在上面的示例中,第一條記錄是DNS請求封包,第二條記錄是DNS響應封包。我們先看看DNS請求封包的資料:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

可以看到,Wireshark将每一層網絡協定都分析了出來,我們先隻關注最上層的Domian Name System部分,這部分的十六進制資料是上圖中選中的部分。可以發現其和我們上面介紹的協定格式是一一對應的。在看響應封包:

自上而下的了解網絡(1)——DNS篇自上而下的了解網絡(1)——DNS篇

資料的格式也是完全對應的,理論誠不欺我啊。

2. 手動實作DNS解析

下面,我們可以以huishao.cc域名為例,手動使用UDP協定來試一試發送DNS請求以及對請求到的資料進行解析。首先先看完整的測試代碼:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>

// 定義NDS伺服器的位址
char *DNSServer = "192.168.1.1";

// DNS封包中查詢區域的查詢類型
#define A 1
#define CNAME 5

/*
**DNS封包首部
**這裡使用了位域
*/
struct DNS_HEADER {
    // 2位元組
    unsigned short ID;
    
    // 需要注意,對于結構體中的位域 資料是從低位元組開始填充的
    // 1位元組
    unsigned char RD :1;
    unsigned char TC :1;
    unsigned char AA :1;
    unsigned char Opcode :4;
    unsigned char QR :1;
    
    // 1位元組
    unsigned char RCODE :4;
    unsigned char Z :3;
    unsigned char RA :1;
    
    // 2位元組
    unsigned short QCOUNT;
    // 2位元組
    unsigned short ANCOUNT;
    // 2位元組
    unsigned short NSCOUNT;
    // 2位元組
    unsigned short ARCOUNT;
};

/*
**DNS封包中查詢問題區域  4個位元組
*/
struct QUESTION {
    unsigned short QTYPE;//查詢類型
    unsigned short QCLASS;//查詢類
};
// 請求部分的結構
typedef struct {
    unsigned char *QNAME;
    struct QUESTION *question;
} QUERY;

/*
**DNS封包中回答區域的常量字段  10個位元組
*/
// 需要注意,因為此結構體中有short和int類型,我們需要将其設定為1位元組對齊
#pragma pack(1)
struct R_DATA {
    unsigned short TYPE; //表示資源記錄的類型
    unsigned short CLASS; //類
    unsigned int TTL; //表示資源記錄可以緩存的時間
    unsigned short RDLENGTH; //資料長度
};
#pragma pack()
/*
**DNS封包中回答區域的資源資料字段
*/
struct RES_RECORD {
    unsigned char *NAME;//資源記錄包含的域名
    struct R_DATA *resource;//資源資料
    unsigned char *rdata;
};

// DNS解析方法
void DNS(unsigned char*);
// 域名轉換方法
int ChangetoDnsNameFormat(unsigned char*, unsigned char*);

/*
**實作DNS查詢功能
*/
void DNS(unsigned char *host) {
    
    // UDP目标位址
    struct sockaddr_in dest;
    // DNS請求的資料結構
    struct DNS_HEADER dns = {};

    printf("\n所需解析域名:%s\n", host);
    
    //建立配置設定UDP套結字
    int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    //IPv4
    dest.sin_family = AF_INET;
    //53号端口 DNS伺服器用的是53号端口
    dest.sin_port = htons(53);
    // 設定IP
    dest.sin_addr.s_addr = inet_addr(DNSServer);//DNS伺服器IP

    /*設定DNS封包首部*/
    dns.ID = (unsigned short) htons(getpid());//id設為程序辨別符
    dns.QR = 0; //查詢
    dns.Opcode = 0; //标準查詢
    dns.AA = 0; //不授權回答
    dns.TC = 0; //不可截斷
    dns.RD = 1; //期望遞歸
    dns.QCOUNT = htons(1); //1個問題
    // 不需要的字段置為0
    dns.RA = 0;
    dns.Z = 0;
    dns.RCODE = 0;
    dns.ANCOUNT = 0;
    dns.NSCOUNT = 0;
    dns.ARCOUNT = 0;

    // 進行查詢的域名處理 先給100個位元組大小
    unsigned char *qname = malloc(100);
    // 轉換後會将長度傳回
    int nameLength = ChangetoDnsNameFormat(qname, host);//修改域名格式
    // 請求結構
    QUERY question = {};
    question.QNAME = qname;
    struct QUESTION qinfo = {};
    qinfo.QTYPE = htons(A); //查詢類型為A
    qinfo.QCLASS = htons(1); //查詢類為1
    question.question = &qinfo;
    // 定義要發送的UDP資料 先給65536個位元組
    unsigned char buf[65536];
    // 複制DNS頭部資料到buf
    memcpy(buf, &dns, sizeof(dns));
    // 移動複制的指針
    unsigned char *point = buf + sizeof(dns);
    // 複制請求的域名到buf
    memcpy(point, question.QNAME, nameLength);
    // 移動複制的指針
    point = point + nameLength;
    // 複制要解析的域名到buf
    memcpy(point, question.question, sizeof(*question.question));
    // buf的總長度
    int length = sizeof(dns) + nameLength + sizeof(*question.question);

    //向DNS伺服器發送DNS請求封包
    printf("\n\n發送封包中...");
    if (sendto(s, (char*) buf, length, 0, (struct sockaddr*) &dest,sizeof(dest)) < 0)
    {
        perror("發送失敗!");
    }
    printf("發送成功!\n");

    // 從DNS伺服器接受DNS響應封包
    unsigned char recvBuf[65536];
    int i = sizeof dest;
    printf("接收封包中...\n");
    recvfrom(s, (char*) recvBuf, 65536, 0, (struct sockaddr*) &dest,(socklen_t*) &i);
    if (length < 0) {
        perror("接收失敗!");
    }
    printf("接收成功!\n");
    // 将接收到的DNS資料頭部解析到結構體
    struct DNS_HEADER recvDNS = *((struct DNS_HEADER *)recvBuf);

    printf("\n\n響應封包包含: ");
    printf("\n %d個問題", ntohs(recvDNS.QCOUNT));
    printf("\n %d個回答", ntohs(recvDNS.ANCOUNT));
    printf("\n %d個授權服務", ntohs(recvDNS.NSCOUNT));
    printf("\n %d個附加記錄\n\n", ntohs(recvDNS.ARCOUNT));
    
    // 頭部,域名部分和問題的靜态部分長度
    size_t headLength = sizeof(struct DNS_HEADER);
    size_t hostLength = strlen((const char*) qname) + 1;
    size_t qusetionLength = sizeof(struct QUESTION);
    
    // 定義指針,将位置移動到封包的Answer部
    unsigned char *reader = &recvBuf[headLength + hostLength + qusetionLength];

    /*
    **解析接收封包
    */
    // 加2個位元組,是因為解析的資料中,域名采用的是指針方式,占兩個位元組(實際情況這裡需要判斷是否是指針還是真的域名)
    reader = reader + 2;
    // 将Answer部分的靜态資料解析到結構體
    struct R_DATA answer = *((struct R_DATA*) (reader));
    printf("回答類型:%x\n", ntohs(answer.TYPE));
    printf("緩存時間:%d秒\n",ntohl(answer.TTL));
    //指向回答問題區域的資源資料字段
    reader = reader + sizeof(struct R_DATA);
    //判斷資源類型是否為IPv4位址
    unsigned char *ip = NULL;
    if (ntohs(answer.TYPE) == A) {
        //解析到的IP資料 指針
        ip = (unsigned char*) malloc(ntohs(answer.RDLENGTH)+1);
        for (int j = 0; j < ntohs(answer.RDLENGTH); j++) {
            ip[j] = reader[j];
        }
        ip[ntohs(answer.RDLENGTH)] = '\0';
    }

    //顯示查詢結果
    if (ip) {
        long *p;
        p = (long*) ip;
        // inet_ntoa用來進行IP轉換
        printf("IPv4位址:%s\n", inet_ntoa(*(struct in_addr*)ip));
    }
    return;
}

/*
**從www.baidu.com轉換到3www5baidu3com
*/
int ChangetoDnsNameFormat(unsigned char* dns, unsigned char* host) {
    int lock = 0, i, length = 0;
    strcat((char*) host, ".");

    for (i = 0; i < strlen((char*) host); i++) {
        if (host[i] == '.') {
            *dns++ = i - lock;
            length ++;
            for (; lock < i; lock++) {
                *dns++ = host[lock];
                length ++;
            }
            lock++;
        }
    }
    *dns++ = '\0';
    length ++;
    return length;
}

int main(int argc, const char * argv[]) {
    unsigned char hostname[100] = "huishao.cc";
    //由域名獲得IPv4位址,A是查詢類型
    DNS(hostname);
    return 0;
}           

上面的代碼有詳細的注釋,你可以嘗試運作下進行域名解析,需要注意,上面填寫的192.168.1.1是本地路由器的域名伺服器位址,你需要将其替換成自己的,當然你也可以使用通用的域名解析伺服器,如114.114.114.144。上面的代碼采用C語言編寫,是以在處理資料的時候會有一些複雜,有些點需要注意。

1. 關于結構體位域

簡單了解,位域可以讓結構體中的資料以Byte為機關已經存儲,例如上面定義的DNS_HEADER結構體,我們按照DNS協定的結構對其内資料所占的位進行了定義,有一點需要額外注意,在定義結構體時,位域字段的順序與實際填充的順序是相反的,位域的填充是從低位元組開始的,如上代碼所示,對于1個位元組的位域來說,我們定義的時候,先定義的RD字段,最後定義的QR字段,實際在存儲資料時,這一個位元組的最高位會存儲QR,最低位會存儲RD。

2. 關于位元組對齊

在定義結構體時,還有一個細節需要注意,如果結構體中的資料位元組數不是一緻的,則其建立的記憶體大小可能和實際所需要的并不一緻,例如R_DATA結構體,其中有int和short類型的資料,則其會以4位元組為标準進行對齊,我們需要手動設定其對齊位數,不然後續資料填充時會出現偏差。

3.網絡位元組序與主機位元組序

網絡位元組序是TCP/IP協定中定義的一種資料格式,其采用的是大端(big-endian)的排序方式,即對于一個字(兩個位元組)的資料,低位元組在前,高位元組在後。這與我們可讀的主機位元組序剛好是相反的,在C語言中,使用htons可以把short類型的資料進行網絡和主機位元組序的轉換,htonl把long類型的資料進行網絡和主機位元組序的轉換。

可以在如下位址下載下傳到完整的上述代碼:

https://gitee.com/jaki/dns_c

溫馨提示,上面代碼中解析的域名隻傳回了一個A類型的解析應答,如果你解析其他域名,可能會有很多CNAME類型的應答,應答個數也可能不止一個,你可以嘗試下優化下代碼,完整的實作DNS的解析邏輯。

七. 結尾

本篇部落格到此就結束了,我相信你對從域名擷取到IP的過程有了更多的認識,如果遇到了域名解析的問題,你應該明白如何檢視響應結果來定位問題了,但是,這隻是我們日常使用的網絡中的第一步,目前我們連應用層的核心都還沒有接觸到,不積跬步,無以至千裡,與君共勉。

專注技術,熱愛生活,交流技術,也做朋友。

繼續閱讀