以下内容均为本人学习笔记,若有错误,欢迎指出
在前面讲了五种基本IO模型:初识化五种IO模型
其实我们用到的最多的就是多路IO,今天来学习了解select()实现多路IO
先看接口如何使用
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
除了第一个参数,其他参数都是既为输入型参数又为输出型参数 rdset、wrset、exset结构是一个整数数组,理解为一个位图: *作为输入型参数,用户告诉操作系统,你要等待哪些文件描述符的就绪状态 *作为输出行参数,操作系统告诉用户哪些文件描述符状态已经就绪 timeout结构: 若等待时间没有超过timeout,则返回为剩余时间 |
其实这里存在一个问题,我们每次非阻塞形式读取文件并不是只读一次,都是循环等待文件描述符状态,读取数据,那么上面的fd_set结构在每次返回的时候,都会更新为就绪的文件描述符集,再次等待的时候,就不能直接使用了,所以这里需要一个第三方变量保存作为输入的文件描述集。需要将输入输出参数关联性分离,其实这也是select的一个短板。
下面的接口用来操作fd_set:
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
什么是读就绪状态
什么是写就绪状态
使用select只检测标准输入是否就绪
#include
#include
#include
#include
//使用select 检测标准输入
int main()
{
//1.准备好文件描述符集
fd_set read_set;
int fd = 0;//stdin
FD_ZERO(&read_set);
FD_SET(fd,&read_set);
while(1)
{
int ret = select(fd + 1,&read_set,NULL,NULL,NULL);
//此处只检测读就绪,写和异常不用理会,并设置为阻塞式检测
if(ret < 0)
{
perror("select");
continue;
}
printf("ret : %d\n",ret);
if(FD_ISSET(fd,&read_set) == 0)
{
//说明出错
printf("error!\n");
continue;
}
else
{
char buf[1024] = {0};
if(read(fd,buf,sizeof(buf)-1) > 0)
{
printf("%s\n",buf);
}
}
//一次之后,重新调整read_set
FD_ZERO(&read_set);
FD_SET(fd,&read_set);
}
return 0;
}
之前我们实现的TCP服务器都是采用多进程或者多线程的方式,这种方式其实现实中根本用不到。
用select 实现一个回显服务器
#include
#include
#include
#include
#include
#include
#include
#include
//TCP服务器使用select实现多路复用
//定义一个结构体来保存文件描述符集和当前关注的最大文件描述
typedef struct Fd_set
{
fd_set fds;
int max_fd;
}Fd_set;
//初始化文件描述符集
void Init_set(Fd_set * Fds)
{
if(Fds == NULL)
return;
Fds->max_fd = -1;
FD_ZERO(&Fds->fds);
}
//对文件描述符集进行设置新的文件描述符
void Set_fds(int fd,Fd_set * Fds)
{
if(Fds == NULL)
return;
//更新 max_fd
if(Fds->max_fd < fd)
{
Fds->max_fd = fd;
}
//设置fds
FD_SET(fd,&Fds->fds);
}
//从文件描述符集中删除一个文件描述符
void Delete_fds(int fd,Fd_set * Fds)
{
if(Fds == NULL)
return;
if(fd > Fds->max_fd)
return;
if(Fds->max_fd > fd)
{//如果最大的文件描述符大于要删除的文件描述符
//可以直接进行删除
FD_CLR(fd,&Fds->fds);
}
else
{//如果最大的文件描述符等于要删除的文件描述符
//此时就要重新设最大文件描述符
int i = 0;
int max_fd = -1;
FD_CLR(fd,&Fds->fds);
//此处采用从最大的开始找的方法,虽然时间复杂度都为O(N)
for(i = Fds->max_fd;i >= 0; --i)
{
if(FD_ISSET(i,&Fds->fds))
{
max_fd = i ;
}
}
Fds->max_fd = max_fd;
}
}
//获取listen_socket
int server_start(char * IP,short Port)
{
//创建socket
int listen_socket = socket(AF_INET,SOCK_STREAM,0);
if(listen_socket < 0)
{
perror("socket");
return -1;
}
//定义sockaddr_in 结构
sockaddr_in addr;
socklen_t addr_len = sizeof(addr);
addr.sin_family = AF_INET;//IPV4
addr.sin_addr.s_addr = inet_addr(IP);
addr.sin_port = htons(Port);
//绑定IP和端口号
int ret = bind(listen_socket,(sockaddr *)&addr,addr_len);
if(ret < 0)
{
perror("bind");
return -1;
}
//监听socket使其处于可以被建立连接状态
ret = listen(listen_socket,5);
if(ret < 0)
{
return -1;
}
//将listen_socket返回
return listen_socket;
}
int process_connection(int new_socket,sockaddr_in *peer)
{
//这里处理连接就只读一次,因为我们将所有的等到都交给了select
//这里只进行读操作和响应
char buf[1024] = {0};
ssize_t read_size = read(new_socket,buf,sizeof(buf) - 1);
if(read_size < 0)
{
perror("read");
return -1;
}
if(read_size == 0)
{
printf("read done\n");
return 0;
}
buf[read_size] = '\0';
printf("perr [%s:%d] say : %s\n",inet_ntoa(peer->sin_addr),ntohs(peer->sin_port),buf);
//回显服务,收到什么内容,就回复什么内容
write(new_socket,buf,strlen(buf));
return 1;
}
int main(int argc ,char * argv[])
{
//1.判断命令行参数正确性
if(argc != 3)
{
printf("Usage [./server] [IP] [Port]\n");
return 1;
}
int listen_socket =server_start( argv[1],atoi(argv[2]));
if(listen_socket < 0)
{
printf("start faile\n");
return -1;
}
printf("start ok\n");
//定义文件描述符集并进行初始化和设置
Fd_set in_put;
Init_set(&in_put);
Set_fds(listen_socket,&in_put);
//因为这里第二个参数既是输入行参数,又是输出型参数
//为了防止将第二次需要再次等待的文件描述符集破坏掉,需要定义一个输出参数用来保存输出文件描述符集
Fd_set out_put = in_put;
//进行事件循环
while(1)
{
sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
in_put = out_put;
int ret = select(in_put.max_fd + 1,&in_put.fds,NULL,NULL,NULL);
if(ret < 0)
{
printf("select filed\n");
continue;
}
if(ret == 0)
{
printf("time put\n");
continue;
}
//判断listen_sock是否就绪,若就绪,就可以进行accpet()
if(FD_ISSET(listen_socket,&in_put.fds))
{
int new_socket = accept(listen_socket,(sockaddr *)&peer,&peer_len);
if(new_socket < 0)
{
perror("accept");
continue;
}
else
{//此处建立连接成功后,应该将 new_socket设置在out_put中保存下来中
Set_fds(new_socket,&out_put);
printf("client %d connect!\n",new_socket);
}
}
else
{//处理已经就绪的new_sock
int i = 0;
for(i = 0; i < in_put.max_fd + 1; ++i)
{
if(FD_ISSET(i , &in_put.fds))
{//如果这个文件描述符已经就绪,就进行处理本次连接
//将这一位文件描述符进行设置
Set_fds(i , &out_put);
int ret = process_connection(i,&peer);
if(ret <= 0)
{//说明本次连接已经结束
//需要将out_put里面的这个文件描述清理掉
Delete_fds(i,&out_put);
close(i);
printf("%d closed \n",i);
}
}
else
{
//如果还没有就绪
//就检测其他的文件描述符
continue;
}
}
}
}
}
//其实这里还存在一个问题,上面我们处理listen_socket和new_socket的处理是is-else结构的
//但是listen_socket和new_socket是很有可能同时就绪的
//但是这里也影响,因为这次没有处理,循环会继续执行,会在下一次循环处理
//我们将这种处理方式称为水平触发
select 的缺点:
1.可监控的文件描述符集大小有限制,sizeof(fd_set)
根据机器不同,这边的数值为128
字节,说明可监控文件描述符为1024
个
2. 每次调用select
,都需要自己手动设置fd_set
,从接口使用来说不方便,并且输入输出参数为一个值,还需要自己维护第三个变量
3. 每次调用select
都需要将fd
集从用户态拷贝到内核态,这边为128*3
字节,虽然感觉上不大,但是拷贝次数太频繁
4.每次不管是内核还是我们自己在使用的时候,都要遍历fd
,开销也挺大的