自上而下的理解网络(1)——DNS篇

自上而下的理解网络(1)——DNS篇

一.引言

现代生活中,网络可谓是无处不在,购物需要网络,付款需要网络,各种生活缴费需要网络,在各行各业的工作中,更是离不开网络。说到底,网络的作用无非是支持计算机间进行数据交换。世界各地有着不计其数的网络设备,这些网络设备是如何有序正常的进行数据交流的呢?网络以及各种协议的工作原理又是怎样的呢?本系列博客,我们将尝试自上而下的对网路的工作原理进行介绍,从应用层开始,逐层向下,详细的帮助你理解网络的核心工作原理。当然,网络协议多如牛毛,在网络分层中每一层的知识也是非常浩渺,希望这些博客可以起到抛砖引玉的作用,能够使你对于天天使用的互联网网络在宏观上有认识,在微观上也有了解。

二.访问网站的第一步是什么?

说到网络,对于普通用户来说,使用最多的可能就是浏览各种网站了,虽然现在移动设备上的App基本代替了传统的PC应用和网站,但是这些App里提供的数据本质上网站中提供的数据并无不同,使用的网络技术并无不同。

我们知道,不论是访问网站还是App内进行接口请求,这些数据都是存储在“服务器”这种特殊的远程设备上的,要向服务器获取数据,首先我们需要找到服务器的位置,这很好理解,只有找到它,我们才能和它产生数据交流。互联网无论多大,本质上依然是通过电缆、光纤或各种无线设备这类连接介质连接在一起的,如果一台设备没有硬件上连接入互联网,那么说破天我们也无法和它产生数据交互。要找到一台互联网设备,实际上是通过其物理Mac地址来找到的,这就像现实中的门牌号一样,每家的门牌号都不同,说到这,我们要再老生常谈一下,抛出网络分层模型给你看:

自上而下的理解网络(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篇

可以发现,映射表中记录了域名与IP间的映射关系,在实际的应用中,上图中描述的场景看似可行,实际却并非如此,世界上的域名与IP总数是一个非常庞大的数字,由一台服务器来维护所有域名IP信息几乎不可能,而且对于域名解析服务,请求量是巨大的,会有大量的用户频繁的进行域名解析请求,单服务器明显是不能满足需求的。因此,实际生产环境中的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篇

更多时候,hosts的正确用法是开发应用程序时,测试环境和正式环境可以将域名配置到不同的IP,这样无需应用程序代码中做逻辑,只需要切换hosts文件即可实现环境的切换。

2. 本机应用缓存

本机应用缓存是多级缓存中的第一级,例如当我们在浏览器中访问过某个域名后,其解析的结果会被浏览器缓存下来,当我们再次访问这个域名时,其首先会检查浏览器缓存,如果缓存能够命中此域名,则直接使用,缓存的有效时间会受TTL配置影响(我们后面会介绍)。

3. 本机系统缓存

与本机应用缓存类似,操作系统中也会有一份域名解析的缓存,如果本机应用缓存中没有命中,会从操作系统缓存中检查是否之前有过此域名的解析记录。如果能够命中则会直接使用。

4. 路由器域名解析缓存

如果本机系统缓存依然没有命中,而你的设备又是通过路由器接入的公网,此时你的域名解析服务很大可能是路由器提供的,可以打开网络设置的DNS一栏,观察DNS服务器的地址,如果是192.168.x.x类型内网地址,则说明是由路由器来完成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篇

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篇

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部分

这部分是响应的返回数据,可能包含多条资源记录,其格式如下:

自上而下的理解网络(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篇

可以看到,Wireshark可以分析出此次网络交互的时间,发起方IP,目标方IP,协议类型,数据长度和相信信息。在上面的示例中,第一条记录是DNS请求报文,第二条记录是DNS响应报文。我们先看看DNS请求报文的数据:

自上而下的理解网络(1)——DNS篇

可以看到,Wireshark将每一层网络协议都分析了出来,我们先只关注最上层的Domian Name System部分,这部分的十六进制数据是上图中选中的部分。可以发现其和我们上面介绍的协议格式是一一对应的。在看响应报文:

自上而下的理解网络(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的过程有了更多的认识,如果遇到了域名解析的问题,你应该明白如何查看响应结果来定位问题了,但是,这只是我们日常使用的网络中的第一步,目前我们连应用层的核心都还没有接触到,不积跬步,无以至千里,与君共勉。

专注技术,热爱生活,交流技术,也做朋友。
上一篇:非常简单的部署脚本(JavaWeb项目)和部署项目教程


下一篇:Java SWT事件