【網路】Select伺服器的實現
五種I/O模型
Unix下共有五種I/O模型,分別是
(1)阻塞式I/O;
(2)非阻塞I/O;
(3)I/O複用(select和(e)poll);
(4)訊號驅動I/O(SIGIO);
(5)非同步I/O(Posix.1的aio_系列函式);
阻塞I/O模型
應用程式呼叫一個IO函式,導致應用程式阻塞,等待資料準備好。如果資料沒有準備好,一直等待。資料準備好了,從核心拷貝到使用者空間,表示IO結束,IO函式返回成功指示。非阻塞I/O模型
我們把一個套介面設定為非阻塞就是告訴核心,當所請求的I/O操作無法完成時,不要將程序睡眠,而是返回一個錯誤。這樣我們的I/O操作函式將不斷的測試 資料是否已經準備好,如果沒有準備好,繼續測試,直到資料準備好為止。在這個不斷測試的過程中,會大量的佔用CPU的時間。
I/O複用模型
該種模型又被稱作是多路轉接,I/O複用模型會用到select或者poll函式,這兩個函式也會使程序阻塞,但是和阻塞I/O所不同的的,這兩個函式可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函式進行檢測,直到有資料可讀或可寫時,才真正呼叫I/O操作函式。訊號驅動I/O模型
首先我們允許套介面進行訊號驅動I/O,並安裝一個訊號處理函式,程序繼續執行並不阻塞。當資料準備好時,程序會收到一個SIGIO訊號,可以在訊號處理函式中呼叫I/O操作函式處理資料。非同步I/O模型
呼叫aio_read函式,告訴核心描述字,緩衝區指標,緩衝區大小,檔案偏移以及通知的方式,然後立即返回。當核心將資料拷貝到緩衝區後,再通知應用程式。也就是它只需要發起這個讀寫事件,不要等待與資料搬遷,只需要在結束之後得到成果。舉例
(1)張三在釣魚,當魚沒有上鉤時,便一直進行等待;當魚上鉤後,將魚調出。這便是基本的阻塞式I/O
(2)張三在釣魚,這次,當魚沒有上鉤時,他便翻著看《C語言入門》這本書,也可以玩玩手機;當魚上鉤後,將魚調出。這是非阻塞I/O
(3)張三在釣魚,這次他放了一百個魚竿。依次檢查這些魚竿,當一個魚竿有魚上鉤後,將魚調出,然後繼續檢查所有魚竿。這是I/O多路複用
(4)張三釣魚時,將魚竿上放一個鈴鐺,當魚上鉤後會響。然後他便可以幹自己的事情,當領響時,他知道上鉤了,再去收魚
(5)張三準備釣魚,這次。。他直接僱了個人給自己釣魚。這便是非同步I/O
相關函式介紹
重定向的dup函式
標頭檔案
#include<unistd.h>
函式原型
int dup(int oldfd);
int dup2(int oldfd, int newfd);
功能
dup
將oldfd檔案描述符進行一份拷貝,返回值為最新拷貝生成的檔案描述符(最小的未被使用的檔案描述符)
dup2
使用newfd對oldfd檔案描述符做一份拷貝,必要是可以先關閉newfd檔案描述符
呼叫dup2之後,oldfd檔案描述符不變,newfd和oldfd相等。
程式碼測試
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
int main()
{
umask(0);
int fd = open("test.txt",O_CREAT|O_RDWR,0666);
const char* msg = "hello world";
printf("fd -> %d\n",fd);
write(fd,msg,strlen(msg));
int new_fd = dup(fd);//將fd進行拷貝,儲存到new_fd中
printf("new_fd -> %d\n",new_fd);
write(new_fd,msg,strlen(msg));
int cpfd = dup(1);//拷貝標準輸出檔案描述符
dup2(fd,1);
printf("nice\n");
dup2(cpfd,1);
printf("niec\n");
return 0;
}
執行結果
select函式
標頭檔案
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
函式原型
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *expectfds,struct timeval *timeout);
函式功能
同時等待多個檔案描述符
引數
nfds
表示的是等待的檔案描述符中的最大的那個+1;
fd_set
表示的是是否需要等待某個檔案描述符,所以這裡的fd_set底層是用點陣圖實現的,所以我們最多可以等待的檔案描述符的個數為sizeof(fd_set)*8;
readfds
表示的是需要等待的讀事件的檔案描述符集;
writefds
表示的是需要等待的寫事件的檔案描述符集;
exceptfds
表示的是需要等待的異常事件的檔案描述符集;
timeout
表示的是每次select的時間為ty_sec秒
返回值
返回集合,該集合表示了收到訊息的檔案描述符與fd_set有關的函式
fd_set為一個集合,底層是用點陣圖進行實現的
它的每一位都可以用來表示對應的檔案描述符是否收到了事件訊息
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
void FD_CLR(int fd, fd_set *set);//將檔案描述符中的fd位去掉
int FD_ISSET(int fd, fd_set *set);//檢測檔案描述符集set中的fd位是否存在
void FD_SET(int fd, fd_set *set);//為set檔案描述符集設定fd為設定
void FD_ZERO(fd_set *set);//將set檔案描述符集清空
Select模型
理解select模型的關鍵在於理解fd_set
假設fd_set長度為1位元組
fd_set中的每⼀一bit
可以對應⼀一個⽂檔案描述符fd
則1位元組長的fd_set最⼤大可以對應8個fd
步驟:
(1)執⾏行fd_set set; FD_ZERO(&set);則set⽤用位表⽰示是0000,0000。
(2)若fd=5,執⾏行FD_SET(fd,&set);後set變為0001,0000(第5位置為1)
(3)若再加⼊入fd=2,fd=1,則set變為0001,0011
(4)執⾏行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發⽣生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件
發⽣生的fd=5被清空
也就是說,readfds[]陣列來表示連線的客戶端,該陣列大小表示最多可以連線的數量
而rfds是一個集合,底層用點陣圖實現
表示的是對應的檔案描述符有沒有收到對方發來的訊息
若有,則對應的位上被置為1
程式碼實現
select伺服器
#include<stdio.h>
#include<stdlib.h>
#include<error.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/time.h>
#include<string.h>
int readfds[sizeof(fd_set)*8];
int writefds[sizeof(fd_set)*8];
static void Usage()
{
printf("Usage: [ipaddr] [port]\n");
}
int startUp(char* ip, int port)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sockfd,5)<0)
{
perror("listen");
exit(4);
}
return sockfd;
}
int main(int argc, char* argv[])
{
int i = 1;
if(argc != 3)
{
Usage();
exit(1);
}
int listen_sock = startUp(argv[1],atoi(argv[2]));
int num = sizeof(fd_set)*8;
writefds[0] = -1;
readfds[0] = listen_sock;
for(; i<num; i++)
{
writefds[i] = -1;
readfds[i] = -1;
}
fd_set rfds,wfds;
while(1)
{
int maxfd = -1;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
for(i = 0; i<num; ++i)
{
if(readfds[i] != -1)
{
FD_SET(readfds[i],&rfds);
}
if(writefds[i] != -1)
{
FD_SET(writefds[i],&wfds);
}
maxfd = readfds[i] > maxfd ? readfds[i] : maxfd;
maxfd = writefds[i] > maxfd ? writefds[i] : maxfd;
}
struct timeval time = {1,0};
int n = select(maxfd+1,&rfds,&wfds,NULL,&time);
switch(n)
{
case 0:
printf("time out...\n");
break;
case -1:
break;
default:
{
for(i = 0; i<num; ++i)
{
if(FD_ISSET(readfds[i],&rfds))
{
if(i == 0)
{//監聽伺服器就緒
struct sockaddr_in client;
socklen_t len = sizeof(client);
int client_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(client_sock < 0)
{
perror("accpet");
exit(5);
}
else
{
int tmp = 0;
for(; tmp < num; ++tmp)
{
if(readfds[tmp] == -1)
{
readfds[tmp] = client_sock;
break;
}
}
if(tmp == num)
{
printf("readfds 滿了\n");
exit(6);
}
}
}
else
{//等待的普通檔案描述符就緒
char buf[1024];
ssize_t s = read(readfds[i],buf,sizeof(buf));
if(s < 0)
{
perror("read");
exit(7);
}
else if(s == 0)
{
printf("客戶端退出..\n");
close(readfds[i]);
readfds[i] = -1;
continue;
}
else
{
buf[s] = 0;
printf("#client: %s",buf);
fflush(stdout);
write(readfds[i],buf,strlen(buf));
}
}
}
}
}
}
}
return 0;
}
select客戶端
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
static void Usage()
{
printf("Usage: [ipaddr] [port]\n");
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage();
exit(1);
}
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0)
{
perror("connect");
exit(3);
}
printf("連線成功...\n");
char buf[1024];
while(1)
{
//發資料
printf("#client: ");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s <= 0)
{
perror("read");
exit(4);
}
int fd = dup(1);
dup2(sockfd,1);
printf("%s",buf);
fflush(stdout);
dup2(fd,1);
//收資料
s = read(sockfd,buf,sizeof(buf)-1);
if(s == 0)
{
printf("伺服器退出...\n");
break;
}
else if(s < 0)
{
perror("read");
exit(5);
}
else
{
buf[s-1] = '\0';
printf("#server: %s\n",buf);
}
}
close(sockfd);
return 0;
}
select伺服器優缺點
優點
1、不需要fork或者pthread_create就可以實現一對多的通訊,簡化了程序執行緒的使用
2、同時等待多個檔案描述符,效率相對較高
缺點
1、每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,在fd很多的情況下,迴圈次數多;
2、每次呼叫select都需要在核心遍歷傳遞進來的所有fd,開銷比較大
3、select支援的檔案描述符數量過小,預設是1024