UNIX网络编程入门——I/O复用

UNIX网络编程入门——TCP客户/服务器程序详解

UNIX网络编程入门——TCP客户/服务器程序存在问题及解决

在介绍I/O复用之前,我们先来看一个情况:运行我们前面两篇文章里面的服务器和客户端程序,当客户端在等待用户输入一行字符时,服务器崩溃或者关机了。此时虽然服务器TCP会正确地发送FIN给客户端TCP,但客户端阻塞于fget函数,等待从标准输入读入,无法及时地知道服务器已经终止,要等到它得到标准输入发送给服务器时才会返回错误。

要解决这个问题,就需要一种能力,能够同时观察多个I/O条件是否就绪,一旦其中一个就绪了,内核就通知进程,这被称为I/O复用。比如在上面这个情况下,就让内核观察标准输入与服务器连接得套接字这两个I/O条件,一旦其中一个就绪就通知客户端,这样客户端就可以同时处理用户输入和服务器TCP返回的信息了。

一、I/O模型

在介绍I/O复用使用的两个函数select和poll之前,我们先了解一些基本的I/O模型。

对于一个套接字的输入操作,通常可分为两个阶段,第一阶段是等待数据从网络上到达内核缓冲区,第二阶段是从内核缓冲区复制数据到进程缓存区。结合这两个阶段来看一下各个I/O模型的区别

1、阻塞式I/O模型

阻塞式io从一开始等待输入就阻塞,直到数据到达并复制到进程缓冲区这两个阶段都完成才返回。

2、非阻塞式I/O模型

如图,应用进程不断询问内核是否已有数据到达,这称为轮询(polling)。非阻塞io在第一个阶段不阻塞,但阻塞于第二个阶段。

3、I/O复用模型

i/o复用模型并不阻塞于单个系统调用,而是阻塞于select调用上,select可以同时监听多个描述符,只要有其中一个就绪就退出阻塞状态,这就相当于同时阻塞在多个系统调用之上。

4、信号驱动式I/O模型

信号驱动io在安装了信号处理函数后就继续执行,并不阻塞于第一阶段,等到接收到信号后再处理第二阶段。

5、异步I/O模型

异步io可以不阻塞于两个阶段,等到数据复制完成才调用信号处理函数。不过这个io模型比较少见。

二、select函数

函数原型:int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);

我们可以调用select告知内核当一个或多个条件发生时或者超时才返回。最后一个参数timeout指定超时时间,中间三个参数readset、writeset、errfds指定我们要让内核等待的可读、可写或异常的描述符集,第一个参数是我们所指定的描述符里面id值最大的那个,表示这些描述符的范围。

那么内核要等待到什么时候才返回呢?这里我们以套接字描述符为例

对于等待可读条件的套接字描述符,下列情况就绪:

  • 1、socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时可以无阻塞地读该socket,并且读操作返回的字节数大于0。
  • 2、socket通信对方关闭连接。此时对该socket读操作将返回0。
  • 3、监听socket上有新的连接请求。
  • 4、socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

对于等待可写条件的套接字描述符,下列情况就绪:

  • 1、socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞写该socket,并且写操作返回的字节数大于0。
  • 2、socket写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
  • 3、socket使用非阻塞connect连接成功或者失败(超时)之后。
  • 4、socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

也就是说,当在readset中指定一个套接字描述符,如果该描述符有上面可读的情况,select就返回到用户进程。

三、客户端使用select进行I/O复用

1、逐行输入情况

现在我们重写之前的客户端处理输入输入的函数str_cli,该函数位于我们的myunp.c里

void str_cli(FILE *fp, int sockfd)
{
    int         maxfdp1; //最大描述符编号
    fd_set      rset; //我们要等待读的描述符集
    char        sendline[MAXLINE], recvline[MAXLINE];

    FD_ZERO(&rset); //先清零
    for ( ; ; ) {
        FD_SET(fileno(fp), &rset); //把标准输入放入描述符集
        FD_SET(sockfd, &rset); //把套接字放入描述符集
        maxfdp1 = max(fileno(fp), sockfd) + 1; //最大描述符编号,因为是从0开始,所以要加1
        Select(maxfdp1, &rset, NULL, NULL, NULL); //我们只等待可读的描述符,其他不设置,置为NULL指针

        //当select跳出阻塞后判断是那个描述符可读
        if (FD_ISSET(sockfd, &rset)) {  /* socket is readable */ //套接字可读的情况
            if (Readline(sockfd, recvline, MAXLINE) == 0)
                err_quit("str_cli: server terminated prematurely");
            Fputs(recvline, stdout);
        }

        if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */ //标准输入可读的情况
            if (Fgets(sendline, MAXLINE, fp) == NULL)
                return;     /* all done */
            Writen(sockfd, sendline, strlen(sendline));
        }
    }
}

这样客户端就可以同时处理标准输入和套接字了,当服务器进程终止时也能及时得到通知。

2、批量输入情况

对于某些情况,上面的函数仍然不能正常工作。

我们将客户端和服务器之间的连接作为全双工连接来考虑,也就是说在该连接上可以同时发送或接受消息。假设客户端发送一行文本给服务器,到接收到服务器回送文本的这段时间为8个时间单位,也就是往返时间RRT(round-trip time)为8个时间单位。我们把输入保存在一个文件里面,总共9行文本,重定向到标准输入,下面是第7、8个时刻网络上的情况,我们的第9行文本在时刻8发出,尔后标准输入读到了文件的EOF,导致了客户端这边连接的关闭。问题出现了,现在在网络连接上还有请求在发送到服务器的路上,还有应答在返回客户端的路上,这些应答都将成为弃子。

回想一下,前面我们提到的TCP协议的终止流程中有一个半关闭状态,在客户端关闭准备关闭连接的情况下,仍然能接收来自服务器的数据。我们可以使用shutdown函数来终止网络连接,之前我们说过close函数也能关闭连接,但close只会是引用计数符减一,同时也终止读和写两个方向的数据传送,而shutdown函数可以通过指定参数只关闭读或者写。

同时,为了防止缓冲区的复杂问题,下面的代码也把我们之前对于文本行操作的代码换成了对于缓冲区进行操作。

void str_cli(FILE *fp, int sockfd)
{
    int         maxfdp1, stdineof;
    fd_set      rset;
    char        buf[MAXLINE];
    int     n;

    stdineof = 0; //标准输入是否结束的标志
    FD_ZERO(&rset);
    for ( ; ; ) {
        if (stdineof == 0) //当文件还没读到EOF时继续select标准输入
            FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        maxfdp1 = max(fileno(fp), sockfd) + 1;
        Select(maxfdp1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(sockfd, &rset)) {  /* socket is readable */
            if ( (n = Read(sockfd, buf, MAXLINE)) == 0) { //套接字缓冲区读完了
                if (stdineof == 1) //如果标准输入也完了,那么就是正常的结束
                    return;     /* normal termination */
                else
                    err_quit("str_cli: server terminated prematurely"); //否则就是服务器提前终止了
            }

            Write(fileno(stdout), buf, n);
        }

        if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
            if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) { //当输入文件读到EOF时
                stdineof = 1; //设置标志
                Shutdown(sockfd, SHUT_WR);  /* send FIN */ //半关闭TCP,关闭写,但仍可读sockfd
                FD_CLR(fileno(fp), &rset); //清除select描述符集中的标准输入
                continue;
            }

            Writen(sockfd, buf, n);
        }
    }
}

四、服务器使用select进行I/O复用

之前是使用fork派生子进程来实现并发的,这里重写它使用I/O复用来实现并发。

我们在程序中维护两个数组,一个是记录连接到这台服务器上的客户机的数组client[],初始化为-1,另一个是服务器select监听的可读描述符集,前三个是标准输入、标准输出、标准错误,fd3是监听描述符listenfd。

当有一个客户端连接建立时,select的描述符集增加了这个客户端的套接字描述符,它的描述符是fd4,因此在client[]中第一个位置置为4,表示连接了一个客户,这个客户的套接字描述符为fd4.

下面是第二个客户建立连接的情况

当第一个客户结束连接时,情况如下图所示,注意,maxfd的值并没有改变。

服务器程序代码改写如下:

int main(int argc, char **argv)
{
    int                 i, maxi, maxfd, listenfd, connfd, sockfd;
    int                 nready, client[FD_SETSIZE];
    ssize_t             n;
    fd_set              rset, allset;
    char                buf[MAXLINE];
    socklen_t           clilen;
    struct sockaddr_in  cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    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);

    maxfd = listenfd;           /* initialize */
    maxi = -1;                  /* index into client[] array */
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;         /* -1 indicates available entry */
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);
/* end fig01 */

/* include fig02 */
    for ( ; ; ) {
        rset = allset;      /* structure assignment */
        nready = Select(maxfd+1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(listenfd, &rset)) {    /* new client connection */
            clilen = sizeof(cliaddr);
            connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef  NOTDEF
            printf("new client: %s, port %d\n",
                    Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
                    ntohs(cliaddr.sin_port));
#endif

            for (i = 0; i < FD_SETSIZE; i++)
                if (client[i] < 0) {
                    client[i] = connfd; /* save descriptor */
                    break;
                }
            if (i == FD_SETSIZE)
                err_quit("too many clients");

            FD_SET(connfd, &allset);    /* add new descriptor to set */
            if (connfd > maxfd)
                maxfd = connfd;         /* for select */
            if (i > maxi)
                maxi = i;               /* max index in client[] array */

            if (--nready <= 0)
                continue;               /* no more readable descriptors */
        }

        for (i = 0; i <= maxi; i++) {   /* check all clients for data */
            if ( (sockfd = client[i]) < 0)
                continue;
            if (FD_ISSET(sockfd, &rset)) {
                if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
                        /*4connection closed by client */
                    Close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                } else
                    Writen(sockfd, buf, n);

                if (--nready <= 0)
                    break;              /* no more readable descriptors */
            }
        }
    }
}
/* end fig02 */

五、poll与select

poll函数提供的功能与select类似,只是实现不同相同,这里我就不继续下去了。

原文地址:https://www.cnblogs.com/silence1772/p/9372063.html

时间: 2024-11-05 02:42:13

UNIX网络编程入门——I/O复用的相关文章

UNIX网络编程入门——TCP客户/服务器程序详解

前言 最近刚开始看APUE和UNP来学习socket套接字编程,因为网络这方面我还没接触过,要等到下学期才上计算机网络这门课,所以我就找了本教材啃了一两天,也算是入了个门. 至于APUE和UNP这两本书,书是好书,网上也说这书是给进入unix网络编程领域初学者的圣经,这个不可置否,但这个初学者,我认为指的是接受过完整计算机本科教育的研究生初学者,需要具有完整计算机系统,体系结构,网络基础知识.基础没打好就上来啃书反而会适得其反,不过对于我来说也没什么关系,因为基础课也都上得差不多了,而且如果书读

UNIX网络编程:I/O复用技术(select、poll、epoll)

Unix下可用的I/O模型一共有五种:阻塞I/O .非阻塞I/O .I/O复用 .信号驱动I/O .异步I/O.此处我们主要介绍第三种I/O符复用. I/O复用的功能:如果一个或多个I/O条件满足(输入已准备好读,或者描述字可以承接更多输出)时,我们就被通知到.这就是有select.poll.epoll实现. I/O复用应用场合: 1.当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用.在这前一段中已做描述. 2.一个客户同时处理多个套接口是可能的,但很少出现. 3.如果

【Unix网络编程】chapter6 IO复用:select和poll函数

chapter6 6.1 概述 I/O复用典型使用在下列网络应用场合. (1):当客户处理多个描述符时,必须使用IO复用 (2):一个客户同时处理多个套接字是可能的,不过不叫少见. (3):如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字. (4):如果一个服务器既要处理TCP,又要处理UDP (5):如果一个服务器要处理多个服务或多个协议 IO复用并非只限于网络,许多重要的应用程序也需要使用这项技术. 6.2 I/O模型 在Unix下可用的5种I/O模型的基本区别: (1)阻塞式I

《UNIX网络编程》入门客户端服务器例子

最近在看<UNIX网络编程>(简称unp)和<Linux程序设计>,对于unp中第一个获取服务器时间的例子,实践起来总是有点头痛的,因为作者将声明全部包含在了unp.h里,导致后面编写代码会对这个头文件造成依赖,而学习不到调用了相应功能之后,应该包含哪些确切的头文件. 再者,我下载了unp.h之后,头文件包含再次产生了其他的依赖缺失,因此便参考了<Linux程序设计>中socket一章的入门例子的头文件包含,并且编译中仍然找不到的包含或者是宏定义在unp.h中搜索并粘贴

【unix网络编程第三版】阅读笔记(五):I/O复用:select和poll函数

本博文主要针对UNP一书中的第六章内容来聊聊I/O复用技术以及其在网络编程中的实现 1. I/O复用技术 I/O多路复用是指内核一旦发现进程指定的一个或者多个I/O条件准备就绪,它就通知该进程.I/O复用适用于以下场合: (1) 当客户处理多个描述符(一般是交互式输入或网络套接字),必须适用I/O复用 (2) 当一个客户处理多个套接字时,这种情况很少见,但也可能出现 (3) 当一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用 (4) 如果一个服务器既要适用TCP,

Unix网络编程中的五种I/O模型_转

转自:Unix网络编程中的的五种I/O模型 下面主要是把unp第六章介绍的五种I/O模型. 1. 阻塞I/O模型 例如UDP函数recvfrom的内核到应用层.应用层到内核的调用过程是这样的:首先把描述符.接受数据缓冲地址.大小传递给内核,但是如果此时 该与该套接口相应的缓冲区没有数据,这个时候就recvfrom就会卡(阻塞)在这里,知道数据到来的时候,再把数据拷贝到应用层,也就是传进来的地址空 间,如果没有数据到来,就会使该函数阻塞在那里,这就叫做阻塞I/O模型,如下图: 2. 非阻塞I/O模

Unix网络编程--卷一:套接字联网API 读书笔记

UNIX网络编程--卷一:套接字联网API 本书面对的读者是那些希望自己编写的程序能够使用成为套接字(socket)的API进行彼此通信的人. 目录: 1.简介 2.传输层:TCP.UDP和SCTP 3.套接字编程简介 4.基本TCP套接字编程 5.TCP客户/服务器程序例子 6.I/O复用:select和poll函数 7.套接字选项 8.基本UDP套接字编程 9.基本SCTP套接字编程 10.SCTP客户/服务器程序例子 11.名字与地址转换 12.IPV4与IPV6互操作性 13.守护进程和

UNIX网络编程——网络I/O模型

在学习UNIX网络编程的时候.一開始分不清 同步 和 异步,所以还是总结一下,理清下他们的差别比較好. IO分类 IO依据对IO的调度方式可分为堵塞IO.非堵塞IO.IO复用.信号驱动IO.异步IO. IO操作整个流程分为 可操作推断 和 实际IO操作 两个区间,我们能够称之为两个半程,前半程推断是否可操作,后半程进行实际操作. 当中堵塞IO.非堵塞IO.IO复用.信号驱动IO由于其[实际的IO操作是同步堵塞]的,所以一般把他们归为同步IO,异步IO的实际IO操作是在独立的线程中完毕的,所以称为

【实习记】2014-08-25版本管理svn与git学习对比+看书UNIX网络编程

git也算中等熟练了,对其哲学也明白.但是svn一直半桶水. 上网搜索了几篇svn的好文,做一下总结: <svn分支开发与主干合并(branch & merge) >http://blog.csdn.net/bbirdsky/article/details/24620155 TortoiseSVN图形操作,适合初学者. <SVN中Branch的创建与合并>http://www.cnblogs.com/huang0925/p/3254243.html命令行的svn,适合真正要