1、前言
1.1、IO模型
下面用服務器比成車站,客戶端比喻成小明。
- 傳統阻塞模型:小明去車站買票,沒買到票就在車站等待,直到有車票爲止。
- 非阻塞模型:小明去車站買票,沒票的話,他沒過一段時間就去看看有沒有票,沒有票就回去。他消耗了來回的這一個過程,但是不用等待。
- 多路轉接IO複用:委託黃牛來購票。select模型就是屬於這一類。
下面我用兩張圖來描述兩種
1.2、select模型概念
- select能監聽的文件描述符個數受限於FD_SETSIZE,一般爲1024,單純改變進程 打開的文件描述符個數,並不能改變select監聽文件個數。
- 解決1024以下客戶端時使用select是很合適的,但如果鏈接客戶端過多,select採用 的是輪詢模型,會大大降低服務器響應效率,不應在select上投入更多精力(即超過了這個1024就不適合使用這個模型了)
1.3、select模型使用場景
一般常用在局域網,一些部分公司有在用,現在一般不常用,但是要學其他模型的話,這個模型是基礎。
2、select模型需要用到的函數
2.1、模型建立主要函數
- 需要的頭文件:
- #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,因爲此參數會告訴內核檢測前多少個文件描述符的狀態 。比如在下面的文件描述符表中,最大的是在28位,那麼我們在這個參數就要寫28+1個
-
readfds:監控有讀數據到達文件描述符集合,傳入傳出參數
-
writefds:監控寫數據到達文件描述符集合,傳入傳出參數
-
exceptfds:監控異常發生達文件描述符集合,如帶外數據到達異常,傳入傳出參數
-
timeout:定時阻塞監控時間,3種情況
- NULL,永遠等下去
- 設置timeval,等待固定時間
- 設置timeval裏時間均爲0,檢查描述字後立即返回(意思就是輪詢)
-
- 返回值:所監聽的所有文件描述符 滿足的總數,返回的是int類型。
比如在下圖中,2、3、4分別表示的是第二、第三、第四個參數,他們都傳進去的A、B、C三個字符集,畫黃色圈的就是符合對應功能的(比如2集合的,b就符合可讀)。那麼這個函數的返回值將會是4。
2.2、文件描述符集合處理函數
這一小節主要就是要爲第2、3、4個參數服務的,我們要做的就是將文件操作符加入到字符集中。
void FD_CLR(int fd, fd_set *set); 把文件描述符集合裏fd清0
int FD_ISSET(int fd, fd_set *set); 測試文件描述符集合裏fd是否置1,如果有返回1,無則返回0
void FD_SET(int fd, fd_set *set); 把文件描述符集合裏fd位置1
void FD_ZERO(fd_set *set); 把文件描述符集合裏所有位清0
使用步驟:
- 建立fd_set 類型文件描述符集合 變量
- 使用FD_ZERO將該集合清零(就類似於初始化的步驟)
- 使用FD_SET將客戶端的文件操作符加入指定集合
- 使用上文2.1中的select函數測得符合條件的總數。
- 遍歷循環,使用FD_ISSET判斷某個文件描述符,在指定的集合中是否符合條件。(看誰就緒了)
3、示例代碼——生產消費者模型
在下面的代碼會有點多,我主要分成3大部分
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 8000 //端口號
int main()
{
int i, maxi, maxfd, connfd, sockfd;
int listenfd; //監聽套接字建立
int nready, client[FD_SETSIZE]; //FD_SETSIZE 時FD的一個宏,默認爲 1024
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16
socklen_t cliaddr_len;
struct sockaddr_in cliaddr, servaddr; //socket需要的sockaddr_in變量
/*第一部分,socket基礎部分*/
//套接字初始化
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//綁定
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任意ip號
servaddr.sin_port = htons(SERV_PORT); //綁定端口號
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
//監聽
listen(listenfd, 20); //最多監聽20個用戶
//尋找最大的文件描述符,並且清空一下client數組
maxfd = listenfd; //在文件創建符裏,目前最大的是listenfd,因爲你這個代碼裏面現在只創建了一個listenfd文件操作符
maxi = -1; // client[]的下標,具體請看後面的解析
for (i = 0; i < FD_SETSIZE; i++)
{
client[i] = -1; // 用-1初始化client[]
}
//FD_ZERO初始化集合,把監聽套接字加入FD_SET
FD_ZERO(&allset); //清空allset字符集
FD_SET(listenfd, &allset); //把listenfd加入到allset字符集,後面用於select函數的監聽
/*第二部分,判斷誰就緒了*/
for ( ; ; )
{
rset = allset; /* 每次循環時都重新設置select監控信號集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //用nready 來接收select返回的總數,這裏我們只監聽一個rset文件操作符集,符合可讀條件的文件操作符個數將會返回
//如果沒有符合的就報錯
if (nready < 0)
{
perror("select error");
}
//如果listenfd在rset裏爲1的話,就代表有客戶端要對服務器訪問
if (FD_ISSET(listenfd, &rset))
{
/*新的client連接 */
cliaddr_len = sizeof(cliaddr);
//客戶端連接上,並且記錄IP和端口號
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
{
//在client[]找到空位,保存,因爲我前面有給client[]初始化過全部爲-1
if (client[i] < 0)
{
client[i] = connfd; //保存accept返回的文件描述符到client[]裏
break;
}
}
// 達到select能監控的文件個數上限 1024
if (i == FD_SETSIZE)
{
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); // 添加一個新的文件描述符到監控信號集裏
if (connfd > maxfd)
{
/* select第一個參數需要,因爲你剛剛添加了connfd文件操作符,
那麼在client[]中最大的文件操作符將會換成connfd*/
maxfd = connfd;
}
if (i > maxi)
{
maxi = i; /* 更新client[]最大下標值 */
}
//重置一下nready 給下一次使用
if (--nready == 0)
{
continue;
}
}
/*第三部分,對就緒者進行讀寫數據*/
for (i = 0; i <= maxi; i++)
{
//檢測哪個clients 有數據就緒,將clients 賦值給臨時變量sockfd
if ( (sockfd = client[i]) < 0)
continue;
//判斷一下有數據的clients 在不在rset裏
if (FD_ISSET(sockfd, &rset))
{
//讀數據到buf
if ( (n = read(sockfd, buf, MAXLINE)) == 0)
{
/* 當client關閉鏈接時,服務器端也關閉對應鏈接 */
close(sockfd);
FD_CLR(sockfd, &allset); /* 解除select監控此文件描述符 */
client[i] = -1;
}
else
{
int j;
for (j = 0; j < n; j++)
{
//這裏我把發來的信息,轉化成大寫返回發送回去
buf[j] = toupper(buf[j]);
}
write(sockfd, buf, n);
}
if (--nready == 0)
break;
}
}//for (i = 0; i <= maxi; i++)結束
}//for ( ; ; ) 結束
close(listenfd);
return 0;
}