linux网络编程-多路I/O转接服务器:select

设计思路

多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。实现方法主要有select, poll, epoll

这种思想类似CPU对IO的处理的发展历程, select的地位就像中断管理器, IO设备有中断请求时才通知CPU, 对应的, 只有当客户端有连接请求时才会通知server进行处理. 也就是说只要server收到通知, 就一定有数据待处理或连接待响应, 不会再被阻塞而浪费资源

select函数

  • select能监听的文件描述符个数受限于`FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数

  • 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力

函数原型分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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,此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
*/
  • 重点在于readfds:当客户端有数据发到服务器上时, 触发服务器的读事件. 后面两个一般传NULL
  • 三个传入传出参数都是位图, 每个二进制位代表了一个文件描述符的状态
  • 传入的是想监听的文件描述符集合(对应位置一), 传出来的是实际有事件发生的文件描述符集合(将没有事件发生的位置零)
  • 返回值:
    • 所有监听的文件描述符当中有事件发生的总个数(读写异常三个参数综合考虑)
    • -1说明发生异常, 设置errno

操作文件描述符的函数:

1
2
3
4
void FD_CLR(int fd, fd_set *set); 	//把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0

select实现多路IO转接设计思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
listenFd=Socket();								//创建套接字
Bind(); //绑定地址结构
Listen(); //设置监听上限
fd_set rset; //创建读监听集合
fd_set allset;
FD_ZERO(&allset); //将读监听集合清空
FD_SET(listenFd,&allset); //将listenFd添加到所有读集合当中
while(1){
rset=allset; //保存监听集合
ret=select(listenFd,&rset,NULL,NULL,NULL); //监听文件描述符集合对应事件
if(ret>0){
if(FD_ISSET(listenFd,&rset)){
cfd=accept();
FD_SET(cfd,&allset); //添加到监听通信描述符集合中
}
for(i=listenFd+1;i<=cfd;++i){
FD_ISSET(i,&rset); //有read,write事件
read();
toupper();
write();
}
}
}

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/* select.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define SERVER_PORT 9527

int main(int argc,char* argv[]){
int listenFd,connectFd;
char buf[BUFSIZ];

struct sockaddr_in serverAddr,clientAddr;
socklen_t clientAddrLen;
/*创建一个监听套接字*/
listenFd=Socket(AF_INET,SOCK_STREAM,0);
/*设置端口复用*/
int opt=1;
setsockopt(listenFd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));
/*设置地址结构*/
bzero(&serverAddr,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
serverAddr.sin_port=htons(SERVER_PORT);
/*绑定地址结构*/
Bind(listenFd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
Listen(listenFd,128);
/*定义读集合和备份集合并清空*/
fd_set rset,allset;
FD_ZERO(&allset);
FD_SET(listenFd,&allset);
int ret=0;
int maxfd=listenFd;
int i=0;
int j=0;
int n=0;

while(1){
/*备份*/
rset=allset;
/*使用select监听*/
ret=select(maxfd+1,&rset,NULL,NULL,NULL);
/*出错返回*/
if(ret==-1)
perr_exit("select error");
/*listen满足监听的事件*/
if(FD_ISSET(listenFd,&rset)){
clientAddrLen=sizeof(clientAddr);
/*建立链接,不会阻塞*/
connectFd=Accept(listenFd,(struct sockaddr*)&clientAddr,&clientAddrLen);
/*将connectFd加入集合*/
FD_SET(connectFd,&allset);
/*更新最大值*/
if(maxfd<connectFd)
maxfd=connectFd;
/*如果只有listen事件,只需建立连接即可,无需数据传输,跳出循环剩余部分*/
if(ret==1)
continue;
}

/*否则,说明有数据传输需求*/
for(i=listenFd+1;i<=maxfd;++i){
if(FD_ISSET(i,&rset)){
n=read(i,buf,sizeof(buf));
if(n==-1)
perr_exit("read error");
else if(n==0){
close(i);
FD_CLR(i,&allset);
}
for(j=0;j<n;++j)
buf[j]=toupper(buf[j]);
write(i,buf,n);
write(STDOUT_FILENO,buf,n);
}
}
}
close(listenFd);
return 0;
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 9527

int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;

sockfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);

Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}

自定义数组提高效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>

#include "wrap.h"

#define SERVER_PORT 9527
int main(int argc, char *argv[]){
int i, j, n, maxi;
/*将需要轮询的客户端套接字放入数组client[FD_SETSIZE]*/
int nready, client[FD_SETSIZE];
int listenFd, connectFd, maxFd, socketFd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN];

struct sockaddr_in serverAddr, clientAddr;
socklen_t clientAddrLen;
/*得到监听套接字*/
listenFd = Socket(AF_INET, SOCK_STREAM, 0);
/*定义两个集合,将listenFd放入allset集合当中*/
fd_set rset, allset;
FD_ZERO(&allset);
FD_SET(listenFd, &allset);
/*设置地址复用*/
int opt = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
/*填写服务器地址结构*/
bzero(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(SERVER_PORT);
/*绑定服务器地址结构*/
Bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
Listen(listenFd, 128);
/*将listenFd设置为数组中最大的Fd*/
maxFd = listenFd;
maxi = -1;
/*初始化自己的数组为-1*/
for (i = 0; i < FD_SETSIZE; ++i)
client[i] = -1;

while (1){
/*把allset给rest*/
rset = allset;
nready = select(maxFd + 1, &rset, NULL, NULL, NULL);

if (nready == -1)
perr_exit("select error");

/*如果有了新的连接请求,得到connectFd,并将其放入自定义数组中*/
if (FD_ISSET(listenFd, &rset)){
clientAddrLen = sizeof(clientAddr);
connectFd = Accept(listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen);
printf("Recived from %s at PORT %d\n", inet_ntop(AF_INET, &(clientAddr.sin_addr.s_addr), str, sizeof(str)), ntohs(clientAddr.sin_port));

for (i = 0; i < FD_SETSIZE; ++i)
if (client[i] < 0){
client[i] = connectFd;
break;
}
/*自定义数组满了*/
if(i==FD_SETSIZE){
fputs("Too many clients\n",stderr);
exit(1);
}
/*connectFd加入监听集合*/
FD_SET(connectFd, &allset);

/*更新最大的Fd*/
if (maxFd < connectFd)
maxFd = connectFd;
/*更新循环上限*/
if(i>maxi)
maxi=i;
/*select返回1,说明只有建立连接请求,没有数据传送请求,跳出while循环剩余部分(下面的for循环轮询过程)*/
if (--nready == 0)
continue;
}
/*select返回不是1,说明有connectFd有数据传输请求,遍历自定义数组*/
for (i = 0; i <= maxi; ++i){
if((socketFd=client[i])<0)
continue;
/*遍历检查*/
if (FD_ISSET(socketFd, &rset)){
/*read返回0说明传输结束,关闭连接*/
if ((n=read(socketFd,buf,sizeof(buf)))==0){
close(socketFd);
FD_CLR(socketFd, &allset);
client[i]=-1;
}else if(n>0){
for (j = 0; j < n; ++j)
buf[j] = toupper(buf[j]);
write(socketFd, buf, n);
write(STDOUT_FILENO, buf, n);
}
/*不懂:需要处理的个数减1?*/
if(--nready==0)
break;
}
}
}
close(listenFd);
return 0;
}

select优缺点

缺点:

  • 监听上限受文件描述符显示, 最大1024个

  • 要检测满足条件的fd, 要自己添加业务逻辑, 提高了编码难度

优点:

  • 跨平台, 各种系统都能支持