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. SSH项目搭建(三)——Maven多模块搭建项目

    多模块开发,大致的思想就是把一个项目按某种方式分成多个模块,再把模块们连接成一个整体,我们在开发的时候,可以很清晰的操作每一个模块,可以大大提高开发的效率. Java web项目,最常见的就是按代码的 ...

  2. iOS-----使用NSURLConnection

    使用NSURLConnection 如果只是为了读取HTTP等服务器数据,或向服务器提交数据,iOS还提供了NSURLConnection类,NSURLConnection使用NSURLRequest ...

  3. OK335xS UART device registe hacking

    /************************************************************************* * OK335xS UART device reg ...

  4. c++ json 详解

    一. 使用jsoncpp解析json Jsoncpp是个跨平台的开源库,首先从http://jsoncpp.sourceforge.net/上下载jsoncpp库源码,我下载的是v0.5.0,压缩包大 ...

  5. 拒绝了对对象 '****'(数据库 '******',所有者 '***')的 SELECT 权限

    数据库(xxx) --->安全性---->架构---->dbo(属性)--->权限--->添加--->浏览-->public 添加 select 等您需要的权 ...

  6. LeetCode Pascal's Triangle && Pascal's Triangle II Python

    Pascal's Triangle Given numRows, generate the first numRows of Pascal's triangle. For example, given ...

  7. 模板引擎jade学习

    语言参考 标签列表 doctype Tags Block Expansion Attributes Boolean Attributes Class Attributes Class Literal ...

  8. RAC3——RAC原理开始

    1.RAC并发 RAC的本质是一个数据库,只不过现在这个数据库运行在了多台计算机上,在原先的单实例中,一个进程是否可以修改一条数据,取决于是否有其他进程(同一台计算机上)并发修改.在RAC环境下,这种 ...

  9. java成员内部类

    java成员内部类依赖于外部类而存在,故创建内部类需要首先创建其关联的外部类. public class Test { public static void main(String args[]) { ...

  10. linux非root用户执行开机启动程序

    问题 开机启动其他用户的程序或者说非root用户执行开机启动 编写开机启动脚本 编写开机启动脚本apple_tree,放到/etc/init.d,系统启动时会自动执行. 例如,/etc/init.d/ ...