DNS协议与请求:c++实现

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;  // 程序正常退出
}

技术支持:https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值