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 結構用於指定這段時間的秒數和微秒數。
- nfds:要監視的檔案描述符的範圍,一般取監視的描述符數的最大值+1,如這裡寫 10, 這樣的話,描述符 0,1, 2 …… 9 都會被監視,在 Linux 上最大值一般為1024;
- 返回值:成功:就緒描述符的數目,超時返回 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