第15章 高并发服务器编程(2)_I/O多路复用
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,表示不等待,测试所有指定的描述符并立即返回。其他具体值表示等待的时间。
|
返回值 |
准备就绪的描述符数,若超时则为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多路复用的更多相关文章
- 第15章 高并发服务器编程(1)_非阻塞I/O模型
1. 高性能I/O (1)通常,recv函数没有数据可用时会阻塞等待.同样,当socket发送缓冲区没有足够多空间来发送消息时,函数send会阻塞. (2)当socket在非阻塞模式下,这些函数不会阻 ...
- SpringCloud、Nginx高并发核心编程 【2020年11月新书 】
文章太长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典极品 : 三大本< Java 高并发 三部曲 > 面试 + 大厂 + 涨薪必备 疯狂创客圈 经 ...
- Linux + C + Epoll实现高并发服务器(线程池 + 数据库连接池)(转)
转自:http://blog.csdn.net/wuyuxing24/article/details/48758927 一, 背景 先说下我要实现的功能,server端一直在linux平台下面跑,当客 ...
- Linux下高并发网络编程
Linux下高并发网络编程 1.修改用户进程可打开文件数限制 在Linux平台上,无论编写客户端程序还是服务端程序,在进行高并发TCP连接处理时, 最高的并发数量都要受到系统对用户单一进程同时可打 ...
- JAVA NIO non-blocking模式实现高并发服务器
JAVA NIO non-blocking模式实现高并发服务器 分类: JAVA NIO2014-04-14 11:12 1912人阅读 评论(0) 收藏 举报 目录(?)[+] Java自1.4以后 ...
- PHP写的异步高并发服务器,基于libevent
PHP写的异步高并发服务器,基于libevent 博客分类: PHP PHPFPSocketLinuxQQ 本文章于2013年11月修改. swoole已使用C重写作为PHP扩展来运行.项目地址:h ...
- JAVA NIO non-blocking模式实现高并发服务器(转)
原文链接:JAVA NIO non-blocking模式实现高并发服务器 Java自1.4以后,加入了新IO特性,NIO. 号称new IO. NIO带来了non-blocking特性. 这篇文章主要 ...
- 高并发服务器建议调小 TCP 协议的 time_wait 超时时间。
1. [推荐]高并发服务器建议调小 TCP 协议的 time_wait 超时时间. 说明:操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服 务器端会因为处于 ...
- 为一个支持GPRS的硬件设备搭建一台高并发服务器用什么开发比较容易?
高并发服务器开发,硬件socket发送数据至服务器,服务器对数据进行判断,需要实现心跳以保持长连接. 同时还要接收另外一台服务器的消支付成功消息,接收到消息后控制硬件执行操作. 查了一些资料,java ...
随机推荐
- Ubuntu 12.04硬盘安装教程
从服务器下载Ubuntu 12.04光盘镜像文件到 C 盘.下载地址:\\192.167.100.225\share\Tool\Ubuntu\ubuntu-12.04.1-desktop-amd64. ...
- 新手小白Linux(Centos6.5)部署java web项目(总)
一.准备 1.linux centos版本的相关命令操作,千万别找ubuntu的,好多命令都不一样,新手小白我傻傻不知道硬是浪费了一天的时间……(百度百科linux版本了解一下) 2.远程登录: P ...
- 第32课 初探C++标准库
有趣的重载: 实验: 将1左移到cout对象中. 将Test改名为Console,此时我们的本意是想让这个cout代表当前的命令行: cout代表命令行的一个实例,本意是想将1打印到命令行上. 我们在 ...
- 字符串的比较【string和字符数组】
无论是string 还是 字符数组的字符串比较函数,返回的都是字典序的大小.如 1234 和 5 比较时就是1234的字典序小于5,要想比较字符串表示的数字的大小,需要自己写函数比较
- day41 mysql 学习 练习题 重要*****
MySQL 练习题[二1.表如下: 收获和注意点:***** #1 GROUP by 可以放到where s_id in ()条件局后边 GROUP BY s_id having 详见题12 #2 做 ...
- drill 集成开源s3 存储minio
drill 支持s3数据的查询,同时新版的通过简单配置就可以实现minio 的集成 测试使用docker 运行drill 参考 https://www.cnblogs.com/rongfenglian ...
- Sqoop之 Sqoop 1.4.6 安装
1. sqoop数据迁移 1.1 概述 sqoop是apache旗下一款“Hadoop和关系数据库服务器之间传送数据”的工具. 导入数据:MySQL,Oracle导入数据到Hadoop的HDFS.HI ...
- nginx 各参数说明
nginx 各参数说明: 参数 所在上下文 含义
- yii2 数据库查询
下面介绍一下 Yii2.0 对数据库 查询的一些简单的操作 User::find()->all(); 此方法返回所有数据: User::findOne($id); 此方法返回 主键 id=1 的 ...
- JVM内存模型(一)
主要澄清之前对JVM内存模型的一些误区: JMV内存主要分为5块:方法区(Method Area),堆区(Heap),虚拟机栈(VM stack),本地方法栈(Native Method stack) ...