Linux C语言实现DNS请求

实际应用中基本用不到DNS自己解析
要获取域名在终端下用host 或者nslookup指令
在c里面使用gethostbyname或者getaddrinfo也能将域名解析为ip
如果不想看文章就用上面的函数。


一、DNS解析过程

域名解析总体可分为两大步骤,
第一个步骤是本机向本地域名服务器发出一个 DNS 请求报文,报文里携带需要查询的域名;
第二个步骤是本地域名服务器向本机回应一个 DNS 响应报文,里面包含域名对应的 IP 地址。

从下面对 baidu.com 进行域名解析的报文中可明显看出这两大步骤。

其具体的流程可描述如下:

  1. 主机 192.168.1.123 先向本地域名服务器 192.168.1.1 进行递归查询
  2. 本地域名服务器采用迭代查询(192.168.1.1),向一个根域名服务器(114.114.114.114)进行查询
  3. 根域名服务器(114.114.114.114)告诉本地域名服务器(192.168.1.1),下一次应该查询的顶级域名服务器 baidu.com 的 IP 地址
  4. 本地域名服务器(192.168.1.1)向顶级域名服务器 baidu.com 进行查询
  5. 顶级域名服务器 .com 告诉本地域名服务器,下一步查询权限服务器 www.baidu.com 的 IP 地址
  6. 本地域名服务器(192.168.1.1)向权限服务器 www.baidu.com 进行查询
  7. 权限服务器 www.baidu.com 告诉本地域名服务器(192.168.1.1)所查询的主机的 IP 地址
  8. 本地域名服务器(192.168.1.1)最后把查询结果36.152.44.96告诉主机(192.168.1.123)

其中有两个概念递归查询和迭代查询:
递归查询: 本机向本地域名服务器发出一次查询请求,就静待最终的结果。如果本地域
名服务器无法解析,自己会以 DNS 客户机的身份向其它域名服务器查询,直到得到最
终的 IP 地址告诉本机

迭代查询: 本地域名服务器向根域名服务器查询,根域名服务器告诉它下一步到哪里去
查询,然后它再去查,每次它都是以客户机的身份去各个服务器查询

二、DNS协议报文格式

在这里插入图片描述

1 Header(12字节)

1.1 Transaction ID (会话标识)(2 字节)

是 DNS 报文的 ID 标识,对于请求报文和其对应的应答报文,这个字段请丢和响应时是相同的,通过它可以区分 DNS 应答报文是哪个请求的响应。

1.2 Flags(标志) (2 字节)

在这里插入图片描述

标识解释
QR(1bit)查询/响应标志,0 为查询,1 为响应
opcode(4bit)0 表示标准查询,1 表示反向查询,2 表示服务器状态请求
AA(1bit)表示授权回答
TC(1bit)表示可截断的
RD(1bit)表示期望递归
RA(1bit)表示可用递归
rcode(4bit)表示返回码,0 表示没有差错,3 表示名字差错,2 表示服务器错误(Server Failure)

1.3 数量字段(总共 8 字节)

Questions、Answer RRs、Authority RRs、Additional RRs 各自表示后面的四个区域的数目。

标识解释
Questions表示查询问题区域节的数量
Answers表示回答区域的数量
Authoritative namesversers表示授权区域的数量
Additional recoreds表示附加区域的数量

2 正文( 字节)

2.1 Queries (查询)

在这里插入图片描述
Name(查询名): 长度不固定,且不使用填充字节,一般该字段表示的就是需要查询的域名(如果是反向查询,则为 IP,反向查询即由 IP 地址反查域名),一般的格式如下图所示。
在这里插入图片描述
Type(查询类型)

类型助记符说明
1A由域名获得 IPv4 地址
2NS查询域名服务器
5CNAME查询规范名称
6SOA开始授权
11WKS熟知服务
12PTR把 IP 地址转换成域名
13HINFO主机信息
15MX邮件交换
28AAAA由域名获得 IPv6 地址
252AXFR传送整个区的请求
255ANY对所有记录的请求

Class(查询类)
通常为 1,表明是 Internet 数据

2.2 RR (源记录)

源记录(RR) 包括回答区域(Answers)授权区域(Authoritative nameservers)附加区域(Additional recoreds)
在这里插入图片描述
Name(域名)(2 字节或不定长)
格式和 Queries 区域的查询名字字段是一样的。
有一点不同就是,当报文中域名重复出现的时候,该字段使用 2 个字节的偏移指针来表示。
比如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用 2 字节的指针来表示,具体格式是最前面的两个高位是 11,用于识别指针。
其余的 14 位从 DNS 报文的开始 处 计 数 ( 从 0 开 始 ), 指出该报文中的相应字节数 。

**Type(查询类型)**表明资源纪录的类型
Class(查询类): 对于 Internet 信息,总是 IN
TTL(生存时间): 以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程
序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳
定程度,极为稳定的信息会被分配一个很大的值(比如 86400,这是一天的秒数)。
Data(资源数据): 该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数
据。可以是 Address(表明查询报文想要的回应是一个 IP 地址)或者 CNAME(表明查询
报文想要的回应是一个规范主机名)等。

三、C实现查询DNS

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define DNS_SERVER_PORT		53
#define DNS_SERVER_IP		"114.114.114.114"

#define DNS_HOST			0x01
#define DNS_CNAME			0x05

struct dns_header {

	unsigned short id;
	unsigned short flags;

	unsigned short questions; // 问题数 1 
	unsigned short answer;    // 答案数

	unsigned short authority; // 权威答案数
	unsigned short additional;// 附加答案数

};


struct dns_question {
	int length;
	unsigned char *name; // 
	unsigned short qtype;
	unsigned short qclass;
};

struct dns_item {
	char *domain;
	char *ip;
};


//client sendto dns server
int dns_create_header(struct dns_header *header) {

	if (header == NULL) return -1;
	//把传进来的header置空
	memset(header, 0, sizeof(struct dns_header));

	//给Header的ID随机附值
	srandom(time(NULL));
	header->id = random();

	//标识符大部分好像都是0x0100
	header->flags = htons(0x0100);
	//只有一个问题区域节
	header->questions = htons(1); 
	return 0;
}

// hostname: www.0voice.com
// www
// 0voice
// com

// name: 3www60voice3com0

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

	//申请查询名的内存空间,因为name的长度是不固定的 所以多加2个冗余位
	question->name = (char*)malloc(strlen(hostname) + 2);
	if (question->name == NULL) {
		return -2;
	}

	//设置question的数据长度信息
	question->length = strlen(hostname) + 2;
	//设置question的类型 A类
	question->qtype = htons(1); 
	//设置question的class 通常为1
	question->qclass = htons(1);

	// name 
	const char delim[2] = ".";
	char *qname = question->name;
	
	//strdup()会先用maolloc()配置与参数s 字符串相同的空间大小,然后将参数hostname字符串的内容复制到该内存地址,然后把该地址返回。
	char *hostname_dup = strdup(hostname);
	//分解字符串 hostname_dup 为一组字符串,delim 为分隔符。
	char *token = strtok(hostname_dup, delim); // www.0voice.com 

	//继续分割剩下的字符串
	while (token != NULL) {

		//size_t 为 long unsigned int
		size_t len = strlen(token); //第一次是www 长度为3

		// 更新qname //3www.4xxxx.3com
		// 先把数字塞进去
		*qname = len;

		// 指向下一个位置 填充后面的内容
		qname ++;
		//把token放到qname里面,复制长度是len+1 是把结尾的/0也放进去了
		strncpy(qname, token, len + 1);
		qname += len;
		
		//此处此一个参数填NULL,会继续以delim为分隔符分割上一步分割后的字符串
		token = strtok(NULL, delim); //xxxx.com ,  com
	}

	free(hostname_dup);
}

// struct dns_header *header
// struct dns_question *question
// char *request

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

	// header --> request
	memcpy(request, header, sizeof(struct dns_header));
	int offset = sizeof(struct dns_header);

	// question --> request
	// 把question的name放进request
	memcpy(request+offset, question->name, question->length);
	offset += question->length;
	// 把question的type放进request
	memcpy(request+offset, &question->qtype, sizeof(question->qtype));
	offset += sizeof(question->qtype);
	// 把question的class放进request
	memcpy(request+offset, &question->qclass, sizeof(question->qclass));
	offset += sizeof(question->qclass);

	//返回request的长度
	return offset;
}


static int is_pointer(int in) {
	return ((in & 0xC0) == 0xC0);
}


static void dns_parse_name(unsigned char *chunk, unsigned char *ptr, char *out, int *len) {

	int flag = 0, n = 0, alen = 0;
	char *pos = out + (*len);

	while (1) {

		flag = (int)ptr[0];
		if (flag == 0) break;

		if (is_pointer(flag)) {
			
			n = (int)ptr[1];
			ptr = chunk + n;
			dns_parse_name(chunk, ptr, out, len);
			break;
			
		} else {

			ptr ++;
			memcpy(pos, ptr, flag);
			pos += flag;
			ptr += flag;

			*len += flag;
			if ((int)ptr[0] != 0) {
				memcpy(pos, ".", 1);
				pos += 1;
				(*len) += 1;
			}
		}
	
	}
	
}



//解析响应信息				  buffer为response返回的信息						
static int dns_parse_response(char *buffer, struct dns_item **domains) {

	int i = 0;
	//初始化一个工作指针 指向reponse返回过来的信息的头部
	unsigned char *ptr = buffer;

	//
	ptr += 4;
	int querys = ntohs(*(unsigned short*)ptr);

	ptr += 2;
	int answers = ntohs(*(unsigned short*)ptr);

	ptr += 6;
	for (i = 0; i < querys; i++) {
		while (1) {
			int flag = (int)ptr[0];
			ptr += (flag + 1);

			if (flag == 0) break;
		}
		ptr += 4;
	}

	char cname[128], aname[128], ip[20], netip[4];
	int len, type, ttl, datalen;

	int cnt = 0;
	struct dns_item *list = (struct dns_item*)calloc(answers, sizeof(struct dns_item));
	if (list == NULL) {
		return -1;
	}

	for (i = 0;i < answers;i ++) {
		
		bzero(aname, sizeof(aname));
		len = 0;

		dns_parse_name(buffer, ptr, aname, &len);
		ptr += 2;

		type = htons(*(unsigned short*)ptr);
		ptr += 4;

		ttl = htons(*(unsigned short*)ptr);
		ptr += 4;

		datalen = ntohs(*(unsigned short*)ptr);
		ptr += 2;

		if (type == DNS_CNAME) {

			bzero(cname, sizeof(cname));
			len = 0;
			dns_parse_name(buffer, ptr, cname, &len);
			ptr += datalen;
			
		} else if (type == DNS_HOST) {

			bzero(ip, sizeof(ip));

			if (datalen == 4) {
				memcpy(netip, ptr, datalen);
				inet_ntop(AF_INET , netip , ip , sizeof(struct sockaddr));

				printf("%s has address %s\n" , aname, ip);
				printf("\tTime to live: %d minutes , %d seconds\n", ttl / 60, ttl % 60);

				list[cnt].domain = (char *)calloc(strlen(aname) + 1, 1);
				memcpy(list[cnt].domain, aname, strlen(aname));
				
				list[cnt].ip = (char *)calloc(strlen(ip) + 1, 1);
				memcpy(list[cnt].ip, ip, strlen(ip));
				
				cnt ++;
			}
			ptr += datalen;
		}
	}

	*domains = list;
	ptr += 2;

	return cnt;
}


int dns_client_commit(const char *domain) {

    //套接字(socket 文件描述符) AF_INET指 IPv4 SOCK_DGRAM是UDP套接字的名字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd < 0) {
		return  -1;
	}

    //把目标服务器的信息存在sockaddr_in结构体中
	struct sockaddr_in servaddr = {0}; //初始化置空
	servaddr.sin_family = AF_INET;  //IPv4协议
	servaddr.sin_port = htons(DNS_SERVER_PORT); //端口号,htons是将整型变量从主机字节顺序转变成网络字节顺序
	//IP地址 是在结构体内嵌套结构体 最内层的s_addr是一个unsigned long类型
    //inet_addr()的功能是将一个点分十进制的IP转换成一个长整数型数(unsigned long类型)
    servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP); 

    //使用connect()将套接字与特定的IP地址和端口绑定起来,建立这样绑定好数据的连接
    //                套接字   sockaddr 结构体变量的指针     addr 变量的大小
	int ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
	//connect() Return 0 on success, -1 for errors.
	printf("connect : %d\n", ret);

	struct dns_header header = {0};	//Hearder先全部置零
	//给Header赋上信息值
	dns_create_header(&header);

	struct dns_question question = {0}; //Question先全部置零
	//给question赋上信息值
	dns_create_question(&question, domain);

	//构造一个请求(request)先全部置零
	char request[1024] = {0};
	int length = dns_build_request(&header, &question, request, 1024);

	/**开始请求
	sendto() 用来将数据由指定的socket 传给对方主机
	参数1 为已建好连线的socket, 如果利用UDP协议则不需经过连线操作
	参数2 指向欲连线的数据内容,
	参数3 为数据内容的长度
	参数4 一般设0
	参数5 用来指定欲传送的网络地址, 结构sockaddr 请参考bind(). 
	参数6 为sockaddr 的结果长度
	返回值:成功则返回实际传送出去的字符数, 失败返回-1, 错误原因存于errno 中.**/
	int slen = sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, sizeof(struct sockaddr));
	
	//构造一个响应(response)先全部置零
	char response[1024] = {0};
	//返回的地址
	struct sockaddr_in addr;
	size_t addr_len = sizeof(struct sockaddr_in);
	
	/**开始接收响应
	recv()用来接收远程主机经指定的socket 传来的数据, 
	参数1 为已建好连线的socket, 如果利用UDP协议则不需经过连线操作
	参数2 为数据存到由指向的内存空间 
	参数3 为可接收数据的最大长度
	参数4 一般设0
	参数5 用来指定欲传送的网络地址, 结构sockaddr
	参数6 为sockaddr 的结构长度.
	返回值:成功则返回接收到的字符数, 失败则返回-1, 错误原因存于errno 中.**/
	int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr*)&addr, (socklen_t*)&addr_len);

	//处理接收到的响应
	struct dns_item *dns_domain = NULL;
	// dns_parse_response(response, &dns_domain);

	for(int i = 0; i < n; i++)
	{
		printf("%x",response[i]);
	}
	printf("\n");

	free(dns_domain);
	//返回接收到的字符数
	return n;
}

int main(int argc, char *argv[]) {
	if (argc < 2) return -1;
	dns_client_commit(argv[1]);
}

  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C语言可以通过使用Socket库来实现HTTP请求。需要进行以下步骤: 1. 创建一个Socket:首先,使用socket()函数创建一个Socket,该Socket将用于与服务器进行通信。可以使用AF_INET作为参数来指定使用IPv4地址族,并使用SOCK_STREAM指定使用TCP协议。 2. 连接到服务器:使用connect()函数将Socket连接到服务器的IP地址和端口号。可以使用gethostbyname()函数来获取服务器的IP地址。 3. 构建HTTP请求:使用C语言的字符串操作函数来构建HTTP请求。可以包括请求方法(例如GET或POST),请求头部(例如Host和Content-Type),请求体(对于POST请求),以及其他必要的数据。 4. 发送请求:使用send()函数将构建好的HTTP请求发送到服务器。可以使用strlen()函数来获取请求的长度。 5. 接收响应:使用recv()函数从服务器接收HTTP响应。可以先使用recv()接收响应头部,并使用字符串操作函数来解析响应头部信息(例如状态码和响应内容的长度)。然后,根据长度使用recv()继续接收响应内容。 6. 处理响应:根据HTTP响应的格式进行处理。可以根据响应头部的Content-Type来决定如何处理响应内容。 7. 关闭Socket:最后,使用close()函数关闭Socket,释放资源。 需要注意的是,以上步骤只是一个基本的框架,实际实现时可能需要处理异常情况(例如网络连接错误)和进行更复杂的处理(例如使用SSL加密连接)。 总结:使用C语言的Socket库可以实现HTTP请求,通过创建Socket、连接服务器、构建请求、发送请求、接收响应、处理响应等步骤来完成HTTP请求和响应的交互。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值