https://blog.csdn.net/majianfei1023/article/details/45788591
epoll除了提供select/poll那種IO事件的電平觸發 (Level Triggered)外,還提供了邊沿觸發(Edge Triggered)
”可讀事件"表示有數據到來,"可寫事件"表示內核緩沖區有剩余發送空間,“錯誤事件“表示socket發生了一些網絡錯誤。
socket可讀可寫條件,經常做為面試題被問,因為它考察被面試者對網絡編程的基礎了解的是不是夠深入。
要了解socket可讀可寫條件,我們先了解幾個概念:
1.接收緩存區低水位標記(用於讀)和發送緩存區低水位標記(用於寫):
每個套接字有一個接收低水位和一個發送低水位。他們由select函數使用。
接收低水位標記是讓select返回"可讀"時套接字接收緩沖區中所需的數據量。對於TCP,其默認值為1。 【已用空間超過低水平位,可讀】
發送低水位標記是讓select返回"可寫"時套接字發送緩沖區中所需的可用空間。對於TCP,其默認值常為2048. 【剩余空間的大小超過低水平位,可寫】
通俗的解釋一下,緩存區我們當成一個大小為 n bytes的空間,那么:
接收區緩存的作用就是,接收對面的數據放在緩存區,供應用程序讀。當然了,只有當緩存區可讀的數據量(接收低水位標記)到達一定程度(eg:1)的時候,我們才能讀到數據,不然不就讀不到數據了嗎。
發送區緩存的作用就是,發送應用程序的數據到緩存區,然后一起發給對面。當然了,只有當緩存區剩余一定空間(發送低水位標記)(eg:2048),你才能寫數據進去,不然可能導致空間不夠。
2.FIN: (結束標志,Finish)用來結束一個TCP回話.但對應端口仍處於開放狀態,准備接收后續數據.
- 首先來看看socket可讀的條件.
一、下列四個條件中的任何一個滿足時,socket准備好讀:
1. socket的接收緩沖區中的【已用】數據字節大於等於該socket的接收緩沖區低水位標記的當前大小。對這樣的socket的讀操作將不阻塞並返回一個大於0的值(也就是返回准備好讀入的數據)。我們可以用SO_RCVLOWAT socket選項來設置該socket的低水位標記。對於TCP和UDP .socket而言,其缺省值為1.
2. 該連接的讀這一半關閉(也就是接收了FIN的TCP連接)。對這樣的socket的讀操作將不阻塞並返回0
3.socket是一個用於監聽的socket,並且已經完成的連接數為非0.這樣的soocket處於可讀狀態,是因為socket收到了對方的connect請求,執行了三次握手的第一步:對方發送SYN請求過來,使監聽socket處於可讀狀態;正常情況下,這樣的socket上的accept操作不會阻塞;
4.有一個socket有異常錯誤條件待處理.對於這樣的socket的讀操作將不會阻塞,並且返回一個錯誤(-1),errno則設置成明確的錯誤條件.這些待處理的錯誤也可通過指定socket選項SO_ERROR調用getsockopt來取得並清除;
- 再來看看socket可寫的條件.
二、下列三個條件中的任何一個滿足時,socket准備好寫:
1. socket的發送緩沖區中的【剩余】數據字節大於等於該socket的發送緩沖區低水位標記的當前大小。對這樣的socket的寫操作將不阻塞並返回一個大於0的值(也就是返回准備好寫入的數據)。我們可以用SO_SNDLOWAT socket選項來設置該socket的低水位標記。對於TCP和UDP socket而言,其缺省值為2048
2. 該連接的寫這一半關閉。對這樣的socket的寫操作將產生SIGPIPE信號,該信號的缺省行為是終止進程。
3.有一個socket異常錯誤條件待處理.對於這樣的socket的寫操作將不會阻塞並且返回一個錯誤(-1),errno則設置成明確的錯誤條件.這些待處理的錯誤也可以通過指定socket選項SO_ERROR調用getsockopt函數來取得並清除;
解釋一下連接的讀/寫這一半關閉:
如圖:
終止一個連接需要4個分節,主動關閉的一端(A)調用close發送FIN到另一端(B),B接收到FIN后,知道A已經主動關閉了,也就是,A不會發數據來了,那么這一端調用read必然可讀,且返回0(read returns 0).
---------------------
https://www.cnblogs.com/my_life/articles/5320230.html
根據聖經《UNIX網絡編程卷1》,當如下任一情況發生時,會產生套接字的可讀事件:
- 該套接字的接收緩沖區中的數據字節數大於等於套接字接收緩沖區低水位標記的大小;
- 該套接字的讀半部關閉(也就是收到了FIN),對這樣的套接字的讀操作將返回0(也就是返回EOF);
- 該套接字是一個監聽套接字且已完成的連接數不為0;
- 該套接字有錯誤待處理,對這樣的套接字的讀操作將返回-1。
當如下任一情況發生時,會產生套接字的可寫事件:
- 該套接字的發送緩沖區中的可用空間字節數大於等於套接字發送緩沖區低水位標記的大小;
- 該套接字的寫半部關閉,繼續寫會產生SIGPIPE信號;
- 非阻塞模式下,connect返回之后,該套接字連接成功或失敗;
- 該套接字有錯誤待處理,對這樣的套接字的寫操作將返回-1。
-----------------------
https://www.cnblogs.com/my_life/articles/5382399.html
LT模式的特點是:
- 若數據可讀,epoll返回可讀事件
- 若開發者沒有把數據完全讀完,epoll會不斷通知數據可讀,直到數據全部被讀取。
- 若socket可寫,epoll返回可寫事件,而且是只要socket發送緩沖區未滿,就一直通知可寫事件。
- 優點是對於read操作比較簡單,只要有read事件就讀,讀多讀少都可以。
- 缺點是write相關操作較復雜,由於socket在空閑狀態時發送緩沖區一定是不滿的,故若socket一直在epoll wait列表中,則epoll會一直通知write事件,所以必須保證沒有數據要發送的時候,要把socket的write事件從epoll wait列表中刪除。而在需要的時候在加入回去,這就是LT模式的最復雜部分。
ET模式的特點是:
- 若socket可讀,返回可讀事件
- 若開發者沒有把所有數據讀取完畢,epoll不會再次通知epoll read事件,也就是說存在一種隱患,如果開發者在讀到可讀事件時,如果沒有全部讀取所有數據,那么可能導致epoll在也不會通知該socket的read事件。(其實這個問題並沒有聽上去難,參見下文)。
- 若發送緩沖區未滿,epoll通知write事件,直到開發者填滿發送緩沖區,epoll才會在下次發送緩沖區由滿變成未滿時通知write事件。 【應該:剩余可寫緩沖區的大小高於低水平位時,通知可寫】
- ET模式下,只有socket的狀態發生變化時才會通知,也就是讀取緩沖區由無數據到有數據時通知read事件,發送緩沖區由滿變成未滿通知write事件。
- 缺點是epoll read事件觸發時,必須保證socket的讀取緩沖區數據全部讀完(事實上這個要求很容易達到)
- 優點:對於write事件,發送緩沖區由滿到未滿時才會通知【不會一直通知】,若無數據可寫,忽略該事件,若有數據可寫,直接寫。Socket的write事件可以一直放在epoll的wait列表。Man epoll中我們知道,當向socket寫數據,返回的值小於傳入的buffer大小或者write系統調用返回EWouldBlock時,表示發送緩沖區已滿。
讓我們換一個角度來理解ET模式,事實上,epoll的ET模式其實就是socket io完全狀態機。
需要讀者注意的是,socket模式是可寫的,因為發送緩沖區初始時空的。故應用層有數據要發送時,直接調用write系統調用發送數據,若write系統調用返回EWouldBlock則表示socket變為不可寫,或者write系統調用返回的數值小於傳入的buffer參數的大小,這時需要把未發送的數據暫存在應用層待發送列表中,等待epoll返回write事件,再繼續發送應用層待發送列表中的數據,同樣若應用層待發送列表中的數據沒有一次性發完,那么繼續等待epoll返回write事件,如此循環往復。
所以可以反推得到如下結論,若應用層待發送列表有數據,則該socket一定是不可寫狀態,那么這時候要發送新數據直接追加到待發送列表中。若待發送列表為空,則表示socket為可寫狀態,則可以直接調用write系統調用發送數據。
總結如下:
- 當發送數據時,若應用層待發送列表有數據,則將要發送的數據追加到待發送列表中。否則直接調用write系統調用。
- Write系統調用發送數據時,檢測write返回值,若返回數值>0且小於傳入的buffer參數大小,或返回EWouldBlock錯誤碼,表示,發送緩沖區已滿,將未發送的數據追加到待發送列表
- Epoll返回write事件后,檢測待發送列表是否有數據,若有數據,依次嘗試發送直到數據全部發送完畢或者發送緩沖區被填滿。
總結
LT模式主要是讀操作比較簡單,但是對於ET模式並沒有優勢,因為將讀取緩沖區數據全部讀出並不是難事。而write操作,ET模式則流程非常的清晰,按照完全狀態機來理解和實現就變得非常容易。而LT模式的write操作則復雜多了,要頻繁的維護epoll的wail列表。
在代碼編寫時,把epoll ET當成狀態機,當socket被創建完成(accept和connect系統調用返回的socket)時加入到epoll列表,之后就不用在從中刪除了。為什么呢?man epoll中的FAQ告訴我們,當socket被close掉后,其自動從epoll中刪除。對於監聽socket簡單說幾點注意事項:
- 監聽socket的write事件忽略
- 監聽socket的read事件表示有新連接,調用accept接受連接,直到返回EWouldBlock。
- 對於Error事件,有些錯誤是可以接受的錯誤,比如文件描述符用光的錯誤
GitHub :https://github.com/fanchy/FFRPC
ffrpc 介紹: http://www.cnblogs.com/zhiranok/p/ffrpc_summary.html
故,綜上所述,服務器程序中推薦使用epoll 的ET 模式!!!!
http://www.cnblogs.com/yuuyuu/p/5103744.html
對於監聽的socket文件描述符我們用sockfd表示,對於accept()返回的文件描述符(即要讀寫的文件描述符)用connfd表示。
五.總結
1.對於監聽的sockfd,最好使用水平觸發模式,邊緣觸發模式會導致高並發情況下,有的客戶端會連接不上。如果非要使用邊緣觸發,網上有的方案是用while來循環accept()。
2.對於讀寫的connfd,水平觸發模式下,阻塞和非阻塞效果都一樣,不過為了防止特殊情況,還是建議設置非阻塞。
3.對於讀寫的connfd,邊緣觸發模式下,必須使用非阻塞IO,並要一次性全部讀寫完數據(用while來循環讀)。
https://www.cnblogs.com/my_life/articles/4727771.html
epoll的工作方式
epoll分為兩種工作方式LT和ET。
LT(level triggered) 是默認/缺省的工作方式,同時支持 block和no_block socket。這種工作方式下,內核會通知你一個fd是否就緒,然后才可以對這個就緒的fd進行I/O操作。就算你沒有任何操作,系統還是會繼續提示fd已經就緒,不過這種工作方式出錯會比較小,傳統的select/poll就是這種工作方式的代表。
ET(edge-triggered) 是高速工作方式,僅支持no_block socket,這種工作方式下,當fd從未就緒變為就緒時,內核會通知fd已經就緒,並且內核認為你知道該fd已經就緒,不會再次通知了,除非因為某些操作導致fd就緒狀態發生變化。如果一直不對這個fd進行I/O操作,導致fd變為未就緒時,內核同樣不會發送更多的通知,因為only once。所以這種方式下,出錯率比較高,需要增加一些檢測程序。
https://www.cnblogs.com/my_life/articles/5315364.html
對每一網絡連接均需要維持其接收與發送數據緩沖區,當連接可讀取時則先讀取數據到接收緩沖區,然后判斷是否完整並處理之;
當向連接發送數據時一般都直接發送,若不能立即完整發送時則將其緩存到發送緩沖區,然后等連接可寫時再發送,但需要注意的是,若在可寫緩沖區非空且可寫之前需要發送新數據,則此時不能直接發送而是應該將其追加到發送緩沖區后統一發送,否則會導致網絡數據竄包。
https://www.zhihu.com/question/22840801
##水平觸發模式(Level-Triggered)第一種方法
-
當需要向socket寫數據時,將該socket加入到epoll模型(epoll_ctl);等待可寫事件。
-
接收到socket可寫事件后,調用write()或send()發送數據。
-
當數據全部寫完后, 將socket描述符移出epoll模型。
這種方式的缺點是: 即使發送很少的數據,也要將socket加入、移出epoll模型,有一定的操作代價。
##水平觸發模式(Level-Triggered)第二種方法
-
向socket寫數據時,不將socket加入到epoll模型;而是直接調用send()發送;
-
只有當或send()返回錯誤碼EAGAIN(系統緩存滿),才將socket加入到epoll模型,等待可寫事件后,再發送數據。
-
全部數據發送完畢,再移出epoll模型。
這種方案的優點:當用戶數據比較少時,不需要epool的事件處理。
在高壓力的情況下,性能怎么樣呢?
對一次性直接寫成功、失敗的次數進行統計。如果成功次數遠大於失敗的次數, 說明性能良好。(如果失敗次數遠大於成功的次數,則關閉這種直接寫的操作,改用第一種方案。同時在日志里記錄警告)
##第三種方法使用Edge-Triggered(邊沿觸發)
這樣socket有可寫事件,只會觸發一次。
可以在應用層做好標記。以避免頻繁的調用 epoll_ctl( EPOLL_CTL_ADD, EPOLL_CTL_MOD)
。 這種方式是epoll 的 man 手冊里推薦的方式, 性能最高。但如果處理不當容易出錯,事件驅動停止。
=====
epoll的兩種模式LT和ET
二者的差異在於level-trigger模式下只要某個socket處於readable/writable狀態,無論什么時候進行epoll_wait都會返回該socket;
而edge-trigger模式下只有某個socket從unreadable變為readable或從unwritable變為writable時,epoll_wait才會返回該socket。
正確的accept,accept 要考慮 2 個問題
(1) 阻塞模式 accept 存在的問題
考慮這種情況:TCP連接被客戶端夭折,即在服務器調用accept之前,客戶端主動發送RST終止連接,導致剛剛建立的連接從就緒隊列中移出,如果套接口被設置成阻塞模式,服務器就會一直阻塞在accept調用上,直到其他某個客戶建立一個新的連接為止。但是在此期間,服務器單純地阻塞在accept調用上,就緒隊列中的其他描述符都得不到處理。
解決辦法是把監聽套接口設置為非阻塞,當客戶在服務器調用accept之前中止某個連接時,accept調用可以立即返回-1,這時源自Berkeley的實現會在內核中處理該事件,並不會將該事件通知給epool,而其他實現把errno設置為ECONNABORTED或者EPROTO錯誤,我們應該忽略這兩個錯誤。
(2)ET模式下accept存在的問題
考慮這種情況:多個連接同時到達,服務器的TCP就緒隊列瞬間積累多個就緒連接,由於是邊緣觸發模式,epoll只會通知一次,accept只處理一個連接,導致TCP就緒隊列中剩下的連接都得不到處理。
解決辦法是用while循環抱住accept調用,處理完TCP就緒隊列中的所有連接后再退出循環。如何知道是否處理完就緒隊列中的所有連接呢?accept返回-1並且errno設置為EAGAIN就表示所有連接都處理完。
綜合以上兩種情況,服務器應該使用非阻塞地accept,accept在ET模式下的正確使用方式為:
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) { |
handle_client(conn_sock); |
} |
if (conn_sock == -1) { |
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) |
perror("accept"); |
} |
#include <sys/socket.h> #include <sys/wait.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <sys/epoll.h> #include <sys/sendfile.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <fcntl.h> #include <errno.h> #define MAX_EVENTS 10 #define PORT 8080 //設置socket連接為非阻塞模式 void setnonblocking(int sockfd) { int opts; opts = fcntl(sockfd, F_GETFL); if(opts < 0) { perror("fcntl(F_GETFL)\n"); exit(1); } opts = (opts | O_NONBLOCK); if(fcntl(sockfd, F_SETFL, opts) < 0) { perror("fcntl(F_SETFL)\n"); exit(1); } } int main(){ struct epoll_event ev, events[MAX_EVENTS]; int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n; struct sockaddr_in local, remote; char buf[BUFSIZ]; //創建listen socket if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("sockfd\n"); exit(1); } setnonblocking(listenfd); bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_addr.s_addr = htonl(INADDR_ANY);; local.sin_port = htons(PORT); if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0) { perror("bind\n"); exit(1); } listen(listenfd, 20); epfd = epoll_create(MAX_EVENTS); if (epfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE); } ev.events = EPOLLIN; ev.data.fd = listenfd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_pwait"); exit(EXIT_FAILURE); } for (i = 0; i < nfds; ++i) { fd = events[i].data.fd; if (fd == listenfd) { while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) { setnonblocking(conn_sock); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) { perror("epoll_ctl: add"); exit(EXIT_FAILURE); } } if (conn_sock == -1) { if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) perror("accept"); } continue; } if (events[i].events & EPOLLIN) { n = 0; while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { n += nread; } if (nread == -1 && errno != EAGAIN) { perror("read error"); } ev.data.fd = fd; ev.events = events[i].events | EPOLLOUT; if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) { perror("epoll_ctl: mod"); } } if (events[i].events & EPOLLOUT) { sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11); int nwrite, data_size = strlen(buf); n = data_size; while (n > 0) { nwrite = write(fd, buf + data_size - n, n); if (nwrite < n) { if (nwrite == -1 && errno != EAGAIN) { perror("write error"); } break; } n -= nwrite; } close(fd); } } } return 0; }