基于C语言实现域名解析(附带源码)

 更新时间:2025年12月12日 08:45:57   作者:南城花随雪。  
域名解析(DNS,Domain Name System)是互联网最基础的服务之一,本项目通过两种实现方式讲解域名解析,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

一、项目背景详细介绍

域名解析(DNS,Domain Name System)是互联网最基础的服务之一:将人类可读的域名(例如 www.example.com)转换为计算机可用的 IP 地址(例如 93.184.216.34)。几乎所有互联网通信都依赖 DNS。作为系统/网络编程、运维与安全工程师的基础技能,理解并实现域名解析有助于掌握以下关键点:

  • 套接字(socket)编程基础;
  • 使用系统库(如 getaddrinfo)进行易用高层解析;
  • 理解 DNS 协议(基于 UDP 的报文格式)并自己实现一个简易 DNS 客户端以加深理解;
  • IPv4 与 IPv6 的区别、反向解析(PTR)、超时、重试与报错处理;
  • 在受控环境下测试与调试(如使用本地 DNS 服务器或公共 DNS)。

本项目通过两种实现方式讲解域名解析:

  1. 系统接口法(推荐, 简单可靠):使用标准库函数 getaddrinfo() / getnameinfo() 实现同步解析。优点:跨平台、支持 IPv4/IPv6、自动处理搜索域和服务名。适合大多数应用场景。
  2. 自实现 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 链表,使用 getnameinfoinet_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语言域名解析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用C++ MFC编写一个简单的五子棋游戏程序

    使用C++ MFC编写一个简单的五子棋游戏程序

    这篇文章主要介绍了使用C++ MFC编写一个简单的五子棋游戏程序,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-02-02
  • 详解C++ 智能指针的删除器

    详解C++ 智能指针的删除器

    标准库为智能指针提供了两个默认版本的删除器,可简化智能指针的代码编写,这篇文章主要介绍了C++智能指针的删除器的相关知识,需要的朋友可以参考下
    2025-05-05
  • C语言编写扫雷小程序

    C语言编写扫雷小程序

    这篇文章主要为大家详细介绍了C语言编写扫雷小程序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-09-09
  • C++中的bind实践代码

    C++中的bind实践代码

    std::bind是C++中的一个函数适配器,用于预先固定函数的某些参数,生成一个新的函数对象,它通过占位符来实现参数的占位和重排,本文介绍C++中的bind代码实践,感兴趣的朋友跟随小编一起看看吧
    2025-12-12
  • C++读写.mat文件的方法

    C++读写.mat文件的方法

    本文介绍了“C++读写.mat文件的方法”,需要的朋友可以参考一下
    2013-03-03
  • C语言解决百钱买百鸡问题

    C语言解决百钱买百鸡问题

    本文给大家分享的是一个经典的算法(百元百鸡)的C语言版的解决方法,使用的是比较偷懒的穷举法,有需要的小伙伴可以参考下。
    2016-02-02
  • 用C语言画一个圆

    用C语言画一个圆

    大家好,本篇文章主要讲的是用C语言画一个圆,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览
    2022-01-01
  • 使用C++一步步实现俄罗斯方块后续

    使用C++一步步实现俄罗斯方块后续

    本文主要给大家分享的是作者在使用C++制作俄罗斯方块小游戏的时候所需要的常用的函数,有需要的小伙伴可以借鉴下,希望大家能够喜欢。
    2017-12-12
  • C语言中循环嵌套的应用方式

    C语言中循环嵌套的应用方式

    这篇文章主要介绍了C语言中循环嵌套的应用方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • 浅谈VC中预编译的头文件放那里的问题分析

    浅谈VC中预编译的头文件放那里的问题分析

    本篇文章是对VC中预编译的头文件放那里的问题进行了详细的分析介绍,需要的朋友参考下
    2013-05-05

最新评论