1,socket
網(wǎng)絡(luò)上的兩個程序通過一個雙向的通信連接實現(xiàn)數(shù)據(jù)的交換吨悍,這個連接的一端稱為一個socket百匆,用于描述IP地址和端口。
int socket(int domain, int type, int protocol);
domain:協(xié)議域设江。常用的有AF_INET、AF_INET6攘轩、AF_LOCAL叉存、AF_ROUTE等。協(xié)議族決定了socket的地址類型度帮,在通信中必須采用對應(yīng)的地址歼捏,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
type:指定Socket類型瞳秽。常用的socket類型有SOCK_STREAM瓣履、SOCK_DGRAM、SOCK_RAW寂诱、SOCK_PACKET拂苹、SOCK_SEQPACKET等安聘。流式Socket(SOCK_STREAM)是一種面向連接的Socket痰洒,針對于面向連接的TCP服務(wù)應(yīng)用。數(shù)據(jù)報式Socket(SOCK_DGRAM)是一種無連接的Socket浴韭,對應(yīng)于無連接的UDP服務(wù)應(yīng)用丘喻。
protocol:指定協(xié)議。常用協(xié)議有IPPROTO_TCP念颈、IPPROTO_UDP泉粉、IPPROTO_STCP、IPPROTO_TIPC等榴芳,分別對應(yīng)TCP傳輸協(xié)議嗡靡、UDP傳輸協(xié)議、STCP傳輸協(xié)議窟感、TIPC傳輸協(xié)議讨彼。
注意:type和protocol不可以隨意組合,如SOCK_STREAM不可以跟IPPROTO_UDP組合柿祈。當(dāng)?shù)谌齻€參數(shù)為0時哈误,會自動選擇第二個參數(shù)類型對應(yīng)的默認協(xié)議
int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len);
socket:是一個套接字描述符。
address:是一個sockaddr結(jié)構(gòu)指針躏嚎,該結(jié)構(gòu)中包含了要結(jié)合的地址和端口號蜜自。
address_len:確定address長度。
int accept( int fd, struct socketaddr* addr, socklen_t* len);
fd:套接字描述符卢佣。
addr:返回連接著的地址
len:接收返回地址的緩沖區(qū)長度
注意:accept的第一個參數(shù)為服務(wù)器的socket描述字重荠,是服務(wù)器開始調(diào)用socket()函數(shù)生成的,稱為監(jiān)聽socket描述字虚茶;而accept函數(shù)返回的是已連接的socket描述字歪架。一個服務(wù)器通常通常僅僅只創(chuàng)建一個監(jiān)聽socket描述字,它在該服務(wù)器的生命周期內(nèi)一直存在瞻凤。內(nèi)核為每個由服務(wù)器進程接受的客戶連接創(chuàng)建了一個已連接socket描述字骡送,當(dāng)服務(wù)器完成了對某個客戶的服務(wù),相應(yīng)的已連接socket描述字就被關(guān)閉
int recv(SOCKET socket, char FAR* buf, int len, int flags);
socket:一個標(biāo)識已連接套接口的描述字待笑。
buf:用于接收數(shù)據(jù)的緩沖區(qū)鸣皂。
len:緩沖區(qū)長度。
flags:指定調(diào)用方式。取值:MSG_PEEK 查看當(dāng)前數(shù)據(jù)寞缝,數(shù)據(jù)將被復(fù)制到緩沖區(qū)中癌压,但并不從輸入隊列中刪除;MSG_OOB 處理帶外數(shù)據(jù)荆陆。
ssize_t recvfrom(int sockfd, void buf, int len, unsigned int flags, struct socketaddr* from, socket_t* fromlen);
sockfd:標(biāo)識一個已連接套接口的描述字滩届。
buf:用于接收數(shù)據(jù)的緩沖區(qū)。
len:緩沖區(qū)長度被啼。
flags:調(diào)用操作方式帜消。是以下一個或者多個標(biāo)志的組合體,可通過or操作連在一起:
(1)MSG_DONTWAIT:操作不會被阻塞浓体;
(2)MSG_ERRQUEUE: 指示應(yīng)該從套接字的錯誤隊列上接收錯誤值泡挺,依據(jù)不同的協(xié)議,錯誤值以某種輔佐性消息的方式傳遞進來命浴,使用者應(yīng)該提供足夠大的緩沖區(qū)娄猫。導(dǎo)致錯誤的原封包通過msg_iovec作為一般的數(shù)據(jù)來傳遞。導(dǎo)致錯誤的數(shù)據(jù)報原目標(biāo)地址作為msg_name被提供生闲。錯誤以sock_extended_err結(jié)構(gòu)形態(tài)被使用媳溺。
(3)MSG_PEEK:指示數(shù)據(jù)接收后,在接收隊列中保留原數(shù)據(jù)碍讯,不將其刪除悬蔽,隨后的讀操作還可以接收相同的數(shù)據(jù)。
(4)MSG_TRUNC:返回封包的實際長度冲茸,即使它比所提供的緩沖區(qū)更長屯阀, 只對packet套接字有效。
(5)MSG_WAITALL:要求阻塞操作轴术,直到請求得到完整的滿足难衰。然而,如果捕捉到信號逗栽,錯誤或者連接斷開發(fā)生盖袭,或者下次被接收的數(shù)據(jù)類型不同,仍會返回少于請求量的數(shù)據(jù)彼宠。
(6)MSG_EOR:指示記錄的結(jié)束鳄虱,返回的數(shù)據(jù)完成一個記錄。
(7)MSG_TRUNC:指明數(shù)據(jù)報尾部數(shù)據(jù)已被丟棄凭峡,因為它比所提供的緩沖區(qū)需要更多的空間
(8)MSG_CTRUNC:指明由于緩沖區(qū)空間不足拙已,一些控制數(shù)據(jù)已被丟棄。
(9)MSG_OOB:指示接收到out-of-band數(shù)據(jù)(即需要優(yōu)先處理的數(shù)據(jù))摧冀。
(10)MSG_ERRQUEUE:指示除了來自套接字錯誤隊列的錯誤外倍踪,沒有接收到其它數(shù)據(jù)系宫。
int sendto( SOCKET s, const char FAR* buf, int size, int flags, const struct sockaddr FAR* to, int tolen);
2,socketpair
socketpair創(chuàng)建了一對無名的套接字描述符(只能在AF_UNIX域中使用)建车,存儲于一個二元數(shù)組,例如sv[2] .每一個描述符既可以讀也可以寫扩借。在同一個進程中也可以進行通信,向sv[0]中寫入缤至,就可以從sv[1]中讀瘸弊铩(只能從sv[1]中讀取)领斥,也可以在sv[1]中寫入嫉到,然后從sv[0]中讀取戒突;但是屯碴,若沒有在0端寫入描睦,而從1端讀取膊存,則1端的讀取操作會阻塞,即使在1端寫入忱叭,也不能從1讀取隔崎,仍然阻塞;
int socketpair(int d, int type, int protocol, int sv[2]);
第1個參數(shù)d韵丑,表示協(xié)議族爵卒,只能為AF_LOCAL或者AF_UNIX
第2個參數(shù)type,表示類型撵彻,只能為0
第3個參數(shù)protocol钓株,表示協(xié)議,可以是SOCK_STREAM或者SOCK_DGRAM陌僵。用SOCK_STREAM建立的套接字對是管道流轴合,與一般的管道相區(qū)別的是,套接字對建立的通道是雙向的碗短,即每一端都可以進行讀寫受葛。
參數(shù)sv,用于保存建立的套接字對
3偎谁,IO多路復(fù)用
I/O復(fù)用模型會用到select总滩、poll、epoll函數(shù)巡雨,這幾個函數(shù)也會使進程阻塞闰渔,但是和阻塞I/O所不同的的,這兩個函數(shù)可以同時阻塞多個I/O操作铐望。而且可以同時對多個讀操作冈涧,多個寫操作的I/O函數(shù)進行檢測向挖,直到有數(shù)據(jù)可讀或可寫時,才真正調(diào)用I/O操作函數(shù)炕舵。
異步IO:異步IO不是順序執(zhí)行何之。用戶進程進行aio_read系統(tǒng)調(diào)用之后,無論內(nèi)核數(shù)據(jù)是否準(zhǔn)備好咽筋,都會直接返回給用戶進程溶推,然后用戶態(tài)進程可以去做別的事情。等到socket數(shù)據(jù)準(zhǔn)備好了奸攻,內(nèi)核直接復(fù)制數(shù)據(jù)給進程蒜危,然后從內(nèi)核向進程發(fā)送通知。IO兩個階段睹耐,進程都是非阻塞的辐赞。
用戶進程發(fā)起aio_read操作之后,立刻就可以開始去做其它的事硝训。而另一方面响委,從kernel的角度,當(dāng)它收到一個asynchronous read之后窖梁,首先它會立刻返回赘风,所以不會對用戶進程產(chǎn)生任何block。然后纵刘,kernel會等待數(shù)據(jù)準(zhǔn)備完成邀窃,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后假哎,kernel會給用戶進程發(fā)送一個signal或執(zhí)行一個基于線程的回調(diào)函數(shù)來完成這次 IO 處理過程瞬捕,告訴它read操作完成了Linux提供了AIO庫函數(shù)實現(xiàn)異步,但是用的很少舵抹。目前有很多開源的異步IO庫肪虎,例如libevent、libev掏父、libuv笋轨。
Select:
select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds赊淑、readfds爵政、和exceptfds。調(diào)用后select函數(shù)會阻塞陶缺,直到有描述符就緒(有數(shù)據(jù)可讀钾挟、可寫、或者有except)饱岸,或者超時(timeout指定等待時間掺出,如果立即返回設(shè)為null即可)徽千,函數(shù)返回。當(dāng)select函數(shù)返回后汤锨,可以通過遍歷fdset双抽,來找到就緒的描述符。
缺點:
1闲礼、 單個進程可監(jiān)視的fd數(shù)量被限制牍汹,即能監(jiān)聽端口的大小有限。 一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大柬泽,具體數(shù)目可以cat /proc/sys/fs/file-max察看慎菲。32位機默認是1024個
2、 對socket進行掃描時是線性掃描锨并,即采用輪詢的方法露该,效率較低
當(dāng)套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調(diào)度,不管哪個Socket是活躍的,都遍歷一遍第煮。這會浪費很多CPU時間解幼。如果能給套接字注冊某個回調(diào)函數(shù),當(dāng)他們活躍時空盼,自動完成相關(guān)操作书幕,那就避免了輪詢新荤,這正是epoll與kqueue做的
3揽趾、需要維護一個用來存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時復(fù)制開銷大
poll:
poll本質(zhì)上和select沒有區(qū)別苛骨,將用戶傳入的數(shù)組拷貝到內(nèi)核空間篱瞎,然后查詢每個fd對應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊列中加入一項并繼續(xù)遍歷痒芝,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設(shè)備俐筋,則掛起當(dāng)前進程,直到設(shè)備就緒或者主動超時严衬,被喚醒后它又要再次遍歷fd澄者。這個過程經(jīng)歷了多次無謂的遍歷。
它沒有最大連接數(shù)的限制请琳,原因是它是基于鏈表來存儲的粱挡,但是同樣有一個缺點:
1、大量的fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核地址空間之間俄精,而不管這樣的復(fù)制是不是有意義询筏。
2、poll還有一個特點是“水平觸發(fā)”竖慧,如果報告了fd后嫌套,沒有被處理逆屡,那么下次poll時會再次報告該fd。
epoll:
epoll支持水平觸發(fā)和邊緣觸發(fā)踱讨,最大的特點在于邊緣觸發(fā)魏蔗,它只告訴進程哪些fd剛剛變?yōu)榫途w態(tài),并且只會通知一次痹筛。還有一個特點是沫勿,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd味混,一旦該fd就緒产雹,內(nèi)核就會采用類似callback的回調(diào)機制來激活該fd,epoll_wait便可以收到通知
epoll的優(yōu)點:
1翁锡、沒有最大并發(fā)連接的限制蔓挖,能打開的FD的上限遠大于1024(1G的內(nèi)存上能監(jiān)聽約10萬個端口);
2馆衔、效率提升瘟判,不是輪詢的方式,不會隨著FD數(shù)目的增加效率下降角溃。只有活躍可用的FD才會調(diào)用callback函數(shù)拷获;
即Epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關(guān)减细,因此在實際的網(wǎng)絡(luò)環(huán)境中匆瓜,Epoll的效率就會遠遠高于select和poll。
3未蝌、 內(nèi)存拷貝驮吱,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復(fù)制開銷萧吠。
4左冬,libevent
libevent就是一個基于事件通知機制的庫,支持/dev/poll纸型、kqueue拇砰、event ports、select狰腌、poll和epoll事件機制除破,也因此它是一個跨操作系統(tǒng)的庫
為了實際處理每個請求,libevent 庫提供一種事件機制癌别,它作為底層網(wǎng)絡(luò)后端的包裝器皂岔。事件系統(tǒng)讓為連接添加處理函數(shù)變得非常簡便,同時降低了底層 I/O 復(fù)雜性展姐。這是 libevent 系統(tǒng)的核心躁垛。
默認情況下是單線程的剖毯,每個線程有且只有一個event_base,對應(yīng)一個struct event_base結(jié)構(gòu)體(以及附于其上的事件管理器)教馆,用來schedule托管給它的一系列event逊谋,可以和操作系統(tǒng)的進程管理類比,當(dāng)然土铺,要更簡單一點胶滋。當(dāng)一個事件發(fā)生后,event_base會在合適的時間(不一定是立即)去調(diào)用綁定在這個事件上的函數(shù)(傳入一些預(yù)定義的參數(shù)悲敷,以及在綁定時指定的一個參數(shù))究恤,直到這個函數(shù)執(zhí)行完,再返回schedule其他事件后德。
struct event_base *base = event_base_new();
event_base內(nèi)部有一個循環(huán)部宿,循環(huán)阻塞在epoll/kqueue等系統(tǒng)調(diào)用上,直到有一個/一些事件發(fā)生瓢湃,然后去處理這些事件理张。當(dāng)然,這些事件要被綁定在這個event_base上绵患。
每個事件對應(yīng)一個struct event雾叭,可以是監(jiān)聽一個fd或者POSIX信號量之類。struct event使用event_new來創(chuàng)建和綁定落蝙,使用event_add來啟用:
struct event listen_event;
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, (void)base);
event_add(listen_event, NULL);
然后需要啟動event_base的循環(huán)织狐,這樣才能開始處理發(fā)生的事件。循環(huán)的啟動使用event_base_dispatch掘殴,循環(huán)將一直持續(xù)赚瘦,直到不再有需要關(guān)注的事件,或者是遇到event_loopbreak()/event_loopexit()函數(shù)
event_base_dispatch(base);
typedef void(* event_callback_fn)(evutil_socket_t sockfd, short event_type, void *arg)
對于一個服務(wù)器而言奏寨,上面的流程大概是這樣組合的:
1. listener = socket(),bind()鹰服,listen()病瞳,設(shè)置nonblocking
2. 創(chuàng)建一個event_base
3. 創(chuàng)建一個event,將該socket托管給event_base悲酷,指定要監(jiān)聽的事件類型套菜,并綁定上相應(yīng)的回調(diào)函數(shù)。對于listener socket來說设易,只需要監(jiān)聽EV_READ|EV_PERSIST
4. 啟用該事件
5. 進入事件循環(huán)
6. (異步) 當(dāng)有client發(fā)起請求的時候逗柴,調(diào)用該回調(diào)函數(shù),進行處理顿肺。