自上而下的了解網絡(1)——DNS篇
一.引言
現代生活中,網絡可謂是無處不在,購物需要網絡,付款需要網絡,各種生活繳費需要網絡,在各行各業的工作中,更是離不開網絡。說到底,網絡的作用無非是支援計算機間進行資料交換。世界各地有着不計其數的網絡裝置,這些網絡裝置是如何有序正常的進行資料交流的呢?網絡以及各種協定的工作原理又是怎樣的呢?本系列部落格,我們将嘗試自上而下的對網路的工作原理進行介紹,從應用層開始,逐層向下,詳細的幫助你了解網絡的核心工作原理。當然,網絡協定多如牛毛,在網絡分層中每一層的知識也是非常浩渺,希望這些部落格可以起到抛磚引玉的作用,能夠使你對于天天使用的網際網路網絡在宏觀上有認識,在微觀上也有了解。
二.通路網站的第一步是什麼?
說到網絡,對于普通使用者來說,使用最多的可能就是浏覽各種網站了,雖然現在移動裝置上的App基本代替了傳統的PC應用和網站,但是這些App裡提供的資料本質上網站中提供的資料并無不同,使用的網絡技術并無不同。
我們知道,不論是通路網站還是App内進行接口請求,這些資料都是存儲在“伺服器”這種特殊的遠端裝置上的,要向伺服器擷取資料,首先我們需要找到伺服器的位置,這很好了解,隻有找到它,我們才能和它産生資料交流。網際網路無論多大,本質上依然是通過電纜、光纖或各種無線裝置這類連接配接媒體連接配接在一起的,如果一台裝置沒有硬體上連接配接入網際網路,那麼說破天我們也無法和它産生資料互動。要找到一台網際網路裝置,實際上是通過其實體Mac位址來找到的,這就像現實中的門牌号一樣,每家的門牌号都不同,說到這,我們要再老生常談一下,抛出網絡分層模型給你看:
關于這個網絡分層模型,它在我們後面部落格中的出境還少不了,現在你可以先不用管它,你隻需要先知道實體層是負責裝置實體媒介相關的協定,資料鍊路層通過硬體的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伺服器,下圖可以很形象的表示域名伺服器的工作方式:
可以發現,映射表中記錄了域名與IP間的映射關系,在實際的應用中,上圖中描述的場景看似可行,實際卻并非如此,世界上的域名與IP總數是一個非常龐大的數字,由一台伺服器來維護所有域名IP資訊幾乎不可能,而且對于域名解析服務,請求量是巨大的,會有大量的使用者頻繁的進行域名解析請求,單伺服器明顯是不能滿足需求的。是以,實際生産環境中的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,你将無法再通路到珲少的部落格網站,如下圖所示:
更多時候,hosts的正确用法是開發應用程式時,測試環境和正式環境可以将域名配置到不同的IP,這樣無需應用程式代碼中做邏輯,隻需要切換hosts檔案即可實作環境的切換。
2. 本機應用緩存
本機應用緩存是多級緩存中的第一級,例如當我們在浏覽器中通路過某個域名後,其解析的結果會被浏覽器緩存下來,當我們再次通路這個域名時,其首先會檢查浏覽器緩存,如果緩存能夠命中此域名,則直接使用,緩存的有效時間會受TTL配置影響(我們後面會介紹)。
3. 本機系統緩存
與本機應用緩存類似,作業系統中也會有一份域名解析的緩存,如果本機應用緩存中沒有命中,會從作業系統緩存中檢查是否之前有過此域名的解析記錄。如果能夠命中則會直接使用。
4. 路由器域名解析緩存
如果本機系統緩存依然沒有命中,而你的裝置又是通過路由器接入的公網,此時你的域名解析服務很大可能是路由器提供的,可以打開網絡設定的DNS一欄,觀察DNS伺服器的位址,如果是192.168.x.x類型内網位址,則說明是由路由器來完成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的資料結構:
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指明,通常隻會攜帶一個問題。每個問題的格式定義如下:
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部分
這部分是響應的傳回資料,可能包含多條資源記錄,其格式如下:
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為例,如下圖所示:
可以看到,Wireshark可以分析出此次網絡互動的時間,發起方IP,目标方IP,協定類型,資料長度和相信資訊。在上面的示例中,第一條記錄是DNS請求封包,第二條記錄是DNS響應封包。我們先看看DNS請求封包的資料:
可以看到,Wireshark将每一層網絡協定都分析了出來,我們先隻關注最上層的Domian Name System部分,這部分的十六進制資料是上圖中選中的部分。可以發現其和我們上面介紹的協定格式是一一對應的。在看響應封包:
資料的格式也是完全對應的,理論誠不欺我啊。
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的過程有了更多的認識,如果遇到了域名解析的問題,你應該明白如何檢視響應結果來定位問題了,但是,這隻是我們日常使用的網絡中的第一步,目前我們連應用層的核心都還沒有接觸到,不積跬步,無以至千裡,與君共勉。
專注技術,熱愛生活,交流技術,也做朋友。