1. 客戶端服務(wù)器建立連接過程
1.1 編寫一個server的步驟是怎么樣的?
int main(){
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
for( ; ; ){
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *)&cliaddr, &clilen);
if((childpid = fork()) == 0){
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
- serverfd = socket( opt ):調(diào)用socket( )方法創(chuàng)建一個對應(yīng)的serverfd
- bind( serverfd, address ):調(diào)用bind( )方法將fd和指定的地址( ip + port )進(jìn)行綁定
- listen( serverfd ):調(diào)用listen( )方法監(jiān)聽前面綁定時指定的地址
- clientfd = accept( serverfd ):進(jìn)入無限循環(huán)等待接受客戶端連接請求
1.2 server是怎么處理建立連接后的client請求的?
void str_echo(int sockfd){
ssize_t n;
char buf[MAXLINE];
again:
while((n = read(sockfd, buf, MAXLINE)) > 0) // 從client讀數(shù)據(jù)
writen(sockfd, buf, n); // 給client寫數(shù)據(jù)
if(n < 0 && errno == EINTR)
goto again;
else if(n < 0)
err_sys("str_echo: read error");
}
- n = read( clientfd, buf, size ):從客戶端clientfd里讀取傳輸進(jìn)來的數(shù)據(jù),并將數(shù)據(jù)存放到buf中
- writen( clientfd, buf, n ):往客戶端clientfd寫出數(shù)據(jù)n個字節(jié)的數(shù)據(jù),寫出的數(shù)據(jù)存放在buf中
1.3 server和client完整交互過程
2.網(wǎng)絡(luò)演變過程
2.1 演變的本質(zhì)
2.2 阻塞IO:Blocking IO
- 阻塞io:在內(nèi)核中發(fā)生兩次阻塞,一個是沒有數(shù)據(jù)就緒的時候會發(fā)生阻塞,另一個是數(shù)據(jù)準(zhǔn)備就緒的時候?qū)?shù)據(jù)從內(nèi)核態(tài)copy到用戶態(tài)的時候會阻塞
- 優(yōu)點(diǎn):
- 可以實(shí)現(xiàn)client和server端通信
- 實(shí)現(xiàn)簡單,通常一個client連接分配一個線程進(jìn)行處理
- 缺點(diǎn):
- 能支持的并發(fā)client連接數(shù)較少,因?yàn)橐慌_server能分配的線程是有限的,8個核最多能開8個線程;并且大量線程會造成上下文切換過多而影響性能
2.3 非阻塞IO:Nonblocking IO
-
核心矛盾:之所以一個client連接分配一個線程是因?yàn)樘幚砜蛻舳说淖x寫時阻塞式的,為避免該阻塞影響后續(xù)接收新的client連接,所以將阻塞邏輯交由單獨(dú)線程處理
-
非阻塞io:上層應(yīng)用每過一段時間就向內(nèi)核詢問是否有數(shù)據(jù)就緒,如果沒有數(shù)據(jù)就返回,如果有數(shù)據(jù)了就會從內(nèi)核態(tài)cpoy數(shù)據(jù)到用戶態(tài)
-
阻塞和非阻塞IO的區(qū)別:在于內(nèi)核中數(shù)據(jù)尚未就緒時如何處理
- 對于非阻塞IO,則直接返回給用戶態(tài)RWOULDBLOCK狀態(tài)碼錯誤
- 對于阻塞IO則一直處于阻塞狀態(tài),直到數(shù)據(jù)就緒并從內(nèi)核態(tài)拷貝到用戶態(tài)后才返回
-
如何設(shè)置非阻塞
- 方法1:
- 通過socket( )方法中的type參數(shù)來指定為SOCK_NONBLOCK即可設(shè)置該socket為非阻塞方式
- int socket( int domain, int type, int protocol );
- 方法2:
- 通過fcntl( )方法中args參數(shù)設(shè)置為O_NONBLOCK即可設(shè)置該socket為非阻塞方式
- int fcntl( int fd, int cmd, … /*arg*/ );
- fcntl( socket_fd, F_SETFL, flags | O_NONBLOCK )
- 方法1:
-
非阻塞的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):將socket設(shè)置成非阻塞后,在讀取時如果數(shù)據(jù)未就緒就直接返回,得益于非阻塞的特性可以通過一個線程管理多個client連接
- 缺點(diǎn):需要不斷輪詢詢問內(nèi)核數(shù)據(jù)是否已經(jīng)就緒,涉及很多無效的頻繁的系統(tǒng)調(diào)用
2.4 IO多路復(fù)用第一版:select poll
-
核心矛盾:涉及很多次無用的平凡的系統(tǒng)調(diào)用,非阻塞socket在read時并不知道什么時候數(shù)據(jù)會準(zhǔn)備好,所以需要不斷的主動詢問
-
所謂io多路復(fù)用:
- 網(wǎng)上大多數(shù)的觀點(diǎn)是可以使用單個線程管理多個客戶端的連接
- 另一個個人觀點(diǎn)說io多路復(fù)用的是系統(tǒng)調(diào)用,原先是一個客戶端通過一個系統(tǒng)調(diào)用去處理,現(xiàn)在轉(zhuǎn)變成通過一次系統(tǒng)調(diào)用select/poll由內(nèi)核主動通知用戶哪些client數(shù)據(jù)已就緒,大大減少了無效的系統(tǒng)調(diào)用次數(shù)
select
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
- maxfd:表示被select管理的描述符個數(shù),值為最大描述符+1
- fd_set:表示一組描述符集合,select中是用一個位數(shù)組來實(shí)現(xiàn)的,要給描述符占一位
- readset、writeset、exceptset:可讀事件集合、可寫事件集合、異常事件集合
- timeout:等于0立即返回,大于0設(shè)置一個超時時間,小于0永遠(yuǎn)等待
poll
struct pollfd{
int fd;
short events; // 關(guān)心的事件
short revents; // 發(fā)生的事件
};
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
- poll參數(shù)解釋:
- fdarray:為傳入的pollfd數(shù)組的首地址,該數(shù)組中的每一個元素為一個poll結(jié)構(gòu)體鏡像,關(guān)聯(lián)一個管理的描述符fd
- nfds:傳入的值為fdarray數(shù)組的長度,表示管理的描述符個數(shù),主要原因在于前面的fdarray是一個可變長度的數(shù)組,因此需要指定數(shù)組長度
- timeout:無限等待(INFTIM,一個負(fù)值)、立即返回不阻塞(0)、等待指定的超時時間(timeout)
- poll事件定義:四類處理輸入事件、三類處理輸出事件、三類處理錯誤事件
- poll識別三類事件:普通(normal)、優(yōu)先級帶(priority band)、高優(yōu)先級(priority)
select 和 poll 的區(qū)別
- 在實(shí)現(xiàn)上
- select底層實(shí)現(xiàn)是采用位數(shù)組來實(shí)現(xiàn)的,一個描述符對應(yīng)一位
- poll底層是通過pollfd結(jié)構(gòu)體來實(shí)現(xiàn)的,管理的描述符通過pollfd數(shù)組來組織,一個描述符對應(yīng)一個pollfd對象
- 在用法上
- select默認(rèn)大小是FD_SETSIZE(1024),修改的話需要修改配置參數(shù)同時重新編譯內(nèi)核來實(shí)現(xiàn)
- poll是采用變長數(shù)組管理的,理論上可以支持海量連接
- 相同點(diǎn)
- 二者在調(diào)用時,都需要從用戶態(tài)拷貝管理的全量描述符到內(nèi)核態(tài),返回時也需要拷貝全量描述符從內(nèi)核態(tài)到用戶態(tài),再有用戶態(tài)遍歷全量描述符判斷哪些描述符有就緒事件
優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):
- 充分利用了一次系統(tǒng)調(diào)用select/poll就可以實(shí)現(xiàn)管理多個client事件,大大降低了非阻塞IO頻繁無效的系統(tǒng)調(diào)用
- 核心是將主動詢問內(nèi)核轉(zhuǎn)變?yōu)榈却齼?nèi)核通知,提升性能
- 缺點(diǎn):
- 每次都需要將管理的多個client從用戶態(tài)拷貝到內(nèi)核態(tài),在管理百萬連接時,由拷貝帶來的資源開銷較大,影響性能
2.5 IO多路復(fù)用第二版:epoll
- 核心矛盾:select/poll每次都需要將管理的多個client從用戶態(tài)拷貝到內(nèi)核態(tài),影響性能
epoll三大核心接口
1. epoll_create( )
#include<sys/epoll.h>
int epoll_create(int size);
- 從linux2.6.8以后,size參數(shù)已經(jīng)被忽略,大于0即可
- epoll_create( )創(chuàng)建返回的epollfd指向內(nèi)核中的一個epoll實(shí)例,同時該epollfd用來調(diào)用所有和epoll相關(guān)的接口(epoll_ctl和epoll_wait)
- 當(dāng)epollfd不再使用時,需要調(diào)用close關(guān)閉。當(dāng)所有指向epoll的文件描述符關(guān)閉后,內(nèi)核會摧毀該epoll實(shí)例并釋放和其關(guān)聯(lián)的資源
- 成功會返回大于0的epollfd,失敗返回-1
2. epoll_ctl( )
- 核心思想:將哪個客戶端(fd)的哪些事件(event)交給哪個epoll(epfd)來管理(op)
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd:通過epoll_create( )創(chuàng)建的epollfd
- op:EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL
- fd:待監(jiān)聽的描述符fd
- event:要監(jiān)聽的fd的時間(讀、寫、接收連接等),具體如下:
3. epoll_wait( )
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);
- epfd:通過epoll_create( )創(chuàng)建的epollfd
- events:返回就緒的事件列表,就緒的事件列表個數(shù)通過epoll_wait( )的返回值來傳遞
- maxevents:最多返回的events個數(shù),該值用來告訴內(nèi)核創(chuàng)建的events有多大
- timeout:超時時間
- 返回值cnt:
- 0表示超時時間范圍內(nèi)無就緒隊(duì)列
- 大于0表示返回就緒列表的個數(shù)(后續(xù)通過循環(huán)遍歷events[0]~events[cnt-1])
- -1表示錯誤
- event檢測:
if(event & EPOLLHUP){ ... } if(event & (EPOLLPRI | EPOLLERR | EPOLLHUP)){ ... }
epoll的ET模式和LT模式區(qū)別
epoll內(nèi)核實(shí)現(xiàn)
2.5 異步IO
- 異步io,兩個階段都不會被阻塞
同步IO和異步IO的區(qū)別
- 第二階段copy階段,如果是用戶線程來完成的就是同步io,如果是內(nèi)核線程來完成的就是異步io
3. 主流網(wǎng)絡(luò)模型
3.1 thread-based架構(gòu)模型
-
適用場景:并發(fā)量不大的場景
-
原因:
- 線程的創(chuàng)建、銷毀開銷較大
- 創(chuàng)建的線程需要占用一定的資源
- 線程切換需要一定的資源開銷
- 一個進(jìn)程能開辟的線程數(shù)據(jù)有限
-
對應(yīng)的是阻塞IO
3.2 single-reactor單線程網(wǎng)絡(luò)模型
- 核心:IO中的accept、read、write都是在一個線程完成的
- 存在問題:目前該模型中,除了IO操作在reactor線程外,業(yè)務(wù)邏輯處理操作也在reactor線程上,當(dāng)業(yè)務(wù)邏輯處理比較耗時時,會大大降低了IO請求的處理效率
- 典型實(shí)現(xiàn):redis(4.0之前)
3.3 single-reactor線程池模型
- 如何改進(jìn):引入了線程池,用來專門處理業(yè)務(wù)邏輯操作,提升IO響應(yīng)速度
- 缺陷:雖然在引入線程池后IO響應(yīng)速度提升了,但在管理百萬級連接、高并發(fā)大數(shù)據(jù)量時,單個reactor線程仍然會效率比較低下
3.4 multi-reactor多線程模型
- 如何改進(jìn):保留原先single-reactor引入的線程池外,新擴(kuò)展了reactor線程。引入了多個reactor線程,也稱為主從結(jié)構(gòu)
- 擴(kuò)展方法:
- 單進(jìn)程(多線程)模式
- 多進(jìn)程模式
- 典型實(shí)現(xiàn):
- netty
- memcached
文章來源:http://www.zghlxwxcb.cn/news/detail-845653.html
3.5 multi-reactor多進(jìn)程模型
- mainreactor進(jìn)程主要負(fù)責(zé)接收客戶端連接,并將建立的客戶端連接進(jìn)行分發(fā)給subreactor進(jìn)程中
- subreactor進(jìn)程主要負(fù)責(zé)處理客戶端的數(shù)據(jù)讀寫和業(yè)務(wù)邏輯的處理
- 經(jīng)典實(shí)現(xiàn):nginx
兩種multi-reactor模型對比
文章來源地址http://www.zghlxwxcb.cn/news/detail-845653.html
到了這里,關(guān)于網(wǎng)絡(luò)編程詳解(select poll epoll reactor)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!