网络编程(二、IO模型)

253 阅读5分钟

上一节我们讲了客户端的编程(现在还没有,之后加上),客户端编程比较简单,fd就只有一个,但是服务器会连接很多个客户端,所以服务器对客户端发送过来的请求(IO操作),做了一个总结,一共有5种,接下来我们就好好分析分析。

2.1 阻塞式IO模型

最流行的IO模型是阻塞式IO模型,基本所有函数默认都是阻塞的,阻塞式编程是最简单的,也是很多人学习网络编程的时候,都使用的方法。 在这里插入图片描述 用户进程调用recvfrom函数的时候,kernel就开始装备数据了,一般数据都不是立刻就能获取到,这个时候kernel会等待数据的到来,而应用层就会把这个线程阻塞,当kernel一直等到数据准备好了,就会把数据从内核态拷贝到用户态,然后才返回结果,这时候线程的阻塞才解除,重新开始运行。

2.1.1 服务器模型

用阻塞式IO实现的服务器模型,大部分都是一请求一应答,如果多个客户顿请求,服务器也接收不到数据,所以这个模型比较少用,对于这个模型,有一种改进的方法:就是使用多线程。

多线程的目的就是让每一个客户端的连接的fd都创建一个线程去处理,阻塞的话也是阻塞新创建的线程,并不会影响主线程,主线程还是不断的要接收客户的消息。

缺点: 如果同时相应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。

可以考虑使用线程池,不过所谓的池也是有上限的,当请求大大超过上限时,池构成的系统对外界的响应并不比没有池的时候效果好多少,所以使用池必须考虑其面临的响应规模,并根据响应规模调整池的大小。

线程池可以缓解部分压力,但是不能解决所有问题,总之,多线程模式可以方便高效解决小规模的服务请求,但是面对大规模的服务请求,多线程模型也会遇到瓶颈。

程序:

//fcntl(listenfd,  F_SETFL,  O_NONBLOCK);
        threadPool_t *pool = threadPool_creat(8, 200, threadpool_eventfd_type);
        if(pool == NULL) {
            printf("pool is null\n");
            return -1;
        }

        while(1) {
            struct sockaddr_in client_addr ;
            memset(&client_addr, 0, sizeof(struct sockaddr_in));
            socklen_t client_len = {0};

            int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_len);
            if(clientfd <= 0)  continue;
 
           // fcntl(clientfd,  F_SETFL,  O_NONBLOCK);
            threadPool_add(pool, callback, (void*)&clientfd);

            usleep(100);
        }

这个是伪代码,有关线程池的问题,可以关系另一个专题进程/线程/协程

2.2 非阻塞式IO模型

既然有阻塞IO模型,就有非阻塞IO模型,接下来就说说这个非阻塞IO模型。

我们可以通过一个函数把上面的阻塞函数设置成非阻塞,非阻塞的情况是这样的,在请求IO的线程中,不能把当前线程睡眠,而是立即返回一个错误,直到kernel装备好所有数据,并且把数据从内核态拷贝到应用态,才返回读取到的数据。 在这里插入图片描述 前三次调用recvfrom时没有数据返回,因此内核立即返回一个EWOULDBLOCK错误。第四次调用redvfrom时已有有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成返回。

设置非阻塞的函数:

fcntl( fd, F_SETFL, O_NONBLOCK );

2.2.1 服务器模型

这种基本都有实用的服务器模型,如果使用的话,一个线程不断的轮询查看recvfrom数据是否准备好,这样做会耗费大量的CPU时间,不过这种模型偶尔也会遇到,通常是在专门提供某一种功能的系统才会被使用。

2.3 IO复用模型

IO 复用这个词可能都有点陌生,但是提到select/poll/epoll,大概就都能明白了,有些地方也称这种IO方式为事件驱动IO,我们知道,select/poll/epoll的好处就在于单个线程就可以同时处理多个网络连接的IO,它的基本原理就是select/poll/epoll这个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户。

在这里插入图片描述 我们使用select的时候,其实整个线程也是阻塞的,只不过这次阻塞是select阻塞,不是recvfrom函数阻塞,select阻塞期间,kernel会监视所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回,这个模型的只要优势,在于一个线程可以监控多个fd。

2.3.1 服务器模型

这个IO模型用的地方还是比较多的,一个线程负责监控很多个fd,所以需要好好掌握。

程序:

 // select 
    fd_set rfds, rset;
    FD_ZERO(&rfds);
    FD_SET(listenfd, &rfds);

    int i = 0;
    int max_fd = listenfd;
    rset = rfds;

    while(1) {

        printf("aagag %d\n", max_fd);
        int nready = select(max_fd+1, &rset, NULL, NULL, NULL);
        if(nready <= 0) continue;

        if(FD_ISSET(listenfd, &rset)) {
            struct sockaddr_in client_addr ;
            memset(&client_addr, 0, sizeof(struct sockaddr_in));
            socklen_t client_len;

            int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_len);
            if(clientfd <= 0)  continue;

            FD_SET(clientfd, &rset);
            printf("clientfd  %d\n", clientfd);
            if(clientfd > max_fd) max_fd = clientfd;

            if(--nready == 0) continue;
        }

        for(i = listenfd+1; i<=max_fd; i++)  {  
            if(FD_ISSET(i, &rset))
            {
                char buffer[1024] = {0};
                if( recv(i, buffer, 1024, 0) > 0 ) {
                    printf("client fd %d %s\n", i, buffer);
                } else {   // 这个是连接断开了
                    printf("diconnect %d\n", i);
                    FD_CLR(i, &rset);
                    close(i);
                }

                if(--nready == 0) break;  
            }
        }
    }

这段程序是select模型的

 int efd = epoll_create(1);
    if(efd <= 0)  return -1;

    struct epoll_event ev, events[1024];

    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &ev);

    while(1) {
        int nready = epoll_wait(efd, events, 1024, -1);
        if(nready <=0 ) continue;

        for(int i=0; i<nready; i++) {
            if(events[i].data.fd == listenfd)  {
                struct sockaddr_in client_addr ;
                memset(&client_addr, 0, sizeof(struct sockaddr_in));
                socklen_t client_len;

                int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_len);
                if(clientfd <= 0)  continue;

                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = clientfd;
                epoll_ctl(efd, EPOLL_CTL_ADD, clientfd, &ev);
                printf("clientfd  %d\n", clientfd);

                if(--nready == 0) continue;
            } else {
                int clientfd = events[i].data.fd;
                char buffer[1024] = {0};
                if( recv(clientfd, buffer, 1024, 0) > 0 ) {
                    printf("client fd %d %s\n", clientfd, buffer);
                } else {   // 这个是连接断开了
                    printf("diconnect %d\n", clientfd);
                    epoll_ctl(efd, EPOLL_CTL_DEL, clientfd, NULL);
                    close(clientfd);
                }

                if(--nready == 0) break;  
            }
        }

    }

这段程序是epoll的

2.3.2 select和epoll比较

select: 优点: select模型事件驱动模型只用单线程执行,占用资源少,不消耗太多CPU,同时能够为客户端提供服务 缺点: 当需要探测的句柄值较大时,select()接口本身需要消耗大量事件去轮询各个句柄。 改模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的,很大程度降低了事件探测的及时性

改进: 有很多高效的事件驱动库可以屏蔽上述困难,有libevent、libev

找到了一篇关系这个介绍很详细的文章,大家可以看看: select、poll、epoll之间的区别(搜狗面试)

下面是我总结的思维导图: 在这里插入图片描述

2.4 信号驱动IO模型

我们可以直接用信号,让内核处理完成了之后,通过SIGIO信号通知我们,这种模型就是信号驱动IO。

在这里插入图片描述 首先开启套接字的信号驱动式IO,并通过sigaction系统调用安装一个信号处理函数。该系统调用立即返回,我们的进程继续工作,也就是我们进程没有被阻塞,也不用去轮询检测,当数据报准备好读取时,内核自己产生一个SIGIO的信号。我们可以在信号处理函数中接收数据,并通知主线程读取数据。

这个模型的好处就是不用等待数据和检测数据,只要等待来自信号处理函数的通知,然后去读取数据即可。 (服务器模型以后再补)

2.5 异步IO模型

linux下的异步IO用在磁盘IO读写操作,不用于网络IO,从内核2.6版本才开始引入: 在这里插入图片描述 用户进程发起read操作之后,立刻就可以开始去做其他的事了。而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

信号驱动IO和异步IO的区别: 信号驱动IO:内核通知我们可以去操作一个IO操作,需要进程去读取数据。 异步IO:由内核通知我们IO操作何时完成,直接拷贝到用户内存中。

2.6 总结:

在这里插入图片描述 同步IO操作:导致请求进程阻塞,直到IO操作完成 异步IO操作:不导致请求进程阻塞

同步IO:阻塞式IO模型、非阻塞式IO模型、IO复用模型和信号驱动IO模型。

异步IO:异步IO模型。