本文通过纯 C 语言手动构造 DNS 请求报文,使用 UDP 协议发送到公共 DNS 服务器,并接收响应,完整演示 DNS 请求流程。
主流程:dns_client_commit()
这是整个流程的核心函数,下面我们按顺序拆解每一步的逻辑,尤其突出发送 sendto 与接收 recvfrom 的设计思路和实现。
第一步:创建 UDP 套接字
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
return -1;
}
-
使用 IPv4(
AF_INET
)+ UDP(SOCK_DGRAM
)创建 socket; -
返回值
sockfd
是文件描述符,用于后续发送接收; -
创建失败时直接返回错误。
第二步:配置目标服务器地址
cpp
struct sockaddr_in servaddr = {0};
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(DNS_SERVER_PORT);
servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
-
填写目标 DNS 服务器信息,本例中使用的是
114.114.114.114
; -
注意端口是 53,必须使用
htons()
转换为网络字节序; -
IP 地址也需通过
inet_addr()
转换。
第三步(可选):connect() 设置默认目标地址
cpp
connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
虽然 UDP 不需要建立连接,但 connect()
可以用来为 socket 设置默认收发对象 ,使 send()
和 recv()
也能直接用。
不过后续我们仍然使用 sendto()
和 recvfrom()
,所以这步不是必须的。
第四步:构造 DNS 请求结构体
cpp
struct dns_header header = {0};
dns_create_header(&header);
struct dns_question question = {0};
dns_create_question(&question, domain);
-
dns_create_header
随机生成请求 ID,设置标准查询标志和问题数; -
dns_create_question
将域名编码成符合 DNS 协议的 QNAME 格式,并设置查询类型(A)和类(IN);
第五步:拼接报文
cpp
char request[1024] = {0};
int length = dns_build_request(&header, &question, request, sizeof(request));
-
将 header + QNAME + QTYPE + QCLASS 依次拷贝到
request
中; -
返回整个 DNS 报文的长度(以字节计);
-
最终得到一个可以直接发送的字节数组。
第六步:使用 sendto() 发送数据包
cpp
int slen = sendto(sockfd, request, length, 0,
(struct sockaddr*)&servaddr,
sizeof(struct sockaddr_in));
-
把完整请求通过 UDP 发送给服务器;
-
sendto()
第一个参数是 socket; -
最后两个参数是目标地址和长度。
第七步:使用 recvfrom() 接收响应
cpp
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);
-
recvfrom()
会阻塞,直到收到服务器响应为止; -
将响应保存到
response
中; -
n
表示收到的字节数。
第八步:输出结果(测试用)
cpp
printf("recvfrom : %d, %s\n", n, response);
DNS 客户端请求流程图(文字版)
cpp
┌────────────────────────────────────────────┐
│ 程序入口 main() │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 判断命令行参数个数是否 < 2? │
│ 是 → 直接 return -1 │
│ 否 → 提取域名参数 argv[1] │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 调用 dns_client_commit(domain) │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 创建 UDP socket(socket(AF_INET, SOCK_DGRAM))│
│ 创建失败 → return -1 │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 设置服务器地址 servaddr │
│ - IP: 114.114.114.114 │
│ - 端口: 53 │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 可选:connect(sockfd, servaddr) │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 调用 dns_create_header(&header) │
│ - 填写请求 ID(随机) │
│ - 设置 Flags = 0x0100 │
│ - 设置 Questions = 1 │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 调用 dns_create_question(&question, domain)│
│ - 编码域名为 QNAME(带长度前缀 + 0 结尾) │
│ - 设置 QTYPE = 1(A记录) │
│ - 设置 QCLASS = 1(IN) │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 调用 dns_build_request(...) 构建请求报文 │
│ - 拷贝 header → request │
│ - 拷贝 QNAME → request │
│ - 拷贝 QTYPE → request │
│ - 拷贝 QCLASS → request │
│ 返回 request 长度 length │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 使用 sendto(...) 发送请求报文给 DNS 服务器 │
│ 参数:socket、request、servaddr │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 使用 recvfrom(...) 等待接收服务器响应 │
│ 将响应数据保存至 response[] 中 │
│ 返回接收的字节数 n │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 打印响应长度和部分内容(简单调试) │
│ printf("recvfrom : %d, %s\n", n, response); │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 函数结束,返回 n 字节长度 │
└────────────────────────────────────────────┘
完整代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/socket.h> // socket相关函数
#include <netinet/in.h> // sockaddr_in结构体
#include <unistd.h> // close()
// DNS服务器配置(可换成8.8.8.8等)
#define DNS_SERVER_PORT 53
#define DNS_SERVER_IP "114.114.114.114"
// =======================
// DNS 报文头部结构体(固定12字节)
// =======================
struct dns_header {
unsigned short id; // 标识符(客户端生成,响应中应一致)
unsigned short flags; // 标志位(是否递归、是否响应等)
unsigned short questions; // 问题数量(通常为1)
unsigned short answers; // 回答资源记录数量(请求为0)
unsigned short authority; // 授权资源记录数量
unsigned short additional; // 附加资源记录数量
};
// =======================
// DNS 问题部分结构体(QNAME + QTYPE + QCLASS)
// =======================
struct dns_question {
int length; // QNAME部分长度(带点标签形式)
unsigned short qtype; // 查询类型(A记录为1)
unsigned short qclass; // 查询类(IN类为1)
unsigned char *name; // 编码后的QNAME(域名)
};
// =======================
// 构造DNS报文头部
// =======================
int dns_create_header(struct dns_header *header) {
if (header == NULL) return -1;
memset(header, 0, sizeof(struct dns_header));
// 使用时间种子生成随机 ID
srand((unsigned int)time(NULL));
header->id = (unsigned short)rand(); // 随机标识符
header->flags = htons(0x0100); // 标准查询 + 递归请求
header->questions = htons(1); // 只查询一个问题
return 0;
}
// =======================
// 构造DNS问题部分:编码域名 + QTYPE + QCLASS
// =======================
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));
size_t hostlen = strlen(hostname);
question->name = (unsigned char*)malloc(hostlen + 2); // 预留一个结尾的 0x00
if (question->name == NULL) return -2;
question->length = (int)hostlen + 2;
question->qtype = htons(1); // 查询A记录(IPv4地址)
question->qclass = htons(1); // IN类(Internet)
const char delim[2] = ".";
unsigned char *qname = question->name;
// 拷贝hostname,避免破坏原始字符串
char *hostname_dup = strdup(hostname);
if (hostname_dup == NULL) {
free(question->name);
return -3;
}
// 用strtok按"."分割,每一段前写一个长度字节
char *token = strtok(hostname_dup, delim);
while (token != NULL) {
size_t len = strlen(token);
*qname = (unsigned char)len;
qname++;
memcpy(qname, token, len);
qname += len;
token = strtok(NULL, delim);
}
*qname = 0; // 最后补0,表示QNAME结束
free(hostname_dup);
return 0;
}
// =======================
// 构造完整DNS请求报文:header + question
// =======================
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);
int offset = 0;
// 拷贝DNS头部到请求buffer
memcpy(request, header, sizeof(struct dns_header));
offset += sizeof(struct dns_header);
// 拷贝QNAME
memcpy(request + offset, question->name, question->length);
offset += question->length;
// 拷贝QTYPE
memcpy(request + offset, &question->qtype, sizeof(question->qtype));
offset += sizeof(question->qtype);
// 拷贝QCLASS
memcpy(request + offset, &question->qclass, sizeof(question->qclass));
offset += sizeof(question->qclass);
return offset; // 返回构建好的报文长度
}
// =======================
// 核心函数:发送DNS请求并接收响应
// =======================
int dns_client_commit(const char *domain) {
// 创建UDP socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
return -1;
}
// 配置目标DNS服务器地址
struct sockaddr_in servaddr = {0};
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(DNS_SERVER_PORT);
servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
// 可选:使用connect设置默认目标地址(UDP也可以)
connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
// 构造DNS请求头
struct dns_header header = {0};
dns_create_header(&header);
// 构造问题部分(域名)
struct dns_question question = {0};
dns_create_question(&question, domain);
// 构造完整请求包
char request[1024] = {0};
int length = dns_build_request(&header, &question, request, sizeof(request));
// 使用sendto发送数据
int slen = sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, sizeof(struct sockaddr_in));
// 接收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);
// 打印响应长度和部分内容(调试用)
printf("recvfrom : %d, %s\n", n, response);
return n;
}
// =======================
// 程序主入口
// =======================
int main(int argc, char *argv[]) {
if (argc < 2) return -1; // 没传域名参数,退出
dns_client_commit(argv[1]); // 调用主流程
return 0;
}