编程实战:如何在程序中解析域名

网络 通信技术
网络应用客户端想要跟服务端通信,必须先向 DNS 服务器查询域名对应的 IP 地址。举个例子,读者访问我的网站 fasionchan.com 时,浏览器需要先根据域名查询网站的 IP 地址,再和网站的 Web 服务器进行通信。

[[403061]]

本文转载自微信公众号「小菜学编程」,作者fasionchan。转载本文请联系小菜学编程公众号。

由于域名比 IP 地址更便于记忆,我们通常使用它来访问网络服务。

网络应用客户端想要跟服务端通信,必须先向 DNS 服务器查询域名对应的 IP 地址。举个例子,读者访问我的网站 fasionchan.com 时,浏览器需要先根据域名查询网站的 IP 地址,再和网站的 Web 服务器进行通信。

那么,如何通过编程实现域名查询呢?这是开发网络应用无法回避的问题。

我们知道,DNS 服务器和客户端之间使用 DNS 协议进行通信:客户端先向服务器发送 请求报文 ,服务器将查询结果封装成 应答报文 ,回复客户端。DNS 可以使用 UDP 或 TCP 作为传输层协议,通信端口号为 53 。

假设客户端使用 UDP 协议,一次域名查询的步骤大致如下:

  1. 创建一个 UDP 套接字;
  2. 封装 DNS 请求报文,待查询域名位于问题节;
  3. 通过 UDP 套接字,将请求报文发给 DNS 服务器(服务端端口一般是 53 );
  4. 等待服务端响应,并从 UDP 套接字读取应答报文;
  5. 解析应答报文,获得查询结果;
  6. 关闭 UDP 套接字;

如果每个网络应用都需要自行封装 DNS 报文实现域名查询,未免太麻烦了!为此,C库提供了一系列工具函数。应用程序只需调用这些工具函数,即可完成域名查询,不用自己操作套接字,或者封装 DNS 报文。

示例程序

这个程序调用 C 库函数 gethostbyname ,将用户在命令行参数中指定的域名查询出来:

  1. #include <arpa/inet.h> 
  2. #include <netdb.h> 
  3. #include <stdio.h> 
  4.  
  5. int main(int argc, char *argv[]) { 
  6.     if (argc != 2) { 
  7.         fprintf(stderr, "bad arguments"); 
  8.         return -1; 
  9.     } 
  10.  
  11.     char *name = argv[1]; 
  12.     printf("resolve domain name: %s\n"name); 
  13.  
  14.     struct hostent *result = gethostbyname(name); 
  15.     if (result == NULL) { 
  16.         if (h_errno == HOST_NOT_FOUND) { 
  17.             fprintf(stderr, "Hostname not found!\n"); 
  18.         } 
  19.  
  20.         if (h_errno == NO_DATA) { 
  21.             fprintf(stderr, "No such record\n"); 
  22.         } 
  23.  
  24.         if (h_errno == NO_RECOVERY) { 
  25.             fprintf(stderr, "\n"); 
  26.         } 
  27.  
  28.         if (h_errno == TRY_AGAIN) { 
  29.             fprintf(stderr, "Temporary error occurred, please try again!\n"); 
  30.         } 
  31.  
  32.         return -1; 
  33.     } 
  34.  
  35.     int i = 0; 
  36.     while (result->h_addr_list[i] != NULL) { 
  37.         printf("IP: %s\n", inet_ntoa(*(struct in_addr *)result->h_addr_list[i])); 
  38.         i++; 
  39.     } 
  40.  
  41.     return 0; 

顾名思义,gethostbyname 根据域名查询主机的地址,结果一般是 IP 地址或者 IPv6 地址。

请看程序第 14 行,以待查询域名为参数调用 gethostbyname 函数;它返回一个 hostent 结构体指针,结构体中保存着域名查询结果。

第 15-33 行,检查域名解析结果,空表示出错;出错时根据 h_errno 的值,分情况处理(详情请见后文)。

第 35-39 行,从 hostent 结构体中取出查询结果,并打印到屏幕上。

那么, gethostbyname 库函数内部都做了些什么呢?答案其实不难猜到。它会帮我们创建 UDP 套接字、发送 DNS 请求报文、接收并解析应答报文。以这个程序为例,它的执行流(蓝线)大致如下:

域名查询库函数

实际上,C 库提供了一系列工具函数,用于域名查询:

  • gethostbyname ,查询指定域名,查询结果保存在 hostent 结构体中,指针被返回给调用者;
  • gethostbyname_r ,同上,为线程安全版本,可在多线程环境中使用;
  • gethostbyname2 ,同一,但支持通过 af 参数指定查询地址类型;
  • gethostbyname2_r ,同三,为线程安全版本,可在多线程环境中使用;

以 gethostbyname 为例,如果查询成功,它将返回一个 hostent 结构体指针,结构体保存着查询结果。如果查询出错,它将返回 NULL ,并将错误保存 h_errno 全局变量。一般而言,域名查询出错,可以分为这几种情况:

HOST_NOT_FOUND ,表示指定主机不存在,即域名不存在;

NO_DATA ,表示域名存在其他记录,但没有地址相关记录( A 或者 AAAA );

NO_RECOVERY ,域名服务器出现不可恢复错误;

TRY_AGAIN ,临时出错,可通过重试恢复;

当域名查询失败时,调用者必须检查 h_errno 变量,分情况进行处理。

局限性

在网络爬虫、Socks5 代理等应用场景,域名查询非常频繁。这时直接使用 gethostbyname 系列库函数,很有可能会面临性能瓶颈。

一方面,gethostbyname 库函数每次查询域名时,都要创建一个 UDP 套接字来跟 DNS 服务器通信。这意味着,频繁的域名查询背后,必然伴随着大量套接字的创建和销毁,开销可想而知!

另一方面,gethostbyname 库函数将一直阻塞,直到 DNS 服务器返回结果或者查询超时。这将严重制约系统的并发处理能力。

因此,在高频查询场景,不能直接使用 gethostbyname 等库函数,必须采用一些经过优化的异步域名解析库。

扩展阅读

 

gethostbyname

 

责任编辑:武晓燕 来源: 小菜学编程
相关推荐

2023-12-28 10:30:56

类型系统Rust

2021-09-16 17:38:49

UbuntuLinuxJava

2009-06-08 21:35:02

Java启动程序

2016-07-29 11:21:16

Ubuntulinux程序

2018-12-29 09:45:28

Linux编程语言命令

2021-09-13 09:01:02

Vue 技巧 开发工具

2014-05-28 09:45:55

CentOS域名服务器

2023-09-27 23:24:50

C++链表

2018-10-29 10:13:29

Windows 10应用程序卸载

2018-08-27 14:50:46

LinuxShellBash

2019-03-27 13:20:31

Windows 10更新驱动程序

2022-04-01 07:35:45

IDEAJavaWeb 项目

2010-05-20 17:52:02

2018-06-05 08:51:04

Linux结束进程中止程序

2021-09-14 12:34:33

LinuxLinux终端

2020-08-28 07:00:00

WSLLinuxWindows 10

2015-08-31 13:42:06

IDEDockerdoclipser

2021-08-30 07:50:42

脚本语言命令行

2024-01-15 07:15:05

函数式编程代码

2019-05-08 11:30:41

MicrosoftWindows 10后台应用程序
点赞
收藏

51CTO技术栈公众号