unix网络编程笔记(四)--IO复用

第六章笔记

1. IO复用:

(1)需要IO复用的原因:
echo客户端同时需要读终端和套接字会遇到这样的问题:如果客户端面向两个文件描述符,控制和连接套接字,那么就不能同时收到终端和套接字的数据,如果客户端阻塞在终端,那即使服务器发来了close也不能立即处理。
这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个IO条件就绪(也就是说输入已准备好被读取,或者描述符已能承担更多的输入),它就通知进程。这个能力成为IO复用
(2)IO复用的特点:
使用IO复用,使进程不再阻塞在真正的IO系统调用上,而是阻塞在select poll epoll这其中某一个系统调用之上,而它们的优势在于它们可以帮助我们同时等待多个描述符就绪。
(3)IO复用适用的场景:

  • 客户端处理多种描述符(交互性输入和网络套接字)必须使用IO复用
  • 客户端需要同时处理多个套接字
  • 服务器既要处理监听套接字又要处理已连接套接字,一般就要使用IO复用

2. IO模型

IO复用是IO模型的一种,共有五种IO模型:阻塞IO、非阻塞IO、IO复用、信号驱动式IO、异步IO
以套接字recvfrom为例:recvfrom操作包括两个阶段(1)等待内核缓冲区的数据是否准备好。(2)从内核向进程复制数据。而这几种IO模型的主要区别就在于当内核缓冲区的数据已经准备好之后以何方方式通知用户进程

2.1 阻塞IO

默认情况下,所有套接字都是阻塞的。这种模型表现为:如果数据没有准备好,调用recvfrom的当前线程进入阻塞状态,直到数据准备好并复制到应用程序缓冲区才返回。它的缺点是:当应用程序遇到多个文件描述符时,前面的文件描述符会阻塞后面的文件描述符,即:即使有某个文件描述符可读,也要等到前面的文件描述符变成可读才能处理。

2.2 多线程/多进程+阻塞IO

每个线程处理一个IO,这样即使有多个文件描述符也可同时处理。这种方式可以处理上面那种非阻塞IO不能有效处理多个文件描述的情况。《unix网络编程笔记(三)》服务器代码就是这种模型。但缺点是:当需要成千上万设置更多文件描述符,多进程非常耗费资源。多线程的切换会影响性能。

2.3 非阻塞IO

如果数据没有准备好,调用recvfrom会返回错误,使用返回值告诉应用程序数据是否可读且已经复制到应用程序缓冲区中,因此应用程序需要不断轮询查看数据是否准备好。缺点为:轮询往往耗费大量CPU时间

2.4 IO复用

使进程不再阻塞在真正的IO系统调用上,而是阻塞在select poll epoll这其中某一个系统调用之上,虽然与阻塞IO一样如果数据没有准备好还是阻塞,但是其优势为可以同时等待多个文件描述符,只要有任何一个准备好就返回。之后应用进程调用read读取数据。特点:当同时处理大量描述符时性能由于多线程+阻塞IO。

2.5 信号驱动式IO

使用信号SIGIO来通知应用程序数据已准备好,用户需要捕获信号并在信号处理程序中读取数据

2.5 异步IO

支持POSIX异步IO模型的系统较罕见。工作机制为:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。与信号驱动模型的主要区别是:信号驱动IO是由内核通知我们何时可以启动一个IO操作,而异步IO模型是由内核通知我们IO操作何时完成,即通知我们时数据已经复制到应用程序缓冲区内了。

2. 6 总结:

阻塞IO、非阻塞IO、IO复用、信号驱动式IO前四种IO模型有一个共同点:对于recvfrom的第二个阶段即把数据从内核缓冲区复制到调用缓冲区期间,进程都阻塞与recvfrom调用。即都有阻塞。而异步IO模型复制操作由异步IO函数完成,不需要因为调用recvfrom而阻塞。即:全程没有阻塞,异步IO参与了recvfrom的两个阶段。

3. select 函数

linux支持三种IO复用,select poll epoll

int select(int maxfdpl,fd_set* readset,fd_set* writeset,fd_set* exceptset,const struct timeval* timeout);

3.1 参数解释:

  • timeout:NULL,永远等待下去;正常时间:等待一段固定时间; 0:检查描述符后立即返回,可用于轮询查看是否有描述符可读
  • readset writeset exceptset:是三个文件描述符集,用于存储检查用的文件描述符。可使用函数FD_ZERO、FD_SET、FD_CLR、FD_ISSET来清空文件描述符集、添加描述符、删除描述符、判断描述符是否位于文件描述符集中。也可用赋值语句将某个描述符集的值赋值给另一个描述符集。注意:在设置描述集时,必须给描述集初始化,否则可能发生不可预期的结果。
    readset writeset exceptset中,如果我们对其中某一个条件不感兴趣,就可以把它设为空指针。这三个参数为输入输出型参数。输入时表示检查哪些文件描述符,当函数返回时,指示哪些描述符已就绪,可用FD_ISSET来测试文件描述符集中哪些描述符已就绪。因为未就绪的文件描述符都被清为0,所以再次调用select时需要再次把所有关心的描述符置为1。
    select检测是否就绪的描述符不局限于套接字,任何描述符都可以使用select
  • maxfdp1: 指定待测试的描述符个数,它的值是待测试的最大描述符+1,即调用select时描述符0 1 2 …maxfdp1-1都将检查。因为readset writeset exceptset文件描述符集最多存放的描述符个数为FD_SETSIZE,所以maxfdp1不能大于FD_SETSIZE。FD_SETSIZE定义在/usr/include/sys/select.h中。
  • 返回值:表示所有描述符已就绪的总位。如果为0表示定时器到时无描述符就绪。为-1表示出错,如果错误为EINTR,忽略该错误。注意:当某个描述符即准备好读又准备好写,则返回次数为2。
    select会被信号中断,并从信号处理函数返回。需要做好返回EINTR的准备

3.2 使用select常见的两个错误:

maxfdp1为检测的最大描述值+1,不要忘加1;描述符集是输入输出参数再次调用select时忘记再次设置描述符。

3.3 select返回的条件:

(1)select返回的条件为:等待的文件描述符有一个或多个就绪,或者指定的时间到达。
(2)套接字读描述符就绪的条件:

  • 该套接字接收缓冲区的数据字节数>=套接字接收缓冲区低水位标记,可以使用SO_RCVLOWAT套接字选项设置套接字低水位标记。对于Tcp和Udp套接字,默认值为1。
  • 该连接的读半部关闭(接收到了FIN),这时read返回0。
  • 捕获了某个信号且从响应信号处理函数返回时,read中断
  • 其他错误 3)对于监听套接字,已完成连接数>0

(3)套接字写描述符就绪的条件:

  • 该套接字发送缓冲区的可用空间字节数>=套接字发送缓冲区低水位标记,可以使用SO_SNDLOWAT套接字选项设置套接字低水位标记。对于Tcp和Udp套接字,默认值为2048。注意:对于Udp并没有发送缓冲区,因为不像Tcp一样因为超时重传而为应用程序传递给它的数据报保留副本。只有发送缓冲区大小这一属性,所以只要Udp套接字的发送缓冲区大小大于该套接字的低水位标记(默认应该总是这种关系),该Upd套接字就总是可写,即Udp套接字总是可写的 7.5.9)
  • 连接的写半部关闭(接收了RST)再次write,这时返回-1并产生SIGPIPE信号
  • 捕获了某个信号且从响应信号处理函数返回时,read中断
  • 其他错误
  • 使用非阻塞式的connect已建立连接或者connect已经以失败告终

4. 使用select实现echo客户端

4.1 使用select实现echo客户端注意两个问题

(1)不能在读输入读到eof时就认为程序处理完毕
问题描述:
当实现echo客户端,如果从终端读数据,当读数据读到eof时,我们认为数据传送完毕close套接字是没有问题的。但如果把从终端读数据换成从文件读数据,当我们read到eof后close套接字就会发现从服务器传回来的数据少于我们发送的数据。
问题原因:
这是因为从终端读数据,发送数据和读取数据是停等模式的,我们只有等待对端发来了数据才向对端发送数据,所以当从终端输入了eof时我们可以确认之前发送对端的数据对端已经全部收到,而且对端发来的数据我们也全部收到。但把终端换成文件,那么向服务器写完一行数据后会立即写一行,两个写动作之间几乎没有时间延迟,这样当读文件读到了eof,可能连接通道中还有数据连接通道中可能还有其他的请求和应答,或者是去往服务器的请求数据,或者是返回客户端的应答数据。
也就是说当从文件读到了eof,只意味着我们已经写完了数据,但我们并不知道全部数据有没有全部发往对端,或者对端是否还有数据发给我们。
解决方案:我们需要一种只关闭Tcp写的方式,也就是说我们告诉服务器我们已经完成了数据发送,但还可以继续接受对端发送的数据,只有读到对端发来了eof才认为数据传送结束。而因为本端发送的eof和对端发送的eof是本端和对端最后发送的消息,如果两端都收到了对端发送的eof,则可以保证两端之前发送的消息肯定都收到了。这可以调用shutdown完成。
(2)不要让select和stdio和其他带有缓冲区的函数混合使用
**原因:**stdio具有自己的缓冲区,虽然fgets只返回一行,但是其他输入行的数据仍然在stdio缓冲区中。这样再次调用select等待新的输入时,由于select不知道stdio的缓冲区数据,它只单纯从read系统调用的角度判断是否有数据可读,而不是从fget角度考虑。这样可能就会产生虽然fgets能读到数据,但read却发现数据已经读完,而select返回后发现无数据可读的问题。

4.2 shutdown

int shutdown(int sockfd,int howto);

shutdown与close的区别:close只有当文件描述符引用计数为0才发送FIN,而shutdown不管引用计数就发送FIN。close终止读和写,而shutdown可用于只终止读半步或写半步。当告知对端我们已经完成了数据发送,但还可以读取对端发送的数据,可让howto设置为SHUT_WR。调用shutdown(fd,SHUT_WR)后,当前留在套接字发送缓冲区中的数据将被发送掉,后跟Tcp的FIN终止序列。

4.3 select echo客户端代码

#include "unp.h"
void str_cli(FILE* fp,int sockfd);
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        err_quit("usage: ip port\n");
    }

    int sockfd = Socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(struct sockaddr_in));
    servaddr.sin_family = AF_INET;
    Inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
    servaddr.sin_port = htons(atoi(argv[2]));
    Connect(sockfd,(const struct sockaddr*)&servaddr,sizeof(struct sockaddr_in));

    str_cli(stdin,sockfd);

    exit(0);

}

void str_cli(FILE* fp,int sockfd)
{
    int fd = fileno(fp);
    char buf[MAXLINE] = {0};
    fd_set rds;
    FD_ZERO(&rds);
    int eof = 0;
    int maxfds = -1;
    ssize_t size = 0;
    for(;;)
    {
        FD_SET(sockfd,&rds);
        if(!eof)
        {
            FD_SET(fd,&rds);
            maxfds = (fd > sockfd ? fd : sockfd) + 1;
        }
        else
        {
            maxfds = sockfd + 1;
        }
        Select(maxfds,&rds,NULL,NULL,NULL);
        if(FD_ISSET(sockfd,&rds))
        {
            if( (size = Read(sockfd,buf,MAXLINE)) == 0)
            {
                if(eof)
                {
                    break;
                }
                else
                {
                    err_quit("str_cli:server terminate prematurely");
                }
            }
            else 
            {
                Write(fd,buf,size);
            }
        }
        else if(FD_ISSET(fd,&rds))
        {
            if( (size = Read(fd,buf,MAXLINE)) == 0)
            {
                eof = 1;
                Shutdown(sockfd,SHUT_WR);
                FD_CLR(fd,&rds);    //不要忘记清除fd
            }
            else
            {
                writen(sockfd,buf,size);
            }
        }
    }
}

(1)select只能检测文件描述符,fileno用于把标准IO文件指针转换为对应的描述符
(2)使用eof标记是否标准输入了eof,当eof为1时不再把它放入文件描述符集中

5. 使用select实现echo客户端

#include "unp.h"
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        err_quit("usage:port listen_backlog");
    }
    int listenfd = Socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(struct sockaddr_in));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(atoi(argv[1]));
    Bind(listenfd,(const struct sockaddr*)&servaddr,sizeof(struct sockaddr_in));
    Listen(listenfd,atoi(argv[2]));

    char buf[MAXLINE] = {0};
    int clientfd[FD_SETSIZE];
    for(int i = 0;i < FD_SETSIZE; i++)
    {
        clientfd[i] = -1;
    }
    int maxfd = listenfd;
    int maxi = -1;
    ssize_t size;
    fd_set rds,allds;
    FD_ZERO(&allds);
    FD_SET(listenfd,&allds);
    for(;;)
    {
        rds = allds;
        int n = select(maxfd + 1,&rds,NULL,NULL,NULL);
        if(FD_ISSET(listenfd,&rds))
        {
            struct sockaddr_in clientaddr;
            socklen_t addrlen;
            int connfd = Accept(listenfd,(struct sockaddr*)&clientaddr,&addrlen);
            int i;
            for(i = 0;i<FD_SETSIZE;i++)
            {
                if(clientfd[i] < 0)
                {
                    clientfd[i] = connfd;
                    break;
                }
            }
            if( i >= FD_SETSIZE)
            {
                err_msg("too many client");
                continue;
            }
            FD_SET(connfd,&allds);
            if(maxfd < clientfd[i])
                maxfd = clientfd[i];
            if(maxi < i)
                maxi = i;
            n--;
            if(n <= 0)
                continue;
        }
        for(int i = 0; i<= maxi;i++)
        {
            if(clientfd[i] < 0)
                continue;
            if(FD_ISSET(clientfd[i],&rds))
            {
                ssize_t size = Read(clientfd[i],buf,MAXLINE);
                if(size == 0)
                {
                    close(clientfd[i]);
                    FD_CLR(clientfd[i],&allds);
                    clientfd[i] = -1;
                }
                else if(size > 0)
                {
                    writen(clientfd[i],buf,size);
                }
                n--;
                if(n == 0)
                    break;
            }
        }
    }
}

使用clientfd数组来存储已连接的客户端fd,使用maxi标记clientfd数组中有效fd最小index,这样select后,就不用从0到maxfd开始遍历,而是从0到maxi遍历,减少遍历时间。

6. 总结:

(1)select文件描述符集:

  • 使用函数FD_ZERO、FD_SET、FD_CLR、FD_ISSET来清空文件描述符集、添加描述符、删除描述符、判断描述符是否位于文件描述符集中。
  • 可用赋值语句将某个描述符集的值赋值给另一个描述符集。
  • 在设置描述集时,必须给描述集初始化,否则可能发生不可预期的结果。
  • 如果我们对其中某一个条件不感兴趣,就可以把它设为空指针。
  • readset writeset exceptset是输入输出参数,当select返回时描述符集变成就绪的文件描述符,当再次调用select时不要忘记再次设置需要检测的描述符。
  • select检测是否就绪的描述符不局限于套接字,任何描述符都可以使用select

(2)maxfd:

  • maxfd:表示select检测的最大描述值+1,调用select时会检测即调用select时描述符0 1 2 …maxfdp1-1都将检查。不要忘加1。
  • maxfdp1不能大于FD_SETSIZE

(3) select会被信号中断,需要做好返回EINTR的准备
(4) 套接字读描述符就绪条件之一:
该套接字接收缓冲区的数据字节数>=套接字接收缓冲区低水位标记,可以使用SO_RCVLOWAT套接字选项设置套接字低水位标记。对于Tcp和Udp套接字,默认值为1。
(5)套接字写描述符就绪的条件之一:
该套接字发送缓冲区的可用空间字节数>=套接字发送缓冲区低水位标记,可以使用SO_SNDLOWAT套接字选项设置套接字低水位标记。对于Tcp和Udp套接字,默认值为2048。
(6)使用select实现echo客户端注意两个问题:

  • 不能在读输入读到eof时就认为程序处理完毕
  • 不要让select和stdio和其他带有缓冲区的函数混合使用

(7)fileno用于把标准IO文件指针转换为对应的描述符

你可能感兴趣的:(unix,网络编程)