IP地址是网络层通信的基础,但是并不方便人类记忆,所以需要DNS域名系统来实现将IP地址和DNS之间的解析,简单来说,域名解析分为两个关键步骤:
- 本机向本地域名服务器发送一个请求报头(header)
- 本地域名服务器返回一个响应报文(response),其中包含了IP地址
第一个步骤比较关键,因为构造请求报头的内容需要我们熟悉相关协议的内容和结构,而解析响应报文的代码一般比较固定,在实际项目中可以直接借用,并不需要做深入理解。为了用UDP来发送报文,我们还需要一些网络编程的知识,本文集中了介绍请求报文所应该包含的内容,并且使用C语言实现,如果你缺乏基本的网络编程知识可以看我的另一篇文章:CSAPP网络编程章节实验:Proxylab
DNS请求报文格式
根据上图,我们可以知道请求报头(Header)中应该包含的内容有:transaction id、flags、questions、anwers、authority、additional,值得注意的是长度为16,所以使用short类型。
struct dns_header {
unsigned short id;
unsigned short flags;
unsigned short question;
unsigned short answer;
unsigned short authority;
unsigned short additional;
};
我们的正文(queries)中,name存放了需要查询的名称,比如www.google.com,这个的长度是不固定的,也就是说,如果超出了32,是可以拓展的。
struct dns_question {
int length;
unsigned short qtype;
unsigned short qclass;
unsigned char *name;
};
这些内容中,有一些是比较关键的,比如name不能直接使用www.google.com,而需要构造成特别的形式:3www6google3com,这样比较便于DNS服务器进行递归查询。有一些则不那么重要,比如authority, type之类的,不需要记住。
创建报头函数
int dns_create_header(struct dns_header *header) {
if (header == NULL) return -1;
memset(header, 0, sizeof(struct dns_header));
//随机id即可
srandom(time(NULL));
header->id = random();
//调整为网络字节序
header->flags = htons(0x0100);
header->question = htons(1);
return 0;
}
关键技术阐述:当我们进行网络编程的时候,一定要注意使用htons函数进行网络字节序的转化(比如第11行),即从小端序,转为大端序
-
大端序:最高有效字节存储在最低的内存地址(“大端在前”)。
- 举例:数值
0x12345678在内存中的存储顺序(从低地址到高地址):12 34 56 78 - 网络字节序就是采用这种格式
- 举例:数值
-
小端序:最低有效字节存储在最低的内存地址(“小端在前”)。
- 举例:数值
0x12345678在内存中的存储顺序(从低地址到高地址):78 56 34 12 - x86/x64架构的PC采用这种格式
- 举例:数值
创建DNS查询函数
//header填充
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));
question->name = (char*)malloc(strlen(hostname) + 2);
if (question->name == NULL) {
return -2;
}
question->length = strlen(hostname) + 2;
//网络字节序
question->qtype = htons(1);
question->qclass = htons(1);
// name,查询名字格式:3www6google3com
const char delim[2] = ".";
char *qname = question->name;
//复制hostname以方便操作
char *hostname_dup = strdup(hostname);
char *token = strtok(hostname_dup, delim);
while (token != NULL) {
size_t len = strlen(token);
*qname = len;
qname ++;
strncpy(qname, token, len+1);//这里已经包括了最后的'\0'
qname += len;
token = strtok(NULL, delim);
}
free(hostname_dup);
return 0;
}
关键技术阐述:以上代码中的22到37行实现了域名格式的转换,实现的关键在于使用strtok函数,这个函数的功能是从前往后查找delim所在位置,一旦遇到,就将delim前的内容返回,得到token。定义delim为“.”的时候,就会将www返回到token当中。同样的道理,使用while循环不断向后寻找google和com这两个token,并且将长度返回到指定的字符串qname的位置上,最后我们就获得了需要的查询名格式。
提交DNS请求
将header和question整合进request里之后,就可以提交DNS请求了。
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);
int ret = connect(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr));
printf("connect: %d\n", ret);
struct dns_header header = {0};
dns_create_header(&header);
struct dns_question question = {0};
dns_create_question(&question, domain);
//将header和question整合进request里
char request[1024] = {0};
int length = dns_build_request(&header, &question, request, 1024);
//提交DNS请求
int slen = sendto(sockfd, request, length, 0, (struct sockaddr*) &servaddr, sizeof(struct sockaddr_in));
//recvfrom函数,接收DNS服务器的响应
char response[1024] = {0};
struct sockaddr_in addr;
size_t addr_len = sizeof(struct sockaddr_in);
int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr*) &addr, (socklen_t*) &addr_len);
printf("recvfrom : %d, %s\n", n, response);
return n;
}
关键技术阐述:这些代码是比较固定的创建一个套接字连接的方式,(2~6行)先使用socket函数返指定使用的IPv4(参数AF_INET)、UDP(SOCK_DGARM)的协议,成功后返回socket描述符。(8~17行)然后指定servaddr结构体当中的内容:包括端口,DNS服务器的IP以及所使用的协议族。值得注意的是,13~14行使用了connect,其实在UDP编程中,并不必须要设置好connect,因为不需要像TCP一样三次握手,但是为了方便调试,还是先建立连接。最后,(16行以后)调用我们之前实现的header构造函数和整合函数后发送请求到DNS服务器,用recvfrom接收回应报文即可。