下面是关于高级IO我总结的一篇文章:
https://blog.csdn.net/qq_37941471/article/details/80952057
可以了解一下 五种IO模型 以及 它们之间的关系;
当然还有IO多路转接的其他实现方式:poll epoll 以及三者之间的对比
poll
select poll epoll之间的对比:
下面我讲一下我对select poll epoll的理解:
首先这三个都是实现 IO多路转接 的方式:一个进程同时监视多个文件描述符
也就是三者之间的共同优点
1. select
缺点:
1. 代码编写复杂,维护起来较麻烦
2. 每次调用select,都需要重新设置文件描述符(从用户态拷贝到内核态),开销大
为什么需要重新设置?
因为select的输入输出都调用的是同一个函数select,并且输入和输出
是单独作为参数的这个时候我们就需要用一个第三方数组来保存之前的
所关心的文件描述符,以便进行select返回后,和fdset进行FDISSET
判断哪一个所监听的描述符哪个就绪,进行accept操作,并且方便下一次监听
3. 使用过程中,从内核遍历文件描述符,当fd很多的时候,则会开销很大
需要以轮询的方式去获取就绪的文件描述符
4. 能够接收的文件描述符有上限
因为有第三方数组去维护,而这个数组开的最大空间就是:sizeof(fd_set)*8
一般的操作系统,默认的是1024(一个bit位表示一个文件描述符
(因为fd_set的底层是一个位图))
2. poll
优点 :
1. select的输入输出都是调用一个函数,参数是分开的,用位图来描述,
使用起来开销会比较大;而poll使用一个pollfd的结构体来实现的
2. 解决了select能处理的文件描述符有上限的问题
因为poll解决了selec输入输出参数分开的问题,进而当然不需要再用第三方数组
去维护;所以poll能处理的文件描述符可以说是无上限了
(而这里肯定有它的一个上限,但是这个上限是操作系统的上限,和poll没有关系)
缺点:
除了解决了select的部分缺点以外,其他的缺点poll也是有的
3. epoll
在poll的基础上,又做了改进:处理了大批量句柄问题
所以这三个是一步一步改进的,最终epoll是最高效的IO多路的就绪通知机制;
(这个高效的基础是:多连接,少量活跃的机制;如果场景不合适的话,有可能适得其反)
实现 poll版本的TCP服务器:
Makefile :
.PHONY:poll_server clean
poll_server:poll_server.c
gcc -o $@ $^
clean:
rm -rf poll_server
poll_server.c :
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int startup( int port )
{
// 1. 创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);//这里第二个参数表示TCP
if( sock < 0 )
{
perror("socket fail...\n");
exit(2);
}
// 2. 解决TIME_WAIT时,服务器不能重启问题;使服务器可以立即重启
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);// 地址为任意类型
local.sin_port = htons(port);// 这里的端口号也可以直接指定8080
// 3. 绑定端口号
if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0 )
{
perror("bind fail...\n");
exit(3);
}
// 4. 获得监听套接字
if( listen(sock,5) < 0 )
{
perror("listen fail...\n");
exit(4);
}
return sock;
}
int main(int argc,char* argv[] )
{
if( argc != 2 )
{
printf("Usage:%s port\n ",argv[0]);
return 1;
}
// 1. 获得监听套接字
int listen_sock = startup(atoi(argv[1]));//端口号传入的时候是以字符串的形式传入的,需要将其转为整型
// 2. 初始化结构体----监听的结构列表
struct pollfd fd_list[1024];
int num = sizeof(fd_list)/sizeof(fd_list[0]);
int i = 0;
for(; i < num ; i++ )
{
fd_list[i].fd = -1;// 文件描述符
fd_list[i].events = 0;// 要监听的事件集合
fd_list[i].revents = 0;// 关心的描述符的就绪事件集合
}
// 3. 添加要关心的文件描述符的只读事件
i = 0;
for( ; i < num; i++ )
{
if( fd_list[i].fd == -1 )
{
fd_list[i].fd = listen_sock;
fd_list[i].events = POLLIN;// 关心只读事件
break;
}
}
while(1)
{
//4 . 开始调用poll等待所关心的文件描述符集就绪
switch( poll(fd_list,num,3000) )
{
case 0:// 表示词状态改变前已经超过了timeout的时间
printf("timeout...\n");
continue;
case -1:// 失败了
printf("poll fail...\n");
continue;
default: // 成功了
{
// 如果是监听文件描述符,则调用accept接受新连接
// 如果是普通文件描述符,则调用read读取数据
int i = 0;
for( ;i < num; i++ )
{
if( fd_list[i].fd == -1 )
{
continue;
}
if( fd_list[i].fd == listen_sock && ( fd_list[i].revents & POLLIN ))
{
// 1. 如果监听套接字上读就绪,此时提供接受连接服务
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr *)&client,&len);
if(new_sock < 0)
{
perror("accept fail...\n ");
continue;
}
//获得新的文件描述符之后,将该文件描述符添加进数组中,以供下一次关心该文件描述符
int i = 0;
for( ; i < num; i++ )
{
if( fd_list[i].fd == -1 )//放到数组中第一个值为-1的位置
break;
}
if( i < num )
{
fd_list[i].fd= new_sock;
fd_list[i].events = POLLIN;
}
else
{
close(new_sock);
}
printf("get a new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
continue;
}
//2. 此时关心的是普通文件描述符
// 此时提供读取数据的服务
if( fd_list[i].revents & POLLIN )
{
char buf[1024];
ssize_t s = read(fd_list[i].fd,buf,sizeof(buf)-1);
if( s < 0 )
{
printf("read fail...\n");
continue;
}
else if( s == 0 )
{
printf("client quit...\n");
close(fd_list[i].fd);
fd_list[i].fd = -1;
}
else
{
buf[s] = 0;
printf("client# %s\n",buf);
}
}
}
}
break;
}
}
return 0;
}