1. 程式人生 > >網路程式設計:epoll、accept觸發模式及阻塞方式的選擇

網路程式設計: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_wait就會返回該socket。
所以,在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;
}

相關推薦

網路程式設計epollaccept觸發模式阻塞方式選擇

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

網路程式設計htonshtonlntohsntohl簡析

/******************************************************************************************************************說明: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/IPSocketC/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的增強版本,它能顯著減少程式在大量併發連線中只有少量活躍