1. 程式人生 > >IO 多路複用之select(高效併發伺服器)

IO 多路複用之select(高效併發伺服器)

一、I/O 多路複用概述

  I/O 多路複用技術是為了解決程序或執行緒阻塞到某個 I/O 系統呼叫而出現的技術,使程序不阻塞於某個特定的 I/O 系統呼叫。

  select,poll,epoll都是I/O多路複用的機制。I/O多路複用通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒,就是這個檔案描述符進行讀寫操作之前),能夠通知程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。

  與多執行緒和多程序相比,I/O 多路複用的最大優勢是系統開銷小,系統不需要建立新的程序或者執行緒,也不必維護這些執行緒和程序。

【I/O多路複用使用的場合】:

  • 當客戶處理多個描述符(通常是互動式輸入、網路套接字)時,必須使用I/O多路複用;
  • tcp伺服器既要處理監聽套接字,又要處理已連線套接字,一般要使用I/O多路複用;
  • 如果一個伺服器既要處理tcp又要處理udp,一般要使用I/O多路複用;
  • 如果一個伺服器要處理多個服務時,一般要使用I/O多路複用。

二、select函式

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 功能:輪詢監視並等待多個檔案描述符的屬性變化(可讀、可寫或錯誤異常);
  • 引數
    • nfds:要監視的檔案描述符的範圍,一般取監視的描述符數的最大值+1,如這裡寫 10, 這樣的話,描述符 0,1, 2 …… 9 都會被監視,在 Linux 上最大值一般為1024;
    • readfd:監視的可讀描述符集合,只要有檔案描述符即將進行讀操作,這個檔案描述符就儲存到這;
    • writefds:監視的可寫描述符集合;
    • exceptfds:監視的錯誤異常描述符集合;
    • timeout:超時時間,它告知核心等待所指定描述字中的任何一個就緒可花多少時間。其 timeval 結構用於指定這段時間的秒數和微秒數。
  • 返回值:成功:就緒描述符的數目,超時返回 0,出錯:-1。

  中間的三個引數 readfds、writefds 和 exceptfds 指定我們要讓核心監測讀、寫和異常條件的描述字。如果不需要使用某一個的條件,就可以把它設為空指標( NULL )。集合fd_set 中存放的是檔案描述符,可通過以下四個巨集進行設定:

// 清空集合
void FD_ZERO(fd_set *fdset); 
// 將一個給定的檔案描述符加入集合之中
void FD_SET(int fd, fd_set *fdset);
// 將一個給定的檔案描述符從集合中刪除
void FD_CLR(int fd, fd_set *fdset);
 // 檢查集合中指定的檔案描述符是否可以讀寫 
int FD_ISSET(int fd, fd_set *fdset);

三、select高併發伺服器的流程

#include <標頭檔案>

int main(int argc, char const *argv[])
{
	lfd = socket();
	bind();
	listen();
	fd_set rset, allset; // 讀集合,所有描述符集合
	int maxfd = lfd; // 最大描述符
	FD_ZERO(&allset); // 所有描述符清零
	FD_SET(lfd, &allset); // 把listen返回的描述符置1
	vector<int> flag;
	while(1)
	{
		rset = allset; // allset是想監聽的套接字描述符集合,rset是實際返回的套接字描述符集合
		// select IO多路複用
		// rset是傳入傳出引數,傳入是想監聽的檔案描述符,返回是實際監聽到的檔案描述符
		int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
		if (nready > 0)
		{
			if (FD_ISSET(lfd, &rset)) // 判斷lfd是否在監聽集合中
			{
				cfd = accept();
				// 把cfd加入到想監聽的檔案描述符集合中
				FD_SET(cfd, &allset);
				flag.push_back(cfd);
				if (maxfd < cfd)
					maxfd = cfd;
			}
			// 掃描所有檔案描述符,看是否有讀操作(最大不超過1024)
			for (int i = 0; i < flag.size(); ++i)
			{
				// i所在的檔案描述符有讀操作
				if (FD_ISSET(flag[i], &rset))
					/*事務處理*/
			}
		}
	}	
	close(lfd);
	return 0;
}

四、select高併發伺服器demo(tcp)

#pragma GCC diagnostic error "-std=c++11"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <ctype.h>
#include <vector>
using namespace std;

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char **argv)
{
    int lfd, cfd;
    socklen_t clt_addr_len;
    struct sockaddr_in srv_addr, clt_addr;
    // 將地址結構清零(按位元組),容易出錯(後面兩個引數容易顛倒)
    // memset(&srv_addr, 0, sizeof(srv_addr));
    // bzero也可以用來清零操作 
    bzero(&srv_addr, 0);
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(8080);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int opt = 1;
    // 設定套接字選項
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 建立套接字
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 繫結套接字
    bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
    
    // 監聽客戶端的連線
    listen(lfd, 128);
    
    fd_set rset, allset; // 讀集合,所有描述符集合
    int maxfd = lfd; // 最大描述符
    FD_ZERO(&allset); // 所有描述符清零
    FD_SET(lfd, &allset); // 把listen返回的描述符置1

    char buf[512];
    vector<int> flag;

    while (1)
    {
        rset = allset; // allset是想監聽的套接字描述符集合,rset是實際返回的套接字描述符集合
        // select IO多路轉接
        // rset是傳入傳出引數,傳入是想監聽的檔案描述符,返回是實際監聽到的檔案描述符
        int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nready < 0)
        {
            sys_err("select");
        }
        if (FD_ISSET(lfd, &rset)) // 判斷lfd是否在監聽集合中
        {
            clt_addr_len = sizeof(clt_addr);
            // 非阻塞接收客戶端的連線
            cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
            memset(buf, 0, 512);
            // 列印已經連線的客戶端的資訊
            cout << "客戶端連線:" << inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, buf, sizeof(buf)) 
                 << "," << ntohs(clt_addr.sin_port) << endl;
            // 把cfd加入到想監聽的檔案描述符集合中
            FD_SET(cfd, &allset);
            flag.push_back(cfd);

            // 更新最大描述符
            if (maxfd < cfd)
                maxfd = cfd;
            if (0 == --nready) // 說明select只返回一個lfd,即沒有客戶端連線上來,則無須執行後面的內容
                continue;
        }
        // 掃描所有檔案描述符,看是否有讀操作
        for (int i = 0; i < flag.size(); ++i)
        {
            if (FD_ISSET(flag[i], &rset)) // i所在的檔案描述符有讀操作
            {
                memset(buf, 0, 512);
                // 接收來自客戶端的資料
                recv(flag[i], buf, sizeof(buf), 0);
                int ret = strlen(buf);
                if (ret == 0) // 讀套接字返回零表明客戶端關閉了
                {
                    close(flag[i]);
                    cout << "客戶端關閉:" << inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, buf, sizeof(buf)) 
                        << "," << ntohs(clt_addr.sin_port) << endl;
                    FD_CLR(flag[i], &allset); // 解除select對此檔案描述符的監聽
                }
                for (int i = 0; i < ret; ++i)
                    buf[i] = toupper(buf[i]);
                // 回射到客戶端
                send(flag[i], buf, ret, 0);
                // 客戶端寫到標準輸出
                write(STDOUT_FILENO, buf, ret);
            }
        }
    }
    close(lfd);
    return 0;
}

五、select高併發伺服器總結

【優點】:

  select目前幾乎在所有的平臺上支援,其良好跨平臺支援也是它的一個優點。

【缺點】:

  • 每次呼叫 select(),都需要把 fd 集合從使用者態拷貝到核心態,這個開銷在 fd 很多時會很大,同時每次呼叫 select() 都需要在核心遍歷傳遞進來的所有 fd,這個開銷在 fd 很多時也很大;
  • 單個程序能夠監視的檔案描述符的數量存在最大限制,在 Linux 上一般為 1024,可以通過修改巨集定義甚至重新編譯核心的方式提升這一限制,但是這樣也會造成效率的降低。

  為什麼select只能監聽1024個檔案描述符?
在這裡插入圖片描述
在這裡插入圖片描述

  核心定義了fd_set中1024為監聽個數上限同時也是檔案描述符上限,如果要擴大,只能重新編譯核心。

參考:https://blog.csdn.net/tennysonsky/article/details/45745887
https://www.cnblogs.com/99code/p/5829425.html