上一篇主要介绍了任务管理器,这篇开始介绍socket管理器,我们知道服务器在支持并发的时候会有几种不同的方式,在讲解下面的内容之前,先回顾一下传统的tcp和udp是怎么进行通信的,这里由简单到复杂
1.第一种是最原始的tcp调用,服务端调用socket创建套接字进行通信,调用bind绑定端口,调用listen来监听端口是否有客户端请求过来,该函数用一个队列来保存未处理的连接请求,然后阻塞到accept系统调用,accept如果发现有连接请求过来了,他就会获取到该连接请求,这个时候accept就会返回一个新的fd,用于客户端和服务端的通信,否则服务端就继续阻塞的等待客户端的连接。比如有三个客户端连接服务端,那么这个时候总共有四个fd,三个是accept返回的用于传输数据的新的fd,一个是socket创建的用于监听的fd。
最原始的udp调用,传统的udp没有tcp复杂,因为它在通信之前不需要三次握手,因此它的调用socket以及bind以后,服务端和客户端就可以进行通信了
2.为了提高并发量,提出了多进程多线程的方式,对于tcp服务器来说,它其实是有多个文件描述符,包括监听描述符和已连接描述符,而对于udp来说只有一个描述符就够了,因为它本身是没有连接的概念的。因此针对tcp服务器来说,它可以在accept以后把新的fd交给新的进程或者线程,然后主进程继续阻塞到accept上,实现并发;而对于udp来说,并发的常见思路是使用多线程。服务器在读取一个新请求之后,可以交由一个线程处理,该线程在处理之后直接将响应内容发给客户端。另一方面,udp服务器和多个客户端交互,但是却没有多个socket。典型的解决方案是,服务器为每个客户端创建一个新的socket,并绑定一个新的端口。客户端以后就通过这个新的socket与服务器通信,获得响应。总结来说,udp并发服务器,针对多个客户端,可以创建多个socket;针对多个请求,可以使用多线程(线程池)进行处理。
3.还有一种比较好的方式是多路复用,介于后面用到了select,因此这里主要分析一下select,程序在读取数据之前,需要等待数据就绪。在select机制下,数据就绪之后,程序会被通知有数据就绪,但是并不知道是哪个文件描述符的数据就绪,需要遍历所有文件描述符,因此操作的时间复杂性是线性的;在epoll机制下,数据就绪后,程序不仅知道有数据就绪,而且可以知道是哪个文件描述符的数据就绪,因此操作时间是常数的。由于select的操作是线性复杂的,在使用select时,当文件描述符的数量较大后(一般以1024为界限),就需要再起额外的线程。而epoll的操作是常数的,所以无论有多少个文件描述符,都不影响其性能。不过,一般epoll仍然需要和多线程配合。常见的做法是,主进程通过epoll机制观察有无新的请求,当有新的请求,就由一个工作线程进行处理。比如说针对tcp来说,它会监听listenfd和普通的accepdfd,后者是前者调用accept以后返回的,listenfd就是监听来自客户端的新的连接请求
以上为背景,我们开始分析bind是如何做的,因为bind同样即支持tcp协议,支持tcp的原因在于udp协议会有长度的限制,在数据包的大小超过一定的范围以后就会采用tcp协议;又支持udp协议,但是介于俩者会有重复的地方,所以会以udp为主,tcp为辅
socket的这一部分要和server那一篇中的client_start结合起来看,我这里先做一个概要,如果看不下去了,可以看一下这个概要,你可以把它理解为一条主线:
首先要搞清楚,无论是tcp协议还是udp协议,最终处理的都是dns的查询请求,只不过一个是通过udp协议进行的,一个是通过tcp协议进行的,那么tcp协议最终在监听到数据以后,还是会转为和对udp请求一样的处理方式,也就是最终都是通过client_request来处理请求的
server在启动的时候,会让一个工作线程去加载配置文件,就是run_server这个函数,它扫描出来要监听的端口,对于udp来说,他在socket,bind以后就开始监听该端口了,这里开了cpu个client,他们等待watcher线程分配连接,但是那个时候还没有把它放入到select中去监听,加载完成以后调用clinet_start,这个时候它会直接调用recvmsg,如果有请求的话直接发送client_request事件给工作线程让它去处理udp请求,如果没有数据的话则把它放入到sock->recv_list中,然后把该socket放入到select中监听(也就是交给watcher线程),watcher线程在监听到该socket有数据的时候,调用dispatch_recv函数,把sock封装到internal_recv事件中,然后把事件发送给工作线程,工作线程执行internal_recv,它的内部跑的还是doio_recv,如果返回结果为success,那么他就调用sendevent发送task给客户端,同时把本次事件从sock的recv_list事件中清除,其余的交给client_request处理,然后再次监听该socket,如果返回soft,那么继续监听就可以
对于tcp来说,它扫描端口,然后在run_server中socket,bind,listen,然后在client_start中把该监听socket放入到sock->accept队列中,同时通知watcher线程去监听该socket,当有连接请求过来的时候,他就调用dispatch_accept,在这个函数中注册internal_accept事件,然后通知工作线程去处理该请求,工作线程会调用Internal_accept,该工作线程会调用accept获取新的请求,设置该事件对应的newsocket的信息,把该事件从accept_list中删除,然后用新的socket更新旧的manger的fds,maxfd等信息,注意这里还没有把该新的fd添加到watcher线程中监控,然后触发工作线程去调用client_newconn函数,这里获取到client信息,将client更新为新的socket,然后调用client_read
而udp在启动的时候,先自己监听udp的主端口,然后如果那会没有请求过来的话才把他交给watcher线程去处理,当有新的数据来的时候,他就驱动工作线程去调用internal_recv,工作线程直接调用recvmsg,如果调用成功的话,则直接驱动工作线程去调用client_request,同时再次监听该socket的可读性
tcp则是在启动的时候,就把自己的监听socket交给watcher线程去监听,当有新的连接过来的时候,它就驱动工作线程去调用internal_accept,然后工作线程调用它,其实就是调用accept获取到一个新的连接,然后更新事件中的socket,注意此时并没有把该新的socket交给watcher线程去处理,然后驱动工作线程去调用client_newconn,然后工作线程获取到该事件中的client,然后调用client_read,而client_read处理的其实就是把它的事件转为socketevent事件,同样最终的目的还是驱动client_request事件
也就是说他相当于有一组连接池,一组线程池,读写分开,watcher线程监听socket,可读的时候就把一个连接分给线程池去做,线程池的某个线程可以拿到一个客户连接,然后就可以去处理业务了,线程处理业务,并不阻塞当前监听状况,而watcher线程此时仍然可以再次监听该socket,监听它的可读,当然这个时候也可以监听它的可写性了,因为如果业务请求处理完了,就可以驱动线程去回写请求了。就是这种模式下,无论是udp还是tcp都可以实现高并发的操作。
//以该函数作为入口函数
result = isc_socketmgr_create(ns_g_mctx, &ns_g_socketmgr);
{
isc_socketmgr_t *manager = isc_mem_get(mctx, sizeof(*manager))
//同样初始化manger的一些信息
//把所有的fd设置为0
memset(manager->fds, 0, sizeof(manager->fds));
//初始sock列表
ISC_LIST_INIT(manager->socklist);
//条件变量
isc_condition_init(&manager->shutdown_ok) != ISC_R_SUCCESS
//创建一对pipe用于线程之间的通信
pipe(manager->pipe_fds) != 0
//初始化select用到的fd集合
FD_ZERO(&manager->read_fds);
FD_ZERO(&manager->write_fds);
//将pipe[0]加入到读中,同时设置maxfd
FD_SET(manager->pipe_fds[0], &manager->read_fds);
manager->maxfd = manager->pipe_fds[0];
//启动watcher线程,可以看到这里只启动了一个
isc_thread_create(watcher, manager, &manager->watcher)
}
它是一个线程,工作线程和该线程通过pipe进行通信,该线程负责fd的增删以及初始化相应的action
watcher
{
//获取到父线程用于读的那个fd
ctlfd = manager->pipe_fds[0]
//开始进入循环
while (!done)
{
//获取select用到的三个参数
readfds = manager->read_fds;
writefds = manager->write_fds;
maxfd = manager->maxfd + 1;
//调用select
cc = select(maxfd, &readfds, &writefds, NULL, NULL);
//说明工作线程发来对线程的修改信息,注意这个是特定的只处理ctrl_fd的
if (FD_ISSET(ctlfd, &readfds))
{
//注意这是个死循环,处理内部的对一些fd的处理,注意该fd的状态一定是MANAGED
for (;;)
{
//读取待操作fd到fd,以及要监听该fd的什么状态到msg
select_readmsg(manager, &fd, &msg)
if (msg == SELECT_POKE_NOTHING)
{
break;
}
//如果是shutdown的话,则直接跳出最外层的循环退出该watcher线程
if (msg == SELECT_POKE_SHUTDOWN) {
done = ISC_TRUE;
break;
}
//对于内部来的信息进行一些处理
wakeup_socket(manager, fd, msg)
{
//如果他是快关闭的,那么直接从fd_set中清除掉
if (manager->fdstate[fd] == CLOSE_PENDING)
{
manager->fdstate[fd] = CLOSED;
FD_CLR(fd, &manager->read_fds);
FD_CLR(fd, &manager->write_fds);
(void)close(fd);
}
//注意通过pipe[0]传来的fd肯定是这个状态的
if (manager->fdstate[fd] != MANAGED)
return;
//通过它获取到对应的socket,依据msg的信息要么监听该fd的读要么是写
sock = manager->fds[fd]
if (msg == SELECT_POKE_READ)
FD_SET(sock->fd, &manager->read_fds);
if (msg == SELECT_POKE_WRITE)
FD_SET(sock->fd, &manager->write_fds)
}
}
}
//开始处理别的fd
process_fds(manager, maxfd, &readfds, &writefds)
{
//注意他处理的所有fd是有限的
REQUIRE(maxfd <= (int)FD_SETSIZE);
for (i = 0; i < maxfd; i++)
{
//排除过pipe的俩个fd
if (i == manager->pipe_fds[0] || i == manager->pipe_fds[1])
continue;
//对于这个状态的fd采用和上面一样的处理方式
if (manager->fdstate[i] == CLOSE_PENDING)
{}
//获取到对于的sock
sock = manager->fds[i];
//描述符可读的情况,针对同一个socket,它的task是从sock->accept_list或者scok->recv_list中获取到的,因此针对同一个socket的请求,它的连接请求被看做是一个task,它的数据发送也被看做是一个task,
read:
if (!SOCK_DEAD(sock))//如果socket的引用计数不为0才去进行一系列的操作
{
//如果该sock正处于listen的状态,当有数据的时候,它需要调用accept返回一个已连接套接字,注意这里listen的永远是同一个端口
if (sock->listener)
{
dispatch_accept(sock)
{
//声明事件和连接
intev_t *iev
isc_socket_newconnev_t *ev
//获取到该sock对应的accept_list的一个accept事件,这个的赋值操作是在server刚启动的时候有一个client_start进行赋值的
ev = ISC_LIST_HEAD(sock->accept_list);
if (ev == NULL)
return;
sock->pending_accept = 1;
//这是注册该socket在可读的时候应该要调用的函数,就是下面的internal_accept
iev = &sock->readable_ev
sock->references++;
iev->ev_sender = sock;
iev->ev_action = internal_accept;
iev->ev_arg = sock;
isc_task_send(ev->ev_sender, (isc_event_t **)&iev)
{
如果task是idle的,那么就代表该sock是第一次被投入使用,因此在把event添加到task的events集合中以后,修改该task的状态为ready,同时通过task获取到manger,isc_taskmgr_t *manager = task->manager;把刚刚的task再添加到ready_task中,SIGNAL(&manager->work_available)唤醒某个工作线程去处理该任务;如果task本身已经是ready的了,那么直接把task添加到ready_task中就可以了,因为该task本身也处于ready状态,所以也不需要被唤醒了
}
}
}
else//这就是对一个已连接socket进行正常的数据传输,是调用accept以后返回的sockfd
{
//对于tcp来说他就是调用accept以后返回的那个fd,对于udp来说就是套接字
dispatch_recv(sock)
{
isc_socketevent_t *ev = ISC_LIST_HEAD(sock->recv_list);
sock->pending_recv = 1;
//前面处于listen的时候,给sock->readable_ev赋值的是internal_accept,意思是listen到数据了,然后调用accept;这里处于acceptd状态,给sock->readable_ev赋值的是internal_recv,意思是该fd有数据可读了,应该调用internal_recv
iev = &sock->readable_ev;
sock->references++;
iev->ev_sender = sock;
iev->ev_action = internal_recv;//注册接收数据的函数
iev->ev_arg = sock;
isc_task_send(ev->ev_sender, (isc_event_t **)&iev)
}
}
FD_CLR(i, &manager->read_fds);
}
//如果是描述符可写的话
if (!SOCK_DEAD(sock))
{
//可写的时候,作为客户端需要主动连接对方了
if (sock->connecting)
{
dispatch_connect(sock);
{
isc_socket_connev_t *ev = ev = sock->connect_ev;
sock->references++;
iev->ev_sender = sock;
iev->ev_action = internal_connect;
iev->ev_arg = sock;
isc_task_send(ev->ev_sender, (isc_event_t **)&iev)
}
}
else//这是真的要发送数据了
{
dispatch_send(sock);
{
isc_socketevent_t *ev = ISC_LIST_HEAD(sock->send_list);
sock->pending_send = 1;
iev = &sock->writable_ev;
sock->references++;
iev->ev_sender = sock;
iev->ev_action = internal_send;
iev->ev_arg = sock;
isc_task_send(ev->ev_sender, (isc_event_t **)&iev);
}
}
//处理完该fd的情况,则清除掉该fd
FD_CLR(i, &manager->write_fds);
}
}
}
}
}
小结:
可以看出来bind用了一个单独的线程去做fd的处理,通过pipe与主进程和各个线程进行通信,每一个fd对应一个isc_socket_t的结构,该fd的连接,该fd的数据的接收和发送都通过该socket里面对应的不同的task进行,可以理解为一个fd会有四个task,包括accept_list,recv_list,connetc_list,send_list中获取到的task,而事件则包括Internal_accept,internal_recv,internal_connect,internal_send四个事件,最终工作线程要处理的就是这些task对应的事件,我们的udp对查询的响应就是通过internal_recv和internal_send来实现的
在server刚启动的时候会执行一个client_start函数,该函数会把要监听的socket加入到select模型中,它在执行过程中会注册一个client_newconn事件,因为下面的accept事件会用到它,所以在这里也一并做分析,这样就可以把tcp事件的建立连接到数据数据传输以及udp的数据传输整体的串联起来了
下面可以具体分析一下这五个事件是做了一些什么,首先是accept事件
该函数主要做的事情就是:
1.当客户端有连接请求过来的时候,它会触发调用inetrnal_accept,这个函数做的就是调用accept初始化一个新的fd,更新manger的一些manfd信息,这个时候并不会把它添加到watcher线程的select中进行监控,因为还没有添加到manger的readfds中去;
2.如果需要增加新的监听socket则通过pipe告诉watcher线程,但是往往不会这样做,因为该监听socket已经在server启动的时候在client_start中做了;
3.它会把该事件发送到task中,该task通过sock->accept_list->ev_sender获取到,事件就是sock->accept_list的,只不过它会把新的fd复制到它内部,而该事件对应的action就是client_newconn
internal_accept(isc_task_t *me,isc_event_t *ev)
{
//这个通过上面的赋值就可以看出来
isc_socket_t *sock = ev->ev_sender
isc_socketmgr_t *manager = sock->manger
代表处理了
sock->pending_accept = 0
sock->references--
if (sock->references == 0)
{
return
}
isc_socket_newconnev_t *dev = ISC_LIST_HEAD(sock->accept_list)
//通过accept_list获取到task,进而获取到address信息,下面调用accept转变socket
addrlen = sizeof(dev->newsocket->address.type);
fd = accept(sock->fd, &dev->newsocket->address.type.sa, (void *)&addrlen)
//把新的fd赋值到dev中
if (fd != -1)
{
dev->newsocket->address.length = addrlen;
dev->newsocket->pf = sock->pf;
}
//把事件从accept_list中删除
ISC_LIST_UNLINK(sock->accept_list, dev, ev_link);
//因为每次listen的都是同一个端口,所以if一般不会进去,因为那个listen的端口一直在被监听,server刚启动的时候会有一个client_start,它在client_start会执行一个isc_socket_accept,这个函数做了很多重要的事情,包括声明isc_socket_newconnev_t *dev事件,为他的action注册client_newconn事件,同时把该dev事件enqueue到sock->accept_list中,然后他会把这个监听scoket通过Pipe[1]告诉watcher线程进行监听,注意这是唯一一次enqueue监听socket的地方,这里该if条件一般不会进去
if (!ISC_LIST_EMPTY(sock->accept_list))
{
//这个是告诉他需要监听一个新的端口
select_poke(sock->manager, sock->fd, SELECT_POKE_ACCEPT);
}
if (fd != -1)
{
//dev->newsocket在上面被赋值了,这里把这个已连接套接字添加到socklist中
ISC_LIST_APPEND(manager->socklist, dev->newsocket, link);
//上面赋值协议类型以及长度,这里赋值关键的fd
dev->newsocket->fd = fd;
dev->newsocket->bound = 1;
dev->newsocket->connected = 1;
//赋值本地的地址
dev->address = dev->newsocket->address;
//这里把新的socket信息赋值到全局的ns_g_socket中,并且设置状态为MANAGED,还记得上面状态为MANAGED的才会处理,注意这里只是把该新的fd添加到fds,更新到maxfd,并没有添加到resdfds和writefds中,因此在执行完这个函数以后select并不会对他进行监控
manager->fds[fd] = dev->newsocket;
manager->fdstate[fd] = MANAGED;
manager->maxfd = fd;
}
它的task没变,通过dev->ev_sender获取到(这个的赋值操作是在client_start的时候有一个isc_socket_accept函数中执行的),而它的事件的ev_sender同样还是sock,这次把该dev事件发送到task中,而它要执行的事件就是client_newconn事件
dev->result = result;
task = dev->ev_sender;
dev->ev_sender = sock;
isc_task_sendanddetach(&task, ISC_EVENT_PTR(&dev))
}
下面就分析一下上面的client_newconn事件,该事件其实是在server启动的时候注册的,注册的流程等到讲解client_start的时候再讲解,总之这里就可以理解为它是server一开始启动的时候必做的事情。这里重点分析一下client_newconn做了什么,因为他是上面inetrnal_accept函数做完以后就要做得事情,这个函数的重点部分放到udp接收数据的时候处理,毕竟对于dns协议来说,udp才是正统夫人
client_newconn(isc_task_t *task, isc_event_t *event)
{
//获取到client
ns_client_t *client = event->ev_arg;
isc_socket_newconnev_t *nevent = (isc_socket_newconnev_t *)event;
该client对应的连接数减减
client->naccepts--;
client->interface->ntcpcurrent--;
//就是internal_accept里面的那个result,就是反应调用accept以后的一些情况
if (nevent->result == ISC_R_SUCCESS)
{
//这样该client就拿到新的tcp连接
client->tcpsocket = nevent->newsocket;
client->state = NS_CLIENTSTATE_READING;
//把连接信息放到client中
(void)isc_socket_getpeername(client->tcpsocket,&client->peeraddr);
//转换地址
isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr);
dns_tcpmsg_init(client->mctx, client->tcpsocket,&client->tcpmsg);
//这里如果是tcpmsg,也就是紧急数据的话,那么bind就优先处理改数据了
client_read(client);
{
//同样是注册client_request事件,它是通过tcpmsg消息注册的
result = dns_tcpmsg_readmessage(&client->tcpmsg, client->task,client_request, client);
{
//注册新的事件
ISC_EVENT_INIT(&tcpmsg->event, sizeof(isc_event_t), 0, 0,DNS_EVENT_TCPMSG, action, arg, tcpmsg,NULL, NULL);
result = isc_socket_recv(tcpmsg->sock, ®ion, 0,tcpmsg->task, recv_length, tcpmsg);
{
//这里再次转化为socketevent事件,最终的处理和udp是一样的
isc_socketevent_t *dev = allocate_socketevent(sock, ISC_SOCKEVENT_RECVDONE, action, arg);
//这个函数就和udp的过来的一样了
return (isc_socket_recv2(sock, region, minimum, task, dev, 0));
{
}
}
}
}
}
}
下面分析internal_recv这个事件,在udp来数据的时候就会调用这个函数,具体的分析如下
在介绍下面这个函数之前需要有俩个前提强调一下,这些都是client_start中针对udp数据要做的,这里只是简单的说明一下:
1.在server启动的时候会调用client_start,对于udp来说调用client_udprecv,进而调用isc_socket_recv2(client->udpsocket, &r, 1,client->task, client->recvevent, 0)进而把client_request事件作为action注册到isc_socketevent_t *类型的dev事件中,这个事件要做的就是处理udp的请求,这么做的目的是在调用下面这个internal_recv函数的时候会有task_send的调用,它意味着此时有udp的数据来了,也就是有udp的请求来了,那么通知watcher工作线程去调用client_request去处理该请求
2.同样还是在server启动的时候函数在调用recvmsg的那一刻未必刚好有udp请求过来,因此会把它放到sock->recv_list中,与此同时如果它的socket还没有被监控的话,就把该fd加到watcher线程中进行监控,注意整个server监听的只有这一个端口
3.对于udp来说,他会有多个client同时调用client_start,也就意味着在sock->recv_list中可能会有多个请求,所以才有后面的循环处理
下面的函数是,针对udp做的,当发现socket有数据的时候就说明有udp请求了,这个时候调用该函数进行数据的接收工作
internal_recv(isc_task_t *me, isc_event_t *ev)
{
isc_socket_t *sock = ev->ev_sender;
sock->pending_recv = 0;
sock->references--
if (sock->references == 0)
{
return
}
//上面在client_start的时候没有udp请求过来的数据被放到这个列表里,因此这里把它取出来进行处理,因此这个列表里可能会有多个请求
isc_socketevent_t *dev = ISC_LIST_HEAD(sock->recv_list);
//这里开始调用recv接收数据了,bind是使用recvmsg这个系统调用来接收数据的,该函数的分析放到下面
while(dev!=NULL)
{
switch (doio_recv(sock, dev))
{
//取第一个请求出来进行处理,然后发生错误,这个时候不进行该请求的处理,因为watcher线程每次处理完sock以后都会把sock移除掉,但是介于这里可能还有别的请求在,因此需要再次监控该socket
case DOIO_SOFT:
goto poke;
//读到success的处理方式和DOIO_HARD的方式是一样的,直接让工作线程调用clieng_request进行请求的处理,它会认为本次请求结束,然后继续下一个请求的处理,处理完所有请求以后,watcher线程对fd的监控是有client_request来做的
case DOIO_SUCCESS:
//表明io发生了一个错误,这个时候还是会把一些信息返回给发送者,因为是io错误,同时发生了错误就break,而且可以看到如果发生错误,则要把该事件从sock->recv_list中移除
case DOIO_HARD:
send_recvdone_event(sock, &dev);
{
isc_task_t *task = (*dev)->ev_sender;
(*dev)->ev_sender = sock;
//移除该事件
if (ISC_LINK_LINKED(*dev, ev_link))
ISC_LIST_DEQUEUE(sock->recv_list, *dev, ev_link);
//下面发送任务,然后执行lient_request
if (((*dev)->attributes & ISC_SOCKEVENTATTR_ATTACHED)== ISC_SOCKEVENTATTR_ATTACHED)
{
//注意这个分支的在发送完事件以后会把task赋值为NULL,意思就是这次上次调用recvmsg但是没有收到信息的那个socket,这次可以清除掉了
isc_task_sendanddetach(&task, (isc_event_t **)dev);
}
else
{
isc_task_send(task, (isc_event_t **)dev);
}
}
break;
//读到这个说明从对端读到的数据字节数为0,这个时候可以直接分发掉dev事件,但是把它的result设置为ISC_R_EOF,就是不处理了,然后继续监听该socket
case DOIO_EOF:
do {
dev->result = ISC_R_EOF;
send_recvdone_event(sock, &dev);
dev = ISC_LIST_HEAD(sock->recv_list);
} while (dev != NULL);
goto poke;
}
dev = ISC_LIST_HEAD(sock->recv_list);
}
if (!ISC_LIST_EMPTY(sock->recv_list))
select_poke(sock->manager, sock->fd, SELECT_POKE_READ);
}
在分析下面这个函数之前有必要看一下recvmsg函数,因为它的一些特性会衍生出一些不同的写法
1.一个重要的数据结构:
struct msghdr {
void *msg_name; /* 消息的协议地址 */
socklen_t msg_namelen; /* 地址的长度 */
struct iovec *msg_iov; /* 多io缓冲区的地址 */
int msg_iovlen; /* 缓冲区的个数 */
void *msg_control; /* 辅助数据的地址 */
socklen_t msg_controllen; /* 辅助数据的长度 */
int msg_flags; /* 接收消息的标识 */
};
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
}
前面俩个存储的是协议的地址以及长度,主要是未连接的udp套接字,对于tcp来说可以设置为null和0,而recvmsg是一个值结果参数,它在调用完成以后会保存着对端的地址,msg_iov和msg_iovlen两个成员用于指定数据缓冲区数组,iov_base是数据的起始地址,iov_len是数组的长度,在调用函数之前应该事先分配好这些内存;msg_control和msg_controllen是用来设置辅助数据的位置和大小的,辅助数据(ancillary data)也叫作控制信息
doio_recv函数的分析,因为是调用recvmsg,所以就会有一系列相关的数据结构声明
doio_recv()
{
struct msghdr msghdr;
//在调用recvmsg之前要分配好一些内存以及内存的大小
build_msghdr_recv(sock, dev, &msghdr, iov, &read_count);
{
memset(msghdr, 0, sizeof(struct msghdr));
if (sock->type == isc_sockettype_udp)
{
依据sock->pf的是ipv4,ipv6,unix赋值msg的信息
}
//获取buffer
buffer = ISC_LIST_HEAD(dev->bufferlist);
iovcount = 0
//这里有单io buffer和多io buffer,就是iov数组的大小,这里写的是多io buffer
while(buffer!=NULL)
{
isc_buffer_availableregion(buffer, &available);
iov[iovcount].iov_base = (void *)(available.base);
iov[iovcount].iov_len = available.length;
read_count += available.length;
iovcount++;
buffer = ISC_LIST_NEXT(buffer, link);
}
//存储到msg中
msg->msg_iov = iov;
msg->msg_iovlen = iovcount;
}
//系统调用接收数据
cc = recvmsg(sock->fd, &msghdr, 0);
//中间有一系列的对返回出错的处理
//这是对控制数据的处理
if (sock->type == isc_sockettype_udp)
process_cmsg(sock, &msghdr, dev);
更新dev的buffer
dev->n += cc;
actual_count = cc;
buffer = ISC_LIST_HEAD(dev->bufferlist);
while (buffer != NULL && actual_count > 0U)
{
//这里意味着它有多个io_buffer接收了数据
if (isc_buffer_availablelength(buffer) <= actual_count)
{
actual_count -= isc_buffer_availablelength(buffer);
isc_buffer_add(buffer,isc_buffer_availablelength(buffer));
}
else//这里就意味着只有一个io_buffer
{
isc_buffer_add(buffer, actual_count);
actual_count = 0;
break
}
buffer = ISC_LIST_NEXT(buffer, link);
}
//同样是对返回结果的处理
if (((size_t)cc != read_count) && (dev->n < dev->minimum))
return (DOIO_SOFT);
dev->result = ISC_R_SUCCESS;
return (DOIO_SUCCESS);
}
上面在执行完task_send以后,就要开始处理请求了,对于udp来说就是client_request,那么下面重点分析一下这个函数,他其实就是处理dns请求的
client_request