赞
踩
在实际的开发中,我们经常会遇到这样的场景,我们需要接受多个端口的数据、多个终端的数据抑或是多个文件描述符对应的数据。那么,遇到这样的问题,你在程序中该怎么做呢?通常的做法,在程序中对数据交互的描述符进行轮询。那么问题来了,轮询的时间设置为多少呢?设置的太短,可以保证处理性能和速度,但是CPU的使用率太高,一旦处理的描述符数量多了起来,CPU可能就扛不住了。设置的时间太长,描述符处理的时间片太短,处于空闲的时间较长,性能和速度达不到要求。如果是服务器的话,面对多个用户的连接,处理速度和CPU使用性能是必须考虑的,而且最好要兼顾。这里就需要使用到I/O多路复用机制,这就是博主即将要和小伙伴们探讨的内容。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set*writeset,fd_set *exceptset,const struct timeval *timeout);
maxfdp1:待监控的最大描述符数值加1。
readset、writeset和exceptset:指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。
struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set*fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否就绪
fd_set定义如下:
typedefstructfd_set {
u_int fd_count; //fd_set中监听的文件描述符个数
intfd_array[FD_SETSIZE]; //存放了要监听的文件描述符
} fd_set;
timeout:告知内核等待轮询的时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
(1)永远等待下去:直到有至少一个描述字准备就绪才返回。将timeout设置为NULL。
(2)等待一段固定时间:在超时前,有一个描述字准备就绪就返回。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须大于0。
(3)不等待:检查描述符后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
返回值:就绪描述符的数目,超时返回0,出错返回-1
原理图
select的工作模式:每次调用select,都需要把fd集合从用户态拷贝到内核态,在内核中完成轮询的工作,待轮询结束再将fd集合从内核态拷贝到用户态。注意,每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核做了修改。我们可以将需要监控的描述符(包括文件、终端、套接字等等)添加到fd_set集合中,由select来监控,这样可以将多处阻塞转移到一处。例如,既要接收终端数据录入,又要接收socket(阻塞socket)传递过来的数据,那么在socket数据接收(read)和终端I/O(fgets)处均会阻塞。使用select后就不一样了,我们可以将这些描述符加入fd_set集合中,阻塞的地方只有一处,就是select调用处。
下面我们通过select实现一个简单的服务器回射实例,服务端监听客户端的连接请求,将已连接客户端描述符添加到监控描述符集合中,select对监听描述符和已连接客户端描述符进行监控。当监听描述符就绪,表示有新客户端连接服务端,调用处理连接逻辑;当有客户端描述符就绪,标识符已连接客户端有数据发送给服务器,服务端调用数据接收逻辑,将客户端发送过来的数据原样发送给客户端。好了,废话不多说了,直接看代码吧!
echo_svr.h
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <error.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
const int MAXFD = FD_SETSIZE;//FD_SETSIZE 1024 可以监控的最大描述符数量
const char *SERVERIP = "127.0.0.1";//服务端IP
const unsigned short PORT = 6666;//服务端端口号
typedef struct server_st
{
int cli_num; //已连接客户端数量
int cli_fd[MAXFD-1];//存放已连接客户端描述符
int maxfd; //存放描述符最大值
int index; //保存最大的索引号(已连接客户端数据下标)
fd_set allset; //监控的文件描述符集合(select参数)
fd_set set; //监控的文件描述符集合(中间值)
int ready; //已就绪描述符数量(select返回值)
}server_st_t;
//打印出错信息并推出程序
#define handle_error(msg)\
do{perror(msg); exit(EXIT_FAILURE);}while(0)
//定义类
class selectsocket
{
public:
//构造函数
selectsocket(const char *server_ip = SERVERIP, unsigned short port =PORT);
//析构函数
virtual~selectsocket();
//客户处理函数(对外接口)
inthandle_cli_proc();
private:
intserver_init();//初始化server_st_t结构体
intserver_uninit();//释放server_st_t结构体
inthandle_create_proc();//创建服务端监听套接字
int handle_accept_proc();//处理客户端连接
inthandle_recv_proc();//处理客户端数据发送
selectsocket(const selectsocket &ref);
selectsocket& operator=(const selectsocket &ref);
private:
server_st_t *m_server_st;
char m_server_IP[16];
unsigned short m_port;
int m_server_fd;
};
selectsocket::selectsocket(const char *server_ip /*=SERVERIP*/, unsigned short port /*= PORT*/)
{
bzero(m_server_IP, sizeof(m_server_IP));//将类成员地址空间清零
memcpy(m_server_IP, server_ip, strlen(server_ip));//给类成员负值
m_port =port;
m_server_st= NULL;
server_init();//初始化
handle_create_proc();//创建监听套接字
}
selectsocket::~selectsocket()
{
int cli_fd= -1;
for (int i= 0; i < MAXFD; ++i)
{
cli_fd= m_server_st->cli_fd[i];
if (-1!= cli_fd)
{
close(cli_fd);
}
}
server_uninit();
close(m_server_fd);
}
int selectsocket::server_init()
{
m_server_st= (server_st_t*)malloc(sizeof(server_st_t));
if (NULL ==m_server_st) handle_error("malloc");
bzero(m_server_st, sizeof(server_st_t));
//memset(m_server_st->cli_fd, 0xFF, sizeof(m_server_st->cli_fd));
//初始化描述符数组
for (inti=0; i<MAXFD; ++i)
{
m_server_st->cli_fd[i] = -1;
}
m_server_st->index = -1;//初始索引号
FD_ZERO(&m_server_st->allset);//情况集合
//FD_ZERO(&m_server_st->set);
return 0;
}
int selectsocket::server_uninit()
{
if (NULL !=m_server_st)
{
free(m_server_st);
}
m_server_st= NULL;
}
int selectsocket::handle_create_proc()
{
//创建TCP套接字
//参数1:协议族
//参数2:套接字类型
//参数3:使用的协议(0:使用套接字类型对应的默认协议)
if (-1 ==(m_server_fd = socket(AF_INET, SOCK_STREAM, 0)))handle_error("socket");
//地址结构体
structsockaddr_in serveraddr;
//将结构体变量空间清零
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;//协议族
//注意,设置端口和IP时,要将主机字节序转换为网络字节序
serveraddr.sin_port = htons(m_port);
//serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
inet_pton(AF_INET, m_server_IP, &serveraddr.sin_addr);
int op =true;
///*一个端口释放后会等待两分钟之后才能再被使用(TIME_WAIT状态),SO_REUSEADDR是让端口释放后立即就可以被再次使用*/
if (-1 ==setsockopt(m_server_fd, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op)))handle_error("setsockopt");
//命名套接字
if (-1 ==bind(m_server_fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)))handle_error("serveraddr");
//将套接字由主动变为被动(接受客户端连接状态)
if (-1 ==listen(m_server_fd, SOMAXCONN)) handle_error("listen");
return 0;
}
int selectsocket::handle_accept_proc()
{
//地址结构体
structsockaddr_in cli_addr;
socklen_tlen = (socklen_t)sizeof(cli_addr);
//将结构体变量空间清零
bzero(&cli_addr, sizeof(cli_addr));
//接受客户端的连接请求
int cli_fd= accept(m_server_fd, (struct sockaddr*)&cli_addr, &len);
if (-1 ==cli_fd) handle_error("accept");
fprintf(stdout,"#%d %s:%d connected server!\n",m_server_st->cli_num, inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
int i = 0;
//将新链接的客户存入客户端数组中
for (i = 0;i < MAXFD; ++i)
{
if (-1== m_server_st->cli_fd[i])
{
//将已连接套接字描述符存入数组
m_server_st->cli_fd[i] = cli_fd;
//m_server_st->maxfd保存值最大的已连接套接字描述符
m_server_st->maxfd = m_server_st->maxfd > cli_fd ?m_server_st->maxfd : cli_fd;
//m_server_st->index保存客户数组中已连接套接字描述符的最大索引
m_server_st->index = m_server_st->index > i ?m_server_st->index : i;
//将套接字描述符关联到集合中
//FD_SET(cli_fd, &m_server_st->allset);
break;
}
}
//如果已连接套接字描述符超过了FD_SETSIZE报错
if (i ==FD_SETSIZE) handle_error("too many connect");
++m_server_st->cli_num;//已连接客户数加1
//m_server_st->set = m_server_st->allset;
}
int selectsocket::handle_recv_proc()
{
//如果数组中不至当前一个用户,遍历数组
for (int i= 0; i <= m_server_st->index; ++i)
{
//如果为空位置,continue
if (-1== m_server_st->cli_fd[i])
{
continue;
}
//如果非空位置,cli_fd保存当前描述符
intcli_fd = m_server_st->cli_fd[i];
//如果当前描述符就绪,执行以下代码
if(FD_ISSET(cli_fd, &m_server_st->allset))
{
char buf[256] = {0};
//从网络中读取数据
intr = read(cli_fd, buf, sizeof(buf));
//如果读取出错
if(r <= 0)
{
//在数组中将当前位置设为空位置
m_server_st->cli_fd[i] = -1;
//将当前描述符从集合中清除
FD_CLR(cli_fd, &m_server_st->allset);
//关闭当前描述符
close(cli_fd);
--m_server_st->cli_num;//已连接客户数减1
}
//如果接受成功,将数据写回网络
write(cli_fd, buf, sizeof(buf));
//清空缓存区
memset(buf, 0x00, sizeof(buf));
}
//每处理一个描述符,read减1
//如果read为0(所有就绪描述符都处理完毕),就不用继续向后扫描
//if(--m_server_st->ready <= 0) break;
}
return 0;
}
int selectsocket::handle_cli_proc()
{
while (1)
{
/*每次调用select前都要重新设置文件描述符集合和超时时间,因为事件发生后,文件描述符和时间都被内核修改啦*/
FD_ZERO(&m_server_st->allset);
//将监听套接字描述符装入描述符集合
FD_SET(m_server_fd, &m_server_st->allset);
//保存最大的描述符
m_server_st->maxfd = m_server_fd;
//设置超时时间为5秒
structtimeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
//更新集合
//m_server_st->set = m_server_st->allset;
//将已连接客户端描述符添加到集合中
for(int i = 0; i<=m_server_st->index; ++i)
{
intcli_fd = m_server_st->cli_fd[i];
if(-1 != cli_fd)
{
FD_SET(cli_fd,&m_server_st->allset);
}
//m_server_st->maxfd保存值最大的已连接套接字描述符
m_server_st->maxfd = m_server_st->maxfd > cli_fd ?m_server_st->maxfd : cli_fd;
}
//select阻塞等待描述符集合中是否有就绪的描述符
//参数1:描述符集合中最大描述符值加1
//参数2:读就绪集合
//参数3:写就绪集合
//参数4:异常就绪集合
//参数5:最长等待时间(NULL:无限等待,直到有描述符就绪)
//一旦有描述符就绪则返回,返回值为以就绪描述符的个数
m_server_st->ready = select(m_server_st->maxfd+1,&m_server_st->allset, NULL, NULL, &timeout);
//select函数返回异常
if (-1== m_server_st->ready) break;
//select函数等待超时
if (0== m_server_st->ready)
{
fprintf(stdout, "select timeout\n");
continue;
}
//如果有客户断连接服务器,监听套接字就绪,执行以下代码
if(FD_ISSET(m_server_fd, &m_server_st->allset))
{
//处理客户端的连接
handle_accept_proc();
//已经处理过来sfd描述符,read减1
//如果此时read<=0,表示所有就绪描述符已处理完毕,返回select处继续等待就绪描述符
if (--m_server_st->ready <= 0)
{
continue;
}
}
else
{
//处理客户端发送过来的数据
handle_recv_proc();
}
}
return 0;
}
main.cpp
#include "echo_svr.h"
int main()
{
selectsocket selectsock;
selectsock.handle_cli_proc();
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
int main()
{
//创建IPV4 TCP套接字
int sfd =socket(AF_INET, SOCK_STREAM, 0);
if (-1 ==sfd) perror("socket"), exit(EXIT_FAILURE);
//地址结构体
structsockaddr_in addr;
addr.sin_family = AF_INET;//协议族
addr.sin_port = htons(6666);//端口
//将地址串转换为网络字节序,存储到addr.sin_addr中
inet_aton("127.0.0.1", &addr.sin_addr);//连接的服务端IP地址
//连接服务器
if (-1 ==connect(sfd, (struct sockaddr *)&addr, sizeof(addr)))
{
perror("connect");
exit(EXIT_FAILURE);
}
charbuf[256] = {};
//从标准输入读取数据
while (NULL!= fgets(buf, sizeof(buf), stdin))
{
//将数据发送给服务器
write(sfd, buf, strlen(buf));
//清空缓存区
memset(buf, 0x00, sizeof(buf));
//读取服务器放送过来的数据
int r =read(sfd, buf, sizeof(buf));
//接受失败
if (r<= 0)
{
break;
}
//输出
fprintf(stdout, buf, r);
//清空缓存区
memset(buf, 0x00, sizeof(buf));
}
//关闭套接字描述符
close(sfd);
return 0;
}
程序运行截图:
说到这里,我们来谈一谈select的缺陷吧,主要有一下三点。
①每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
②同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
③select支持的文件描述符数量太少了,默认是1024
关于select、poll和epoll的对比,请观看博主的另外一篇博文poll epoll select,在那里博主对他们之间的区别、性能和消息传递方式进行了总结,希望能对你有一点帮助。最后附上一张select的实现结构图,如果想了解跟多关于select的实现原理和源码剖析,请参考文末连接。
参考:select实现分析
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。