C++后端开发(3.1.3)——dns异步请求池原理与实现


小节提纲

1.异步请求处理流程
2.四元组,create,commit,callback,destroy
3.异步请求框来封装
4.应用协议redis/dns/http请求封装


0.简介

当业务服务器访问需要等待的服务时,业务访问线程需要等待挂起直到该服务给出反馈,例如对mysql,redis,http,dns等服务的请求,这些请求的返回需要等待一段时间,大大加深了业务服务器的承载压力,也会影响其总体性能,异步请求池就是为解决这个问题应运而生的。

本文通过epoll实现了一个简单的dns请求

1.同步与异步

组件A 组件B 同步 调用 返回 调用 返回 组件A 组件B
组件A 组件B 异步 调用 调用 返回 返回 组件A 组件B

2. DNS协议(域名解析协议 Domain Name System)

DNS实际上是由一个分层的DNS服务器实现的分布式数据库和一个让主机能够查询分布式数据库的应用层协议组成。因此,要了解DNS的工作原理,需要从以上两个方便入手。

参考两个博客
DNS(域名解析协议)详解
自己动手实现DNS协议

2.1 DNS定义

DNS协议是用来将域名转换为IP地址(也可以将IP地址转换为相应的域名地址)

另外DNS通常还提供主机名到以下几项的转换服务

  1. 主机命名(host aloasing) 。有着复杂规范主机名(canonical hostname)的主机可能有一个或多个别名,通常规范主机名较复杂,而别名让人更容易记忆。应用程序可以调用DNS来获得主机别名对应的规范主机名,以及主机的ip地址。
  2. 邮件服务器别名(mail server aliasing) 。DNS也能完成邮件服务器别名到其规范主机名以及ip地址的转换。
  3. 负载均衡(load distribution)。DNS可用于冗余的服务器之间进行负载均衡。一个繁忙的站点,如abc.com,可能被冗余部署在多台具有不同ip的服务器上。在该情况下,在DNS数据库中,该主机名可能对应着一个ip集合,但应用程序调用DNS来获取该主机名对应的ip时,DNS通过某种算法从该主机名对应的ip集合中,挑选出某一ip进行响应。

2.2 DNS 分布式架构

DNS服务器根据域名命名空间(domian name space)组织成如下图所示的 树形结构(当然,只给出部分DNS服务器,只为显示出DNS服务器的层次结构):

在这里插入图片描述

在图中,根节点代表的是根DNS服务器,因特网上共有13台,编号从A到M;根DNS服务器之下的一层被称为顶级DNS服务器;再往下一层被称为权威DNS服务器。

  1. 当一个应用要通过DNS来查询某个主机名,比如www.google.com的ip时,粗略地说,查询过程是这样的:它先与根服务器之一联系,根服务器根据顶级域名com,会响应命名空间为com的顶级域服务器的ip;
  2. 于是该应用接着向com顶级域服务器发出请求,com顶级域服务器会响应命名空间为google.com的权威DNS服务器的ip地址;
  3. 最后该应用将请求命名空间为google.com的权威DNS服务器,该权威DNS服务器会响应主机名为www.google.com的ip。

实际上,除了上图层次结构中所展示的DNS外,还有一类与我们接触更为密切的DNS服务器,它们是 本地DNS服务器,我们经常在电脑上配置的DNS服务器通常就是此类。它们一般由某公司,某大学,或某居民区提供,比如Google提供的DNS服务器 8.8.8.8;比如常被人诟病的114.114.114.114 等。

加入了本地DNS的查询过程跟之前的查询过程基本上是一致的,查询流程如下图所示:

在这里插入图片描述

在实际工作中,DNS服务器是带缓存的。即DNS服务器在每次收到DNS请求时,都会先查询自身数据库包括缓存中有无要查询的主机名的ip,若有且没有过期,则直接响应该ip,否则才会按上图流程进行查询;而服务器在每次收到响应信息后,都会将响应信息缓存起来;

2.3 DNS 应用层协议

2.3.1 DNS 资源记录

在介绍DNS层协议之前,先了解一下DNS服务器存储的资源记录(Resource Records,RRs),一条资源记录(RR)记载着一个映射关系。每条RR通常包含如下表所示的一些信息:

字段含义
NAME名字
TYPE类型
CLASS
TTL生存时间
RDLENGTHRDATA所占的字节数
RDATA数据

NAME和RDATA表示的含义根据TYPE的取值不同而不同,常见的:

  1. 若TYPE=A,则name是主机名,value是其对应的ip;
  2. 若TYPE=NS,则name是一个域,value是一个权威DNS服务器的主机名。该记录表示name域的域名解析将由value主机名对应的DNS服务器来做;
  3. 若TYPE=CNAME,则value是别名为name的主机对应的规范主机名;
  4. 若TYPE=MX,则value是别名为name的邮件服务器的规范主机名;
    ……

TYPE实际上还有其他类型,所有可能的type及其约定的数值表示如下

TYPEvaluemeaning
A1a host address
NS2an authoritative name server
MD3a mail destination (Obsolete - use MX)
MF4a mail forwarder (Obsolete - use MX)
CNAME5the canonical name for an alias
SOA6marks the start of a zone of authority
MB7a mailbox domain name (EXPERIMENTAL)
MG8a mail group member (EXPERIMENTAL)
MR9a mail rename domain name (EXPERIMENTAL)
NULL10a null RR (EXPERIMENTAL)
WKS11a well known service description
PTR12a domain name pointer
HINFO13host information
MINFO14mailbox or mail list information
MX15mail exchange
TXT16text strings

2.3.2 DNS协议整体

DNS请求与响应的格式是一致的,其整体分为Header、Question、Answer、Authority、Additional5部分,如下图所示:

在这里插入图片描述

2.3.3 Header部分

Header部分是一定有的,长度固定为12个字节;其余4部分可能有也可能没有,并且长度也不一定,这个在Header部分中有指明。Header的结构如下:

在这里插入图片描述

  1. ID:占16位。该值由发出DNS请求的程序生成,DNS服务器在响应时会使用该ID,这样便于请求程序区分不同的DNS响应。
  2. QR:占1位。指示该消息是请求还是响应。0表示请求;1表示响应。
  3. OPCODE:占4位。指示请求的类型,有请求发起者设定,响应消息中复用该值。0表示标准查询;1表示反转查询;2表示服务器状态查询。3~15目前保留,以备将来使用。
  4. AA(Authoritative Answer,权威应答):占1位。表示响应的服务器是否是权威DNS服务器。只在响应消息中有效。
  5. TC(TrunCation,截断):占1位。指示消息是否因为传输大小限制而被截断。
  6. RD(Recursion Desired,期望递归):占1位。该值在请求消息中被设置,响应消息复用该值。如果被设置,表示希望服务器递归查询。但服务器不一定支持递归查询。
  7. RA(Recursion Available,递归可用性):占1位。该值在响应消息中被设置或被清除,以表明服务器是否支持递归查询。
  8. Z:占3位。保留备用。
  9. RCODE(Response code):占4位。该值在响应消息中被设置。取值及含义如下:

    0:No error condition,没有错误条件;
    1:Format error,请求格式有误,服务器无法解析请求;
    2:Server failure,服务器出错。
    3:Name Error,只在权威DNS服务器的响应中有意义,表示请求中的域名不存在。
    4:Not Implemented,服务器不支持该请求类型。
    5:Refused,服务器拒绝执行请求操作。
    6~15:保留备用。

  10. QDCOUNT:占16位(无符号)。指明Question部分的包含的实体数量。
  11. ANCOUNT:占16位(无符号)。指明Answer部分的包含的RR(Resource Record)数量。
  12. NSCOUNT:占16位(无符号)。指明Authority部分的包含的RR(Resource Record)数量。
  13. ARCOUNT:占16位(无符号)。指明Additional部分的包含的RR(Resource Record)数量。

2.3.3 Question部分

Question部分的每一个实体的格式如下图所示:

在这里插入图片描述

  1. QNAME:字节数不定,以0x00作为结束符。表示查询的主机名。注意:众所周知,主机名被"."号分割成了多段标签。在QNAME中,每段标签前面加一个数字,表示接下来标签的长度。比如:api.sina.com.cn表示成QNAME时,会在"api"前面加上一个字节0x03,"sina"前面加上一个字节0x04,"com"前面加上一个字节0x03,而"cn"前面加上一个字节0x02;
  2. QTYPE:占2个字节。表示RR类型,见以上RR介绍;
  3. QCLASS:占2个字节。表示RR分类,见以上RR介绍。

2.3.4 Answer、Authority、Additional部分

Answer、Authority、Additional部分格式一致,每部分都由若干实体组成,每个实体即为一条RR,之前有过介绍,格式如下图所示

在这里插入图片描述

  1. NAME:长度不定,可能是真正的数据,也有可能是指针(其值表示的是真正的数据在整个数据中的字节索引数),还有可能是二者的混合(以指针结尾)。若是真正的数据,会以0x00结尾;若是指针,指针占2个字节,第一个字节的高2位为11。
  2. TYPE:占2个字节。表示RR的类型,如A、CNAME、NS等,见以上RR介绍
  3. CLASS:占2个字节。表示RR的分类,见以上RR介绍;
  4. TTL:占4个字节。表示RR生命周期,即RR缓存时长,单位是秒;
  5. RDLENGTH:占2个字节。指定RDATA字段的字节数;
  6. RDATA:即之前介绍的value,含义与TYPE有关,见以上RR介绍。

3 异步请求池实现

3.1实现异步请求的四个步骤与伪代码

  1. 初始化,创建epoll和线程池,通过一个独立的线程调用epoll_wait()
1. Init 实现异步
	a. epoll_create
	b. pthread_create
  1. 主函数通过socket连接服务器,并提交请求,将fd加入epoll中
2. commit 
	a. socket 
	b. connnect_server
	c. encode-->redis/mysql/dns
	d. send
	e. fd加入到epoll中-->epoll_ctl
  1. callback编写回调函数
3. callback
	while(1)
		epoll_wait()
		recv()
		parse()
		fd-->epoll_ delete
  1. 销毁资源
4. destroy	
	close(epfd);
	pthread_cancel(thid)

3.2 实现

3.2.1 dns请求相关结构体

3.2.1.1 宏定义
#define DNS_SVR "114.114.114.114"

#define DNS_HOST 0x01
#define DNS_CNAME 0x05

#define ASYNC_CLIENT_NUM 1024
3.2.1.2 dns请求相关
  1. dns_header
struct dns_header
{
    unsigned short id;
    unsigned short flags;
    unsigned short qdcount;
    unsigned short ancount;
    unsigned short nscount;
    unsigned short arcount;
};
  1. dns_question
struct dns_question
{
    int length;
    unsigned short qtype;
    unsigned short qclass;
    char *qname;
};
  1. dns_item
struct dns_item
{
    char *domain;
    char *ip;
};
3.2.1.3 epoll相关结构体
// 异步请求上下文
struct async_context
{
    int epfd; // epoll的fd
};

// epoll使用的参数
struct ep_arg
{
    int sockfd;         // socketfd
    async_result_cb cb; //处理返回结果用的回调函数
};

3.2.2 dns协议请求实现

3.2.2.1 dns_create_header
// 创造dns协议中的header
int dns_create_header(struct dns_header *header)
{
    if (header == NULL)
        return -1;
    memset(header, 0, sizeof(struct dns_header));
    srandom(time(NULL));

    header->id = random();
    header->flags |= htons(0x0100);
    header->qdcount = htons(1);

    return 0;
}
3.2.2.2 dns_create_question
// 创造dns协议中的question
int dns_create_question(struct dns_question *question, const char *hostname)
{
    if (question == NULL)
        return -1;
    memset(question, 0, sizeof(struct dns_question));

    question->qname = (char *)malloc(strlen(hostname) + 2);
    if (question->qname == NULL)
        return -2;
    question->length = strlen(hostname) + 2;
    question->qtype = htons(1);
    question->qclass = htons(1);

    const char delim[2] = ".";

    char *hostname_dup = strdup(hostname);
    char *token = strtok(hostname_dup, delim);

    char *qname_p = question->qname;

    while (token != NULL)
    {
        size_t len = strlen(token);

        *qname_p = len;
        qname_p++;

        strncpy(qname_p, token, len + 1);
        qname_p += len;

        token = strtok(NULL, delim);
    }

    free(hostname_dup);

    return 0;
}
3.2.2.3 dns_build_request
// 创造dns请求
int dns_build_request(struct dns_header *header, struct dns_question *question, char *request)
{

    int header_s = sizeof(struct dns_header);
    int question_s = question->length + sizeof(question->qtype) + sizeof(question->qclass);

    int length = question_s + header_s;

    int offset = 0;
    memcpy(request + offset, header, sizeof(struct dns_header));
    offset += sizeof(struct dns_header);

    memcpy(request + offset, question->qname, question->length);
    offset += question->length;

    memcpy(request + offset, &question->qtype, sizeof(question->qtype));
    offset += sizeof(question->qtype);

    memcpy(request + offset, &question->qclass, sizeof(question->qclass));

    return length;
}

3.2.3 dns协议解析实现

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

// fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
// 开启非阻塞
static int set_block(int fd, int block)
{
    // 查询fd状态
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags < 0)
        return flags;

    // 开启非阻塞
    if (block)
        flags &= ~O_NONBLOCK;
    else
        flags |= O_NONBLOCK;

    if (fcntl(fd, F_SETFL, flags) < 0)
        return -1;

    return 0;
}

// dns解析名字
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;
            }
        }
    }
}

fcntl函数的用法总结

3.2.3.2 dns_parse_response
// dns解析请求
static int dns_parse_response(char *buffer, struct dns_item **domains)
{
    int i = 0;
    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;
}

3.2.4 异步请求池初始化Init

// 1. 异步请求池初始化Init
// 	a. epoll_create
// 	b. pthread_create
// 返回包含epoll生成fd的async_context
struct async_context *dns_async_client_init(void)
{
    int epfd = epoll_create(1);
    if (epfd < 0)
        return NULL;
    // C 库函数 void *calloc(size_t nitems, size_t size) 分配所需的内存空间,并返回一个指向它的指针。malloc 和 calloc 之间的不同点是,
    // malloc 不会设置内存为零,而 calloc 会设置分配的内存为零。
    struct async_context *ctx = calloc(1, sizeof(struct async_context));
    if (ctx == NULL)
    {
        close(epfd);
        return NULL;
    }
    ctx->epfd = epfd;

    pthread_t thread_id;
    int ret = pthread_create(&thread_id, NULL, dns_async_client_proc, ctx);
    if (ret)
    {
        perror("pthread_create");
        return NULL;
    }
    usleep(1); // child go first
    return ctx;
}

3.2.5 回调函数

static void dns_async_client_result_callback(struct dns_item *list, int count)
{
    int i = 0;

    for (i = 0; i < count; i++)
    {
        printf("name:%s, ip:%s\n", list[i].domain, list[i].ip);
    }
}

3.2.6 commit提交dns请求

// 2. commit
// 	a. socket
// 	b. connnect_server
// 	c. encode-->redis/mysql/dns
// 	d. send
// 	e. fd加入到epoll中-->epoll_ctl
int dns_async_client_commit(struct async_context *ctx, const char *domain, async_result_cb cb)
{
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("create socket failed\n");
        exit(-1);
    }

    printf("url:%s\n", domain);

    set_block(sockfd, 0); // nonblock

    struct sockaddr_in dest;
    bzero(&dest, sizeof(dest));
    dest.sin_family = AF_INET;
    dest.sin_port = htons(53);
    dest.sin_addr.s_addr = inet_addr(DNS_SVR);

    int ret = connect(sockfd, (struct sockaddr *)&dest, sizeof(dest));
    // printf("connect :%d\n", ret);

    struct dns_header header = {0};
    dns_create_header(&header);

    struct dns_question question = {0};
    dns_create_question(&question, domain);

    char request[1024] = {0};
    int req_len = dns_build_request(&header, &question, request);
    int slen = sendto(sockfd, request, req_len, 0, (struct sockaddr *)&dest, sizeof(struct sockaddr));

    struct ep_arg *eparg = (struct ep_arg *)calloc(1, sizeof(struct ep_arg));
    if (eparg == NULL)
        return -1;

    eparg->sockfd = sockfd;
    eparg->cb = cb;

    struct epoll_event ev;
    ev.data.ptr = eparg;
    ev.events = EPOLLIN;

    ret = epoll_ctl(ctx->epfd, EPOLL_CTL_ADD, sockfd, &ev);

    return ret;
}

3.2.7 等待dns消息线程

// 3. callback
// 	while(1)
// 		epoll_wait()
// 		recv()
// 		parse()
// 		fd-->epoll_ delete

static void *dns_async_client_proc(void *arg)
{
    struct async_context *ctx = (struct async_context *)arg;

    int epfd = ctx->epfd;

    while (1)
    {
        struct epoll_event events[ASYNC_CLIENT_NUM] = {0};

        int nready = epoll_wait(epfd, events, ASYNC_CLIENT_NUM, -1);
        if (nready < 0)
        {
            if (errno == EINTR || errno == EAGAIN)
            {
                continue;
            }
            else
            {
                break;
            }
        }
        else if (nready == 0)
        {
            continue;
        }

        printf("nready %d \n", nready);
        int i = 0;
        for (i = 0; i < nready; i++)
        {
            struct ep_arg *data = (struct ep_arg *)events[i].data.ptr;
            int sockfd = data->sockfd;

            char buffer[1024] = {0};
            struct sockaddr_in addr;
            size_t addr_len = sizeof(struct sockaddr_in);
            int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);

            struct dns_item *domain_list = NULL;
            int count = dns_parse_response(buffer, &domain_list);
            // 回调函数
            data->cb(domain_list, count);

            int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);

            close(sockfd);

            dns_async_client_free_domains(domain_list, count);
            free(data);
        }
    }
}

3.2.8 销毁资源

// 4. destroy
// 	close(epfd);
// 	pthread_cancel(thid)
void dns_async_client_free_domains(struct dns_item *list, int count)
{
    int i = 0;

    for (i = 0; i < count; i++)
    {
        free(list[i].domain);
        free(list[i].ip);
    }

    free(list);
}

3.2.9 主函数

int main(int argc, char *argv[])
{
    struct async_context *ctx = dns_async_client_init();
    if (ctx == NULL)
        return -2;
    int count = sizeof(domain) / sizeof(domain[0]);
    int i = 0;

    for (i = 0; i < count; i++)
    {
        dns_async_client_commit(ctx, domain[i], dns_async_client_result_callback);
    }

    getchar();
}

异步请求池代码下载

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

何蔚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值