Linux高效能伺服器程式設計——I/O複用 select
提出背景
不管是多執行緒,或者多程序,以及執行緒池,程序池。他們都存在一定的效率問題。
1.每個程序或執行緒只能為一個客戶端進行服務,知道該客戶端結束。(如果客戶端在同一時間的訪問數量特別大呢?)
2.當客戶端傳送來資料後,分配執行緒或程序為其服務完後,就要等待該客戶端的下一次資料。(如果該客戶端連線成功僅僅發了一次資料呢?)
他們雖然能夠進行與客戶的互動服務,但是當在規定的時間內,系統(或者池)分配不出足夠的程序或執行緒,達不到及時響應的要求。
I/O複用
1.決解的問題:一個程序或執行緒能夠同時對多個檔案描述符(socket)進行服務。
2.伺服器上的程序或執行緒 如何將多個檔案描述符進行同一監聽,當任意檔案描述符上有事件發生,其都能夠及時處理。
列舉幾種我們要學習的I/O複用技術:1.select ; 2. poll ; 3. epoll(Linux獨有的)。
select函式
1.函式原型:
#include<sys/select.h>
int select (int nfds , fd_set *readfds , fd_set *writefds , fd_set *excefds , struct timeval*timeout);
2.fd_set結構體
#include<typesizes.h> #define _FD_SETSIZE 1024 //決定了fd_set能容納的檔案描述符數量 #include<sys/select.h> #define FD_SETSIZE _FD_SETSIZE typedef long_int _fd_mask; #undef _NFDBITS #define _NFDBITS { 8 *(int)sizeof(_fd_mask) ) // 8 * 4 = 32 typedef struct { #ifdef _USE_XOPEN _fd_mask fds_bits[_FD_SETSIZE / _NFDBITS]; //位陣列 1024 / 32 = 32 #define _FDS_BITS(set) ((set) -> fds_bits) #else fd_mask _fds_bits[_FD_SETSIZE / _NFDBITS]; //位陣列 1024 / 32 = 32 #define _FDS_BITS(set) ((set) -> fds_bits) #endif }fd_set;
其實呢,簡而言之就是
typedef struct
{
int fds_bits[32];
}fd_set;
fd_set結構體僅僅包含一個整型陣列,該陣列的每一個元素的每一位(bit)標記一個檔案描述符。int型別,總共32個元素,那麼總共可以標記32 * 32 = 1024個檔案描述符。
由於位運算操作過於繁瑣,提供了一系列的巨集來訪問fd_set結構體中的位:
標頭檔案: | #include<sys/select.h> |
清除fdset的所有位 | FD_ZERO(fd_set *fdset); |
設定fdset的位fd | FD_SET(int fd, fd_set *fdset); |
清除fdset的位fd | FD_CLR( int fd , fd_set *fdset); |
測試fdset的位fd是否被設定 | int FD_ISSET(int fd , fd_set *fdset); |
3.引數介紹
1.nfd: | 被監聽檔案描述符總數 + 1 |
2.readfds: | 使用者感興趣的可讀事件的檔案描述符集合 |
3.writefds:
|
可寫事件的檔案描述符的集合 |
4.exceptfds: | 異常事件的檔案描述符集合 |
5.timeout | 設定超時時間,如果timeout設定為NULL,則select一直阻塞,直到某個檔案就緒。 |
4.如何將檔案描述符分別設定到readfds, writefds , exceptfds裡?
通過位運算巨集函式FD_SET()。
5.select返回後,如何知道哪些檔案描述符已經就緒?
迴圈探測;通過巨集函式int FD_ISSET(int fd , fd_set *fdset);
6.每次呼叫select之前,需要做什麼準備工作?
將所有的readfds , writefds , exceptfds全部都置為空。
邏輯理念
測試程式碼
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
void Init_fds(int* fds,int len)
{
int i = 0;
for(;i<len;++i)
{
fds[i] = -1;//都是無效的
}
}
void Delete_fds(int* fds,int fd,int len)
{
for(int i = 0;i<len;++i)
{
if(fds[i] == fd)
{
fds[i] = -1;
break;
}
}
}
void Insert_fds(int* fds,int fd,int len)
{
int i =0;
for(;i<len;++i)
{
if(fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
int main()
{ //完成TCP連線服務
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd, (struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
//fd_set,並將sockfd插入進去
fd_set readfds;
int fds[100];//陣列
Init_fds(fds,100);//初始化全部設定為-1,均為無效的檔案描述符。
Insert_fds(fds,sockfd,100);//將sockfd新增到fds中
while(1)
{
int maxfd = -1;
FD_ZERO(&readfds);//清空
for(int i = 0;i<100;++i)//fds[100]的迴圈探測
{
if(fds[i] != -1)//說明陣列有了就緒的事件
{
if((fds[i] > maxfd))//對於maxfd的處理
{
maxfd = fds[i];
}
FD_SET(fds[i],&readfds);//設定fd位。
}
}
int n = select(maxfd + 1,&readfds,NULL,NULL,NULL);//讀取
if(n<=0)
{
printf("select is fail:\n");
continue;
}
//n>0有檔案描述符就緒,如何進行探測呢?
for(int i = 0;i < 100;++i)//迴圈探測
{
if(fds[i] != -1 && FD_ISSET(fds[i],&readfds))//探測fdset的位fd是否被設定
//就緒
{
if(fds[i] == sockfd)//連線請求
{
int len = sizeof(cli);
int c = accept(sockfd,(struct sockaddr*)&cli,(socklen_t*)&len);
if(c<0)
{
printf("accept is fail:\n");
continue;
}
Insert_fds(fds,c,100);//對這個c加入到 readfds中fds[100]
}
else
{
int fd = fds[i];
char buff[128]={0};//有資料可讀取
int n= recv(fd,buff,127,0);//只讀取一次
if(n<=0)
{
close(fd);
Delete_fds(fds,fd,100);
continue;
}
else
{
printf("%d: %s\n",fd,buff);
send(fd,"OK",2,0);
}
}
}
}
}
close(sockfd);
}