高性能网络编程技术
http://blog.csdn.net/heiyeshuwu/article/details/40508683
高性能網(wǎng)絡(luò)編程技術(shù)
作者:jmz (360電商技術(shù)組)
如何使網(wǎng)絡(luò)服務(wù)器能夠處理數(shù)以萬計的客戶端連接,這個問題被稱為C10K?Problem。在很多系統(tǒng)中,網(wǎng)絡(luò)框架的性能直接決定了系統(tǒng)的整體性能,因此研究解決高性能網(wǎng)絡(luò)編程框架問題具有十分重要的意義。
1.?網(wǎng)絡(luò)編程模型
在C10K?Problem中,給出了一些常見的解決大量并發(fā)連接的方案和模型,在此根據(jù)自己理解去除了一些不實際的方案,并做了一些整理。
1.1、PPC/TPC模型
典型的Apache模型(Process?Per?Connection,簡稱PPC),TPC(Thread?Per?Connection)模型,這兩種模型思想類似,就是讓每一個到來的連接都一邊自己做事直到完成。只是PPC是為每個連接開了一個進(jìn)程,而TPC開了一個線程。可是當(dāng)連接多了之后,如此多的進(jìn)程/線程切換需要大量的開銷;這類模型能接受的最大連接數(shù)都不會高,一般在幾百個左右。
1.2、異步網(wǎng)絡(luò)編程模型
異步網(wǎng)絡(luò)編程模型都依賴于I/O多路復(fù)用模式。一般地,I/O多路復(fù)用機(jī)制都依賴于一個事件多路分離器(Event?Demultiplexer)。分離器對象可將來自事件源的I/O事件分離出來,并分發(fā)到對應(yīng)的read/write事件處理器(Event?Handler)。開發(fā)人員預(yù)先注冊需要處理的事件及其事件處理器(或回調(diào)函數(shù));事件分離器負(fù)責(zé)將請求事件傳遞給事件處理器。兩個與事件分離器有關(guān)的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用異步IO。
在Reactor中,事件分離器負(fù)責(zé)等待文件描述符或socket為讀寫操作準(zhǔn)備就緒,然后將就緒事件傳遞給對應(yīng)的處理器,最后由處理器負(fù)責(zé)完成實際的讀寫工作。
而在Proactor模式中,處理器--或者兼任處理器的事件分離器,只負(fù)責(zé)發(fā)起異步讀寫操作。IO操作本身由操作系統(tǒng)來完成。傳遞給操作系統(tǒng)的參數(shù)需要包括用戶定義的數(shù)據(jù)緩沖區(qū)地址和數(shù)據(jù)大小,操作系統(tǒng)才能從中得到寫出操作所需數(shù)據(jù),或?qū)懭霃膕ocket讀到的數(shù)據(jù)。事件分離器捕獲IO操作完成事件,然后將事件傳遞給對應(yīng)處理器。
l?在Reactor中實現(xiàn)讀:
-?注冊讀就緒事件和相應(yīng)的事件處理器
-?事件分離器等待事件
-?事件到來,激活分離器,分離器調(diào)用事件對應(yīng)的處理器
-?事件處理器完成實際的讀操作,處理讀到的數(shù)據(jù),注冊新事件,然后返還控制權(quán)
l?在Proactor中實現(xiàn)讀:
-?處理器發(fā)起異步讀操作(注意:操作系統(tǒng)必須支持異步IO)。在這種情況下,處理器無視IO就緒事件,它關(guān)注的是完成事件。
-?事件分離器等待操作完成事件
-?在分離器等待過程中,操作系統(tǒng)利用并行的內(nèi)核線程執(zhí)行實際的讀操作,并將結(jié)果數(shù)據(jù)存入用戶自定義緩沖區(qū),最后通知事件分離器讀操作完成。
-?事件分離器呼喚處理器。
-?事件處理器處理用戶自定義緩沖區(qū)中的數(shù)據(jù),然后啟動一個新的異步操作,并將控制權(quán)返回事件分離器。
可以看出,兩個模式的相同點,都是對某個IO事件的事件通知(即告訴某個模塊,這個IO操作可以進(jìn)行或已經(jīng)完成)。在結(jié)構(gòu)上,兩者也有相同點:demultiplexor負(fù)責(zé)提交IO操作(異步)、查詢設(shè)備是否可操作(同步),然后當(dāng)條件滿足時,就回調(diào)handler;不同點在于,異步情況下(Proactor),當(dāng)回調(diào)handler時,表示IO操作已經(jīng)完成;同步情況下(Reactor),回調(diào)handler時,表示IO設(shè)備可以進(jìn)行某個操作(can?read?or?can?write)。
1.2.1?Reactor模式框架
使用Proactor模式需要操作系統(tǒng)支持異步接口,因此在日常中比較常見的是Reactor模式的系統(tǒng)調(diào)用接口。使用Reactor模型,必備的幾個組件:事件源、Reactor框架、多路復(fù)用機(jī)制和事件處理程序,先來看看Reactor模型的整體框架,接下來再對每個組件做逐一說明。
?
l?事件源
Linux上是文件描述符,Windows上就是Socket或者Handle了,這里統(tǒng)一稱為“句柄集”;程序在指定的句柄上注冊關(guān)心的事件,比如I/O事件。
l?event?demultiplexer——事件多路分發(fā)機(jī)制
??由操作系統(tǒng)提供的I/O多路復(fù)用機(jī)制,比如select和epoll。
??程序首先將其關(guān)心的句柄(事件源)及其事件注冊到event?demultiplexer上;
??當(dāng)有事件到達(dá)時,event?demultiplexer會發(fā)出通知“在已經(jīng)注冊的句柄集中,一個或多個句柄的事件已經(jīng)就緒”;
??程序收到通知后,就可以在非阻塞的情況下對事件進(jìn)行處理了。
l?Reactor——反應(yīng)器
Reactor,是事件管理的接口,內(nèi)部使用event?demultiplexer注冊、注銷事件;并運行事件循環(huán),當(dāng)有事件進(jìn)入“就緒”狀態(tài)時,調(diào)用注冊事件的回調(diào)函數(shù)處理事件。
一個典型的Reactor聲明方式
class?Reactor?{??
public:??
????int?register_handler(Event_Handler?*pHandler,?int?event);??
????int?remove_handler(Event_Handler?*pHandler,?int?event);??
????void?handle_events(timeval?*ptv);??
????//?...??
};??
l?Event?Handler——事件處理程序
事件處理程序提供了一組接口,每個接口對應(yīng)了一種類型的事件,供Reactor在相應(yīng)的事件發(fā)生時調(diào)用,執(zhí)行相應(yīng)的事件處理。通常它會綁定一個有效的句柄。
下面是兩種典型的Event?Handler類聲明方式,二者互有優(yōu)缺點。
class?Event_Handler?{??
public:??
????virtual?void?handle_read()?=?0;??
????virtual?void?handle_write()?=?0;??
????virtual?void?handle_timeout()?=?0;??
????virtual?void?handle_close()?=?0;??
????virtual?HANDLE?get_handle()?=?0;??
????//?...??
};??
class?Event_Handler?{??
public:??
????//?events?maybe?read/write/timeout/close?.etc??
????virtual?void?handle_events(int?events)?=?0;??
????virtual?HANDLE?get_handle()?=?0;??
????//?...
};??
1.2.2?Reactor事件處理流程
前面說過Reactor將事件流“逆置”了,使用Reactor模式后,事件控制流可以參見下面的序列圖。
?
1.3?Select,poll和epoll
在Linux環(huán)境中,比較常見的I/O多路復(fù)用機(jī)制就是Select,poll和epoll,下面對這三種機(jī)制進(jìn)行分析和比較,并對epoll的使用進(jìn)行介紹。
1.3.1?select模型
1.?最大并發(fā)數(shù)限制,因為一個進(jìn)程所打開的FD(文件描述符)是有限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是1024/2048,因此Select模型的最大并發(fā)數(shù)就被相應(yīng)限制了。
2.?效率問題,select每次調(diào)用都會線性掃描全部的FD集合,這樣效率就會呈現(xiàn)線性下降,把FD_SETSIZE改大的后果就是所有FD處理都慢慢來
3.?內(nèi)核/用戶空間?內(nèi)存拷貝問題,如何讓內(nèi)核把FD消息通知給用戶空間呢?在這個問題上select采取了內(nèi)存拷貝方法。
int?res?=?select(maxfd+1,?&readfds,?NULL,?NULL,?120);
if?(res?>?0)?{
????for?(int?i?=?0;?i?<?MAX_CONNECTION;?i++)?{
????????if?(FD_ISSET(allConnection[i],&readfds))?{
????????????handleEvent(allConnection[i]);
????????}
????}
}
1.3.2?poll模型
基本上效率和select是相同的,select缺點的2和3都沒有改掉。
1.3.3?epoll模型
1.?Epoll沒有最大并發(fā)連接的限制,上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于2048,?一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat?/proc/sys/fs/file-max察看。
2.?效率提升,Epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關(guān),應(yīng)用程序就能直接定位到事件,而不必遍歷整個FD集合,因此在實際的網(wǎng)絡(luò)環(huán)境中,Epoll的效率就會遠(yuǎn)遠(yuǎn)高于select和poll。
int?res?=?epoll_wait(epfd,?events,?20,?120);
for(int?i?=?0;?i?<?res;?i++)?{
????handleEvent(events[n]);
}
3.?內(nèi)存拷貝,Epoll在這點上使用了“共享內(nèi)存”,這個內(nèi)存拷貝也省略了。?
1.3.4?使用epoll
Epoll的接口很簡單,只有三個函數(shù),十分易用。?
int?epoll_create(int?size);
生成一個epoll專用的文件描述符,其實是申請一個內(nèi)核空間,用來存放你想關(guān)注的socket?fd上是否發(fā)生以及發(fā)生了什么事件。size就是你在這個Epoll?fd上能關(guān)注的最大socket?fd數(shù),大小自定,只要內(nèi)存足夠。
int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event);
控制某個Epoll文件描述符上的事件:注冊、修改、刪除。其中參數(shù)epfd是epoll_create()創(chuàng)建Epoll專用的文件描述符。相對于select模型中的FD_SET和FD_CLR宏。
int?epoll_wait(int?epfd,?struct?epoll_event?*?events,?int?maxevents,?int?timeout);
等待I/O事件的發(fā)生;參數(shù)說明:
??epfd:由epoll_create()生成的Epoll專用的文件描述符;
??epoll_event:用于回傳代處理事件的數(shù)組;
??maxevents:每次能處理的事件數(shù);
??timeout:等待I/O事件發(fā)生的超時值;
??返回發(fā)生事件數(shù)。
上面講到了Reactor的基本概念、框架和處理流程,并對基于Reactor模型的select,poll和epoll進(jìn)行了比較分析后,再來對比看網(wǎng)絡(luò)編程框架就會更容易理解了。
?
2.?Libeasy網(wǎng)絡(luò)編程框架
Libeasy底層使用的是Libev事件庫,在分析Libeasy代碼前,首先對Libev有相關(guān)了解。
2.1?Libev簡介
l?Libev是什么?
Libev?is?an?event?loop:?you?register?interest?in?certain?events?(such?as?a?file?descriptor?being?readable?or?a?timeout?occurring),?and?it?will?manage?these?event?sources?and?provide?your?program?with?events.
Libev是一個event?loop:向libev注冊感興趣的events,比如Socket可讀事件,libev會對所注冊的事件的源進(jìn)行管理,并在事件發(fā)生時觸發(fā)相應(yīng)的程序。通過event?watcher來注冊事件
l?libev定義的watcher類型:
??ev_io??//?io?讀寫類型watcher
??ev_timer??//?定時器?類watcher
??ev_periodic
??ev_signal
??ev_child
??ev_stat
??ev_idle
??ev_prepare
??ev_check
??ev_embed
??ev_fork
??ev_cleanup
??ev_async??//?線程同步信號watcher
在libev中watcher還能支持優(yōu)先級?
2.1.1?libev使用
下面以一個簡單例子程序說明libev的使用。這段程序?qū)崿F(xiàn)從標(biāo)準(zhǔn)輸入異步讀取數(shù)據(jù),5.5秒內(nèi)沒有數(shù)據(jù)到來則超時的功能。
#include?<ev.h>
#include?<stdio.h>?
ev_io?stdin_watcher;
ev_timer?timeout_watcher;
//?all?watcher?callbacks?have?a?similar?signature
//?this?callback?is?called?when?data?is?readable?on?stdin
static?void?stdin_cb?(EV_P_?ev_io?*w,?int?revents)?{
??puts?("stdin?ready");
??//?for?one-shot?events,?one?must?manually?stop?the?watcher
??//?with?its?corresponding?stop?function.
??ev_io_stop?(EV_A_?w);
??//?this?causes?all?nested?ev_run's?to?stop?iterating
??ev_break?(EV_A_?EVBREAK_ALL);
}
//?another?callback,?this?time?for?a?time-out
static?void?timeout_cb?(EV_P_?ev_timer?*w,?int?revents)?{
??puts?("timeout");
??//?this?causes?the?innermost?ev_run?to?stop?iterating
??ev_break?(EV_A_?EVBREAK_ONE);
}
int?main?(void)?{
???//?use?the?default?event?loop?unless?you?have?special?needs
???struct?ev_loop?*loop?=?EV_DEFAULT;
???//?initialise?an?io?watcher,?then?start?it
???//?this?one?will?watch?for?stdin?to?become?readable
???ev_io_init?(&stdin_watcher,?stdin_cb,?/*STDIN_FILENO*/?0,?EV_READ);
???ev_io_start?(loop,?&stdin_watcher);
???//?initialise?a?timer?watcher,?then?start?it
???//?simple?non-repeating?5.5?second?timeout
???ev_timer_init?(&timeout_watcher,?timeout_cb,?5.5,?0.);
???ev_timer_start?(loop,?&timeout_watcher);
???//?now?wait?for?events?to?arrive
???ev_run?(loop,?0);
???//?break?was?called,?so?exit
???return?0;
}
2.1.2??Libev和Libevent比較
libevent和libev架構(gòu)近似相同,對于非定時器類型,libevent使用雙向鏈表管理,而libev則是使用數(shù)組來管理。如我們所知,新的fd總是系統(tǒng)可用的最小fd,所以這個長度可以進(jìn)行大小限制的,我們用一個連續(xù)的數(shù)組來存儲fd/watch?信息。如下圖,我們用anfd[fd]就可以找到對應(yīng)的fd/watcher?信息,當(dāng)然可能遇到anfd超出我們的buffer長度情形,這是我們用類似relloc?的函數(shù)來做數(shù)組遷移、擴(kuò)大容量,但這種概率是很小的,所以其對系統(tǒng)性能的影響可以忽略不計。
?
我們用anfd[fd]找到的結(jié)構(gòu)體中,有一個指向io_watch_list的頭指針,以epoll為例,當(dāng)epoll_wait返回一個fd_event時?,我們就可以直接定位到對應(yīng)fd的watch_list,這個watch_list的長度一般不會超過3?,fd_event會有一個導(dǎo)致觸發(fā)的事件,我們用這個事件依次和各個watch注冊的event做?“&”?操作,?如果不為0,則把對應(yīng)的watch加入到待處理隊列pendings中(當(dāng)我們啟用watcher優(yōu)先級模式時,pendings是個2維數(shù)組,此時僅考慮普通模式)所以我們可以看到,這個操作是非常非常快。
?
再看添加watch的場景,把watch插入到相應(yīng)的鏈表中,這個操作也是直接定位,然后在fdchange隊列中,加入對應(yīng)的fd(如果這個fd已經(jīng)被添加過,則不會發(fā)生這一步,我們通過anfd[fd]中一個bool?值來判斷)
注意,假如我們在某個fd上已經(jīng)有個watch?注冊了read事件,這時我們又再添加一個watch,還是read?事件,但是不同的回調(diào)函數(shù),在此種情況下,我們不應(yīng)該調(diào)用epoll_ctrl?之類的系統(tǒng)調(diào)用,因為我們的events集合是沒有改變的,所以為了達(dá)到這個目的,anfd[fd]結(jié)構(gòu)體中,還有一個events事件,它是原先的所有watcher的事件的“|”操作,向系統(tǒng)的epoll從新添加描述符的操作是在下次事件迭代開始前進(jìn)行的,當(dāng)我們依次掃描fdchangs,找到對應(yīng)的anfd結(jié)構(gòu),如果發(fā)現(xiàn)先前的events與當(dāng)前所有的watcher的“|”操作結(jié)果不等,則表示我們需要調(diào)用epoll_ctrl之類的函數(shù)來進(jìn)行更改,反之不做操作,作為一條原則,在調(diào)用系統(tǒng)調(diào)用前,我們已經(jīng)做了充分的檢查,確保不進(jìn)行多余的系統(tǒng)調(diào)用。
再來看刪除和更新一個watcher造作,基于以上分析,這個操作也是近乎O(1)?的,當(dāng)然,如果events事件更改,可能會發(fā)生一次系統(tǒng)調(diào)用。
所以我們對io?watcher的操作,在我們的用戶層面上,幾乎總是是O(1)的復(fù)雜度,當(dāng)然如果牽涉到epoll?文件結(jié)構(gòu)的更新,我們的系統(tǒng)調(diào)用?epoll_ctrl?在內(nèi)核中還是?O(lgn)的復(fù)雜度,但我們已經(jīng)在我們所能掌控的范圍內(nèi)做到最好了。
2.1.3??性能測試對比
?
結(jié)論:The?cost?for?setting?up?or?changing?event?watchers?is?clearly?much?higher?for?libevent?than?for?libev,詳細(xì)性能對比測試參考這http://libev.schmorp.de/bench.html
2.2?libeasy
2.2.2?Server端使用
?
1、啟動流程
eio_?=?easy_eio_create(eio_,?io_thread_count_);
easy_eio_create(eio_,?io_thread_count_)做了如下幾件事:
1.?分配一個easy_pool_t的內(nèi)存區(qū),存放easy_io_t對象?
2.?設(shè)置一些tcp參數(shù),比如tcp_nodelay(tcp_cork),cpu親核性等參數(shù)
3.?分配線程池的內(nèi)存區(qū)并初始化
4.?對每個線程構(gòu)建client_list,client_array,?初始化雙向鏈表conn_list?session_list?request_list
5.?設(shè)置listen?watcher的ev回調(diào)函數(shù)為easy_connection_on_listen
6.?調(diào)用easy_baseth_init初始化io線程
easy_listen_t*?listen?=?easy_connection_add_listen(eio_,?NULL,?port_,?&handler_);
1.?從eio->pool中為easy_listen_t和listen?watcher(在這里listen的watcher數(shù)默認(rèn)為2個)分配空間
2.?開始監(jiān)聽某個地址
3.?初始化每個read_watcher
4.?關(guān)注listen?fd的讀事件,設(shè)置其回調(diào)函數(shù)easy_connection_on_accep(在這里僅僅是初始化read_watcher,?還沒有激活,激活在每個IO線程啟動調(diào)用easy_io_on_thread_start的時候做。一旦激活后,當(dāng)有連接到來的時候,觸發(fā)easy_connection_on_accept)
rc?=?easy_eio_start(eio_);
1.?調(diào)用pthread_create啟動每個io線程,線程執(zhí)行函數(shù)easy_io_on_thread_start,在easy_io_on_thread_start中
a)?設(shè)置io線程的cpu親核性sched_setaffinity
b)?如果不是listen_all或者只有一個線程,則發(fā)出ev_async_send喚醒下一個線程的listen_watcher(實現(xiàn)連接請求的負(fù)載均衡)
2.?線程執(zhí)行ev_run
easy_eio_wait(eio_);
調(diào)用pthead_join等待線程結(jié)束
2、處理流程
l?當(dāng)連接到來時觸發(fā)easy_connection_on_accept
1.?調(diào)用accept獲得連接fd,構(gòu)建connection(easy_connection_new),設(shè)置非阻塞,初始化connection參數(shù)和read、write、timeout的watcher
2.?切換listen線程,從自己切換到下一個io線程,調(diào)用ev_async_send激活下一個io線程的listen_watcher,實現(xiàn)負(fù)載均衡
3.?將connection加入到線程的connected_list線程列表中,并開啟該連接上的read、write、timeout的watcher
?
l?當(dāng)數(shù)據(jù)包到來時觸發(fā)easy_connection_on_readable回調(diào)函數(shù)
1.?檢查當(dāng)前IO線程同時正在處理的請求是否超過EASY_IOTH_DOING_REQ_CNT(8192),當(dāng)前連接上的請求數(shù)是否超過EASY_CONN_DOING_REQ_CNT(1024),如果超過,則調(diào)用easy_connection_destroy(c)將連接銷毀掉,?提供了一種負(fù)載保護(hù)機(jī)制
2.?構(gòu)建message空間
3.?調(diào)用read讀取socket數(shù)據(jù)
4.?作為服務(wù)端調(diào)用easy_connection_do_request
? ? a)?從message中解包
? ? b)?調(diào)用easy_connection_recycle_message看是否需要釋放老的message,構(gòu)建新的message空間
? ? c)?調(diào)用hanler的process處理數(shù)據(jù)包,如果返回easy_ok則調(diào)用easy_connection_request_done
? ? d)?對發(fā)送數(shù)據(jù)進(jìn)行打包
? ? e)?對返回碼是EASY_AGAIN的request將其放入session_list中
? ? f)?對返回碼是EASY_OK的request將其放入request_done_list中,更新統(tǒng)計計數(shù)
? ? g)?統(tǒng)計計數(shù)更新??
? ? h)?調(diào)用easy_connection_write_socket發(fā)送數(shù)據(jù)包
? ? i)?調(diào)用easy_connection_evio_start中ev_io_start(c->loop,?&c->read_watcher);開啟該連接的讀watcher
? ? j)?調(diào)用easy_connection_redispatch_thread進(jìn)行負(fù)載均衡
如果負(fù)載均衡被禁或者該連接的message_list和output不為空,則直接返回,否則調(diào)用easy_thread_pool_rr從線程池中選擇一個io線程,將該連接從原來io線程上移除(停止讀寫timeout?的watcher),將該連接加入到新的io線程中的conn_list中,調(diào)用ev_async_send喚醒新的io線程,在easy_connection_on_wakeup中調(diào)用easy_connection_evio_start將該連接的read、write、timeou的watcher再打開。
?
l?當(dāng)socket可寫時觸發(fā)easy_connection_on_writable回調(diào)函數(shù):
1.?調(diào)用easy_connection_write_socket寫數(shù)據(jù)
2.?如果沒有數(shù)據(jù)可寫,將該連接的write_watcher停掉
?
2.2.3?客戶端使用
libeasy作為客戶端時,將每個發(fā)往libeasy服務(wù)器端的請求包封裝成一個session(easy_session_t),客戶端將這個session放入連接的隊列中然后返回,隨后收到包后,將相應(yīng)的session從連接的發(fā)送隊列中刪除。詳細(xì)流程如下:
easy_session_t?*easy_session_create(int64_t?asize)
這個函數(shù)主要就做了一件事分配一個內(nèi)存池easy_pool_t,在內(nèi)存池頭部放置一個easy_session_t,剩下部分存放實際的數(shù)據(jù)包Packet,然后將session的type設(shè)置為EASY_TYPE_SESSION。
異步請求
int?easy_client_dispatch(easy_io_t?*eio,?easy_addr_t?addr,?easy_session_t?*s)
1.?根據(jù)socket?addr從線程池中選擇一個線程,將session加入該線程的session_list,然后將該線程喚醒
2.?線程喚醒后調(diào)用easy_connection_send_session_list
? ? ?a)??其中首先調(diào)用easy_connection_do_client,這里首先在該線程的client_list中查找該addr的client,如果沒找到,則新建一個client,初始化將其加入client_list,如果該client的connect未建立,調(diào)用easy_connection_do_connect建立該連接,然后返回該連接
? ? b)?easy_connection_do_connect中首先創(chuàng)建一個新的connection結(jié)構(gòu),和一個socket,設(shè)置非阻塞,并調(diào)用connect進(jìn)行連接,初始化該連接的read、write、timeout?watcher(連接建立前是write,建立后是read)
? ? c)?調(diào)用easy_connection_session_build,其中調(diào)用encode函數(shù)對數(shù)據(jù)包進(jìn)行打包,調(diào)用easy_hash_dlist_add(c->send_queue,?s->packet_id,?&s->send_queue_hash,?&s->send_queue_list)將這個session添加到連接的發(fā)送隊列中。這個函數(shù)將session添加到發(fā)送隊列的同時,同時將相應(yīng)的項添加到hash表的相應(yīng)的bucket的鏈表頭
? ? d)?開啟timeout?watcher?????
? ? e)?調(diào)用easy_connection_write_socket發(fā)送數(shù)據(jù)包?
?
l?當(dāng)回復(fù)數(shù)據(jù)包到達(dá)觸發(fā)easy_connection_on_readable回調(diào)函數(shù)時
1.?初始化一個easy_message_t存放數(shù)據(jù)包
2.?從內(nèi)核緩沖區(qū)讀入數(shù)據(jù)到應(yīng)用層輸入緩沖區(qū)中,然后調(diào)用easy_connection_do_response進(jìn)行處理
? ? a)?先解包,將該packet_id數(shù)據(jù)包從發(fā)包隊列中刪除,更新統(tǒng)計信息,停止timeout?watcher,
? ? b)?如果是同步請求,則調(diào)用session的process函數(shù),從而調(diào)用easy_client_wait_process函數(shù),喚醒客戶端接收數(shù)據(jù)包
l?當(dāng)超時時間到還沒有收到回復(fù)數(shù)據(jù)包時觸發(fā)easy_connection_on_timeout_mesg回調(diào)函數(shù)
1.?從發(fā)送隊列中刪除請求數(shù)據(jù)包
2.?調(diào)用session的process函數(shù),從而調(diào)用easy_client_wait_process函數(shù),喚醒客戶端接
3.?釋放此連接
同步請求
void?*easy_client_send(easy_io_t?*eio,?easy_addr_t?addr,?easy_session_t?*s)
同步請求是通過異步請求實現(xiàn)的,easy_client_send方法封裝了異步請求接口easy_client_dispatch
1.?easy_client_send將session的process置為easy_client_wait_process方法
2.?初始化一個easy_client_wait_t?wobj
3.?調(diào)用easy_client_dispatch方法發(fā)送異步請求
4.?客戶端調(diào)用wait在wobj包裝的信號量上等待
5.?當(dāng)這個請求收到包的時候觸發(fā)session的process函數(shù),回調(diào)easy_client_wait_process方法,其中會給wobj發(fā)送信號喚醒客戶端,返回session封裝的請求的ipacket
?
2.2.4??特性總結(jié)
1.?多個IO線程/epoll,大大提升了數(shù)據(jù)包處理性能,特別是處理小數(shù)據(jù)包的性能
針對多核處理器,libeasy使用多個IO線程來充分發(fā)揮處理器性能,提升IO處理能力。特別是針對小數(shù)據(jù)包IO處理請求數(shù)較多的情況下,性能提升十分明顯。
2.?短任務(wù)和長任務(wù)區(qū)分,處理短任務(wù)更加高效(編碼了內(nèi)存拷貝,線程切換)
同步處理
對于短任務(wù)而言,調(diào)用用戶process回調(diào)函數(shù)返回EASY_OK的數(shù)據(jù)包直接被加入該連接的發(fā)送隊列,發(fā)送給客戶端,這樣避免了數(shù)據(jù)包的內(nèi)存拷貝和線程切換開銷。
異步處理
對于耗時較長的長任務(wù)而言,如果放在網(wǎng)絡(luò)庫的IO線程內(nèi)執(zhí)行,可能會阻塞住IO線程,所以需要異步處理。
?
3.?應(yīng)用線程CPU親核性,避免線程調(diào)度開銷,提升處理性能
開啟親核特性將線程與指定CPU核進(jìn)行綁定,避免了線程遷移導(dǎo)致的CPU?cache失效,同時它允許我們精確控制線程和cpu核的關(guān)系,從而根據(jù)需要劃分CPU核的使用。
sched_setaffinity(pid_t?pid,?unsigned?int?cpusetsize,?cpu_set_t?*mask)??
該函數(shù)設(shè)置進(jìn)程為pid的這個進(jìn)程,讓它運行在mask所設(shè)定的CPU上.如果pid的值為0,則表示指定的是當(dāng)前進(jìn)程,使當(dāng)前進(jìn)程運行在mask所設(shè)定的那些CPU上.第二個參數(shù)cpusetsize是mask所指定的數(shù)的長度.通常設(shè)定為sizeof(cpu_set_t).如果當(dāng)前pid所指定的進(jìn)程此時沒有運行在mask所指定的任意一個CPU上,則該指定的進(jìn)程會從其它CPU上遷移到mask的指定的一個CPU上運行.?
4.?內(nèi)存管理,減少小內(nèi)存申請開銷,避免內(nèi)存碎片化
Libeasy的內(nèi)存管理和nginx一致,有興趣的可以去學(xué)習(xí)下,下面大致介紹其思想。
1)?創(chuàng)建一個內(nèi)存池
2)?分配小塊內(nèi)存(size?<=?max)
小塊內(nèi)存分配模型:
?
上圖這個內(nèi)存池模型是由上3個小內(nèi)存池構(gòu)成的,由于第一個內(nèi)存池上剩余的內(nèi)存不夠分配了,于是就創(chuàng)建了第二個新的內(nèi)存池,第三個內(nèi)存池是由于前面兩個內(nèi)存池的剩余部分都不夠分配,所以創(chuàng)建了第三個內(nèi)存池來滿足用戶的需求。由圖可見:所有的小內(nèi)存池是由一個單向鏈表維護(hù)在一起的。這里還有兩個字段需要關(guān)注,failed和current字段。failed表示的是當(dāng)前這個內(nèi)存池的剩余可用內(nèi)存不能滿足用戶分配請求的次數(shù),如果下一個內(nèi)存池也不能滿足,那么它的failed也會加1,直到滿足請求為止(如果沒有現(xiàn)成的內(nèi)存池來滿足,會再創(chuàng)建一個新的內(nèi)存池)。current字段會隨著failed的增加而發(fā)生改變,如果current指向的內(nèi)存池的failed達(dá)到了一個閾值,current就指向下一個內(nèi)存池了。
3)、大塊內(nèi)存的分配(size?>?max)
大塊內(nèi)存的分配請求不會直接在內(nèi)存池上分配內(nèi)存來滿足,而是直接向操作系統(tǒng)申請這么一塊內(nèi)存(就像直接使用malloc分配內(nèi)存一樣),然后將這塊內(nèi)存掛到內(nèi)存池頭部的large字段下。內(nèi)存池的作用在于解決小塊內(nèi)存池的頻繁申請問題,對于這種大塊內(nèi)存,是可以忍受直接申請的。同樣,用圖形展示大塊內(nèi)存申請模型:
?
4)、內(nèi)存釋放
nginx利用了web?server應(yīng)用的特殊場景來完成;一個web?server總是不停的接受connection和request,所以nginx就將內(nèi)存池分了不同的等級,有進(jìn)程級的內(nèi)存池、connection級的內(nèi)存池、request級的內(nèi)存池。也就是說,創(chuàng)建好一個worker進(jìn)程的時候,同時為這個worker進(jìn)程創(chuàng)建一個內(nèi)存池,待有新的連接到來后,就在worker進(jìn)程的內(nèi)存池上為該連接創(chuàng)建起一個內(nèi)存池;連接上到來一個request后,又在連接的內(nèi)存池上為request創(chuàng)建起一個內(nèi)存池。這樣,在request被處理完后,就會釋放request的整個內(nèi)存池,連接斷開后,就會釋放連接的內(nèi)存池。
5)、總結(jié)
通過內(nèi)存的分配和釋放可以看出,nginx只是將小塊內(nèi)存的申請聚集到一起申請(內(nèi)存池),然后一起釋放,避免了頻繁申請小內(nèi)存,降低內(nèi)存碎片的產(chǎn)生等問題。
5.?網(wǎng)絡(luò)流量自動負(fù)載均衡,充分發(fā)揮多核性能
1、在連接到來時,正在listen的IO線程接受連接,將其加入本線程的連接隊列中,之后主動喚醒下一個線程執(zhí)行l(wèi)isten。通過切換listen線程來使每個線程上處理的連接數(shù)大致相同。
2、每一個連接上的流量是不同的,因此在每次有讀寫請求,計算該線程上近一段時間內(nèi)請求速率,觸發(fā)負(fù)載均衡,將該連接移動到其它線程上,使每個線程處理的IO請求數(shù)大致相同。
6.?將encode和decode接口暴露給應(yīng)用層,實現(xiàn)網(wǎng)絡(luò)編程框架與協(xié)議的分離
Libeasy將網(wǎng)絡(luò)數(shù)據(jù)包打包解包接口暴露給應(yīng)用層,由用戶定義數(shù)據(jù)包內(nèi)容的格式,實現(xiàn)了網(wǎng)絡(luò)編程框架與協(xié)議的分離,能夠支持http等其他協(xié)議類型,格式更改更加方便。
7.?底層采用libev,對于事件的注冊和更改速度更快
?
參考資料
1、?C10K?Problem
2、?Unix環(huán)境高級編程
3、?Unix網(wǎng)絡(luò)編程
4、?Nginx、Libevent
5、?Libevhttp://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#WHAT_TO_READ_WHEN_IN_A_HURRY
6、?Libeasy源碼分析等http://www.cnblogs.com/foxmailed/archive/2013/02/17/2908180.html
-------------------------------------------------------------------------------------
黑夜路人,一個關(guān)注開源技術(shù)、樂于學(xué)習(xí)、喜歡分享的程序員
博客:http://blog.csdn.net/heiyeshuwu
微博:http://weibo.com/heiyeluren
微信:heiyeluren2012 ?
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖
總結(jié)
- 上一篇: CUDA在Windows下的软件开发环境
- 下一篇: 深度神经网络DNN的多GPU数据并行框架