实现多路转接I/O——select服务器
Posted 巴山雨夜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现多路转接I/O——select服务器相关的知识,希望对你有一定的参考价值。
1、五种I/O模型
I/O就是表示的是数据的输入、输出;这就是我们之前所以理解的I/O,但是我要说的是你们之前理解的I/O只是I/O过程的一部分; I/O可以分为是两个步骤 :等待I/O事件就绪 ; 实现数据的搬迁; I/O事件是不是高效,一般与等待就绪的时间在I/O过程中所占的比重的大小有关; Unix下共有五种I/O模型:①、阻塞I/O;
②、非阻塞I/O;
③、I/O复用(select和(e)poll);
④、信号驱动I/O(SIGIO);
⑤、异步I/O(Posix.1的aio_系列函数);
1.1、阻塞I/O模型
应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。如果数据没有准备好,一直等待。数据准备好了,从内核拷贝到用户空间,表示IO结束,IO函数返回成功指示。1.2、 非阻塞I/O模型
我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试 数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。1.3、 I/O复用模型
该种模型又被称作是多路转接,I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。1.4、信号驱动I/O模型
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。1.5、 异步I/O模型
调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。也就是它只需要发起这个读写事件,不要等待与数据搬迁,只需要在结束之后得到成果。2、几种I/O模型的比较
上面的五种I/O模型: 前面的四种模型属于是同步I/O模型都是,自己等待,自己将数据从内核拷贝到调用者的缓冲区。 而异步I/O的两个阶段都不同于前四个模型,它是只发起这个I/O操作,由内核等待,内核将数据拷贝到调用者的缓冲区,在通知它。这五种I/O模型中最高效的是 I/O复用模型,因为它可以一次等待多个I/O事件继续; 所以我们今天主要介绍这种I/O模型。 五种I/O模型比较:
3、实现文件描述符的重定向
在Linux下我们经常使用的命令 > 可以将内容重定向输出到 之后的文件中。 但是我们今天要在代码中,想使用printf函数将内容输出到一个文件中要怎么做呢? 今天我们向大家来介绍两个函数可以 将文件描述符重定向。SYNOPSIS
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
那就是dup 、dup2函数。这两个函数可以实现文件描述符的重定向。
dup函数的作用:
将oldfd文件描述符进行一份拷贝,返回值为最新拷贝生成的文件描述符(最小的未被使用的文件描述符)
dup2函数的作用: 使用newfd对oldfd文件描述符做一份拷贝,必要是可以先关闭newfd文件描述符。
也就是说调用dup2之后,oldfd文件描述符不变,newfd和oldfd相等。
如何将重定向之后的文件描述符恢复?
假设我们要使用printf输出数据到文件描述符 【3】; 我需要这样写:dup2(3,1);//我们将3号文件重定向到1号文件。1号文件为标准输出。
此时printf输出的文件就会全部输出到3号文件描述中。
但是做完之后,我们需要将文件描述符重新还原回去,要怎么做呢? 我们需要在dup2之前现将文件描述符1 使用dup函数保存一份。 之后在调用dup函数重定向回来。
代码演示:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#include<string.h>
//验证dup函数还有dup2函数
int main()
//先设置文件的屏蔽为 0
umask(0);
int fd = open("./log",O_CREAT|O_RDWR,0666);//创建文件 可读可写,文件权限为 666
const char * msg = "hello world";
printf("fd->%d\\n",fd);
write(fd,msg,strlen(msg));
int new_fd = dup(fd);
printf("new_fd->%d\\n",new_fd);
write(new_fd,msg,strlen(msg));
int cpfd = dup(1);//先对标准输出 文件描述符进行拷贝;
//将fd文件描述符重定向到标准输出
dup2(fd,1);
printf("nice to meet you\\n");
//再将标准输出 恢复
dup2(cpfd,1);
printf("nice to meet you\\n");
return 0;
现象演示:
4、select实现多路转接
要实现select服务器我们需要了解一个函数的用法 【那就是】select函数 /* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
这个函数的功能是可以实现同时等待多个文件描述符;
参数nfds表示的是等待的文件描述符中的最大的那个+1;
类型fd_set表示的是是否需要等待某个文件描述符,所以这里的fd_set底层是用位图实现的,所以我们最多可以等待的文件描述符的个数为sizeof(fd_set)*8;
参数readfds表示的是需要等待的读事件的文件描述符集;
参数writefds表示的是需要等待的写事件的文件描述符集;
参数exceptfds表示的是需要等待的异常事件的文件描述符集;
参数timeout的类型为下 ,表示的是 每次select的时间为ty_sec秒
struct timeval
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
;
另外,我们虽然知道fd_set的底层实现,但是我们不能直接对于他们进行操作,操作系统为我们提供了下面的一些接口来实现一些操作:
SYNOPSIS
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
void FD_CLR(int fd, fd_set *set);//将文件描述符中的fd位去掉
int FD_ISSET(int fd, fd_set *set);//检测文件描述符集set中的fd位是否存在
void FD_SET(int fd, fd_set *set);//为set文件描述符集设置fd为设置
void FD_ZERO(fd_set *set);//将set文件描述符集清空
实现代码:
实现select服务器
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
#include<unistd.h>
#include<string.h>
static void usage(const char *proc)
printf("Usage :%s [local_ip] [local_port]\\n",proc);
//定义文件描述符数组 存储需要等待的文件描述符
int readfds[sizeof(fd_set)*8];
//建立监听服务器
int startup(char * ip,int port)
//建立套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
perror("socket");
exit(1);
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
//绑定本地ip 与 端口
if(bind(sock,(struct sockaddr*)&local,sizeof(local))< 0)
perror("bind");
exit(2);
//设置服务器状态为 监听状态
if(listen(sock,5)< 0)
perror("listen");
exit(3);
return sock;
int main(int argc ,char * argv[])
if(argc != 3)
usage(argv[0]);
return 4;
int listen_sock = startup(argv[1],atoi(argv[2]));
int i =1 ;
int num = sizeof(fd_set)*8;
readfds[0] = listen_sock;
for(;i < num;++i)
readfds[i] = -1;
//定义两个文件描述符集 ,来表述要等到的文件描述符
fd_set wfds,rfds;
char buf[1024];
while(1)
int maxfd = -1;
//给文件描述符集 初始化,并设置要等待的文件描述
FD_ZERO(&rfds);
for(i = 0;i <num;++i)
if(readfds[i]!= -1)
FD_SET(readfds[i],&rfds);
//得到最大的文件描述符
maxfd = maxfd < readfds[i] ?readfds[i]:maxfd;
//设置成是5秒select
struct timeval time = 1,0;
int n = select(maxfd+1,&rfds,&wfds,NULL,&time);
switch(n)
//表示的时间结束
case 0:
printf("time out ..\\n");
break;
//表示的select失败
case -1:
break;
//至少有有一个文件描述符就绪
default:
i = 0;
//检查哪个文件描述符就绪
for(;i < num;++i)
//表示的是监听服务器就绪
if(i ==0&&FD_ISSET(readfds[i],&rfds))
struct sockaddr_in client;
socklen_t len = sizeof(client);
//accep来接受客户端
int client_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(client_sock< 0 )
perror("accept");
continue;
else
//将客户端的套接字放到要等待的文件描述符数组中
int j =0;
for(j = 1;j <num;++j)
if(readfds[j] == -1)
readfds[j]= client_sock;
break;
//要是j>num表示的是文件描述符数组已满
if(j >= num)
printf("readfds is full\\n");
return 5;
//等待的普通文件秘书符就位
if(i != 0 &&FD_ISSET(readfds[i],&rfds))
//先读数据
int s = read(readfds[i],buf,sizeof(buf)-1);
if(s < 0)
perror("read");
return 6;
else if(s == 0)
printf("client quit\\n");
readfds[i] =-1;
close(readfds[i]);
continue;
else
buf[s]=0;
printf("client#: %s\\n",buf);
fflush(stdout);
//读完之后直接将读到的数据返给客户端
write(readfds[i],buf,strlen(buf));
break;
return 0;
实现客户端代码:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<unistd.h>
static void usage()
printf(".......:[ipaddr],[port]\\n");
exit(1);
int main(int argc,char * argv[])
if(argc != 3)
usage();
//建立套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(socket < 0)
perror("socket");
return 2;
struct sockaddr_in addr;
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
//连接服务器
if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0 )
perror("connent");
return 3;
//成功之后
else
printf("connect success\\n");
char buf[1024];
while(1)
printf("client #:");
fflush(stdout);
//客户端为服务器发数据
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s <=0 )
perror("read");
return 4;
else
buf[s] = '\\0';
//write(sockfd,buf,strlen(buf));
//在这使用文件描述符重定向dup函数来代替write函数为服务器写数据
int fd = dup(1);//先保存标准输出的文件描述符
dup2(sockfd,1);
printf("%s",buf);
fflush(stdout);
dup2(fd,1);//将标准输出文件描述符恢复
//从服务器中读数据
s= read(sockfd,buf,sizeof(buf)-1) ;
if(s== 0)
printf("server quit\\n");
break;
else if(s <0)
perror("read");
return 5;
else
buf[s-1] = '\\0';
printf("server #:%s\\n",buf);
close(sockfd);
return 0;
5、总结select服务器的优缺点
与多进程/多线程服务器进行对比 它的优点在于:1、不需要建立多个线程、进程就可以实现一对多的通信。 2、可以同时等待多个文件描述符,效率比起多进程多线程来说要高很多;
与多进程/多线程服务器进行对比 它的缺点在于:
1、每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 ,循环次数有点多; 2、 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 。 3、 select支持的文件描述符数量太小了,默认是1024;
以上是关于实现多路转接I/O——select服务器的主要内容,如果未能解决你的问题,请参考以下文章