網路程式設計:epoll、accept觸發模式及阻塞方式的選擇
select(),poll()模型都是水平觸發模式,訊號驅動IO是邊緣觸發模式,epoll()模型即支援水平觸發,也支援邊緣觸發,預設是水平觸發
從表象看epoll的效能最好,但是在連線數少,並且連線都十分活躍的情況下,select和poll的效能可能比epoll好,畢竟epoll的通知機制需要很多回調函式來完成。
epoll工作在兩種觸發模式下:
Level_triggered(水平觸發): 這是epoll預設的觸發方式,既支援阻塞模式,也支援非阻塞模式,當被監控的檔案描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把資料一次性全部讀寫完(如讀寫緩衝區太小),那麼下次呼叫 epoll_wait()時,它還會通知你在上次沒讀寫完的檔案描述符上繼續讀寫
Edge_triggered(邊緣觸發): 這種模式下,epoll只支援非阻塞模式,當被監控的檔案描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把資料全部讀寫完(如讀寫緩衝區太小),那麼下次呼叫epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該檔案描述符上出現第二次可讀寫事件才會通知你。Nginx預設採用ET模式來使用epoll。
二者的差異在於level-trigger模式下只要某個socket處於readable/writable狀態,無論什麼時候進行epoll_wait都會返回該socket;而edge-trigger模式下只要監測描述符上有資料
所以,在epoll的ET模式下,正確的讀寫方式為:
讀:只要可讀,就一直讀,直到返回0,或者 errno = EAGAIN
寫:只要可寫,就一直寫,直到資料傳送完,或者 errno = EAGAIN
這裡的錯誤碼是指:
在一個非阻塞的socket上呼叫read/write函式, 返回的errno為EAGAIN或者EWOULDBLOCK
這個錯誤表示資源暫時不夠,read時,讀緩衝區沒有資料,或者write時,寫緩衝區滿了。遇到這種情況,如果是阻塞socket,read/write就要阻塞掉。而如果是非阻塞socket,read/write立即返回-1, 同時errno設定為EAGAIN。
簡單程式碼示例如下:
讀:
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0)
{
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror("read error");
}
寫:
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;
}
從以上我們就看出來了為何 epoll在ET模式下使用的是非阻塞fd ,由於邊緣觸發的模式,每次epoll_wait返回就緒的fd,必須讀完讀取緩衝區裡的所有資料(直至接收資料返回EAGAIN),必須套上while迴圈,此時若使用阻塞的fd,當讀取完緩衝區裡的資料後,接受資料過程會阻塞,從而無法接受新的連線。
**//////////////////////割割割割///////////////////////////////割割割割////////////////////////**
上面介紹完了epoll的兩種觸發模式以及兩種阻塞方式的使用,下面分析一下網路程式設計中accept函式使用fd時要如何選擇觸發方式以及阻塞方式。
listenfd阻塞還是非阻塞?
如果TCP連線被客戶端夭折,即在伺服器呼叫accept之前,客戶端主動傳送RST終止連線,導致剛剛建立的連線從就緒佇列中移出,如果套介面被設定成阻塞模式,伺服器就會一直阻塞在accept呼叫上,直到其他某個客戶建立一個新的連線為止。但是在此期間,伺服器單純地阻塞在accept呼叫上,就緒佇列中的其他描述符都得不到處理。
解決辦法是把監聽套介面listenfd設定為非阻塞,當客戶在伺服器呼叫accept之前中止某個連線時,accept呼叫可以立即返回-1,這時源自Berkeley的實現會在核心中處理該事件,並不會將該事件通知給epool,而其他實現把errno設定為ECONNABORTED或者EPROTO錯誤,我們應該忽略這兩個錯誤。
ET還是LT?
ET:如果多個連線同時到達,伺服器的TCP就緒佇列瞬間積累多個就緒連線,由於是邊緣觸發模式,epoll只會通知一次,accept只處理一個連線,導致TCP就緒佇列中剩下的連線都得不到處理。
解決辦法是用while迴圈包住accept呼叫,處理完TCP就緒佇列中的所有連線後再退出迴圈。如何知道是否處理完就緒佇列中的所有連線呢?accept返回-1並且errno設定為EAGAIN就表示所有連線都處理完。
LT:在nigix的實現中,accept函式呼叫使用水平觸發的fd,就是出於對丟失連線的考慮(邊緣觸發時,accept只會執行一次接收一個連線,核心不會再去通知有連線就緒),所以使用水平觸發的fd就不存在丟失連線的問題。但是如果系統中有大量你不需要讀寫的就緒檔案描述符,而它們每次都會返回,這樣會大大降低處理程式檢索自己關心的就緒檔案描述符的效率。
所以伺服器應該使用非阻塞的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");
}
一道騰訊後臺開發的面試題:
使用Linuxepoll模型,水平觸發模式;當socket可寫時,會不停的觸發socket可寫的事件,如何處理?
解答:正如我們上面說的,LT模式下不需要讀寫的檔案描述符仍會不停地返回就緒,這樣就會影響我們監測需要關心的檔案描述符的效率。
所以這題的解決方法就是:平時不要把該描述符放進eventpoll結構體中,當需要寫該fd的時候,呼叫epoll_ctl把fd加入eventpoll裡監聽,可寫的時候就往裡寫,寫完再次呼叫epoll_ctl把fd移出eventpoll,這種方法在傳送很少資料的時候仍要執行兩次epoll_ctl操作,有一定的操作代價
改進一下就是:平時不要把該描述符放進eventpoll結構體中,需要寫的時候呼叫write或者send寫資料,如果返回值是EAGAIN(寫緩衝區滿了),那麼這時候才執行第一種方法的步驟。
歸納如下:
1.對於監聽的sockfd要設定成非阻塞型別,觸發模式最好使用水平觸發模式,邊緣觸發模式會導致高併發情況下,有的客戶端會連線不上。如果非要使用邊緣觸發,網上有的方案是用while來迴圈accept()。
2.對於讀寫的connfd,水平觸發模式下,阻塞和非阻塞效果都一樣,不過為了防止特殊情況,還是建議設定非阻塞。
3.對於讀寫的connfd,邊緣觸發模式下,必須使用非阻塞IO,並要一次性全部讀寫完資料。、
下面看一個在邊沿觸發模式下使用epoll的http伺服器程式碼,必要的講解都在註釋裡。
#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
{
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);
{
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);//超時時間-1,永久阻塞直到有事件發生
if (nfds == -1)
{
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
for (i = 0; i < nfds; ++i)
{
fd = events[i].data.fd;
if (fd == listenfd) //如果是監聽的listenfd,那就是連線來了,儲存來的所有連線
{
//每次處理一個連線,while迴圈直到處理完所有的連線
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
(size_t *)&addrlen)) > 0)
{
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;//邊沿觸發非阻塞模式
ev.data.fd = conn_sock;
//把連線socket加入監聽結構體
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: add");
exit(EXIT_FAILURE);
}
}
//已經處理完所有的連:accept返回-1,errno為EAGAIN
//出錯:返回-1,errno另有其值
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;
//修改該fd監聽事件型別,監測是否可寫
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;
}
//寫完就關閉該連線socket
close(fd);
}
}
}
return 0;
}
相關推薦
網路程式設計:epoll、accept觸發模式及阻塞方式的選擇
select(),poll()模型都是水平觸發模式,訊號驅動IO是邊緣觸發模式,epoll()模型即支援水平觸發,也支援邊緣觸發,預設是水平觸發 從表象看epoll的效能最好,但是在連線數少,並且連線都十分活躍的情況下,select和poll的效能可能比ep
java網路程式設計:1、計算機網路?網路通訊的組成?什麼是ip、協議、埠號?
宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 計算機網路 網路ip 網路協議 網路埠號 計算機網路 計算機網路是相互連線的獨立自主的計算機的集合,最簡單的網路形式由兩臺計算機組成。如下圖: 一臺計算機
java網路程式設計:13、基於UDP的socket程式設計(三)實現相互發送接收訊息
宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 一、前言 二、基於UDP伺服器端程式的編寫 三、基於UDP客戶端程式的編寫 四、測試列印 五、系列文章(java網路程式設計) 通過上兩篇文章:1、瞭解了基於UDP
java網路程式設計:12、基於UDP的socket程式設計(二)程式碼通訊-簡單例項
宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 一、基於UDP伺服器端程式的編寫 二、基於UDP客戶端程式的編寫 三、系列文章(java網路程式設計) 通過上篇文章瞭解了基於UDP通訊的理論、基本步驟以及它跟TCP的區別
java網路程式設計:11、基於UDP的socket程式設計(一)理論、基本步驟
宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 一、基於UDP的socket程式設計 二、基本步驟 三、系列文章(java網路程式設計) 一、基於UDP的socket程式設計 對於基於UDP通訊來說,通訊雙方不需要建
java網路程式設計:10、基於TCP的socket程式設計(三)緩衝流、flush方法、關閉流
宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 一、使用緩衝流、註釋流的關閉—帶來的效果 二、使用flush方法—帶來的效果 三、關閉流—帶來的效果 四、系列文章(java網路程式設計) 上篇講了基於tcp的程式設計
java網路程式設計:9、基於TCP的socket程式設計(二)伺服器端迴圈監聽接收多個客戶端_多執行緒伺服器程式
宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 一、核心程式碼編寫 1、伺服器端程式的編寫 2、客戶端程式的編寫 3、測試列印輸出 二、系列文章(java網路程式設計) 上篇講了基於tcp的程式設計的一些基礎知識
java網路程式設計:8、基於TCP的socket程式設計(一)簡單的socket通訊_一個客戶端
宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 一、基於tcp的程式設計,就好像用電話進行交談一樣 二、在java中用於程式設計網路程式的類 三、套接字 + (輸出、輸入流) 1、伺服器程式編寫基本步驟: 2、客戶端程式
linux 網路程式設計:epoll 的例項
在前面已經經過了PPC、TPC、select之類( TPC就是使用程序處理data,TPC就是使用執行緒處理 ),前面兩個的缺點大家應該都是知道的是吧,對於select( 其實poll和他差不多 ),缺點是能同時連線的fd是在是不多,在linux中一般是102
網路程式設計:htons、htonl、ntohs、ntohl簡析
/******************************************************************************************************************說明:htons、htonl、ntohs、n
Linux網路程式設計:socket程式設計簡介、網路位元組序及相關函式
Socket(套接字) socket可以看成是使用者程序與核心網路協議棧的程式設計介面(API函式)。 socket不僅可以用於本機的程序間通訊,還可以用於網路上不同主機的程序間通訊。 IPv4套接字地址結構 IPv4套接字地址結構通常也稱為“網際套接字地址結構”,它以
Python之路(第三十一篇) 網路程式設計:簡單的tcp套接字通訊、粘包現象
一、簡單的tcp套接字通訊 套接字通訊的一般流程 服務端 server = socket() #建立伺服器套接字 server.bind() #把地址繫結到套接字,網路地址加埠 server.listen() #監聽連結 inf_loop:
6.3 SAP ABAP 開放封閉原則(OCP)- 摘自 《SAP ABAP面向物件程式設計:原則、模式及實踐》 6.3 SAP ABAP 開放封閉原則(OCP)- 摘自 《SAP ABAP面向物件程式設計:原則、模式及實踐》
6.3 SAP ABAP 開放封閉原則(OCP)- 摘自 《SAP ABAP面向物件程式設計:原則、模式及實踐》 6.3 開放封閉原則(OCP) 開閉原則(Open-Closed Principle, OCP)指的是,一個類或者模組,如果在業務修改或者功能需要擴充套
【網路程式設計】TCP網路程式設計中connect()、listen()和accept()三者之間的關係
舉個簡單的例子(以下程式碼只是示範性的,用於說明不同套接字的作用,實際的函式會需要更多的引數): /* 建立用於監聽和接受客戶端連線請求的套接字 */ server_sock = socket(); /* 繫結監聽的IP地址和埠 */ bind(server_sock); /* 開始監聽 */ li
【Linux 網路程式設計】TCP網路程式設計中connect()、listen()和accept()三者之間的關係
基於 TCP 的網路程式設計開發分為伺服器端和客戶端兩部分,常見的核心步驟和流程如下: connect()函式:對於客戶端的 connect() 函式,該函式的功能為客戶端主動連線伺服器,建立連線是通過三次握手,而這個連接的過程是由核心完成,不是這個函式完成的,這個函式的作用僅僅是通知 Linux 核心
python網路程式設計:TCP/IP、Socket、C/S架構等
網路程式設計 套接字 Socket來源 通訊的基石,是支援TCP/IP協議的網路通訊的基本操作單元 屬性:三元組(ip地址, 協議,埠) 域 型別 協議 IP地址 IP地址是一個32位的二進位制數就是4個位元組 通過指定的埠和協議找到
TCP網路程式設計中connect()、listen()和accept()三者之間的關係
基於 TCP 的網路程式設計開發分為伺服器端和客戶端兩部分,常見的核心步驟和流程如下:connect()函式對於客戶端的 connect() 函式,該函式的功能為客戶端主動連線伺服器,建立連線是通過三次
網路程式設計:服務端處理多個客戶端----多執行緒實現、建立執行緒特有資料.
重點集中在用多執行緒實現,建立執行緒特有資料,不會發生資料寫入衝突。實現的功能很簡單,客戶端連線成功後,輸入一個整數,服務端返回它的二進位制形式。客戶端輸入0,則主動退出。三個檔案: duoxianc.c ,主檔案binarykey.c,執行緒執行函式及特有資料建立clien
Linux網路程式設計:TCP客戶/伺服器模型及基本socket函式
TCP客戶/伺服器模型 TCP連線的分組交換 在使用socket API的時候應該清楚應用程式和TCP協議棧是如何互動的: 呼叫connect()會發出SYN段(SYN是TCP報文段頭部的一個標誌位,置為1) 阻塞的read()函式返回0就表明收到了FIN段 客戶端呼叫c
Linux學習之網路程式設計(epoll的用法)
言之者無罪,聞之者足以戒。 - “詩序” epoll相關的函式包含在標頭檔案<sys/epoll.h> epoll是Linux核心為處理大批量控制代碼而作了改進的poll,是Linux下多路複用IO介面select/poll的增強版本,它能顯著減少程式在大量併發連線中只有少量活躍