本文聚焦讲解如何通过 C 语言构造并发送一个最小化的 DNS 请求,特别以
dns_client_commit()函数为主线,带你一步步理解 DNS 请求的构造过程。
为什么要学习 DNS 报文构造?
我们平时在浏览器里输入一个网址(比如 www.baidu.com),浏览器其实背后会通过操作系统的 DNS 模块发送一个查询请求,将域名解析为 IP 地址。
而如果我们手动用 C 语言自己构造 DNS 请求,我们可以更深刻地理解底层网络通信的细节,比如 UDP 套接字、报文格式、DNS 协议结构等。
第一步:创建 UDP 套接字
            
            
              cpp
              
              
            
          
          int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
    return -1;
}
        - 
socket(AF_INET, SOCK_DGRAM, 0):创建一个基于 IPv4 的 UDP 套接字。 - 
SOCK_DGRAM指的是数据报套接字(即 UDP)。 - 
创建失败直接返回错误。
 
UDP 是 DNS 最常用的传输方式,简单快速,适合小数据量通信。
第二步:配置服务器地址结构
            
            
              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);
socklen_t addr_len = sizeof(servaddr);
        - 
sin_family设置为AF_INET表示 IPv4; - 
sin_port使用htons转换为网络字节序的端口(这里是 53); - 
sin_addr.s_addr是将字符串 IP 地址"114.114.114.114"转为 32 位网络地址。 
使用 inet_addr() 转换字符串 IP 为二进制,便于系统识别。
第三步:创建 DNS 报文头部结构
            
            
              cpp
              
              
            
          
          struct dns_header header = {0};
dns_create_header(&header);
        - 
报文头结构体填入了如下字段:
- 
随机 ID:标记请求和响应是否匹配;
 - 
标志位:设置为
0x0100(标准查询,递归); - 
查询数量设为 1。
 
 - 
 
所有字段都转为 网络字节序 (用 htons())是网络编程常识。
第四步:构造域名查询部分(Question 区)
            
            
              cpp
              
              
            
          
          struct dns_question question = {0};
dns_create_question(&question, domain);
        将传入的 domain 字符串,如 www.baidu.com,转换为 DNS 格式的 QNAME:
            
            
              cpp
              
              
            
          
          03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00
        - 
每段前面加一个长度字节,末尾以
0x00结尾。 - 
同时设置 QTYPE=1(A记录)、QCLASS=1(IN 类)。
 
第五步:构造完整 DNS 请求报文
            
            
              cpp
              
              
            
          
          char request[1024] = {0};
int length = dns_build_requestion(&header, &question, request);
        - 
该函数负责将 header + question 拼接到请求缓冲区中;
 - 
返回实际请求报文的长度(单位:字节);
 - 
request就是我们最终要发出去的数据。 
第六步:发送 UDP 请求
            
            
              cpp
              
              
            
          
          sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, addr_len);
        - 
使用
sendto()直接把 DNS 报文发往目标服务器; - 
无需建立连接;
 - 
这是 UDP 的典型使用方式。
 
小结:dns_client_commit 的完整流程图
            
            
              cpp
              
              
            
          
          ┌─────────────┐
│ 输入域名    │
└────┬────────┘
     ↓
┌──────────────────────┐
│ 创建 UDP socket      │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 填充服务器地址结构   │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 构造 DNS Header      │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 构造 DNS Question     │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 拼接完整报文         │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 发送 UDP 请求         │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 关闭 socket           │
└──────────────────────┘
        完整代码
            
            
              cpp
              
              
            
          
          #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define DNS_SERVER_PORT     53  
#define DNS_SERVER_IP       "114.114.114.114"
struct dns_header{
    unsigned short id;
    unsigned short flags;
    unsigned short questions;
    unsigned short answers;
    unsigned short authority;
    unsigned short additional;
};
struct dns_question{
    int length;
    unsigned short qtype;
    unsigned short qclass;
    unsigned char *name;
};
int dns_create_header(struct dns_header *header){
    if(header == NULL) return -1;
    memset(header,0,sizeof(struct dns_header));
    srand((unsigned int)time(NULL));
    header->id = (unsigned short)rand();
    header->flags = htons(0x0100);
    header->questions = htons(1);
    return 0;
}
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);
    if(question->name == NULL) return -2;
    question->length = (int)hostlen + 2;
    question->qtype = htons(1);
    question->qclass = htons(1);
    //name
    const char delim[2] = ".";
    unsigned char *qname = question->name;
    char *hostname_dup = strdup(hostname);
    if(hostname_dup == NULL) {
        free(question->name);
        return -3;
    }
    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; // QNAME 结尾补0
    free(hostname_dup);
    return 0;
}
int dns_client_commit(const char *domain){
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if (sockfd < 0)
    {
        return -1;
    }
    
    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);
    socklen_t addr_len = sizeof(servaddr); 
    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_requestion(&header,&question,request);
    // request
    sendto(sockfd,request,length,0,(struct sockaddr*)&servaddr,addr_len);
    close(sockfd);
}