自上而下的理解網絡(1)——DNS篇
一.引言
現代生活中,網絡可謂是無處不在,購物需要網絡,付款需要網絡,各種生活繳費需要網絡,在各行各業的工作中,更是離不開網絡。說到底,網絡的作用無非是支持計算機間進行數據交換。世界各地有著不計其數的網絡設備,這些網絡設備是如何有序正常的進行數據交流的呢?網絡以及各種協議的工作原理又是怎樣的呢?本系列博客,我們將嘗試自上而下的對網路的工作原理進行介紹,從應用層開始,逐層向下,詳細的幫助你理解網絡的核心工作原理。當然,網絡協議多如牛毛,在網絡分層中每一層的知識也是非常浩渺,希望這些博客可以起到拋磚引玉的作用,能夠使你對於天天使用的互聯網網絡在宏觀上有認識,在微觀上也有了解。
二.訪問網站的第一步是什麼?
說到網絡,對於普通用戶來說,使用最多的可能就是瀏覽各種網站了,雖然現在移動設備上的App基本代替了傳統的PC應用和網站,但是這些App裡提供的數據本質上網站中提供的數據並無不同,使用的網絡技術並無不同。
我們知道,不論是訪問網站還是App內進行接口請求,這些數據都是存儲在“服務器”這種特殊的遠程設備上的,要向服務器獲取數據,首先我們需要找到服務器的位置,這很好理解,只有找到它,我們才能和它產生數據交流。互聯網無論多大,本質上依然是通過電纜、光纖或各種無線設備這類連接介質連接在一起的,如果一臺設備沒有硬件上連接入互聯網,那麼說破天我們也無法和它產生數據交互。要找到一臺互聯網設備,實際上是通過其物理Mac地址來找到的,這就像現實中的門牌號一樣,每家的門牌號都不同,說到這,我們要再老生常談一下,拋出網絡分層模型給你看:
關於這個網絡分層模型,它在我們後面博客中的出境還少不了,現在你可以先不用管它,你只需要先知道物理層是負責設備物理媒介相關的協議,數據鏈路層通過硬件的Mac地址找到具體要網絡設備,網絡層通過IP協議來封裝真實的Mac地址,傳輸層是對網絡層的一種封裝,TCP,UDP等傳輸協議在這一層工作,而最上層的應用層就是我們常說的網絡應用協議,如DNS,HTTP,HTTPS和FPT協議工作在這一層。
關於網絡分層模型,我們先把多說了,我們的宗旨是自上而下的理解網絡,那麼還是回到第一步來。我們在訪問網站時,都會現在瀏覽器輸入網站的地址,這通常是一個域名,例如我要訪問自己的技術博客網站,我會在瀏覽器輸入如下的地址:
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查詢 | 1 |
CS:棄用,RFC查詢 | 2 |
CH:the CHOAS class | 3 |
HS:Hesiod | 4 |
進行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類型的數據進行網絡和主機字節序的轉換。
可以在如下地址下載到完整的上述代碼:
溫馨提示,上面代碼中解析的域名只返回了一個A類型的解析應答,如果你解析其他域名,可能會有很多CNAME類型的應答,應答個數也可能不止一個,你可以嘗試下優化下代碼,完整的實現DNS的解析邏輯。
七. 結尾
本篇博客到此就結束了,我相信你對從域名獲取到IP的過程有了更多的認識,如果遇到了域名解析的問題,你應該明白如何查看響應結果來定位問題了,但是,這只是我們日常使用的網絡中的第一步,目前我們連應用層的核心都還沒有接觸到,不積跬步,無以至千里,與君共勉。
專注技術,熱愛生活,交流技術,也做朋友。