4.I/O复用以及基于I/O复用的回射客户端/服务器
I/O复用:当一个或多个I/O条件满足时,我们就被通知到,这种能力被称为I/O复用。
1.I/O复用的相关系统调用
posix的实现提供了select、poll、epoll两类系统调用以及相关的函数来实现I/O复用。select以及相关联的函数如下所示:
- #include <sys/select.h>
- /* 功能:监听多个fd,等待指定的fd指定的事件发生或者超时。
- * nfds: 最大描述符加1。
- * readfds:监听读fd集合。
- * writefds:监听写fd集合。
- * exceptfds:监听出错fd集合。
- * timeout:超时时间,null-select一直阻塞直到指定时间发生,0-select不阻塞立刻返回。
- * 返回值: 正常-返回可用fd数量,0-超时返回, -1-select调用出错,errno被设置。
- */
- int select(int nfds, fd_set *readfds, fd_set *writefds,
- fd_set *exceptfds, struct timeval *timeout);
- /* 功能: 清除set中fd对应的位。
- * 返回值:无。
- */
- void FD_CLR(int fd, fd_set *set);
- /* 功能: 判断set中fd对应的位是否被设置。
- * 返回值:0-表示没有被设置,1-表示已经被设置。
- */
- int FD_ISSET(int fd, fd_set *set);
- /* 功能: 设置set中fd对应的位。
- * 返回值:无。
- */
- void FD_SET(int fd, fd_set *set);
- /* 功能: set置空。
- * 返回值:无。
- */
- void FD_ZERO(fd_set *set);
值得注意的是,不同的posix实现支持的最大fd_set不用,使用大描述字集应小心。另外Posix.1g还提供了系统调用pselect,如下:
- #include <sys/select.h>
- int pselect(int nfds, fd_set *readfds, fd_set *writefds,
- fd_set *exceptfds, const struct timespec *timeout,
- const sigset_t *sigmask);
pselect与select存在两点不同:1、pselect支持更细粒度的超时时间(纳秒级别),而select支持微妙级别的超时时间。2、支持第六个参数,pselect调用允许传入一个信号掩码,在pselect调用期间进程的信号掩码是由sigmask指定,pselect返回前将进程的信号掩码恢复为之前的。
函数poll起源于SVR3,开始用于流设备上,现在支持任何类型的fd。poll以及相关数据结构:
- #include <poll.h>
- /* 功能:监听多个fd,等待感兴趣的事件发生或者超时。
- * fds:pollfd类型变量的集合。
- * nfds: 最大描述符加1。
- * timeout:超时时间,INFTIM-poll一直阻塞直到指定时间发生,0-select不阻塞立刻返回。
- * 返回值: 正常-返回可用fd数量,0-超时返回, -1-调用出错,errno被设置。
- */
- int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- struct pollfd {
- int fd; /* 文件描述符 */
- short events; /* 对于fd,感兴趣的事件,值参数 */
- short revents; /* 对于fd,发生的事件,结果参数 */
- };
指定events和测试revents用的一些常值:
1.用于读测试:{POLLIN:普通或优先级带数据可读、POLLRDNORM:普通数据可读、POLLRDBAND:优先级带数据可读}。
2.用于写测试:{POLLOUT:普通数据可写、POLLWRNORM:同POLLOUT、POLLWRBAND:优先级带数据可写}。
3.用于出错(仅用于测试revent):{POLLNVAL:描述字不是一个打开的文件、POLLHUP:被挂起、POLLERR:发生错误}。
linux在2.6内核以后加入了epoll的支持,在高性能服务器领域得到了广泛的使用。epoll的使用较为复杂,再次暂略过。select、poll、epoll的比较:
1.select ,对于几乎所有的平台都支持select,但是select存在两个缺点:a、可监听的描述符数量有限制。b、监听的描述符数量增大时,会影响程序的效率,三个描述符集都是值-结果的参数,存在两次内核-用户态之间的数据拷贝,select返回后需要遍历整个fd集合,才能知道是哪一个fd就绪。
2.poll,poll克服的select对描述符数量的限制,但当描述符数量增大时,仍存在效率问题。
3.epoll,即克服了对描述符数量的限制,同时又通过回调函数以及mmap的方式,克服了描述符数量增大时效率的问题,但是epoll的使用较为复杂。
因此当监听的fd数量较少时,采用select或poll较好,当监听的fd数量较多时采用epoll较合适。
2.基于I/O复用的回射客户端
前面的回射客户端存在一个问题:当客户端阻塞与从标准输入读时,服务器的断开不能被客户端立刻感知,造成这个问题的原因是客户端需要同时监听两个I/O:sock和标准输入,不能因为某一个阻塞而当另一个就绪时不被感知。I/O多路复用可解决这一个问题。
- void
- str_cli(FILE *fp, int sockfd)
- {
- int maxfdp1, stdineof = ;
- fd_set rset; /* 监听的可读描述符集合 */
- char sendline[MAXLINE], recvline[MAXLINE];
- FD_ZERO(&rset); /* rset清空 */
- for ( ; ; ) {
- if (stdineof == )
- FD_SET(fileno(fp), &rset); /* 设置标准输入fd对应的位 */
- FD_SET(sockfd, &rset); /* 设置sockfd对应的位 */
- maxfdp1 = max(fileno(fp), sockfd) + ; /* 计算监听的最大描述符 */
- if (select(maxfdp1, &rset, NULL, NULL, NULL) < ) { /* select开始监听 */
- if (errno == EINTR)
- continue;
- else
- err_sys("select error");
- }
- if (FD_ISSET(sockfd, &rset)) { /* sockfd变为可读 */
- if (Readline(sockfd, recvline, MAXLINE) == ) {
- if (stdineof == )
- return; /* sockfd写已经关闭,属于正常的结束 */
- else
- err_quit("str_cli: server terminated prematurely");
- }
- Writen(STDOUT_FILENO, recvline, strlen(recvline));
- }
- if (FD_ISSET(fileno(fp), &rset)) { /* 标准输入可读 */
- if (Fgets(sendline, MAXLINE, fp) == NULL) { /* 读到结束符 */
- stdineof = ; /* 置1,说明已经关闭了sockfd写的一端 */
- Shutdown(sockfd, SHUT_WR); /* 关闭sockfd写的一端,tcp发送fin */
- FD_CLR(fileno(fp), &rset); /* 已经读到结束符,只有不需要再监听其变为可读 */
- continue;
- }
- Writen(sockfd, sendline, strlen(sendline));
- }
- }
- }
上面的程序中,值得注意的是当我们在输入I/O中读到结束符后,函数并没有立刻退出,而是shutdown关掉sock写的一端,因为之后可能还有数据从服务器端回射回来,因此继续从sock中读数据,直到在sock读到结束符。
3.基于I/O复用的回射服务端
- int
- main(int argc, char **argv)
- {
- int i, maxi, listenfd, connfd, sockfd;
- int nready;
- ssize_t n;
- char buf[MAXLINE];
- socklen_t clilen;
- struct pollfd client[OPEN_MAX];
- struct sockaddr_in cliaddr, servaddr;
- /* 创建并启动监听sock */
- listenfd = Socket(AF_INET, SOCK_STREAM, );
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(SERV_PORT);
- Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
- Listen(listenfd, LISTENQ);
- client[].fd = listenfd; /* 初始化监听sock的pollfd结构 */
- client[].events = POLLRDNORM;
- for (i = ; i < OPEN_MAX; i++)
- client[i].fd = -; /* -1 indicates available entry */
- maxi = ; /* max index into client[] array */
- for ( ; ; ) {
- nready = Poll(client, maxi+, INFTIM);
- if (client[].revents & POLLRDNORM) { /* 新的客户连接被创建,listenfd变为可读 */
- clilen = sizeof(cliaddr);
- connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
- #ifdef NOTDEF
- printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
- #endif
- for (i = ; i < OPEN_MAX; i++)
- if (client[i].fd < ) {
- client[i].fd = connfd; /* 保存新的客户连接sockfd */
- break;
- }
- if (i == OPEN_MAX)
- err_quit("too many clients");
- client[i].events = POLLRDNORM;
- if (i > maxi)
- maxi = i; /* 更新maxi */
- if (--nready <= )
- continue; /* 已经不存在就绪的fd,继续poll监听 */
- }
- for (i = ; i <= maxi; i++) { /* 遍历所有的监听fd,判断其是否就绪 */
- if ( (sockfd = client[i].fd) < ) /* 未使用的pollfd结构 */
- continue;
- if (client[i].revents & (POLLRDNORM | POLLERR)) {
- if ( (n = read(sockfd, buf, MAXLINE)) < ) {
- if (errno == ECONNRESET) {
- /* 连接被重置 */
- #ifdef NOTDEF
- printf("client[%d] aborted connection\n", i);
- #endif
- Close(sockfd);
- client[i].fd = -;
- } else
- err_sys("read error");
- } else if (n == ) {
- /* 连接被关闭 */
- #ifdef NOTDEF
- printf("client[%d] closed connection\n", i);
- #endif
- Close(sockfd);
- client[i].fd = -;
- } else
- Writen(sockfd, buf, n); /* 已读到数据,写到sockfd,回射给客户 */
- if (--nready <= )
- break; /* 已经不存在就绪的fd,继续返回poll监听 */
- }
- }
- }
- }
4.I/O复用以及基于I/O复用的回射客户端/服务器的更多相关文章
- 搭建QQ聊天通信的程序:(1)基于 networkcomms.net 创建一个WPF聊天客户端服务器应用程序 (1)
搭建QQ聊天通信的程序:(1)基于 networkcomms.net 创建一个WPF聊天客户端服务器应用程序 原文地址(英文):http://www.networkcomms.net/creating ...
- 第二十二篇:基于UDP的一对回射客户/服务器程序
前言 之前曾经学习过一对回射客户/服务器程序的例子,不过那个是基于TCP协议的.本文将讲解另一对回射客户/服务器程序,该程序基于UDP协议. 由于使用的协议不同,因此编写出的程序也有本质上的区别,应将 ...
- 基于UDP的一对回射客户/服务器程序
前言 之前曾经学习过一对回射客户/服务器程序的例子,不过那个是基于TCP协议的.本文将讲解另一对回射客户/服务器程序,该程序基于UDP协议.由于使用的协议不同,因此编写出的程序也有本质上的区别,应将它 ...
- 第十一篇:基于TCP的一对回射客户/服务器程序及其运行过程分析( 下 )
执行分析 1. 打开服务器进程: 2. 执行netstat -a命令观察当前的连接状态: 第1条连接记录说明:绑定了本地主机的任意IP,端口为9877,目前处于监听状态. 3. 打开客户进程: 4. ...
- 第十篇:基于TCP的一对回射客户/服务器程序及其运行过程分析( 上 )
前言 本文将讲解一对经典的客户/服务器回射程序,感受网络编程的大致框架( 该程序稍作改装即可演变成各种提供其他服务的程序 ):同时,还将对其运行过程加以分析,观察程序背后协议的执行细节,学习调试网络程 ...
- 基于TCP的一对回射客户/服务器程序及其运行过程分析( 下 )
执行分析 1. 打开服务器进程: 2. 执行netstat -a命令观察当前的连接状态: 第1条连接记录说明:绑定了本地主机的任意IP,端口为9877,目前处于监听状态. 3. 打开客户进程: 4. ...
- 并发服务器--02(基于I/O复用——运用Select函数)
I/O模型 Unix/Linux下有5中可用的I/O模型: 阻塞式I/O 非阻塞式I/O I/O复用(select.poll.epoll和pselect) 信号驱动式I/O(SIGIO) 异步I/O( ...
- 一个I/O线程可以并发处理N个客户端连接和读写操作 I/O复用模型 基于Buf操作NIO可以读取任意位置的数据 Channel中读取数据到Buffer中或将数据 Buffer 中写入到 Channel 事件驱动消息通知观察者模式
Tomcat那些事儿 https://mp.weixin.qq.com/s?__biz=MzI3MTEwODc5Ng==&mid=2650860016&idx=2&sn=549 ...
- c++ 网络编程(四) LINUX/windows下 socket 基于I/O复用的服务器端代码 解决多进程服务端创建进程资源浪费问题
原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/9613861.html 好了,继上一篇说到多进程服务端也是有缺点的,每创建一个进程就代表大量的运 ...
随机推荐
- [LeetCode]题解(python):077-Combinations
题目来源: https://leetcode.com/problems/combinations/ 题意分析: 给定一个n和k,输出1到n的所有k个数的组合.比如n = 4,k=2 [ [2,4], ...
- Qt5位置相关函数异同详解(附源码)
Qt5中提供了丰富的位置和区域大小相关函数.下面讲一讲他们的区别. 主要函数: 1.x(),y(),pos():获取整个窗体左上角的坐标位置. 2.frameGeometry():获取整个窗体左上角的 ...
- Java并发编程总结2——慎用CAS(转)
一.CAS和synchronized适用场景 1.对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源:而CAS基于硬件实 ...
- Libev学习笔记4
这一节首先分析Libev的定时器部分,然后分析signal部分. 对定时器的使用主要有两个函数: ev_timer_init (&timeout_watcher, timeout_cb, .) ...
- poj 2051 Argus(优先队列)
题目链接: http://poj.org/problem?id=2051 思路分析: 优先级问题,使用优先队列求解:当执行某个任务后,再增加一个任务到队列中, 该任务的优先级为执行任务的时间加上其时间 ...
- JAVA面试中的几个重要基础问题
1.java是否会出现内存溢出?如何解决? 内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存.为了解决Java中内存溢出问题,我们首先必 ...
- mysql三个应用场景
场景一,数据表自动备份(多个数据表字段同步等),使用触发器.如updatelog记录对资源的所有操作日志,reslastlog记录资源最后操作的日志信息.同步方式实现如下: //创建表 DROP TA ...
- 如何自定义iOS中的控件
本文译自 How to build a custom control in iOS .大家要是有什么问题,可以直接在 twitter 上联系原作者,当然也可以在最后的评论中回复我. 在开发过程中,有时 ...
- asp.NET配置
添加用户 1.选择创建用户 2 可以使用网站管理工具来管理应用程序的所有安全设置.可以设置用户和密码(身份验证),可以创建角色(用户组),还可以创建权限(用于控制对应用程序各个部分的访问的规则). ...
- QT自定义对象导入JavaScript脚本使用
1.对象 项目属性要添加 QT += script自定义的对象头文件如下,实现正常就好,记得脚本里要调用的方法一定要定义在public slots:下,要不然调用时提示该对象没有*方法 #ifnd ...