1.DNS是啥?
DNS(Domain Name System):把域名翻译成IP地址,实现DNS请求
DNS 使用 TCP 和 UDP 端口 53
nslookup命令,可以查到域名所对应的ip的地址
打开cmd输入nslookup www.baidu.com (可替换 别的网址 )将返回ip127.0.0.53
- 静态域名(含 HOST 文件):IP 不变,绑定是 “死的”,不用查外网,直接用固定映射。
- 动态域名:IP 会变,绑定是 “活的”,必须查 DDNS 服务器拿最新 IP,否则访问不到。
域名解析流程:先查本地缓存(浏览器 / 系统)→ 未命中则查本地 DNS 服务器 → 本地 DNS 逐层迭代查询根 DNS→顶级域 DNS→权威 DNS → 权威 DNS 返回最终 IP,本地 DNS 缓存后回传给设备。
例如:本地 DNS 要查 www.taobao.com → 根 DNS 说 “找 .com 顶级域 DNS” → 顶级域 DNS 说 “找 taobao.com 的权威 DNS” → 权威 DNS 返回最终 IP
2.DNS域名解析过程
DNS报文:
DNS报文由头部header(前三行)和查询question组成,我们必须按照DNS报文的格式发送数据,才能被DNS服务器正常识别,字段均为网络字节序,使用 htons() 函数转换

Queries如下:
2.1构造DNS报文头部结构(共12字节)
struct dns_header {
unsigned short id; // 会话ID(客户端生成,用于匹配请求和响应)
unsigned short flags; // 标志位(包含查询/响应、操作码、状态等)
unsigned short questions; // 问题数(本次查询的问题数量,通常为1)
unsigned short answer; // 回答资源记录数(服务器返回的答案数量)
unsigned short authority; // 授权资源记录数
unsigned short additional; // 附加资源记录数
};
2.2DNS查询问题结构
struct dns_question {
int length; // 域名编码后的长度
unsigned short qtype; // 查询类型(如A记录、CNAME等)
unsigned short qclass; // 查询类别(通常为1,表示INternet)
unsigned char *name; // 编码后的域名(采用DNS格式,如"3www60voice3com0")
};
2.3创建DNS请求头部
int dns_create_header(struct dns_header *header) {
if (header == NULL) return -1; // 参数合法性检查
memset(header, 0, sizeof(struct dns_header)); // 初始化头部为0
// 生成随机会话ID(用当前时间作为随机数种子)
srandom(time(NULL));
header->id = random(); // 随机ID,用于匹配请求和响应
// 设置标志位:0x0100表示标准查询(QR=0,Opcode=0,AA=0,TC=0,RD=1,RA=0,Z=0,RCODE=0)
// htons用于将主机字节序转换为网络字节序(大端序)
header->flags = htons(0x0100);
header->questions = htons(1); // 问题数为1(查询一个域名)
return 0;
}
2.4创建DNS查询问题(编码域名并设置查询类型/类别)
域名编码:将"www.0voice.com"转换为DNS格式"3www60voice3com0"
格式说明:每个域名段前加长度,最后以0结尾(如"www"→"3www","0voice"→"60voice")
int dns_create_question(struct dns_question *question, const char *hostname) {
if (question == NULL || hostname == NULL) return -1; // 参数合法性检查
memset(question, 0, sizeof(struct dns_question)); // 初始化问题结构体为0
// 为编码后的域名分配内存(长度=原域名长度+2,预留分隔符和结束符)
question->name = (char*)malloc(strlen(hostname) + 2);
if (question->name == NULL) { // 内存分配失败
return -2;
}
question->length = strlen(hostname) + 2; // 记录编码后域名的长度
question->qtype = htons(1); // 查询类型:1表示A记录(IPv4地址)
question->qclass = htons(1); // 查询类别:1表示INternet
const char delim[2] = "."; // 以"."作为域名段分隔符
char *qname = question->name; // 指向编码后的域名缓冲区
// 复制原域名(strdup会自动分配内存),避免修改原字符串
char *hostname_dup = strdup(hostname);
// 切割域名:第一次切割得到第一个段(如"www")
char *token = strtok(hostname_dup, delim);
while (token != NULL) { // 循环切割剩余域名段
size_t len = strlen(token); // 当前域名段的长度(如"www"长度为3)
*qname = len; // 存储当前段的长度(如3)
qname ++; // 移动指针到下一位
// 复制域名段内容(如"www")
strncpy(qname, token, len+1);
qname += len; // 移动指针到下一段的起始位置
// 切割下一个域名段(如"0voice"、"com")
token = strtok(NULL, delim);
}
free(hostname_dup); // 释放strdup分配的内存
}
2.5构建完整的DNS请求报文
int dns_build_request(struct dns_header *header, struct dns_question *question, char *request, int rlen) {
if (header == NULL || question == NULL || request == NULL) return -1; // 参数合法性检查
memset(request, 0, rlen); // 清空请求缓冲区
if(header==NULL||question==NULL||request==NULL) return -1;
memset(request,0,rlen);
// 1. 复制头部到请求缓冲区(头部占12字节)
memcpy(request, header, sizeof(struct dns_header));
int offset = sizeof(struct dns_header); // 记录当前偏移量(已写入12字节)
// 2. 复制编码后的域名到请求缓冲区
memcpy(request + offset, question->name, question->length);
offset += question->length; // 偏移量增加域名长度
// 3. 复制查询类型(qtype)到请求缓冲区
memcpy(request + offset, &question->qtype, sizeof(question->qtype));
offset += sizeof(question->qtype); // 偏移量增加2字节
// 4. 复制查询类别(qclass)到请求缓冲区
memcpy(request + offset, &question->qclass, sizeof(question->qclass));
offset += sizeof(question->qclass); // 偏移量增加2字节
return offset; // 返回完整请求报文的长度
}
2.6解析DNS服务器的响应报文,提取域名和对应的IP地址
static int dns_parse_response(char *buffer, struct dns_item **domains) {
int i = 0;
unsigned char *ptr = buffer; // 指向响应报文的当前解析位置
ptr += 4; // 跳过ID(2字节)和flags(2字节),指向问题数
int querys = ntohs(*(unsigned short*)ptr); // 问题数(网络字节序转主机字节序)
ptr += 2; // 指向回答数
int answers = ntohs(*(unsigned short*)ptr); // 回答资源记录数
ptr += 6; // 跳过授权数(2字节)和附加数(2字节),指向问题区域
// 跳过问题区域(因为我们只关心回答区域)
for (i = 0; i < querys; i++) {
while (1) {
int flag = (int)ptr[0]; // 域名段长度或结束符
ptr += (flag + 1); // 跳过当前段(长度字节+内容)
if (flag == 0) break; // 遇到0表示问题域名结束
}
ptr += 4; // 跳过问题的qtype(2字节)和qclass(2字节)
}
// 解析回答区域
char cname[128], aname[128], ip[20], netip[4]; // 临时存储解析结果
int len, type, ttl, datalen; // len:域名长度;type:记录类型;ttl:生存时间;datalen:数据长度
int cnt = 0; // 记录有效的IP记录数量
// 为解析结果分配内存(最多answers条记录)
struct dns_item *list = (struct dns_item*)calloc(answers, sizeof(struct dns_item));
if (list == NULL) { // 内存分配失败
return -1;
}
// 遍历所有回答记录
for (i = 0; i < answers; i++) {
bzero(aname, sizeof(aname)); // 清空域名缓冲区
len = 0;
// 解析回答中的域名(可能包含压缩指针)
dns_parse_name(buffer, ptr, aname, &len);
ptr += 2; // 跳过域名后的2字节(实际是类型的前半部分,这里简化处理)
type = htons(*(unsigned short*)ptr); // 记录类型(A记录或CNAME等)
ptr += 4; // 跳过类型(2字节)和类别(2字节)
ttl = htons(*(unsigned short*)ptr); // 生存时间(该记录的有效期,单位秒)
ptr += 4; // 跳过分组长度(2字节)和数据长度(2字节)前的部分
datalen = ntohs(*(unsigned short*)ptr); // 数据部分长度
ptr += 2; // 跳过数据长度字段
if (type == DNS_CNAME) { // 如果是CNAME记录(别名)
bzero(cname, sizeof(cname)); // 清空别名缓冲区
len = 0;
// 解析别名域名
dns_parse_name(buffer, ptr, cname, &len);
ptr += datalen; // 跳过CNAME数据部分
} else if (type == DNS_HOST) { // 如果是A记录(IPv4地址)
bzero(ip, sizeof(ip)); // 清空IP缓冲区
if (datalen == 4) { // IPv4地址固定为4字节
memcpy(netip, ptr, datalen); // 复制网络字节序的IP(4字节)
// 将网络字节序的IP转换为字符串格式(如"192.168.1.1")
inet_ntop(AF_INET, netip, ip, sizeof(struct sockaddr));
// 打印解析结果
printf("%s has address %s\n", aname, ip);
printf("\tTime to live: %d minutes , %d seconds\n", ttl / 60, ttl % 60);
// 保存解析结果到列表
list[cnt].domain = (char*)calloc(strlen(aname) + 1, 1);
memcpy(list[cnt].domain, aname, strlen(aname));
list[cnt].ip = (char*)calloc(strlen(ip) + 1, 1);
memcpy(list[cnt].ip, ip, strlen(ip));
cnt ++; // 有效记录数+1
}
ptr += datalen; // 跳过IP数据部分
}
}
*domains = list; // 将解析结果赋值给输出参数
ptr += 2; // 跳过剩余字段(简化处理)
return cnt; // 返回有效IP记录数量
}
static int is_pointer(int in) {
return ((in & 0xC0) == 0xC0); // 0xC0 = 11000000,判断前2位是否为11
}
static void dns_parse_name(unsigned char *chunk, unsigned char *ptr, char *out, int *len) {
int flag = 0, n = 0;
char *pos = out + (*len); // 指向输出缓冲区的当前位置
while (1) {
flag = (int)ptr[0]; // 获取当前字节(可能是长度、指针或结束符)
if (flag == 0) break; // 遇到0表示域名结束
if (is_pointer(flag)) { // 如果是压缩指针
// 指针由2字节组成:前2位是标志,后14位是偏移量(指向报文中的已有域名)
n = (int)ptr[1]; // 偏移量的低8位(高2位来自flag的低6位)
ptr = chunk + n; // 跳转到指针指向的位置
// 递归解析指针指向的域名
dns_parse_name(chunk, ptr, out, len);
break; // 指针解析完成,退出循环
} else { // 不是指针,是域名段的长度
ptr ++; // 跳过长度字节
// 复制域名段内容到输出缓冲区(长度为flag)
memcpy(pos, ptr, flag);
pos += flag; // 移动输出指针
ptr += flag; // 移动输入指针
*len += flag; // 更新域名总长度
// 如果下一个字节不是结束符,添加"."分隔域名段
if ((int)ptr[0] != 0) {
memcpy(pos, ".", 1);
pos += 1;
(*len) += 1;
}
}
}
}
2.7UDP连接 执行DNS查询(创建请求、发送到DNS服务器、接收响应并解析)
-
socket(AF_INET, SOCK_DGRAM, 0)→ 创建一个「用 IPv4 地址、UDP 传输协议」的网络通信端点,后续用返回的sockfd做 UDP 收发数据 -
UDP 不需要手动 “建立连接”,也不需要手动指定自己的 IP / 端口 —— 系统会自动分配本机 IP 和临时端口,作为通信的 “回传地址”,确保 DNS 服务器能把响应发给你。
-
addr是用来 “接收响应来源地址” 的 —— 帮你确认 “这个响应是不是我要找的 DNS 服务器发的”,避免处理无效数据,是 UDP 通信中验证响应合法性的关键 -
recvfrom会阻塞等待 —— 等网络传输 + 服务器处理完成后,才会收到响应;如果没收到,会一直等直到超时。
int dns_client_commit(const char *domain) {
// 创建UDP套接字(SOCK_DGRAM表示UDP协议)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) { // 套接字创建失败
return -1;
}
// 初始化DNS服务器地址结构
struct sockaddr_in servaddr = {0};
servaddr.sin_family = AF_INET; // IPv4协议
servaddr.sin_port = htons(DNS_SERVER_PORT); // DNS服务器端口(网络字节序)
servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP); // DNS服务器IP
// 连接到DNS服务器(UDP不需要实际连接,此处主要是设置默认发送目标)这里是TCP才需要
int ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
printf("connect : %d\n", ret); // 打印连接结果(UDP连接通常返回0)
// 创建DNS请求头部
struct dns_header header = {0};
dns_create_header(&header);
// 创建DNS查询问题
struct dns_question question = {0};
dns_create_question(&question, domain);
// 构建完整的DNS请求报文
char request[1024] = {0};
int length = dns_build_request(&header, &question, request, 1024);
// 发送DNS请求到服务器
int slen = sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, sizeof(struct sockaddr));
// 接收DNS服务器的响应
char response[1024] = {0}; // 存储响应报文
struct sockaddr_in addr; // 存储服务器地址(接收时填充)
size_t addr_len = sizeof(struct sockaddr_in); // 地址结构长度
int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr*)&addr, (socklen_t*)&addr_len);
// 解析响应报文,提取域名和IP
struct dns_item *dns_domain = NULL;
dns_parse_response(response, &dns_domain);
// 释放解析结果的内存
free(dns_domain);
return n; // 返回接收的响应长度
}
2.8主函数:
程序入口,接收命令行参数(域名)并执行DNS查询
int main(int argc, char *argv[]) {
if (argc < 2) return -1; // 检查命令行参数(需提供域名)
// 执行DNS查询(argv[1]为要查询的域名)
dns_client_commit(argv[1]);
return 0; // 程序正常退出
}
1468

被折叠的 条评论
为什么被折叠?



