目錄
I/O復(fù)用技術(shù)
select函數(shù)
設(shè)置文件描述符
指定監(jiān)視范圍
設(shè)置超時
I/O復(fù)用服務(wù)器端的實現(xiàn)
??????由服務(wù)器創(chuàng)建多個進程來實現(xiàn)并發(fā)的做法有時會帶來一些問題,比如:內(nèi)存上的開銷、CPU的大量占用等,這些因素會消耗掉服務(wù)器端有限的計算資源、進而影響程序之間的執(zhí)行效率。那么,有沒有方法可以在不創(chuàng)建額外進程的條件下實現(xiàn)并發(fā)呢?當(dāng)然有,那就是I/O復(fù)用技術(shù)。
I/O復(fù)用技術(shù)
????????I/O復(fù)用指的是通過單個線程記錄一個或多個I/O流的狀態(tài),并對不同狀態(tài)下的I/O流進行協(xié)調(diào),使進程不阻塞于某個特定的I/O調(diào)用過程中,從而將有限資源最大化利用。
? ? ? ? 引用自一段情景材料,方便大家能更好地理解這個技術(shù)背后的思想:
假設(shè)你是一個機場的空管,你需要管理到你機場的所有的航線,包括進港,出港,有些航班需要放到停機坪等待,有些航班需要去登機口接乘客。
你會怎么做?
最簡單的做法,就是你去招一大批空管員,然后每人盯一架飛機,從進港,接客,排位,出港,航線監(jiān)控,直至交接給下一個空港,全程監(jiān)控。
那么問題就來了:
- 很快你就發(fā)現(xiàn)空管塔里面聚集起來一大票的空管員,交通稍微繁忙一點,新的空管員就已經(jīng)擠不進來了。
- 空管員之間需要協(xié)調(diào),屋子里面就1, 2個人的時候還好,幾十號人以后,基本上就成菜市場了。
- 空管員經(jīng)常需要更新一些公用的東西,比如起飛顯示屏,比如下一個小時后的出港排期,最后你會很驚奇的發(fā)現(xiàn),每個人的時間最后都花在了搶這些資源上。
現(xiàn)實上我們的空管同時管幾十架飛機稀松平常的事情, 他們怎么做的呢?他們用這個東西:
這個東西叫 flight progress strip,每一個塊代表一個航班,不同的槽代表不同的狀態(tài),然后一個空管員可以管理一組這樣的塊(一組航班),而他的工作,就是在航班信息有新的更新的時候,把對應(yīng)的塊放到不同的槽子里面。這個東西現(xiàn)在還沒有淘汰哦,只是變成電子的了而已。是不是覺得一下子效率高了很多,一個空管塔里可以調(diào)度的航線可以是前一種方法的幾倍到幾十倍。?
如果你把每一個航線當(dāng)成一個 Sock(I/O 流),空管當(dāng)成你的服務(wù)端 Sock 管理代碼的話:
- 第一種方法就是最傳統(tǒng)的多進程并發(fā)模型:每進來一個新的 I/O 流會分配一個新的進程管理。
- 第二種方法就是 I/O 多路復(fù)用:單個線程,通過記錄跟蹤每個 I/O 流(sock)的狀態(tài),來同時管理多個 I/O 流 。?
????????????????????????????????????????????????????????????????????????參考資料:IO 多路復(fù)用是什么意思? - 羅志宇
select函數(shù)
????????select是I/O復(fù)用技術(shù)中比較經(jīng)典且常用到的一個函數(shù)(還有poll、epoll),在使用上,需要引入<sys/select.h>和<sys/time.h>兩個頭文件。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd , fd_set * readset , fd_set * writeset , fd_set * exceptset , const struct timeval * timeout);
//成功時返回大于0的值,失敗時返回-1。其余情況為返回發(fā)生事件(監(jiān)視項)的文件描述符數(shù)量
/* 監(jiān)視項 */
// 1.接收數(shù)據(jù)的套接字
// 2.傳輸數(shù)據(jù)(無阻塞)的套接字
// 3.發(fā)生異常的套接字
/* 參數(shù)含義 */
// maxfd: 監(jiān)視對象文件描述符數(shù)量
// readset: 將所有關(guān)注"是否存在待讀取數(shù)據(jù)"的文件描述符注冊到fd_set型變量,并傳遞其地址值。
// writeset: 將所有關(guān)注"是否可傳輸無阻塞數(shù)據(jù)"的文件描述符注冊到fd_set型變量,并傳遞其地址值。
// exceptset: 將所有關(guān)注"是否發(fā)生異常"的文件描述符注冊至fd_set型變量,并傳遞其地址值。
// timeout: 調(diào)用 select 函數(shù)后,為防止陷入無限阻塞的狀態(tài),傳遞超時(time-out)消息。
????????使用select函數(shù)時,一般遵循以下流程步驟:
設(shè)置文件描述符
????????在調(diào)用select函數(shù)之前,我們需要聲明監(jiān)視事件,并用數(shù)據(jù)類型為fd_set的變量來記錄監(jiān)視事件下的每個文件描述符的狀態(tài)。如圖所示,fd_set以數(shù)組的形式記錄每個文件描述符的狀態(tài):
????????以圖中fd0、fd2為例,值為0,代表著所指向的文件描述符不是被監(jiān)視對象;反之,fd1和fd3值為1,則是被監(jiān)視對象。
????????在對fd_set數(shù)組操作時,有些宏可以幫助我們簡化代碼的編寫工作,下列所示為與對fd_set操作相關(guān)的宏:
- FD_ZERO(fd_set * fdset): 將fd_set變量的所有位初始化為0 。
- FD_SET(int fd , fd_set * fdset): 在參數(shù)fdset指向的變量中注冊文件描述符fd的信息。
- FD_CLR(int fd , fd_set * fdset): 從參數(shù)fdset指向的變量中清除文件描述符fd的信息。
- FD_ISSET(int fd , fd_set * fdset): 若參數(shù)fdset指向的變量中包含文件描述符fd的信息,則返回"真",用于對select調(diào)用結(jié)果的驗證。
????????宏對應(yīng)的操作含義如圖所示:
指定監(jiān)視范圍
????????即設(shè)置select函數(shù)中的第一個參數(shù) maxfd,其值用來標(biāo)記限制對監(jiān)視事件中的文件描述符最大監(jiān)視數(shù)。
設(shè)置超時
????????當(dāng)監(jiān)視的文件描述符未發(fā)生變化時,select函數(shù)會導(dǎo)致進程發(fā)生阻塞。為了避免這種情況發(fā)生,我們可以通過設(shè)置超時(select函數(shù)中的最后一個參數(shù)timeout)來防止這種情況的發(fā)生。
????????timeout的結(jié)構(gòu)體定義如下:
struct timeval
{
long tv_sec; // 秒
long tv_usec; // 毫秒
}
//timeval.tv_sec=5,timeval.tv_usec=500; 代表設(shè)置超時等待周期為5秒500毫秒
I/O復(fù)用服務(wù)器端的實現(xiàn)
io_echoserver.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 1024
void Sender_error(char *message);
int main(int argc, char *argv[])
{
int port, serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
//可以改為直接傳參(即使用argv數(shù)組),這里主要方便驗證和展示
printf("Please input the port of socket that you want to create:\n");
scanf("%d", &port);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
{
Sender_error((char *)"Sock creation error");
}
else
{
// 注意:serv_sock初始化成功后值為0
fd_max = serv_sock;
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(port);
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
{
Sender_error((char *)"Bind() error");
}
if (listen(serv_sock, 5) == -1)
{
Sender_error((char *)"Listen error");
}
// 對監(jiān)測項的文件描述符作初始化賦0操作
FD_ZERO(&reads);
// 注冊serv_sock套接字信息至reads變量中
FD_SET(serv_sock, &reads);
while (1)
{
// cpy_reads用來記錄文件描述符變化
cpy_reads = reads;
// 設(shè)置超時等待周期為5s
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 只關(guān)注接收數(shù)據(jù)的套接字,不對傳輸數(shù)據(jù)、出現(xiàn)異常的套接字進行監(jiān)視
// fd_num用來記錄發(fā)生監(jiān)視事件的文件描述符數(shù)量
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
{
break;
}
if (fd_num == 0)
{
continue;
}
for (i = 0; i < fd_max + 1; i++)
{
// 判斷cpy_reads中是否含有文件描述符i的信息
if (FD_ISSET(i, &cpy_reads))
{
// 若當(dāng)前只有服務(wù)器端套接字,則嘗試接收客戶端請求
if (i == serv_sock)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
// 將客戶端的套接字文件描述符信息注冊至reads中
FD_SET(clnt_sock, &reads);
if (fd_max < clnt_sock)
{
// 增加監(jiān)視數(shù)上限(因為有新的客戶端套接字加入)
fd_max = clnt_sock;
}
printf("Connected client: %d \n", clnt_sock);
}
else
{
// 接收數(shù)據(jù)
str_len = read(i, buf, BUF_SIZE);
// 無數(shù)據(jù),則關(guān)閉對應(yīng)套接字
if (str_len == 0)
{
// 清除reads變量中文件描述符i的信息
FD_CLR(i, &reads);
close(i);
printf("Closed client: %d \n", i);
}
else
{
write(i, buf, str_len);
}
}
}
}
}
close(serv_sock);
return 0;
}
void Sender_error(char *message)
{
puts(message);
exit(1);
}
運行結(jié)果:文章來源:http://www.zghlxwxcb.cn/news/detail-483139.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-483139.html
到了這里,關(guān)于【TCP/IP】利用I/O復(fù)用技術(shù)實現(xiàn)并發(fā)服務(wù)器 - select函數(shù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!