采用多进程与多线程的方法来实现并发服务器时,监听的工作由server应用程序自身通过accept函数不断去监听。当客户端连接较多时,这种方法会大大降低程序执行效率,消耗CPU资源(CPU需要在不同进/线程中切换执行)。
多进程与多线程实现并发服务器方法可以参考以下两篇文章:
因为以上两种方法的局限性所以出现了采用多路IO转接的方式来设计服务器,该类服务器实现的思想为应用程序本身不在监听客户端连接,转而由内核来代替监听。主要使用的方法有三种,其一为 select()
函数。
select
函数
select
能监听的文件描述符个数受限于FD_SETSIZE
,一般为1024,单纯改变进程打开的文件描述符个数不能改变select监听文件个数- 解决1024以下个客户端时使用
select
十分合适,但是如果链接客户端过多,select
采用的轮询模型,会大大降低服务器响应效率,应采用其他方法。
函数原型
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
- 返回值
- 成功:返回所监听的所有监听集合中满足条件的总数。
- 失败:返回-1,并设置errno。
- 参数
nfds
:监控的文件描述符集里最大文件描述符加1,此参数会告诉内核检测前多少个文件描述符的状态readfds
:监控有读数据到达的文件描述符集合。传入传出参数writefds
:监控有写数据到达的文件描述符集合。传入传出参数exceptfds
:监控异常发生到达文件描述符集合。传入传出参数timeout
:定时阻塞监控时间,3种情况
- NULL,永远等下去
- 设置timeval,等待固定时间
- 设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval
结构体struct timeval{ long tv_sec;//分 long tv_usec;//微秒 }
四个工具函数
想要看的更详细,请参考man手册
//fd_set类型为位图集合
void FD_CLR(int fd, fd_set *set);//将fd从set集合中清除出去,相当于将对应位置为0
int FD_ISSET(int fd, fd_set *set);//判断fd是否在set集合中,返回1为在集合中,0为假,即不在集合中
void FD_SET(int fd, fd_set *set);//将fd设置到set集合中,相当于将对应位置为1
void FD_ZERO(fd_set *set);//将文件描述符集合清空,位图清空相当于全部置为0
例子
服务端代码实现
特别要举例说的是rset
参数,在调用select
函数前调用了rset = allset
语句,假如此时赋值后rset
中有fd1、fd2、fd3三个需要被监听的文件描述符,但是这三个文件描述符中只有fd2文件描述符满足了读的监听条件,那么在调用完select
函数后,select
函数会将fd1、fd3文件描述符从rset
集合中剔除,rset
集合中只会存在满足读监听条件的文件描述符。
同样地,readfds
、writefds
、exceptfds
三个传入传出参数皆是如此。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<sys/types.h>
#include<sys/time.h>
#include<ctype.h>
#define PORT 8888
#define IP "127.0.0.1"
int main(){
//1、建立套接字
int sfd = socket(AF_INET,SOCK_STREAM,0);
//2、绑定
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr);
socklen_t serv_len = sizeof(serv_addr);
int ret = bind(sfd,(struct sockaddr*)&serv_addr,serv_len);
if(ret != 0){
printf("bind err:%s\n",strerror(ret));
exit(1);
}
//3、监听
listen(sfd,128);
//定义一个客户端数组,用来存放监听文件描述符,以免select到时候循坏0~1023个文件描述符
int client[FD_SETSIZE];
//将数组内值全部初始化为-1
for(int i = 0; i < FD_SETSIZE;i++){
client[i] = -1;
}
fd_set rset,allset;//rset 用来保存满足读监听条件的文件描述符集合,allset用来保存要监听的文件描述符集合
FD_ZERO(&allset);
FD_SET(sfd,&allset);
int nready,i;
int index = -1;//定义数组下标
int maxfd = sfd;//定义一个最大文件描述符,select第一个参数:监听的文件描述符集里最大的文件描述符+1
while(1){
//rset中存放监听条件为读的文件描述符
rset = allset;
//只监听读集合,写集合与异常集合暂时不监听,timeout参数传NULL表示永不超时
/*select调用后会更改rset集中文件描述符集,只保留符合监听条件的文件描述符*/
nready = select(maxfd+1,&rset,NULL,NULL,NULL);
if(nready < 0){
printf("监听的集合中没有满足条件的文件描述符\n");
}
int sockfd;
//判断sfd是否存在于满足监听条件的集合中
if(FD_ISSET(sfd,&rset)){
//如果监听到了有满足条件的文件描述符,开始accept()
struct sockaddr_in clie_addr;
socklen_t clie_len = sizeof(clie_addr);
int cfd = accept(sfd,(struct sockaddr*)&clie_addr,&clie_len);//返回一个新的客户端的文件描述符
if(cfd == -1){
perror("accept error");
exit(2);
}
char buf[BUFSIZ];
printf("%sconnected...;port:%d\n",inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,buf,sizeof(buf)),ntohs(clie_addr.sin_port));//打印谁连接了上来
for(i = 0; i < FD_SETSIZE;i++){
if(client[i] < 0){
client[i] = cfd;
break;
}
}
//由于select()函数只能有1024个文件描述符,所以进行判断,如果超过了就应该退出
if(i == FD_SETSIZE){
printf("too many connecting...\n");
exit(1);
}
FD_SET(cfd,&allset);
//更新文件描述符集中最大的文件描述符
if(cfd > maxfd){
maxfd = cfd;
}
if(i > index){
index = i;
}
if(--nready == 0){
continue;
}
}
//遍历client[i]数组,查看数组中是否需要监听的文件描述符
int len;
char rwbuf[BUFSIZ];
for(i = 0; i <= index;i++)
{
if((sockfd=client[i]) < 0){
//满足此语句说明client[]中i位置是-1,并将client[i]的值赋给sockfd
continue;
}
//如果sockfd文件描述符在rset读文件描述符集中,说明是要监听的
if(FD_ISSET(sockfd,&rset)){
len = read(sockfd,rwbuf,sizeof(rwbuf));
if(len==0){
//说明客户端关闭了
//将这个文件描述符从allset中清除,在对应的client[]中i位置改回-1
printf("------%d--disconnected\n",i);
close(sockfd);
FD_CLR(sockfd,&allset);
client[i] = -1;
}else if(len > 0){
for(int j = 0; j < len;j++){
rwbuf[j] = toupper(rwbuf[j]);
}
write(sockfd,rwbuf,len);
}
if(--nready == 0){
break;
}
}
}
}
close(sfd);
return 0;
}