五种I/O模型介绍

(1)阻塞I/O[默认]

当上层应用App调用recv系统调用时,如果对等方没有发送数据(Linux内核缓冲区中没有数据),上层应用Application1将阻塞;当对等方发送了数据,Linux内核recv端缓冲区数据到达,内核会把数据copy给用户空间。然后上层应用App解除阻塞,执行下一步操作。

(2)非阻塞I/O[少用]

上层应用App将套接字设置成非阻塞模式, 然后循环调用recv函数,接受数据。若缓冲区没有数据,上层应用不会阻塞,recv返回值为-1,错误码是EWOULDBLOCK。

上层应用程序不断轮询有没有数据到来。造成上层应用忙等待。大量消耗CPU。因此非阻塞模式很少直接用。应用范围小,一般和IO复用配合使用。

(3)I/O多路复用[重点]

上层应用App调用select等其他IO复用系统调用(该机制由Linux内核支持,避免了App忙等待),进行轮询文件描述符的状态变化; 当select管理的文件描述符没有数据(或者状态没有变化时),上层应用也会阻塞。

好处是:select机制可以管理多个文件描述符; 可以将select看成一个管理者,用select来管理多个IO, 一旦检测到的一个IO或者多个IO,有我们感兴事件发生时,select函数将返回,返回值为检测到的事件个数。进而可以利用select相关API函数,操作具体事件。

select函数可以设置等待时间,避免了上层应用App长期僵死。

和阻塞IO模型相比,select I/O复用模型相当于提前阻塞了。等到有数据到来时,再调用recv就不会发生阻塞。

(4)信号驱动I/O[并不常用]

上层应用App建立SIGIO信号处理程序。当缓冲区有数据到来,内核会发送信号告诉上层应用App; 当上层应用App接收到信号后,调用recv函数,因缓冲区有数据,recv函数一般不会阻塞。

这种用于模型用的比较少,属于典型的“拉模式(上层应用被动的去Linux内核空间中拉数据)”。即:上层应用App,需要调用recv函数把数据拉进来,会有时间延迟,我们无法避免在延迟时,又有新的信号的产生。

(5)异步I/O[并不常用]

上层应用App调用aio_read函数,同时提交一个应用层的缓冲区buf;调用完毕后,不会阻塞。上层应用程序App可以继续其他任务; 当TCP/IP协议缓冲区有数据时,Linux主动的把内核数据copy到用户空间。然后再给上层应用App发送信号;告诉App数据到来,需要处理!

异步IO属于典型的“推模式”, 是效率最高的一种模式,上层应用程序App有异步处理的能力(在Linux内核的支持下,处理其他任务的同时,也可支持IO通讯, 与Windows平台下的完成端口作用类似IOCP)。

Select-I/O复用

#include <sys/select.h>
#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);

select实现的是一个管理者的功能: 用select来管理多个IO, 一旦其中的一个IO或者多个IO检测到我们所感兴趣的事件, select就返回, 返回值就是检测到的事件个数, 并且由第2~4个参数返回那些IO发送了事件, 这样我们就可以遍历这些事件, 进而处理这些事件;

参数:

nfds: is the highest-numbered file descriptor in any of the three sets,plus 1[读,写,异常集合中的最大文件描述符+1].

fd_set[四个宏用来对fd_set进行操作]

FD_CLR(int fd, fd_set *set);

FD_ISSET(int fd, fd_set *set);

FD_SET(int fd, fd_set *set);

FD_ZERO(fd_set *set);

timeout[从调用开始到select返回前,会经历的最大等待时间, 注意此处是指的是相对时间]

//timeval结构:
struct timeval
{
    long    tv_sec;	    /* seconds */
    long	tv_usec;    /* microseconds */
};
//一些调用使用3个空的set, n为0, 一个非空的timeout来达到较为精确的sleep.

Linux中, select函数改变了timeout值,用来指示还剩下的时间,但很多实现并不改timeout。

为了较好的可移植性,timeout在循环中一般常被重新赋初值。

Timeout取值:

timeout== NULL

无限等待,被信号打断时返回-1, errno 设置成 EINTR

timeout->tv_sec == 0 && tvptr->tv_usec == 0

不等待立即返回

timeout->tv_sec != 0 || tvptr->tv_usec != 0

等待特定时间长度, 超时返回0

返回值:

On success, select() and pselect() return the number of  file  descriptors  contained  in

the  three  returned descriptor sets (that is, the total number of bits that are  set  in

readfds,  writefds,  exceptfds) which  may  be  zero if the timeout expires before anything

interesting happens.  On error, -1 is returned, and errno is set appropriately; the sets

and  timeout  become  undefined, so do not rely on their contents after an error.

如果成功,返回所有sets中描述符的个数;如果超时,返回0;如果出错,返回-1.

读, 写, 异常事件发生条件

可读:

可写:

异常:

套接口缓冲区有数据可读(连接的对等方发送数据过来, 填充了本地套接口缓冲区, 所以导致套接口缓冲区有数据可读);

套接口发送缓冲区有空间容纳数据(因为大部分时间发送缓冲区是未满的, 因此我们一般不关心这个事件);

套接口存在带外数据;

连接的读一半(对端)关闭(对方调用了close), 即接收到FIN段, 读操作将返回0;

连接的写一半关闭. 即收到RST段之后, 再次调用write操作;

如果是监听套接口, 已完成队列不为空时;

套接口发生了一个错误待处理, 错误可以通过getsockopt指定SO_ERROR选项来获取;

套接口发生了一个错误等待处理, 错误可以通过getsockopt指定SO_ERROR选项来获取;

/**示例1: 用select来改进echo回声服务器的client端的echoClient函数
使得可以在单进程的情况下同时监听多个文件描述符;
**/
void echoClient(int sockfd)
{
    char buf[512];
    fd_set rset;
    //确保标准输入不会被重定向
    int fd_stdin = fileno(stdin);
    int maxfd = fd_stdin > sockfd ? fd_stdin : sockfd;
    while (true)
    {
        FD_ZERO(&rset);
        FD_SET(fd_stdin, &rset);
        FD_SET(sockfd, &rset);
        int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nReady == -1)
            err_exit("select error");
        else if (nReady == 0)
            continue;

        /** nReady > 0: 检测到了可读事件 **/

        if (FD_ISSET(fd_stdin, &rset))
        {
            memset(buf, 0, sizeof(buf));
            if (fgets(buf, sizeof(buf), stdin) == NULL)
                break;
            if (writen(sockfd, buf, strlen(buf)) == -1)
                err_exit("write socket error");
        }
        if (FD_ISSET(sockfd, &rset))
        {
            memset(buf, 0, sizeof(buf));
            int readBytes = readline(sockfd, buf, sizeof(buf));
            if (readBytes == 0)
            {
                cerr << "server connect closed..." << endl;
                exit(EXIT_FAILURE);
            }
            else if (readBytes == -1)
                err_exit("read-line socket error");

            cout << buf;
        }
    }
}
/**示例2: 用select来改进echo回声服务器的server端的接受连接与处理连接部分的代码:
使得可以在单进程的情况下处理多客户连接, 对于单核的CPU来说, 单进程使用select处理连接与监听套接字其效率不一定就会比多进程/多线程性能差;

**/
    struct sockaddr_in clientAddr;
    socklen_t addrLen;
    int maxfd = listenfd;
    fd_set rset;
    fd_set allset;
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    //用于保存已连接的客户端套接字
    int client[FD_SETSIZE];
    for (int i = 0; i < FD_SETSIZE; ++i)
        client[i] = -1;
    int maxi = 0;   //用于保存最大的不空闲的位置, 用于select返回之后遍历数组

    while (true)
    {
        rset = allset;
        int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nReady == -1)
        {
            if (errno == EINTR)
                continue;
            err_exit("select error");
        }
        //nReady == 0表示超时, 但是此处是一定不会发生的
        else if (nReady == 0)
            continue;

        if (FD_ISSET(listenfd, &rset))
        {
            addrLen = sizeof(clientAddr);
            int connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);
            if (connfd == -1)
                err_exit("accept error");

            int i;
            for (i = 0; i < FD_SETSIZE; ++i)
            {
                if (client[i] < 0)
                {
                    client[i] = connfd;
                    if (i > maxi)
                        maxi = i;
                    break;
                }
            }
            if (i == FD_SETSIZE)
            {
                cerr << "too many clients" << endl;
                exit(EXIT_FAILURE);
            }
            //打印客户IP地址与端口号
            cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)
                 << ", " << ntohs(clientAddr.sin_port) << endl;
            //将连接套接口放入allset, 并更新maxfd
            FD_SET(connfd, &allset);
            if (connfd > maxfd)
                maxfd = connfd;

            if (--nReady <= 0)
                continue;
        }

        /**如果是已连接套接口发生了可读事件**/
        for (int i = 0; i <= maxi; ++i)
            if ((client[i] != -1) && FD_ISSET(client[i], &rset))
            {
                char buf[512] = {0};
                int readBytes = readline(client[i], buf, sizeof(buf));
                if (readBytes == -1)
                    err_exit("readline error");
                else if (readBytes == 0)
                {
                    cerr << "client connect closed..." << endl;
                    FD_CLR(client[i], &allset);
                    close(client[i]);
                    client[i] = -1;
                }
                //注意此处: Server从Client获取数据之后并没有立即回射回去,
                //        而是等待四秒钟之后再进行回射
                sleep(4);
                cout << buf;
                if (writen(client[i], buf, readBytes) == -1)
                    err_exit("writen error");

                if (--nReady <= 0)
                    break;
            }
    }

完整源代码请参照:

http://download.csdn.net/detail/hanqing280441589/8486517

Socket编程实践(8) --Select-I/O复用的更多相关文章

  1. Socket编程实践(10) --select的限制与poll的使用

    select的限制 用select实现的并发服务器,能达到的并发数一般受两方面限制: 1)一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n(number)来调整或 ...

  2. Socket编程实践(6) --TCP服务端注意事项

    僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 sign ...

  3. Socket编程实践(6) --TCPNotes服务器

    僵尸进程过程 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中加入 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法.解决僵尸进程 sign ...

  4. C# socket编程实践

    C# socket编程实践——支持广播的简单socket服务器   在上篇博客简单理解socket写完之后我就希望写出一个websocket的服务器了,但是一路困难重重,还是从基础开始吧,先搞定C# ...

  5. Socket编程实践(2) Socket API 与 简单例程

    在本篇文章中,先介绍一下Socket编程的一些API,然后利用这些API实现一个客户端-服务器模型的一个简单通信例程.该例子中,服务器接收到客户端的信息后,将信息重新发送给客户端. socket()函 ...

  6. Socket编程实践(1) 基本概念

    1. 什么是socket socket可以看成是用户进程与内核网络协议栈的编程接口.TCP/IP协议的底层部分已经被内核实现了,而应用层是用户需要实现的,这部分程序工作在用户空间.用户空间的程序需要通 ...

  7. C# socket编程实践——支持广播的简单socket服务器

    在上篇博客简单理解socket写完之后我就希望写出一个websocket的服务器了,但是一路困难重重,还是从基础开始吧,先搞定C# socket编程基本知识,写一个支持广播的简单server/clie ...

  8. Socket编程实践(11) --epoll原理与封装

    常用模型的特点 Linux 下设计并发网络程序,有典型的Apache模型(Process Per Connection,PPC), TPC(Thread Per Connection)模型,以及 se ...

  9. Socket编程实践(12) --UDP编程基础

    UDP特点 无连接,面向数据报(基于消息,不会粘包)的传输数据服务; 不可靠(可能会丢包, 乱序, 反复), 但因此普通情况下UDP更加高效; UDP客户/服务器模型 UDP-API使用 #inclu ...

随机推荐

  1. php闭包类外操作私有属性

    Closure::bind() Closure::bindTo(); class person{ private $age; private $sex; public function __const ...

  2. c++类的声明

    就像函数的声明与定义分离一样,我们也可以仅声明类而暂时不定义类: class ClassName;//ClassName类的声明 这种声明有时被称作前向声明 对于一个类来说,我们创建它的对象之前该类必 ...

  3. Python安装与环境变量

    Python安装与环境变量的配置  python下载: Python安装包下载地址:http://www.python.org/ 根据实际的操作系统,安装合适的安装版本.    Python安装: 本 ...

  4. Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计

    与解码相关的主要代码在上一篇博客中已经做了介绍,本篇我们会先讨论一下如何控制解码速度再提供一个我个人的封装思路.最后回归到界面设计环节重点看一下如何保证播放器界面在缩放和拖动的过程中保证视频画面的宽高 ...

  5. AnyConnect使用说明(电脑版Windows)

    一.下载客户端 Anyconnect支持Windows.Mac电脑. 二.安装 1.双击打开下载的文件,点“Next”开始安装. 2.选择“I accept …”,再点下一步. 3.点“Install ...

  6. MongoDB 自动增长

    MongoDB 没有像 SQL 一样有自动增长的功能, MongoDB 的 _id 是系统自动生成的12字节唯一标识. 但在某些情况下,我们可能需要实现 ObjectId 自动增长功能. 由于 Mon ...

  7. Docker容器访问控制

    容器的访问控制,主要通过 Linux 上的 iptables 防火墙来进行管理和实现.iptables 是 Linux 上默认的防火墙软件,在大部分发行版中都自带. 容器访问外部网络 容器要想访问外部 ...

  8. C实战:强大的程序调试工具GDB

    C实战:强大的程序调试工具GDB 1.基本调试 这里只列举最最常用的GDB命令. 1.1 启动GDB gdb program:准备调试程序.也可以直接进入gdb,再通过file命令加载. 1.2 添加 ...

  9. 干货:MySQL 索引原理及慢查询优化

    MySQL凭借着出色的性能.低廉的成本.丰富的资源,已经成为绝大多数互联网公司的首选关系型数据库.虽然性能出色,但所谓"好马配好鞍",如何能够更好的使用它,已经成为开发工程师的必修 ...

  10. 【完整的App项目】颖火虫笔记

    这是本人花大概一个星期开发出来的一款App,这是一款类似印象笔记的App,随时记录您的生活点滴.首先说一下自己为何要开发这款App,因为自己手机系统自带的笔记应用功能太low,界面不够漂亮,所以自己就 ...