ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C语言手写简易 DNS 客户端(接收部分)

2026/8/10 20:07:33 拓冰建站 浏览量
C语言手写简易 DNS 客户端(接收部分)

本文通过纯 C 语言手动构造 DNS 请求报文,使用 UDP 协议发送到公共 DNS 服务器,并接收响应,完整演示 DNS 请求流程。

主流程:dns_client_commit()

这是整个流程的核心函数,下面我们按顺序拆解每一步的逻辑,尤其突出发送 sendto 与接收 recvfrom 的设计思路和实现

第一步:创建 UDP 套接字

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {return -1;
}
  • 使用 IPv4(AF_INET)+ UDP(SOCK_DGRAM)创建 socket;

  • 返回值 sockfd 是文件描述符,用于后续发送接收;

  • 创建失败时直接返回错误。

第二步:配置目标服务器地址

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() 设置默认目标地址

connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

虽然 UDP 不需要建立连接,但 connect() 可以用来为 socket 设置默认收发对象,使 send()recv() 也能直接用。

不过后续我们仍然使用 sendto()recvfrom(),所以这步不是必须的。

第四步:构造 DNS 请求结构体

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);

第五步:拼接报文

char request[1024] = {0};
int length = dns_build_request(&header, &question, request, sizeof(request));
  • 将 header + QNAME + QTYPE + QCLASS 依次拷贝到 request 中;

  • 返回整个 DNS 报文的长度(以字节计);

  • 最终得到一个可以直接发送的字节数组。

第六步:使用 sendto() 发送数据包

int slen = sendto(sockfd, request, length, 0,(struct sockaddr*)&servaddr,sizeof(struct sockaddr_in));
  • 把完整请求通过 UDP 发送给服务器;

  • sendto() 第一个参数是 socket;

  • 最后两个参数是目标地址和长度。

第七步:使用 recvfrom() 接收响应

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 表示收到的字节数。

第八步:输出结果(测试用)

printf("recvfrom : %d, %s\n", n, response);

DNS 客户端请求流程图(文字版)

┌────────────────────────────────────────────┐
│                程序入口 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 字节长度         │
└────────────────────────────────────────────┘

完整代码

#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));// 使用时间种子生成随机 IDsrand((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);  // 预留一个结尾的 0x00if (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头部到请求buffermemcpy(request, header, sizeof(struct dns_header));offset += sizeof(struct dns_header);// 拷贝QNAMEmemcpy(request + offset, question->name, question->length);offset += question->length;// 拷贝QTYPEmemcpy(request + offset, &question->qtype, sizeof(question->qtype));offset += sizeof(question->qtype);// 拷贝QCLASSmemcpy(request + offset, &question->qclass, sizeof(question->qclass));offset += sizeof(question->qclass);return offset;  // 返回构建好的报文长度
}// =======================
// 核心函数:发送DNS请求并接收响应
// =======================
int dns_client_commit(const char *domain) {// 创建UDP socketint 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;
}

https://github.com/0voice