管理器之socket
上一篇主要介紹了工作管理員,這篇開始介紹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