您的位置:首页 > 理论基础 > 计算机网络

网络编程 — 浅析I/O多路转接select技术

2017-08-03 21:40 423 查看

浅析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.它总共监视的文件描述符是有限制的. 最多10242.因为参数为输入输出性,在操作时 得一直进行遍历,查找使用.3.select所监视的文件描述符,select的调用频繁,然后它会反复遍历,可能会达到性能瓶颈.4.当select频繁调用的过程中,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时很大.5.select的参数为输入输出类型.

所以我们其实可以看到select服务器并没有我们想像中那么厉害,其实呢他的缺点大于它的优点,所以呢~它肯定会有优化版本,我们关注下一个博
客 认识和编写poll服务器.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: