大數據

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

自上而下的理解網絡(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查詢 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類型的數據進行網絡和主機字節序的轉換。

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

https://gitee.com/jaki/dns_c

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

七. 結尾

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

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

Leave a Reply

Your email address will not be published. Required fields are marked *