浅析I/O多路转接之select技术
说到select服务器首先提到I/O多路转接,我们就不得不提及I/O的5种工作模式,再然后我们就不得不再提及I/O是什
么了?
I/O
是input/output的缩
写,即输入输出端口。每个设备都会有一个专用的I/O地址,用来处理自己的输入输出
信息.接下来我
们来
认识
一下I/O的5种工作模式.
再说这个之前我说一个小故事,从前有好几个人一起钓鱼,但是他们所使用的方法却大大的不同.
现在开始我们说明
每个人的
方法.
A是一个老头,它
比较固执他就一直坐在湖边等鱼上钩,眼睛一动不动的盯着鱼钩,一旦有情
况他就立
马抬杆,然后鱼上
钩后,他就
恢
复到以前的状态.B是一个年轻的
大学生周末没事干也来钓鱼,但是他是边玩手机变钓鱼
,也就是玩一会手机来
瞧一瞧这个鱼饵有
没有
变化,如果有变化就立马抬杆.C是一个老板,
他的鱼竿具有提示功能,
然后他
就
在干自己的事情,等有
情况他就立马过来把鱼
钓上来,D是一个土豪,然后他就一次性放了好多个具有提示功
能的鱼竿,
然后守
在鱼竿跟前
,一旦有
那个鱼竿出现情况就收拾那个
鱼竿.这时候E出现,E是
一个领导,他让别人帮
他钓鱼,钓到了给它
打电话.
以
上这5个人,其实分别代表这I/O的5中工作模式,分别是
阻塞式I/O模型,非阻塞式I/O模型,信号驱动I/O模型,I/O复用模型,以及异步I/O模型.
1.阻塞I/O模型
应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好.
如果数据没有准备好,一直等待.
数据准备好了,从内核拷贝
到用户空间.
IO函数返回成功指示
2.非阻塞I/O模型
我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错
误.这样我
们的I/O
操作函数将不断地测试数据是否已经准备好,如果没有准备好,进行测试,直到数据准备好为止.在
这个不断测试的过
程中,会大量的
占用CPU的时间
3.I/O复用模型
I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的,这两个函数可以同
时阻塞多
个I/O操
作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写是,才真
正调用I/O操作函
数.
4.信号驱动I/O模型
首先我们允许套接字进行信号驱动I/O,并安装一个信号处理函数,进程继续进行并不阻塞.当数据准备好时,进程会
受到一个SIGIO信
号,可以在信号处理函数中调用I/O
5.异步I/O模型
调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核
讲数据拷贝到缓冲区
后,再通知应用程序
今天我们的主角很明显就是I/O复用模型当中异步I/O中的select函数,以及如何使用它来构造select服务器,现在正是开始,
我
们来了解
select,
系统提供select函数来实现多路复用输入\输出模型.socket系统调用是用来让我们的程序监视多个文
件
句柄的状态变
化的.
程序会停在select这里等
待,知道被监视的文件句柄有一个或多个发生了状态改变.
select函数
函数原型:
参数nfds是需要监视的最大文件描述符值+1;
rdset,wrset,exset分别是对应于需要检测的可读文件描述符集合,可写文件描述符的集合及异常文件描述符集合.
剩下的
timeout就是我们的用来描
述一段时间,如果需要监视的描述符没有事件发生在这段时间里面,则返回返回
值为0.
如果参数timeout设置为NULL,则select一直被阻塞.给定特定时间值:如果在该时间没没有事件发生那么就返回0.
FD_CLR(int fd, fd_set *set); 用来清除描述词组set中相关fd的位.
FD_ISSET(int fd,fd_set *set): 用来测试描述词组set中相关fd的位是否为真.
FD_SET(int fd,fd_set* set); 用来设置描述词组set中的相关fd位
FD_ZERO(fd_set *set); 用来清除描述词组set的全部位
函数返回值:
执行成功则返回文件描述词状态以改变的个数.如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
.如果有
错
误时
返回-1.
注意这里有一个重点! 当select函数返回的时候,它会将读,写,异常fd_set当中有我们关心事件但是没有
发生事件的文件描述符置0
,所以每一次select返回后,我们需要重新置我们需要关心事件的fd_set. 当然!我们最重要的是
看看服务器是如何使用的,以及它的优缺点.
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个⽂
文件描
述
符
fd。则1字节长的fd_set最⼤大可以对应8个fd。实际上就是位图
(1)执⾏行fd_set set; FD_ZERO(&set);则set⽤用位表⽰示是0000,0000。
(2)若fd=5,执⾏行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被 清空。
使用select编写网络服务器
现在呢我们来尝试编写一个select服务器,今天我们来编写一个简易版的就是所有人讲的话都可以在select上面大家都看
到
,也就是
类似于弹幕墙
一样的东西. 首先我们肯定需要一个监听套接字这时候我使用一个stratup()函数来封装.接下来
创建
一个存放文件描述符
数组,首先肯定是把它第一位设为为监听套接字,剩下的都为-1. 调用select函数,如果它的返
回值大
于0,开始给对应的文件描述符
上面设定位,因为我这里只需要读所以全都是&rfds,首先进入进入监听套接字,把
监听套
接
字监听到的文件描述符,交给一个连接套
接字来处理,这里每个客户端都会有一个连接套接字,当客户端关闭时,
连接套接
字也会关闭. 然后把这个连接套接字也放入文件描述符
队列,这样下一次如果它的对应位就绪,那么就会读取到
客户端的内容.
这里肯定不会让监听套接字去维护一个客户端,
一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一
直
存在。所以数据
都是从每个客户端的连接套接字里面读到的,包括以后的写都是由newsock服务的.
服务器server.c
/*************************************************************************
> File Name: server.c
> Author: ma6174
> Mail: ma6174@163.com
> Created Time: Wed 26 Jul 2017 09:26:09 PM PDT
************************************************************************/
#include<stdio.h>
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<arpa/inet.h>
static void usage(const char* proc)
{
printf("Usage : %s [local_ip] [local_port]\n",proc);
}
int fds[sizeof(fd_set)*8];
int startup(char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
error("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock, (struct sockaddr*)&local , sizeof(local))<0)
{
perror("BInd");
exit(3);
}
if(listen(sock ,10)< 0)
{
perror("lisen");
exit(4);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int listen_sock = startup(argv[1],atoi(argv[2]));
fd_set rfds;
int nums = sizeof(fds)/sizeof(fds[0]);
int i =1;
for(; i<nums; i++){
fds[i] = -1;
}
while(1)
{
FD_ZERO(&rfds);
int max = -1;
//struct timevlal timeout = {5.0};
fds[0] = listen_sock;
for(i = 0;i < nums;i++){
if(fds[i] > -1){
FD_SET(fds[i],&rfds);
if(max < fds[i])
{
max = fds[i];
}
}
}
switch(select(max+1,&rfds,NULL,NULL,0))
{
case 0:
printf( "chao shi le");
break;
case -1:
printf("select");
break;
default:
i = 0;
for(; i < nums ; i++)
{
if(fds[i] == -1){
continue;
}
if(i == 0 && FD_ISSET(fds[i],&rfds))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(fds[i],(struct sockaddr*)&client,&len);
if(new_sock < 0)
{
perror("accept") ;
continue;
}
printf("get a client[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
int j = 1;
for(;j< nums;j++){
if(fds[j] == -1){
break;
}
}
if(j == nums){
close(new_sock);
}else{
fds[j] = new_sock;
}
}
else if(i != 0&& FD_ISSET(fds[i],&rfds))
{
char buf[1024];
ssize_t s = read(fds[i],buf,sizeof(buf));
if(s > 0)
{
printf("client# %s\n",buf);
}else if(s == 0)
{
printf("client is quit!!!!!\n");
close(fds[i]);
fds[i] = -1;
}
else{
perror("read");
close(fds[i]);
fds[i] = -1;
}
}else{
}
}
break;
}
}
return 0;
}
客户端client代码:
/*************************************************************************
> File Name: client.c
> Author: ma6174
> Mail: ma6174@163.com
> Created Time: Thu 27 Jul 2017 01:20:36 AM PDT
************************************************************************/
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
void use(char* a)
{
printf("#%s [port_server]\n",a);
}
int main(int argc,char* argv[])
{
printf("main start\n");
if(argc < 3)
{
use(argv[0]);
return 3;
}
printf("use is ok\n");
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
perror("socket");
return 1;
}
printf("create socket is ok\n");
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr((argv[1]));
int conn = connect(sock,(struct sockaddr*)&server , sizeof(server));
if(conn < 0){
perror("connect");
close(sock);
return 2;
}
while(1)
{
printf("please enter#");
fflush(stdout);
char buf[1024];
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf) - 1] = '\0';
write(sock,buf,sizeof(buf));
char* str = "quit";
if(strcmp(buf,str) == 0)
{
break;
}
}
close(sock);
printf("client goodbye!\n");
return 0;
}
注意这里我这是想让客户端在服务器当中输出消息,所以一直写就够了.
最后我们看看运行结果:
我们看到三个客户端发送的内容都通过局域网出现在服务器当中,所以我们最初级的select服务器宣布成功.现在还是有好多个
小
bug,需要我们不断地改进,现在我们的任务是了解这个函数,会使用它就好了.
总结:
优点:
1.select资源占用比较少,
2 用户量较多的时候它的性能和效率比较好.
缺点:
1.它总共监视的文件描述符是有限制的. 最多1024
2.因为参数为输入输出性,在操作时 得一直进行遍历,查找使用.
3.select所监视的文件描述符,select的调用频繁,然后它会反复遍历,可能会达到性能瓶颈.
4.当select频繁调用的过程中,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时很大.
5.select的参数为输入输出类型.
所以我们其实可以看到select服务器并没有我们想像中那么厉害,其实呢他的缺点大于它的优点,所以呢~它肯定会有优化
版本
,
我们关注下一个博
客 认识和编写poll服务器.