基于C语言实现域名解析(附带源码)
一、项目背景详细介绍
域名解析(DNS,Domain Name System)是互联网最基础的服务之一:将人类可读的域名(例如 www.example.com)转换为计算机可用的 IP 地址(例如 93.184.216.34)。几乎所有互联网通信都依赖 DNS。作为系统/网络编程、运维与安全工程师的基础技能,理解并实现域名解析有助于掌握以下关键点:
- 套接字(socket)编程基础;
- 使用系统库(如
getaddrinfo)进行易用高层解析; - 理解 DNS 协议(基于 UDP 的报文格式)并自己实现一个简易 DNS 客户端以加深理解;
- IPv4 与 IPv6 的区别、反向解析(PTR)、超时、重试与报错处理;
- 在受控环境下测试与调试(如使用本地 DNS 服务器或公共 DNS)。
本项目通过两种实现方式讲解域名解析:
- 系统接口法(推荐, 简单可靠):使用标准库函数
getaddrinfo()/getnameinfo()实现同步解析。优点:跨平台、支持 IPv4/IPv6、自动处理搜索域和服务名。适合大多数应用场景。 - 自实现 DNS 客户端(进阶,学习用):直接构造 DNS 请求报文通过 UDP 发送到 DNS 服务器(如 8.8.8.8),解析返回的资源记录(A、AAAA、CNAME)。优点:深入理解 DNS 协议、可以自定义查询行为、用于教学和调试。缺点:需处理更多协议细节(字节序、报文压缩、超时重传等)。
二、项目需求详细介绍
功能性需求
实现一个命令行工具 resolver,支持下列功能:
- 使用系统接口解析域名(显示 IPv4 与 IPv6 地址)。
- 使用自实现 DNS 客户端向指定 DNS 服务器(可配置)发送查询并解析 A/AAAA/CNAME 记录。
- 支持正向解析(域名 → IP)与反向解析(IP → 域名)。
- 对错误(找不到主机、超时、网络不可达等)给出清晰提示。
- 在自实现 DNS 客户端中,处理简单的 DNS 名称压缩,并支持超时与重试(基础级)。
非功能性需求
- 代码用纯 C 实现(兼顾可移植性),在 Linux 下可直接编译。
- 结构清晰,函数注释详尽,便于教学演示。
- 主体代码放在单一代码块内,内部用注释分隔“模块/文件”。
- 提供示例用法与测试样例。
三、相关技术详细介绍
1. 系统 DNS 接口(getaddrinfo/getnameinfo)
getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res):跨 IPv4/IPv6 的推荐方式。返回链表形式的 addrinfo,其中包含 sockaddr 可直接用于 connect 或可转换为字符串。
getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags):用于反向解析(IP → 主机名)或将 sockaddr 转换为数字字符串。
优点:处理搜索域、可并发安全(线程安全实现依赖平台),推荐使用。
2. DNS 协议(报文结构,简要)
DNS 报文(RFC 1035)大体结构:
- 16-bit ID
- 16-bit flags
- 16-bit QDCOUNT(问题条目数)
- 16-bit ANCOUNT(回答条目数)
- 16-bit NSCOUNT(权威记录数)
- 16-bit ARCOUNT(额外记录数)
- 问题(QNAME,QTYPE,QCLASS)
- 资源记录(NAME,TYPE,CLASS,TTL,RDLENGTH,RDATA)
QNAME 为标签序列(每段前缀长度字节,末尾 0)。返回报文中 RDATA 对于 A 为 4 字节 IPv4 地址、AAAA 为 16 字节 IPv6 地址。注意 DNS 报文中会有名称压缩(两个字节的指针以 11 开头);实现时需解析压缩格式。
3. UDP 套接字编程与超时处理
自实现 DNS 客户端使用 UDP 套接字向 DNS 服务器发送查询并等待响应;需设置接收超时(setsockopt(..., SO_RCVTIMEO, ...)),并在超时或错误时实现重试机制(例如最多重试 3 次)。
四、实现思路详细介绍
本文实现分两部分(两种方法):
方法一:系统接口法(getaddrinfo)
- 创建
hints:设置ai_family = AF_UNSPEC(同时支持 IPv4 和 IPv6),ai_socktype = SOCK_STREAM(或 0),ai_flags = AI_CANONNAME(请求返回规范名)。 - 调用
getaddrinfo(domain, NULL, &hints, &res), 遍历res链表,使用getnameinfo或inet_ntop打印 IP 地址字符串。 - 反向解析使用
getnameinfo。
该方法短小精悍且功能完备。
方法二:自实现 DNS 客户端(UDP)
构造 DNS 查询报文:
随机 ID(16-bit)
flags 设置为标准递归查询(Recursion Desired)
QDCOUNT = 1
构造 QNAME(标签序列)
QTYPE = A(1)或 AAAA(28)
QCLASS = IN(1)
发送到 DNS 服务器 UDP 53 端口。
等待响应,解析头部获取 ANCOUNT,解析资源记录:
- 解析 NAME(处理压缩)
- 解析 TYPE、CLASS、TTL、RDLENGTH、RDATA
- 对 A / AAAA / CNAME 做输出
- 处理超时与重试,如 2 秒超时,最多重试 3 次。
- 注意网络字节序(
htons/ntohs)。
实现时会包含名称解压函数 dns_name_unpack(),用于把压缩的域名解析为可读字符串。
五、完整实现代码
/***************************************************************
* 文件:resolver.c
* 功能:C 语言实现域名解析(两种方式)
* - 方法 A:使用系统接口 getaddrinfo/getnameinfo(推荐)
* - 方法 B:自实现简易 DNS 客户端(UDP 查询 A / AAAA / CNAME)
*
* 编译(Linux):
* gcc resolver.c -o resolver
*
* 用法示例:
* ./resolver sys www.example.com # 使用系统接口解析
* ./resolver dns 8.8.8.8 www.example.com # 使用自定义 DNS 客户端,指定 DNS 服务器
* ./resolver reverse sys 93.184.216.34 # 反向解析(IP->域名)使用系统接口
*
***************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <time.h>
#include <sys/time.h>
/* -----------------------------
通用工具函数与定义
----------------------------- */
#define MAXBUF 512
static void pprint_addrinfo(struct addrinfo *res) {
char host[NI_MAXHOST];
for (struct addrinfo *p = res; p != NULL; p = p->ai_next) {
void *addr;
const char *ipver;
if (p->ai_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
addr = &(ipv4->sin_addr);
ipver = "IPv4";
} else if (p->ai_family == AF_INET6) {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
addr = &(ipv6->sin6_addr);
ipver = "IPv6";
} else {
continue;
}
if (inet_ntop(p->ai_family, addr, host, sizeof(host)) == NULL) {
strncpy(host, "unknown", sizeof(host));
host[sizeof(host)-1] = 0;
}
printf(" %-4s %s\n", ipver, host);
}
}
/* -----------------------------
方法 A:使用系统接口 getaddrinfo / getnameinfo
----------------------------- */
static int resolve_with_getaddrinfo(const char *name) {
struct addrinfo hints, *res;
int rv;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // 支持 IPv4 和 IPv6
hints.ai_socktype = 0; // 任意 socket 类型
hints.ai_flags = AI_CANONNAME; // 返回规范名(如可用)
rv = getaddrinfo(name, NULL, &hints, &res);
if (rv != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
printf("Canonical name: %s\n", res->ai_canonname ? res->ai_canonname : "(none)");
printf("Addresses for %s:\n", name);
pprint_addrinfo(res);
freeaddrinfo(res);
return 0;
}
/* 反向解析:IP -> 主机名 */
static int reverse_with_getnameinfo(const char *ipstr) {
struct sockaddr_storage sa;
socklen_t sa_len;
char host[NI_MAXHOST];
memset(&sa, 0, sizeof(sa));
if (strchr(ipstr, ':')) {
// IPv6
struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&sa;
sa6->sin6_family = AF_INET6;
if (inet_pton(AF_INET6, ipstr, &(sa6->sin6_addr)) != 1) {
fprintf(stderr, "Invalid IPv6 address: %s\n", ipstr);
return 1;
}
sa_len = sizeof(struct sockaddr_in6);
} else {
// IPv4
struct sockaddr_in *sa4 = (struct sockaddr_in *)&sa;
sa4->sin_family = AF_INET;
if (inet_pton(AF_INET, ipstr, &(sa4->sin_addr)) != 1) {
fprintf(stderr, "Invalid IPv4 address: %s\n", ipstr);
return 1;
}
sa_len = sizeof(struct sockaddr_in);
}
int rv = getnameinfo((struct sockaddr *)&sa, sa_len, host, sizeof(host), NULL, 0, NI_NAMEREQD);
if (rv != 0) {
fprintf(stderr, "getnameinfo: %s\n", gai_strerror(rv));
return 1;
}
printf("Reverse lookup: %s -> %s\n", ipstr, host);
return 0;
}
/* -----------------------------
方法 B:自实现简易 DNS 客户端(UDP)
仅支持查询 A(1) / AAAA(28) / CNAME (5)
----------------------------- */
/* DNS header: 12 bytes */
#pragma pack(push, 1)
struct dns_header {
unsigned short id;
unsigned short flags;
unsigned short qdcount;
unsigned short ancount;
unsigned short nscount;
unsigned short arcount;
};
#pragma pack(pop)
/* DNS question tail (type & class) */
struct dns_question_tail {
unsigned short qtype;
unsigned short qclass;
};
/* 资源记录固定头部(不含可变长度 NAME 与 RDATA) */
#pragma pack(push,1)
struct dns_rr_fixed {
unsigned short type;
unsigned short class;
unsigned int ttl;
unsigned short rdlength;
};
#pragma pack(pop)
/* 将域名从点分格式转换为 DNS 报文的标签格式:
e.g., "www.example.com" -> [3]www[7]example[3]com[0]
返回写入的字节数到 buf(不超过 buflen)。
*/
static int dns_name_pack(const char *name, unsigned char *buf, int buflen) {
int nlen = strlen(name);
if (nlen == 0) {
if (buflen < 1) return -1;
buf[0] = 0;
return 1;
}
int pos = 0;
const char *label = name;
const char *p = name;
while (1) {
if (*p == '.' || *p == '\0') {
int len = p - label;
if (len > 63) return -1; // label too long
if (pos + 1 + len >= buflen) return -1;
buf[pos++] = (unsigned char)len;
if (len > 0) {
memcpy(&buf[pos], label, len);
pos += len;
}
if (*p == '\0') {
// terminate with zero
if (pos >= buflen) return -1;
buf[pos++] = 0;
break;
}
label = p + 1;
}
p++;
}
return pos;
}
/* 解析 DNS 报文中的名称(包含指针压缩)
packet:完整报文起始地址
pktlen:报文总长度
offset:当前读取偏移(输入时指向名称开始),函数结束时 offset 会更新到名称后的第一个字节
out:输出缓冲区存放解析出的点分格式字符串
outlen:输出长度
返回:0 成功,-1 失败
*/
static int dns_name_unpack(unsigned char *packet, int pktlen, int *offset, char *out, int outlen) {
int orig_offset = *offset;
int pos = 0;
int jumped = 0;
int max_jumps = 0;
int cur = *offset;
while (cur < pktlen) {
unsigned char len = packet[cur];
if (len == 0) {
// end of name
if (!jumped) *offset = cur + 1;
if (pos == 0) {
// root
if (pos + 1 > outlen) return -1;
out[pos] = '\0';
} else {
if (pos + 1 > outlen) return -1;
out[pos] = '\0';
}
return 0;
}
if ((len & 0xC0) == 0xC0) {
// pointer: next byte + (len & 0x3F) << 8
if (cur + 1 >= pktlen) return -1;
int b2 = packet[cur + 1];
int pointer = ((len & 0x3F) << 8) | b2;
if (pointer >= pktlen) return -1;
if (!jumped) *offset = cur + 2;
cur = pointer;
jumped = 1;
if (++max_jumps > 10) return -1; // avoid loops
continue;
} else {
// label
cur++;
if (cur + len > pktlen) return -1;
if (pos + len + 1 >= outlen) return -1;
memcpy(out + pos, packet + cur, len);
pos += len;
out[pos++] = '.';
cur += len;
}
}
return -1;
}
/* 构造并发送 DNS 查询(type: 1=A, 28=AAAA)
dns_server: dotted IP string, e.g., "8.8.8.8"
name: 域名
type: qtype (1/A, 28/AAAA)
timeout_sec: 接收超时时间(秒)
返回:0 成功(并将打印解析结果),非0表示失败
*/
static int dns_query_simple(const char *dns_server, const char *name, unsigned short qtype, int timeout_sec) {
unsigned char buf[512];
memset(buf, 0, sizeof(buf));
struct dns_header hdr;
memset(&hdr, 0, sizeof(hdr));
srand((unsigned int)time(NULL));
hdr.id = (unsigned short) (rand() & 0xFFFF);
hdr.flags = htons(0x0100); // standard query, recursion desired
hdr.qdcount = htons(1);
// 写 header
memcpy(buf, &hdr, sizeof(hdr));
int offset = sizeof(hdr);
// 写 QNAME
int n = dns_name_pack(name, buf + offset, sizeof(buf) - offset);
if (n < 0) {
fprintf(stderr, "dns_name_pack failed\n");
return 1;
}
offset += n;
// 写 QTYPE & QCLASS
struct dns_question_tail qt;
qt.qtype = htons(qtype);
qt.qclass = htons(1); // IN
memcpy(buf + offset, &qt, sizeof(qt));
offset += sizeof(qt);
// 发送 UDP 包
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(53);
if (inet_pton(AF_INET, dns_server, &serv.sin_addr) != 1) {
fprintf(stderr, "Invalid DNS server IP: %s\n", dns_server);
close(sock);
return 1;
}
// 发送并等待回复(带重试)
int tries = 3;
int rv = 1;
for (int t = 0; t < tries; t++) {
ssize_t sent = sendto(sock, buf, offset, 0, (struct sockaddr*)&serv, sizeof(serv));
if (sent != offset) {
perror("sendto");
continue;
}
// 设置 recv 超时
struct timeval tv;
tv.tv_sec = timeout_sec;
tv.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
unsigned char resp[4096];
struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
ssize_t rlen = recvfrom(sock, resp, sizeof(resp), 0, (struct sockaddr*)&from, &fromlen);
if (rlen < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// timeout
if (t == tries - 1) {
fprintf(stderr, "DNS query timeout\n");
} else {
// retry
continue;
}
} else {
perror("recvfrom");
}
continue;
}
// parse response
if (rlen < (ssize_t)sizeof(struct dns_header)) {
fprintf(stderr, "DNS response too short\n");
continue;
}
struct dns_header rhdr;
memcpy(&rhdr, resp, sizeof(rhdr));
unsigned short qdcount = ntohs(rhdr.qdcount);
unsigned short ancount = ntohs(rhdr.ancount);
// skip questions
int roff = sizeof(struct dns_header);
for (int i = 0; i < qdcount; i++) {
char qname[256];
if (dns_name_unpack(resp, rlen, &roff, qname, sizeof(qname)) != 0) {
fprintf(stderr, "Failed to unpack question name\n");
break;
}
// skip qtype & qclass
if (roff + sizeof(struct dns_question_tail) > rlen) break;
roff += sizeof(struct dns_question_tail);
}
// parse answers
printf("Answers (%d):\n", ancount);
for (int i = 0; i < ancount; i++) {
char aname[512];
if (dns_name_unpack(resp, rlen, &roff, aname, sizeof(aname)) != 0) {
fprintf(stderr, "Failed to unpack answer name\n");
break;
}
if (roff + sizeof(struct dns_rr_fixed) > rlen) {
fprintf(stderr, "Truncated RR header\n");
break;
}
struct dns_rr_fixed rrf;
memcpy(&rrf, resp + roff, sizeof(rrf));
roff += sizeof(rrf);
unsigned short type = ntohs(rrf.type);
unsigned short rclass = ntohs(rrf.class);
unsigned short rdlen = ntohs(rrf.rdlength);
if (roff + rdlen > rlen) {
fprintf(stderr, "Truncated RDATA\n");
break;
}
if (type == 1 && rdlen == 4) {
// A
char addrbuf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, resp + roff, addrbuf, sizeof(addrbuf));
printf(" NAME: %s TYPE=A ADDR=%s\n", aname, addrbuf);
} else if (type == 28 && rdlen == 16) {
// AAAA
char addrbuf[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, resp + roff, addrbuf, sizeof(addrbuf));
printf(" NAME: %s TYPE=AAAA ADDR=%s\n", aname, addrbuf);
} else if (type == 5) {
// CNAME (rdata is a name)
int tmp_off = roff;
char cname[512];
if (dns_name_unpack(resp, rlen, &tmp_off, cname, sizeof(cname)) == 0) {
printf(" NAME: %s TYPE=CNAME RDATA=%s\n", aname, cname);
} else {
printf(" NAME: %s TYPE=CNAME (unpack failed)\n", aname);
}
} else {
printf(" NAME: %s TYPE=%d (rdlen=%d bytes)\n", aname, type, rdlen);
}
roff += rdlen;
}
rv = 0; // success
break;
}
close(sock);
return rv;
}
/* -----------------------------
简单命令行接口
支持:
resolver sys <domain>
resolver reverse sys <ip>
resolver dns <dns_server> <domain>
----------------------------- */
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Usage:\n");
fprintf(stderr, " %s sys <domain> # use system resolver\n", argv[0]);
fprintf(stderr, " %s reverse sys <ip> # reverse lookup via system resolver\n", argv[0]);
fprintf(stderr, " %s dns <dns_server> <domain> # use custom DNS client\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "sys") == 0) {
// system resolver
return resolve_with_getaddrinfo(argv[2]);
} else if (strcmp(argv[1], "reverse") == 0 && argc >= 4 && strcmp(argv[2], "sys") == 0) {
return reverse_with_getnameinfo(argv[3]);
} else if (strcmp(argv[1], "dns") == 0 && argc >= 4) {
const char *dns_server = argv[2];
const char *domain = argv[3];
// query A and AAAA
printf("Query A records via DNS server %s for %s\n", dns_server, domain);
dns_query_simple(dns_server, domain, 1, 2);
printf("\nQuery AAAA records via DNS server %s for %s\n", dns_server, domain);
dns_query_simple(dns_server, domain, 28, 2);
return 0;
} else {
fprintf(stderr, "Unknown command or insufficient args\n");
return 1;
}
}
到此这篇关于基于C语言实现域名解析(附带源码)的文章就介绍到这了,更多相关C语言域名解析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!


最新评论