c/c++:端口複用(setsockopt)、io多路轉接(select、 poll、epoll)

目錄

1. 端口複用

setsockopt函數

2. IO多路轉接

2.1 select

select函數演示:

io多路轉接(select函數)服務器代碼模板

3.2 poll

2.3  epoll

epoll_create 創建函數

epoll_ctl 控制函數

epoll_wait 檢測函數

2.4 epoll 的工作模式

2.4.1 LT模式(默認)

epoll LT模式實現的io多路轉接 服務器端代碼模板示例:

2.4.3 ET模式(邊沿觸發模式)

epoll ET模式實現的io多路轉接 服務器端代碼模板示例:


 

1. 端口複用

端口複用最常用用途:

  • 防止服務器重啓時之前綁定的端口還未釋放
  • 程序突然退出而系統沒有釋放端口

 

setsockopt函數

setsockopt函數有很多功能,這裏只講端口複用的功能。

#include <sys/types.h> 
#include <sys/socket.h>
// 設置套接字屬性
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
	參數:
	- sockfd: 要操作的文件描述符
	- level: 級別 -> SOL_SOCKET (端口複用的級別)
        - optname: 端口複用的級別(二選一,隨便選,對於端口複用都可以)
        	- SO_REUSEADDR
        	- SO_REUSEPORT
        - optval: 端口複用-> 對應的是整形數
        	- 1: 可以複用
        	- 0: 不能複用
        - optlen: optval參數對應的內存大小

// 設置端口複用, 設置的時機: 服務器綁定端口之前, 設置端口複用。例如:
setsockopt();
bind();

 

2. IO多路轉接

使用進程/線程方式實現併發:
        - 共同點: 
            有一個父親線程/進程 -> accept 某些情況下是阻塞的
            子進程/子線程   -> 通信 -> read/write 某些情況下是阻塞的
        - 不同點:
            線程更節省系統資源, 一般寫程序, 考慮線程的實現方式
            在一個進程中, 調用另一個進程 -> exec
進程/線程方式實現併發代碼模板:https://blog.csdn.net/qq_35883464/article/details/103643720

改進思路: 所有的阻塞狀態, 程序猿都不去判斷, 委託內核判斷, 得到內核回覆之後進行後續處理
    - 程序猿是不能直接操作內核, 間接 -> 使用系統函數
        - select
        - poll
        - epoll

IO多路轉接核心思想: 不再由應用程序自己監視客戶端連接和數據通信,取而代之由內核替應用程序監視文件。

 

2.1 select

select主旨思想:

  1. 先構造一張有關文件描述符的列表, 將要監聽的文件描述符添加到該表中
  2. 調用一個函數,監聽該表中的文件描述符,直到這些描述符表中的一個進行I/O操作時,該函數才返回。
  3. 函數對文件描述符的檢測操作是由內核完成的
  4.  
  5. 在返回時,它告訴進程有多少(哪些)描述符要進行I/O操作。
#include <sys/select.h>
#include <unistd.h>

// sizeof(fd_set) = 128

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
	參數:
        - nfds: 委託內核檢測的最大文件描述符的值 + 1
        - readfds: 讀集合, 委託內核檢測哪些文件描述符的讀屬性(讀的是對方發送過來的數據)
        	- 傳入傳出的參數
        - fd_set: 寫集合, 委託內核檢測哪些文件描述符的寫屬性
        	- 傳入傳出的參數
        	- 委託內核檢測寫緩衝區是不是還可以寫數據(不滿就可以寫)
        - exceptfds: 異常集合, 委託內核檢測哪些文件描述符出現了異常
            - 傳入傳出的參數

       	- timeout:
struct timeval {
    long    tv_sec;         /* 秒 */
    long    tv_usec;        /* 毫秒 */
};
			- NULL: 永久阻塞, 直到檢測到了文件描述符有變化
			- tv_sec = 0, tv_usec = 0, 不阻塞
			- tv_sec > 0 || tv_usec > 0, 阻塞對應的時間長度
	返回值:
		-1: 失敗
		>0(n): 檢測的集合中有n個文件描述符發送的變化 

// 將參數文件描述符fd對應的標誌位, 設置爲0
void FD_CLR(int fd, fd_set *set);

// 判斷fd對應的標誌位到底是0還是1, 返回值: fd對應的標誌位的值, 0, 返回0, 1->返回1
int  FD_ISSET(int fd, fd_set *set);

// 將參數文件描述符fd對應的標誌位, 設置爲1
void FD_SET(int fd, fd_set *set);

// fd_set 共有1024bit, 全部初始化爲0
void FD_ZERO(fd_set *set);

 

select函數演示:

fd_set表是自己設置的,和文件描述符表一樣。

值爲1:要讀的文件描述符。值爲0:不關心的文件描述符。

之後,select會修改文件描述符表,值爲1:要讀的文件描述符有數據。值爲0:不關心的文件描述符和沒有數據的文件描述符。

 

io多路轉接(select函數)服務器代碼模板

圖示代碼流程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>

int main()
{
    // 1.創建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    // 2. 綁定 ip, port
    struct sockaddr_in addr;
    addr.sin_port = htons(10000);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }
    // 3. 監聽
    ret = listen(lfd, 100);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }
    
    // 4. 等待連接 -> 循環
    // 檢測 -> 讀緩衝區, 委託內核去處理
    // 數據初始化, 創建自定義的文件描述符集
    fd_set rdset, tmp; 
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;
    while(1)
    {
        // 委託內核檢測
        tmp = rdset;
        ret = select(maxfd+1, &tmp, NULL, NULL, NULL);
        if(ret == -1)
        {
            perror("select");
            exit(0);
        }

        // 檢測的度緩衝區有變化
        // 有新連接
        if(FD_ISSET(lfd, &tmp))
        {
            // 接收連接請求
            struct sockaddr_in sockcli;
            int len = sizeof(sockcli);
            // 這個accept是不會阻塞的
            int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
            // 委託內核檢測connfd的讀緩衝區
            FD_SET(connfd, &rdset);
            maxfd = connfd > maxfd ? connfd : maxfd;
        }
        // 通信, 有客戶端發送數據過來
        for(int i=lfd+1; i<=maxfd; ++i)
        {
            // 如果在集合中, 說明讀緩衝區有數據
            if(FD_ISSET(i, &tmp))
            {
                char buf[128];
                int ret = read(i, buf, sizeof(buf));
                if(ret == -1)
                {
                    perror("read");
                    exit(0);
                }
                else if(ret == 0)
                {
                    printf("對方已經關閉了連接...\n");
                    FD_CLR(i, &rdset);
                    close(i);
                }
                else
                {
                    printf("客戶端say: %s\n", buf);
                    write(i, buf, strlen(buf)+1);
                }
            }
        }
    }
    close(lfd);
    return 0;
}

 

3.2 poll

  #include <poll.h>
  struct pollfd {
  	int   fd;         /* 委託內核檢測的文件描述符 */
  	short events;     /* 委託內核檢測文件描述符的什麼事件 */
  	short revents;    /* 文件描述符實際發生的事件 */
  };
例子:
  struct pollfd myfd;
  myfd.fd = 5;
  myfd.events = POLLIN | POLLOUT;
  

  struct pollfd myfd[100];
  int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  	參數:
  		- fds: 這是一個struct pollfd數組, 這是一個要檢測的文件描述符的集合
  		- nfds: 這是第一個參數數組中最後一個有效元素的下標 + 1
          - timeout: 阻塞時長
          	0: 不阻塞
          	-1: 阻塞, 檢測的fd有變化解除阻塞
          	>0: 阻塞時長
        返回值:
  		-1: 失敗
  		>0(n): 檢測的集合中有n個文件描述符發送的變化 

 

2.3  epoll

  • 內核檢測epoll傳遞的fd集合, 是以紅黑樹的形式遍歷的

 

  • epoll_create 創建函數

#include <sys/epoll.h>
  // 創建一棵紅黑樹
  int epoll_create(int size);
  	參數: 
  		size: 沒意義, 隨便寫個數就行
  	返回值;
  		>0: 文件描述符, 操作epoll樹的根節點
  • epoll_ctl 控制函數

#include <sys/epoll.h>

typedef union epoll_data {
  	void        *ptr;	// 複雜
  	int          fd;	// 簡單    一般就使用這個就好,文件描述符傳入即可;
  	uint64_t     u64;
  } epoll_data_t;
  
  struct epoll_event {
  	uint32_t     events;      /* Epoll 事件 */
  	epoll_data_t data;        /* 上面這個共用體中,一般使用fd */
  };
  Epoll檢測的事件:
  	- EPOLLIN    讀
  	- EPOLOUT    寫
  	- EPOLLERR   錯誤
  
  // 對epoll樹進行管理: 添加節點, 刪除節點, 修改已有的節點屬性
  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  	參數:
  		- epfd: epoll_create的返回值, 通過這個值就可以找到紅黑樹
  		- op: 要進行什麼樣的操作
  			EPOLL_CTL_ADD: 註冊新節點, 添加到紅黑樹上
  			EPOLL_CTL_MOD: 修改檢測的文件描述符的屬性
  			EPOLL_CTL_DEL: 從紅黑樹上刪除節點
  		- fd: 要檢測的文件描述符的值
  		- event: 檢測文件描述符的什麼事件

epoll_ctl函數工作流程:

 

  • epoll_wait 檢測函數

#include <sys/epoll.h>
struct epoll_event events[1000];
  int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  	參數:
  		- epfd: epoll_create的返回值, 通過這個值就可以找到紅黑樹
  		- events: 傳出參數, 保存了發生變化的文件描述符的信息
  		- maxevents: 第二個參數結構體數組的大小
  		- timeout: 阻塞時間
  			- 0: 不阻塞
  			- -1: 一直阻塞, 知道檢測的fd有狀態變化, 解除阻塞
  			- >0: 阻塞的時長(毫秒)
       返回值:
  		- 成功: 有多少個文件描述符狀態發生了變化 > 0
          - 失敗: -1

 

2.4 epoll 的工作模式

2.4.1 LT模式(默認)

LT(level triggered)是缺省的工作方式,並且同時支持block(阻塞)和no-block (非阻塞)socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然後可以對這個就緒的fd進行IO操作。只要有數據,內核會一直通知

例如:

假設委託內核檢測讀事件 -> 檢測fd的讀緩衝區

  - 讀緩衝區有數據 -> epoll檢測到了會給用戶通知
    - 用戶就是不讀, 數據一直在緩衝區中, epoll會一直通知
    - 用戶讀了一部分, epoll會通知用戶
    - 緩衝區數據被讀完了, 就不在通知了

  • epoll LT模式實現的io多路轉接 服務器端代碼模板示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>

int main()
{
    // 1.創建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    // 2. 綁定 ip, port
    struct sockaddr_in addr;
    addr.sin_port = htons(10000);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }
    // 3. 監聽
    ret = listen(lfd, 100);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }

    // 創建epoll樹
    int epfd = epoll_create(0);
    if(epfd == -1)
    {
        perror("epoll_create");
        exit(0);
    }

    // 將監聽lfd添加到樹上
    struct epoll_event ev;
    // 檢測事件的初始化
    ev.events = EPOLLIN ;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event events[1024];
    // 開始檢測
    while(1)
    {
        int nums = epoll_wait(epfd, events, sizeof(events)/sizeof(events[0]), -1);
        printf("numbers = %d\n", nums);
        
        // 遍歷狀態變化的文件描述符集合
        for(int i=0; i<nums; ++i)
        {
            int curfd = events[i].data.fd;
            // 有新連接
            if(curfd == lfd)
            {
                struct sockaddr_in clisock;
                int len = sizeof(clisock);
                int connfd = accept(lfd, (struct sockaddr*)&clisock, &len);
                if(connfd == -1)
                {
                    perror("accept");
                    exit(0);
                }
                // 將通信的fd掛到樹上
                //ev.events = EPOLLIN | EPOLLOUT;
                ev.events = EPOLLIN;
                ev.data.fd  = connfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }
            // 通信
            else
            {
                // 讀事件觸發, 寫事件觸發
                if(events[i].events & EPOLLOUT) 
                {
                    continue;
                }
                char buf[5];
                int count = read(curfd, buf, sizeof(buf));
                if(count == 0)
                {
                    printf("client disconnect ...\n");
                    close(curfd);
                    // 從樹上刪除該節點
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                }
                else if(count == -1)
                {
                    perror("read");
                    exit(0);
                }
                else
                {
                    // 正常情況傳輸數據,根據客戶端寫
                    //printf("client say: %s\n", buf);
                    write(STDOUT_FILENO, buf, count);
                    write(curfd, buf, strlen(buf)+1);
                }
            }
        }
    }
    close(lfd);
    return 0;
}

 

2.4.3 ET模式(邊沿觸發模式)

ET(edge-triggered)是高速工作方式,只支持no-block(非阻塞) socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll通知。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了。`但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)`。

ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高`。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。

例子:

假設委託內核檢測讀事件 -> 檢測fd的讀緩衝區
  
  - 讀緩衝區有數據 -> epoll檢測到了會給用戶通知
    - 用戶就是不讀, 數據一直在緩衝區中, epoll下次檢測的時候就不通知了

如何設置ET模式:把 epoll_event 結構體中的 events 參數並上 EPOLLET

// 設置爲邊沿觸發模式, 修改事件的屬性
  struct epoll_event {
  	uint32_t     events;      /* Epoll events */
  	epoll_data_t data;        /* User data variable */
  };
  Epoll檢測的事件:
  	- EPOLLIN
  	- EPOLOUT
  	- EPOLLERR
  	- EPOLLET   -> 設置邊沿觸發模式

設置通信文件描述符爲非阻塞:

  // 獲取文件描述flag屬性
  int flag = fcntl(fd, F_GETFL);
  // 添加非阻塞屬性
  flag = flag | O_NONBLOCK;
  // 將新的flag屬性設置給fd
  fcntl(fd, F_SETFL, flag);

 

  • epoll ET模式實現的io多路轉接 服務器端代碼模板示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main()
{
    // 1.創建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    // 2. 綁定 ip, port
    struct sockaddr_in addr;
    addr.sin_port = htons(10000);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }
    // 3. 監聽
    ret = listen(lfd, 100);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }

    // 創建epoll樹
    int epfd = epoll_create(1000);
    if(epfd == -1)
    {
        perror("epoll_create");
        exit(0);
    }

    // 將監聽lfd添加到樹上
    struct epoll_event ev;
    // 檢測事件的初始化
    ev.events = EPOLLIN ;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event events[1024];
    // 開始檢測
    while(1)
    {
        int nums = epoll_wait(epfd, events, sizeof(events)/sizeof(events[0]), -1);
        printf("numbers = %d\n", nums);
        
        // 遍歷狀態變化的文件描述符集合
        for(int i=0; i<nums; ++i)
        {
            int curfd = events[i].data.fd;
            // 有新連接
            if(curfd == lfd)
            {
                struct sockaddr_in clisock;
                int len = sizeof(clisock);
                int connfd = accept(lfd, (struct sockaddr*)&clisock, &len);
                if(connfd == -1)
                {
                    perror("accept");
                    exit(0);
                }
                // 將通信的fd掛到樹上
                //ev.events = EPOLLIN | EPOLLOUT;
                // 設置爲邊沿觸發
                // 設置fd屬性爲非阻塞
                int flag = fcntl(connfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(connfd, F_SETFL, flag);

                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd  = connfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }
            // 通信
            else
            {
                // 讀事件觸發, 寫事件觸發
                if(events[i].events & EPOLLOUT) 
                {
                    continue;
                }

                char buf[5];
                int count = 0;
                while( (count = read(curfd, buf, sizeof(buf))) > 0)
                {
                    // 處理read數據
                    write(STDOUT_FILENO, buf, count);
                    // 直接發送回客戶端
                    write(curfd, buf, count);
                }

                if(count == 0)
                {
                    printf("client disconnect...\n");
                }
                else if(count == -1)
                {
                    if(errno == EAGAIN)
                    {
                        printf("date over....\n");
                    }
                    else
                    {
                        perror("read");
                        exit(0);
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

 

 

 

 

 

發佈了152 篇原創文章 · 獲贊 149 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章