本文主要讲异步请求池的实现和原理。
什么是异步?和阻塞、非阻塞有什么区别?
同步:
一请求就等返回结果,没有等到就一直等
异步:
只管发送请求,进程不需要一直等下去, 而是继续执行下面的操作
阻塞和非阻塞是针对fd的状态,有数据就返回,没有数据就挂起,这是阻塞状态;非阻塞就压根不管,直接返回。
阻塞【挂起】和非阻塞是线程的一种状态,同步和异步是指的是线程执行方法的一种方式,当然同步执行时,一般都伴随着线程的阻塞。
背景:
客户端与mysql服务器连接后,发送完一个请求没有等到返回结果,要是再要发送请求,只能新建一个与mysql的fd连接。
我们现在要让请求和回复解耦合,而且要避免fd连接会阻塞其他的连接。那么fd要用epoll管理。
如何设计?
1、commit
(1)建立网络连接
(2)建立连接后,组织好对应的协议数据
(3)send发送数据到对应的服务器
(4)监控fd是否可读,并将fd加入epoll监控
2、init
其工作主要内容是:初始化异步操作的上下文,上下文指的是epollfd和线程id;pthread_create开启一个线程,处理所有fd的response;
3、callback函数
其为线程的入口函数,处理所有fd的response。 epoll_wait会获取到可读的fd,通过recv(fd)读出所有的协议数据,将其解析并进行相应的操作;
4、destroy
close(fd)
pthread_cancel;
注意:以往的epoll都是再服务器端实现的,本次是epoll作为客户端来做。
代码实现:
相关结构体:
//上下文
struct async_context {
int epfd; //epollfd
pthread_t threadid; //线程id
};
//存储fd和相关会用到的参数
struct ep_arg {
int sockfd;
async_result_cb cb;
};
init初始化:
// 1 . context
// 2 . return context;
// 初始化 做法两种:(1)将上下文当作参数传入;(2)初始化函数返回上下文
//一般当作参数传比较好
struct async_context* dns_async_client_init(void) {
int epfd = epoll_create(1);
if (epfd < 0) return NULL;
//分配一个上下文,上下文初始化
struct async_context* ctx = calloc(1, sizeof(struct async_context));
if (ctx == NULL) return NULL;
ctx->epfd = epfd;
//response的回调函数,线程初始化
int ret = pthread_create(&ctx->threadid, NULL, dns_async_callback, ctx);
if (ret) {
close(epfd);
free(ctx);
return NULL;
}
return ctx;
}
response的回调函数:
#define ASYNC_EVENTS 128
//response的回调函数
void *dns_async_callback(void *arg) {
//上下文
struct async_context* ctx = (struct async_context*)arg;
//不断的去接收
while (1) {
struct epoll_event events[ASYNC_EVENTS] = {0};
int nready = epoll_wait(ctx->epfd, events, ASYNC_EVENTS, -1);
if (nready < 0) {
continue;
}
int i = 0;
for (i = 0;i < nready;i ++) {
struct ep_arg *ptr = events[i].data.ptr;
int sockfd = ptr->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);
printf("recvfrom n : %d\n", n);
struct dns_item *domains = NULL;
//回调函数里进行解析,已经保证数据接收完了
int count = dns_parse_response(buffer, &domains);
ptr->cb(domains, count);
// sockfd 关闭fd
close (sockfd);
free(ptr);
// epollout -->
//epoll_ctl(ctx->epfd, EPOLL_CTL_MOD, sockfd, NULL);
//
}
}
}
commit:
/ 提交代码,可以理解为请求
//传入上下文,回调函数
int dns_async_client_commit(struct async_context *ctx, async_result_cb cb) {
//1、建立好socket,
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("create socket failed\n");
exit(-1);
}
printf("url:%s\n", domain);
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);
//2、建立好连接
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);
//3、准备好协议
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 *ptr = calloc(1, sizeof(struct ep_arg));
if (ptr == NULL) return -1;
ptr->sockfd = sockfd;
ptr->cb = cb;
//4、将IO加入到epoll中,关注其是否可读
struct epoll_event ev;
ev.data.ptr = ptr; //可传
ev.events = EPOLLIN;
epoll_ctl(ctx->epfd, EPOLL_CTL_ADD, sockfd, &ev);
return 0;
}
destroy:
//销毁函数
int dns_async_client_destroy(struct async_context* ctx) {
close(ctx->epfd);
pthread_cancel(ctx->threadid);
}
思考:
问题1:
response处理完收到的数据之后,fd是否要关闭?
1、关闭可以
close(fd);
将fd从epoll中移除掉:epoll_ctl(ctx->epfd, EPOLL_CTL_DEL,sockfd,NULL);
2、不关闭如何处理?
内部有些资源可重用,不关闭,哪里可以重用,如何用?
a.fd要存储,存储在epoll中,可以改成EPOLL_CTL_MOD。改成可写,sendto会使用。这个fd会一直触发可写,不能 这样操作。应该改成边缘触发。sendcb();改成边缘触发来做
b. sockfd长时间没有用,设置超时,要有时间的管理,超时就关闭
问题2:
若send的数据丢失了,response一直没有返回(epoll没有触发),fd要如何处理?
1、加入fd定时器,超时就重发;
面试题:
异步的mysql、redis怎么做?
回答四元组,异步请求池。
1、commit函数;
2、init;
3、callback;
4、destroy
然后根据业务场景,讲协议组织起来,将其发送出去;收到response的地方,处理协议即可。