3. I/O多路复用:select函数

3.1 I/O多路复用简介

(1)通信领域的时分多路复用

(2)I/O多路复用(I/O multiplexing)

  ①同一线程,通过“拨开关”方式,来同时处理多个I/O流,哪个IO准备就绪就把开关拨向它。(I/O多路复用类似于通信领域中的“时分复用”

  ②通过select/poll函数可以实现IO多路复用,他们采用轮询的方式来监视I/O。而epoll是对select/poll的加强,它是基于事件驱动,epoll_ctl注册事件并注册callback回调函数,epoll_wait只返回发生的事件避免了像select/poll对事件的整个轮询操作。

  ③I/O多路复用避免阻塞在I/O上,使原本为多进程或多线程来接收多个连接的消息变为单进程或单线程保存多个socket的状态后轮询处理。

(3)I/O复用模型

  ①进程阻塞于select调用,等待数据报socket可读。当select返回socket可读条件时,可以再调用recvfrom将数据报拷贝到应用缓冲区中。

  ②使用了系统调用select,要求两次系统调用(另一次为recvfrom),好象使得比单纯使用recvfrom效率更得更差。但实际上使用select给我们带来的好处却是让我们可以同时等待多个socket准备好。

3.2 select函数

(1)select函数

头文件

#include <sys/types.h>

#include <sys/time.h>

#include <unistd.h>

函数

int select(int maxfdp1, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

参数

(1)maxfdp1:最大fd加1(max fd plus 1),在三个描述符集中找出最高描述符编号值,然后加1,这就是第一个参数值。

(2)readfds、writefds和exceptfds:是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处理异常条件的各个描述符。每个描述符集存放在一个fd_set数据类型中。

(3)timeval结构体:指定愿意等待的时间。NULL:表示永远等待,直到列表中的某个套接字就绪才返回。如果timeout中的时间设置为0,表示不等待,测试所有指定的描述符并立即返回其他具体值表示等待的时间

struct timeval{
long tv_set; //秒
long tv_usec; //微秒
};

返回值

准备就绪的描述符数,若超时则为0,若出错则为-1

功能

确定一个或多个套接口的状态,如果有必要,则会等待。

(2)select的作用

备注

传给select的参数告诉内核

①我们所关心的socket

②对于每个socket,我们所关心的条件(是否可读一个socket,是否可写一个socket,是否关心一个socket的异常

③希望等待多长时间(可以永远等待或等待一个固定量时间,或完全不等待)

从select返时时内核告诉我们

①己准好的socket的数量

哪一个socket己准备好读、写或异常条件

③使用这种返回值,就可调用相应的I/O函数(一般是read/write),并且确知该函数不会阻塞因为socket返回就说明要等待的条件己经满足,就可以直接处理而不必阻塞等待了。

(3)处理文件描述符集(socket集)的四个宏

作用

FD_ZERO(fd_set* set)

清除一个文件描述符集

FD_SET(int fd, fd_set* set)

将一个文件描述符加入到fd_set中

FD_CLR(int fd, fd_set* set)

将一个fd从fd_set中清除

FD_ISSET(int fd, fd_set* set)

测试fd_set中的一个给定fd是否有变化

备注:

(1)在使用select函数之前,首先使用FD_ZERO和FD_SET来初始化fd_set,并在使用select函数时,可循环使用FD_ISSET测试fd_set。

【编程实验】echo服务器(利用I/O多路复用方式实现)

//vector_fd.h(与上一例相同)

#ifndef __VECTOR_H__
#define __VECTOR_H__ #include <pthread.h> //用于存放sock的动态数组(线程安全!)
typedef struct{
int *fd;
int counter; //元素个数
int max_counter;//最多存数个数,会动态增长
pthread_mutex_t mutex;
}VectorFD, *PVectorFD; //动态数组相关的操作函数
extern VectorFD* create_vector_fd(void);
extern void destroy_vector_fd(VectorFD* vfd);
extern int get_fd(VectorFD* vfd, int index);
extern void remove_fd(VectorFD* vfd, int fd);
extern void add_fd(VectorFD* vfd, int fd); #endif

//vector_fd.c  //动态数组操作函数(与上一例相同)

#include "vector_fd.h"
#include <memory.h>
#include <malloc.h>
#include <assert.h> //查找指定fd在数组中的索引值
static int indexof(VectorFD* vfd, int fd)
{
int ret = -; int i=;
for(; i<vfd->counter; i++){
if(vfd->fd[i] == fd){
ret = i;
break;
}
} return ret;
} //数组空间的动态增长
static void encapacity(VectorFD* vfd)
{
if(vfd->counter >=vfd->max_counter){
int* fds = (int*)calloc(vfd->counter + , sizeof(int));
assert(fds != NULL);
memcpy(fds, vfd->fd, sizeof(int) * vfd->counter); free(vfd->fd);
vfd->fd = fds;
vfd->max_counter += ;
}
} //动态数组相关的操作
VectorFD* create_vector_fd(void)
{
VectorFD* vfd = (VectorFD*)calloc(, sizeof(VectorFD));
assert(vfd != NULL); //分配存放fd的数组空间
vfd->fd = (int*)calloc(, sizeof(int));
assert(vfd->fd != NULL); vfd->counter = ;
vfd->max_counter = ; //对互斥锁进行初始化
pthread_mutex_init(&vfd->mutex, NULL); return vfd;
} void destroy_vector_fd(VectorFD* vfd)
{
assert(vfd != NULL);
//销毁互斥锁
pthread_mutex_destroy(&vfd->mutex); free(vfd->fd);
free(vfd);
} int get_fd(VectorFD* vfd, int index)
{
int ret = ;
assert(vfd != NULL); pthread_mutex_lock(&vfd->mutex); if(( <= index) && (index < vfd->counter)){
ret = vfd->fd[index];
} pthread_mutex_unlock(&vfd->mutex); return ret;
} void remove_fd(VectorFD* vfd, int fd)
{
assert(vfd != NULL); pthread_mutex_lock(&vfd->mutex); int index = indexof(vfd, fd); if(index >= ){
int i = index;
for(; i<vfd->counter-; i++){
vfd->fd[i] = vfd->fd[i+];
} vfd->counter--;
} pthread_mutex_unlock(&vfd->mutex);
} void add_fd(VectorFD* vfd, int fd)
{
assert(vfd != NULL); encapacity(vfd);
vfd->fd[vfd->counter++] = fd;
}

//echo_tcp_server_select.c(与上一例相同)

#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <time.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>
#include "vector_fd.h"
#include <fcntl.h> /*基于I/O多路复用的高并发服务器编程
测试:telnet 127.0.0.1 xxxx
http://xxx.xxx.xxx.xxx:端口号
注意:演示时可关闭服务器的防火墙,防火墙口被过滤
#service iptables status 查看防火墙
#service iptables stop 关闭防火墙
*/ VectorFD* vfd;
int sockfd;
int bStop = ; void sig_handler(int signo)
{
if(signo == SIGINT){
bStop = ;
printf("server close\n"); close(sockfd);
destroy_vector_fd(vfd); exit();
}
} void out_addr(struct sockaddr_in* clientAddr)
{
char ip[];
memset(ip, , sizeof(ip));
int port = ntohs(clientAddr->sin_port);
inet_ntop(AF_INET, &clientAddr->sin_addr.s_addr, ip, sizeof(ip)); printf("%s(%d) connnected!\n", ip, port);
} /*服务程序
* fd对应于某个连接的客户端,和某一个连接的客户端进行双向通信
*/
void do_service(int fd)
{
/*服务端和客户端进行读写操作(双向通信)*/
char buff[]; memset(buff, , sizeof(buff));
size_t size = read(fd, buff, sizeof(buff)); //读取客户端发送过来的消息
//若读不到数据直接返回了,直接服务于下一个客户端
//因此不需要判断size小于0的情况。
if(size == ){ //客户端己关闭连接
char info[] = "client close\n";
write(STDOUT_FILENO, info, sizeof(info)); //将fd从动态数组中删除
remove_fd(vfd, fd);
close(fd);
}else if(size > ){
write(STDOUT_FILENO, buff, sizeof(buff));//显示客户端发送的消息
//写回客户端(回显功能)
if(write(fd, buff, sizeof(buff)) != size){
if(errno == EPIPE){
//如果客户端己被关闭(相当于管道的读端关闭),会产生SIGPIPE信号
//并将errno设置为EPIPE
perror("write error");
remove_fd(vfd, fd);
close(fd);
}
}
}
} //遍历动态数组中所有的socket描述符,并将之加入到fd_set中。
//同时此函数返回动态数组中最大的那个描述符
static int add_set(fd_set* set)
{
FD_ZERO(set); //清空描述符集
int max_fd = vfd->fd[]; int i=;
for(; i<vfd->counter; i++){
int fd = get_fd(vfd, i);
if(fd > max_fd)
max_fd = fd;
FD_SET(fd, set); //将fd加入到fd_set中
} return max_fd;
} //线程函数
void* th_fn(void* arg)
{
struct timeval t;
t.tv_sec = ;
t.tv_usec = ;
int n = ; //返回select返回的准备好的socket数量
int maxfd; //所有socket描述符的最大值
fd_set set;
maxfd = add_set(&set);
/*
* 调用select函数会阻塞,委托内核去检查传入的描述符集是否有socket己准备好,
* 若有,则返回准备好的socket数量,超时则返回0
* 第1个参数为fd_set中socket的范围(最大描述符+1)
*/
while(((n = select(maxfd + , &set, NULL, NULL, &t)) >=) && (!bStop)){
if(n > ){
int i = ;
//检测哪些socket准备好,并和这些准备好的socket对应的客户端进行双向通信
for(; i<vfd->counter; i++){
int fd = get_fd(vfd, i);
if(FD_ISSET(fd, &set)){
do_service(fd);
}
}
} //重新设置时间
t.tv_sec = ;
t.tv_usec = ; //清空描述符集
//重新遍历动态数组中最新的描述符,并放置到fd_set
maxfd = add_set(&set);
} return (void*);
} int main(int argc, char* argv[])
{
if(argc < ){
printf("usage: %s port\n", argv[]);
exit();
} //按ctrl-c时中止服务端程序
if(signal(SIGINT, sig_handler) == SIG_ERR){
perror("signal sigint error");
exit();
} /*步骤1:创建socket(套接字)
*注:socket创建在内核中,是一个结构体
*AF_INET:IPv4
*SOCK_STREAM:tcp协议
*/
sockfd = socket(AF_INET, SOCK_STREAM, ); /*步骤2:将sock和地址(包括ip、port)进行绑定*/
struct sockaddr_in servAddr; //使用专用地址结构体
memset(&servAddr, , sizeof(servAddr));
//往地址中填入ip、port和Internet地址族类型
servAddr.sin_family = AF_INET;//IPv4
servAddr.sin_port = htons(atoi(argv[])); //port
servAddr.sin_addr.s_addr = INADDR_ANY; //任一可用的IP if(bind(sockfd, (struct sockaddr*)&servAddr, sizeof(servAddr)) < ){
perror("bind error");
exit();
} /*步骤3:调用listen函数启动监听
* 通知系统去接受来自客户端的连接请求
*/
if(listen(sockfd, ) < ){ //队列中最多允许10个连接请求
perror("listen error");
exit();
} //创建放置套接字描述符的动态数组
vfd = create_vector_fd(); //设置线程的分离属性
pthread_t th;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
//启动子线程
int err;
if((err = pthread_create(&th, &attr, th_fn, (void*))) != ){
perror("pthread create error");
exit();
}
pthread_attr_destroy(&attr); /*(1)主线程获得客户端连接,将新的socket描述符放置到动态数组中
*(2)子线程的任务
A.调用select委托内核去检查传入到select中的描述符是否准备好
* B.利用FD_ISSET来找出准备好的那些描述符并和对应的客户端进行双向通信
*/ struct sockaddr_in clientAddr;
socklen_t len = sizeof(clientAddr); while(!bStop){
/*步骤4:调用accept函数,从请求队列中获取一个连接
* 并返回新的socket描述符
* */
int fd = accept(sockfd, (struct sockaddr*)&clientAddr, &len); if(fd < ){
perror("accept error");
continue;
} //输出客户端信息
out_addr(&clientAddr); //将返回的新socket描述符加入到动态数组中
add_fd(vfd, fd);
} close(sockfd);
destroy_vector_fd(vfd);
return ;
}
/*输出结果
* [root@localhost 15.AdvNet]# gcc -o bin/echo_tcp_server_select -Iinclude bin/vector_fd.o src/echo_tcp_server_select.c -lpthread
* [root@localhost 15.AdvNet]# bin/echo_tcp_server_select 8888
* 127.0.0.1(40695) connnected!
* abcdefaaabbbcdef^Cserver close
*/

//echo_tcp_client.c(与上一例相同)

#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h> int main(int argc, char* argv[])
{
if(argc < ){
printf("usage: %s ip port\n", argv[]);
exit();
} /*步骤1: 创建socket(套接字)*/
int sockfd = socket(AF_INET, SOCK_STREAM, );
if(sockfd < ){
perror("socket error");
} //往servAddr中填入ip、port和地址族类型
struct sockaddr_in servAddr;
memset(&servAddr, , sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(atoi(argv[]));
//将ip地址转换成网络字节序后填入servAdd中
inet_pton(AF_INET, argv[], &servAddr.sin_addr.s_addr); /*步骤2: 客户端调用connect函数连接到服务器端*/
if(connect(sockfd, (struct sockaddr*)&servAddr, sizeof(servAddr)) < ){
perror("connect error");
exit();
} /*步骤3: 调用自定义的协议处理函数和服务端进行双向通信*/
char buff[];
size_t size;
char* prompt = ">"; while(){
memset(buff, , sizeof(buff));
write(STDOUT_FILENO, prompt, );
size = read(STDIN_FILENO, buff, sizeof(buff));
if(size < ) continue; buff[size-] = '\0';
//将键盘输入的内容发送到服务端
if(write(sockfd, buff, sizeof(buff)) < ){
perror("write error");
continue;
}else{
memset(buff, , sizeof(buff));
//读取来自服务端的消息
if(read(sockfd, buff, sizeof(buff)) < ){
perror("read error");
continue;
}else{
printf("%s\n", buff);
}
}
} /*关闭套接字*/
close(sockfd);
}

第15章 高并发服务器编程(2)_I/O多路复用的更多相关文章

  1. 第15章 高并发服务器编程(1)_非阻塞I/O模型

    1. 高性能I/O (1)通常,recv函数没有数据可用时会阻塞等待.同样,当socket发送缓冲区没有足够多空间来发送消息时,函数send会阻塞. (2)当socket在非阻塞模式下,这些函数不会阻 ...

  2. SpringCloud、Nginx高并发核心编程 【2020年11月新书 】

    文章太长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典极品 : 三大本< Java 高并发 三部曲 > 面试 + 大厂 + 涨薪必备 疯狂创客圈 经 ...

  3. Linux + C + Epoll实现高并发服务器(线程池 + 数据库连接池)(转)

    转自:http://blog.csdn.net/wuyuxing24/article/details/48758927 一, 背景 先说下我要实现的功能,server端一直在linux平台下面跑,当客 ...

  4. Linux下高并发网络编程

      Linux下高并发网络编程 1.修改用户进程可打开文件数限制 在Linux平台上,无论编写客户端程序还是服务端程序,在进行高并发TCP连接处理时, 最高的并发数量都要受到系统对用户单一进程同时可打 ...

  5. JAVA NIO non-blocking模式实现高并发服务器

    JAVA NIO non-blocking模式实现高并发服务器 分类: JAVA NIO2014-04-14 11:12 1912人阅读 评论(0) 收藏 举报 目录(?)[+] Java自1.4以后 ...

  6. PHP写的异步高并发服务器,基于libevent

    PHP写的异步高并发服务器,基于libevent 博客分类: PHP PHPFPSocketLinuxQQ  本文章于2013年11月修改. swoole已使用C重写作为PHP扩展来运行.项目地址:h ...

  7. JAVA NIO non-blocking模式实现高并发服务器(转)

    原文链接:JAVA NIO non-blocking模式实现高并发服务器 Java自1.4以后,加入了新IO特性,NIO. 号称new IO. NIO带来了non-blocking特性. 这篇文章主要 ...

  8. 高并发服务器建议调小 TCP 协议的 time_wait 超时时间。

    1. [推荐]高并发服务器建议调小 TCP 协议的 time_wait 超时时间. 说明:操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服 务器端会因为处于 ...

  9. 为一个支持GPRS的硬件设备搭建一台高并发服务器用什么开发比较容易?

    高并发服务器开发,硬件socket发送数据至服务器,服务器对数据进行判断,需要实现心跳以保持长连接. 同时还要接收另外一台服务器的消支付成功消息,接收到消息后控制硬件执行操作. 查了一些资料,java ...

随机推荐

  1. pyqt5:图片自适应QLabel大小和图片移除

    参考链接: https://www.e-learn.cn/content/qita/669569 图片自适应QLabel大小 # coding=utf- import sys from PyQt5.Q ...

  2. grafana+influxdb+telegraf监控服务器cpu,内存和硬盘

    随便抄了一篇,目前我们的项目也在用,这个是linux和windows通吃的一种监控方案,非常有效,详细和优美,需要监控什么具体的业务内容,自己向influxdb中插入就行了. 监控服务器状态是运维必不 ...

  3. WebGL编程指南理论分析之物体层次模型(局部运动)

    书中340页,开始讲到层次模型(关节模型),也就是整个物体,可以自由控制其各部位单独运动,就像关节一样,互不干扰或者有一定关联. 就像图中,左右键控制整个物体(arm1和arm2)的Y轴旋转,上下键控 ...

  4. Vue 2.0 学习路线

    「 Vue很难学吗 」 对于我这种从0.x版本就开始体验 vuejs 的人来说,当然不算难,那时候没各种脚手架和复杂搭配,仅仅是一个mvvm的解决方案库而已,解决了jq带来的繁琐操作dom痛点,所以就 ...

  5. PyTorch 数据集类 和 数据加载类 的一些尝试

    最近在学习PyTorch,  但是对里面的数据类和数据加载类比较迷糊,可能是封装的太好大部分情况下是不需要有什么自己的操作的,不过偶然遇到一些自己导入的数据时就会遇到一些问题,因此自己对此做了一些小实 ...

  6. win10笔记本实现双屏显示的自如切换

    前言 使用电脑的过程中想一边看内容,一边进行编辑,这就涉及到双屏显示并实现扩展分屏,本文就介绍一下这些操作. 工具 win10-thinkpad-E470:另一块显示屏(博主的是戴尔的显示器):一条外 ...

  7. .net core grpc 实现通信(一)

    现在系统都服务化,.net core 实现服务化的方式有很多,我们通过grpc实现客户端.服务端通信. grpc(https://grpc.io/)是google发布的一个开源.高性能.通用RPC(R ...

  8. list.stream().parallel() 并行流

    https://blog.csdn.net/u011001723/article/details/52794455/  :  parallel()其实就是一个并行执行的流.它通过默认的ForkJoin ...

  9. js代码中碰到的函数

    第一个--->字符串的截取substring()方法 substring(a,b)--->[a,b)区间截取字符.下标从0开始.从a下标开始,截取到b下标的前一个字符.返回一个新的字符串 ...

  10. 使用docusaurus 搭建开发&&api && 博客站点

    对于日常的开发系统以及产品一个简单,方便的api&&文档网站可以七很大的作用 docusaurus 是facebook开源的文档管理框架,使用它我们可以快速的创建专业. 完备的文档站点 ...