网络编程模型

在使用多进程/多线程服务端框架的时候,如果每次有一个新的客户端连接,都重新去创建一个进程/线程来处理的话,对CPU和内存的开销是比较大的。

于是乎,就有了I/O复用。

I/O复用简介

多进程/多线程并发模型:为每个socket分配一个线程/进程。
I/O复用:采用单个进程/线程来管理多个socket。
这就是他们区别所在。

I/O复用有三种方案:select、poll、epoll,各有优缺点,适应不同场景,但是相对来说,epoll会更常用。

在网络设备(交换机、路由器),网游后台、nginx、redis等都使用了I/O复用。


1.select模型

  • 1.1大致流程

在这里插入图片描述

  • 1.2相关函数及结构体介绍

在接触代码之前,我们先了解一些必要的函数与结构体

fd_set结构体

#undef __FD_SETSIZE
#define __FD_SETSIZE
typedef struct{
	unsigned long fds_bits[__FD_SETSIZE/(8*sizeof(long)];
}__kernel_fd_set;

这个是用来存储网络通信使用的socket的。其中数组大小为什么是__FD_SETSIZE/(8*sizeof(long)呢?

它使用的数据结构是位图bitmap,不是数组的每一个下标存储一个socket,而是一个位存储一个socket。对于fds_bits[0],就可以存储sizeof(long)个字节,而1字节8位,所以光是fds_bits[0]就可以存储8*sizeof(long)个位

每有一个socket被使用于我们的程序,这个socket在位图中对应的位就会置为1。也许位图你还是不了解,但是你知道这个用途就足够了。

由函数定义可以知道,它能存储的就是值为1024,如果再大的话我们可以改宏定义,当然最好还是换一个模型。

select函数

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

(1)maxfdp是所有文件描述符范围,值为范围中最大的文件描述符+1

(2)接下来三个参数,都是传入fd_set结构的,分别用于监视读文件、写文件、异常文件的socket是否有事件发生,select会阻塞等待直至有事件发生(如果你设置了最后一个参数,会超时返回)。
网络通信大多数都只用到第一个readfds,后两个填NULL即可。
同时,这个函数会修改readfds,将没有事件发生的socket对应位置0,视情况我们需要保存select之前的readfds。

(3)最后一个参数

struct timeval
{
__time_t  tv_sec;        /* Seconds. */
__suseconds_t  tv_usec;  /* Microseconds. */
};

分别设置超时的秒和微妙。超时还未检测到事件发生,select也会返回,无需超时设置NULL即可。

(4)如果监测错误(比如fd_set中有socket并未申请等)返回-1;
超时返回0;
监测成功返回发生事件的数量。

(5)一点补充,为什么是监测某个范围的fd呢,因为计算机的socket不是随机分配的,而是从可用的空闲socket中选取最小的来分配,通常在申请完listen_fd以后,没有其他程序申请socket,那么之后产生的client_fd都是从listen_fd开始连续的数字。这一点其实是很有用的。

(6)另外有pselect函数,与信号处理有关,可以另外了解

FD_CLR()函数

void FD_CLR(int fd,fd_set *set);

将set中fd对应位置为0,表示踢出该集合

FD_SET()函数

void FD_SET(int fd,fd_set *set);

将fd对应位置1,表示添加到集合

FD_ISSET()函数

int FD_ISSET(int fd,fd_set *set);

监测fd是否在set中,若不在返回值<=0

FD_ZERO()函数

void FD_ZERO(fd_set *set);

将集合清空。

  • 1.3核心代码

了解完了关键函数,我们就照着流程去实现一遍就好了,我会在代码中写上注释帮助食用。

#include <bits/stdc++.h>

#include "yzz_server.h"

#include <unistd.h>
#include <signal.h>
using namespace std;

int Init(int port, std::string ip = "10.0.16.16"){///封装一个默认参数函数来完成申请socket、bind、listen这三步操作
    int listen_fd = socket(AF_INET,SOCK_STREAM,0);
    if(listen_fd <= 0){
        printf("apply socket error\n");
        return -1;
    }

    sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(ip.data());//指定ip地址,如果没有就用默认参数
    server_addr.sin_port = htons(port);

    if(bind(listen_fd, (sockaddr *)&server_addr, sizeof(server_addr)) != 0){
        close(listen_fd);
        printf("bind error\n");
        return -1;
    }

    if(listen(listen_fd, 10) != 0){
        close(listen_fd);
        printf("listen error");
        return -1;
    }

    return listen_fd;
}

int main(int argc,char* argv[]){
    int listen_fd = -1;
    if(argc == 2)listen_fd = Init(atoi(argv[1]));
    else if(argc == 3)listen_fd = Init(atoi(argv[2]), argv[1]);

    if(listen_fd <= 0){
        printf("Init error\n");
        return -1;
    }

    fd_set sock_set;//初始化一个fd_set,并且把listen_fd加入
    FD_ZERO(&sock_set);
    int maxfd = listen_fd;//当前只有一个listen_fd,最大值就是它自己,之后建立连接了会更新这个值
    FD_SET(listen_fd, &sock_set);

    while(true){
        fd_set tmp_set = sock_set;//select函数会把sock_set里面没有发生事件的socket对应位置0,而这些socket以后可能还有事件,所以我们要保存下来,tmp_set仅供select临时使用
        timeval tv;
        tv.tv_sec = 0;//设置超时
        tv.tv_usec = 500000;//500毫秒
        int retval = select(maxfd + 1, &tmp_set, NULL, NULL, &tv);
        if(retval <= 0){//-1为错误,0为超时
            printf("select error\n");
            continue;
        }

        //到这里说明socket有事件发生并且被select监测到了,需要我们去处理
        for(int event_fd = 3; event_fd <= maxfd; ++event_fd){//0是标准输入,1是标准输出,2是标准错误,所以listen_fd至少是从3开始
            if(FD_ISSET(event_fd, &tmp_set) <= 0)continue;//我们只是枚举小于maxfd的socket,它可能没有事件发生所以没有被select选到tmp_set中,也可能一开始就不在tmp_set中
            
            if(event_fd == listen_fd){//如果当前这个发生事件的socket是listen_fd,说明有客户端产生连接,我们要生成一个新的client_fd去处理这个客户端
                sockaddr_in client_addr;
                int socklen = sizeof(sockaddr_in);
                int client_fd = accept(listen_fd, (sockaddr *)&client_addr, (socklen_t *)&socklen);//这个时候不会阻塞,因为select已经阻塞监听到事件,这里accept是可以直接执行的
                if(client_fd <= 0){
                    printf("accept error\n");
                    continue;
                }
                printf("client %s has been connect\n",inet_ntoa(client_addr.sin_addr));//输出一下表示连接成功
                FD_SET(client_fd, &sock_set);//将client_fd加入,记住,tmp_set仅供select使用,与监听和客户端通信有关的socket我们都是保存到sock_set中
                if(client_fd > maxfd)maxfd = client_fd;//更新一下socket范围的最大值
                continue;
            }else{//到这里说明有客户端发生通信
                int client_fd = event_fd;//为了更清楚而写的
                char buffer[1024];
                std::string ret_buffer;
                memset(buffer,0,sizeof(buffer));
                if(recv(client_fd, buffer, sizeof(buffer), 0) <= 0){//接收失败,可能断开连接了
                    printf("recv error\n");
                    goto to_close_fd;
                }
                printf("%s\n",buffer);
                ret_buffer = "已收到";//补充一下,其实发送由于缓冲区不够大也可能阻塞,但基于现在硬件水平,多半不会发生
                ret_buffer += buffer;
                if(send(client_fd, ret_buffer.data(), ret_buffer.size(),0) <= 0){//发送失败,也可能断连了
                    printf("send error\n");
                    goto to_close_fd;
                }
                continue;//如果正常运行到这里就没错误,不需要走到CLOSE_FD部分
                to_close_fd:
                    close(client_fd);
                    FD_CLR(client_fd, &sock_set);
                    if(client_fd == maxfd){//如果这就是maxfd的话,我们删除client_fd之后需要更新maxfd
                        for(int i = client_fd; i >= 3; --i){//从大到小遍历出第一个存在sock_set的socket即为新的maxfd
                            if(FD_ISSET(i, &sock_set)){
                                maxfd = i;
                                break;
                            }
                        }
                    }
            }
        }
    }
    close(listen_fd);
    return 0;
}

以上是我自己写的select代码,试运行以后暂时没发现问题,就先放着。

注释我认为写的还是挺详细的,不过去看select模型这个讲解也可以,我这个代码也是看视频以后修改了一些写出来的。

  • 1.4水平触发

select采用"水平触发"的方式,如果监测到fd有事件并且报告后,没有处理fd的数据,或者没有读取完数据(例如客户端发来10字节你只收了5字节),那么下次select仍然会报告此fd,这就保证了select不会丢失事件和数据。

  • 1.5select缺点

1.select支持的文件描述符有限(默认1024),当然这个是可以修改的,但是会带来内存和遍历的开销都会增大。

2.select实际上是把用户态传入的数组,拷贝到核心态,再去监测,需要一个O(n)过程

3.select监测的时候,实际上是去遍历扫描文件描述符,这也是一个O(n)的扫描过程


2.poll模型

  • 2.1大致流程

poll和select没有本质区别,只是因为poll采用了数组而非bitmap,可以得到更大数量的fd,但是监听事件的时候同样需要用户态和核心态之间拷贝数组、同样需要采用轮询机制遍历每一个fd。

在我看来,至少应用方面比select好不了多少。

  • 2.2相关函数和结构体介绍

pollfd结构体

struct pollfd{
  int fd;          //文件描述符
  short events;    //请求的事件
  short revents;   //返回的事件
};

我们填入的是fd和events即可,监听事件以后返回的结构体会修改revents的值,并且fd<0的时候,poll函数会自动忽略。

关于events和revents参数如下:
在这里插入图片描述

poll函数

int poll(struct pollfd *fd, nfds_t nfds, int timeout);

第一个参数就是pollfd数组首地址,直接传入数组名即可。

第二个参数类似于select函数的maxfd。

第三个参数设置超时,单位为毫秒,直接填入数字即可,这个比select设置超时要方便不少。
超时部分可填参数如下:
在这里插入图片描述

  • 2.3完整代码

#include <bits/stdc++.h>

#include "yzz_server.h"

#include <poll.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

#define MAXFD 100000
pollfd fds[MAXFD];//创建存储fd的数组

int Init(int port, std::string ip = "10.0.16.16"){///封装一个默认参数函数来完成申请socket、bind、listen这三步操作
    int listen_fd = socket(AF_INET,SOCK_STREAM,0);
    if(listen_fd <= 0){
        printf("apply socket error\n");
        return -1;
    }

    sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(ip.data());//指定ip地址,如果没有就用默认参数
    server_addr.sin_port = htons(port);

    if(bind(listen_fd, (sockaddr *)&server_addr, sizeof(server_addr)) != 0){
        close(listen_fd);
        printf("bind error\n");
        return -1;
    }

    if(listen(listen_fd, 10) != 0){
        close(listen_fd);
        printf("listen error");
        return -1;
    }

    return listen_fd;
}

int main(int argc,char* argv[]){
    int listen_fd = -1;
    if(argc == 2)listen_fd = Init(atoi(argv[1]));
    else if(argc == 3)listen_fd = Init(atoi(argv[2]), argv[1]);

    if(listen_fd <= 0){
        printf("Init error\n");
        return -1;
    }

    int maxfd = listen_fd;//当前只有一个listen_fd,最大值就是它自己,之后建立连接了会更新这个值
    for(int i = 3; i < MAXFD; ++i){//初始化
        fds[i].fd = -1;
    }
    fds[listen_fd].fd = listen_fd;//一般用fd作为下标
    fds[listen_fd].events = POLLIN;

    while(true){
        int retval = poll(fds, maxfd + 1, 1000);
        if(retval <= 0){//-1为错误,0为超时
            printf("poll error\n");
            continue;
        }

        //到这里说明poll有事件发生并且被select监测到了,需要我们去处理
        for(int event_fd = 3; event_fd <= maxfd; ++event_fd){//0是标准输入,1是标准输出,2是标准错误,所以listen_fd至少是从3开始
            if(fds[event_fd].fd <= 0 || fds[event_fd].revents != POLLIN)continue;
            fds[event_fd].revents = 0;//清空
            if(event_fd == listen_fd){//如果当前这个发生事件的socket是listen_fd,说明有客户端产生连接,我们要生成一个新的client_fd去处理这个客户端
                sockaddr_in client_addr;
                int socklen = sizeof(sockaddr_in);
                int client_fd = accept(listen_fd, (sockaddr *)&client_addr, (socklen_t *)&socklen);//这个时候不会阻塞,因为select已经阻塞监听到事件,这里accept是可以直接执行的
                if(client_fd <= 0){
                    printf("accept error\n");
                    continue;
                }
                printf("client %s has been connect\n",inet_ntoa(client_addr.sin_addr));//输出一下表示连接成功
                fds[client_fd].fd = client_fd;//初始化
                fds[client_fd].events = POLLIN;
                fds[client_fd].revents = 0;
                if(client_fd > maxfd)maxfd = client_fd;//更新一下socket范围的最大值
                continue;
            }else{//到这里说明有客户端发生通信
                int client_fd = event_fd;//为了更清楚而写的
                char buffer[1024];
                std::string ret_buffer;
                memset(buffer,0,sizeof(buffer));
                if(recv(client_fd, buffer, sizeof(buffer), 0) <= 0){//接收失败,可能断开连接了
                    printf("recv error\n");
                    goto to_close_fd;
                }
                printf("%s\n",buffer);
                ret_buffer = "已收到";//补充一下,其实发送由于缓冲区不够大也可能阻塞,但基于现在硬件水平,多半不会发生
                ret_buffer += buffer;
                if(send(client_fd, ret_buffer.data(), ret_buffer.size(),0) <= 0){//发送失败,也可能断连了
                    printf("send error\n");
                    goto to_close_fd;
                }
                continue;//如果正常运行到这里就没错误,不需要走到CLOSE_FD部分
                to_close_fd:
                    close(client_fd);
                    fds[client_fd].fd = -1;
                    if(client_fd == maxfd){//如果这就是maxfd的话,我们删除client_fd之后需要更新maxfd
                        for(int i = client_fd; i >= 3; --i){//从大到小遍历出第一个存在sock_set的socket即为新的maxfd
                            if(fds[i].fd > 0){
                                maxfd = i;
                                break;
                            }
                        }
                    }
            }
        }
    }
    close(listen_fd);
    return 0;
}

完整代码只是在select的代码上做一些修改即可,主要是把fd_set操作的地方替换掉就没问题了。


3.epoll模型

  • 3.1简介

epoll模型解决了select和poll的问题(用户态到核心态的拷贝,轮询机制)。简单来说,select和poll可以理解为,有人告诉饭店老板有人买单,老板一桌桌去问;而epoll则是有人来找老板买单。

  • 3.2相关函数

epoll_create函数

int epoll_create(int size);

创建一个epoll对象,并返回一个文件描述符,使用完后需要close关闭。

从Linux内核2.6.8后size参数就被忽略,填入一个正整数即可。

epoll_ctl函数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfd,传入epoll_create创建的fd即可。

op,填入对红黑树的操作,包括添加节点 EPOLL_CTL_ADD,删除节点EPOLL_CTL_DEL,修改节点EPOLL_CTL_MOD。添加和删除即是添加/取消socket监听,修改则是修改监听的事件。

fd,传入需要监听的socket,比如listen_fd。

event,传入该fd需要监听的事件。

epoll_data联合体和epoll_event结构体

typedef union epoll_data {
  void        *ptr;
  int          fd;
  uint32_t     u32;
  uint64_t     u64;
} epoll_data_t;

//以下是我们要传入的
struct epoll_event {
  uint32_t     events;      /* Epoll 事件 */
  epoll_data_t data;        /* 用户数据 */
};

events:
我们在listen和accept的时候,肯定都是EPOLLIN事件。
在这里插入图片描述
epoll_data,这个我并没有去好好了解,我去别人博客找到了一些介绍。
来自epoll函数原理和使用介绍
在这里插入图片描述

epoll_wait函数

int epoll_wait(int epid, struct epoll_event *events, int maxevents, int timeout);

epid,使用epoll_create创建的fd即可。

events,存储监听到发生事件的集合,可以传入数组去接收。

maxevents,集合的最大数量限制,假如有200个事件发生,而这里只填入100,那么多余的事件不会丢失,下一次epoll_wait的时候就会拿到。

timeout,超时时间,timeout>0则等待对应毫秒,等于0则立即返回,传入负数则一直阻塞。

返回值:返回值是正整数代表监听到并返回事件的数量(<=maxevents),返回0则是超时,返回-1则是出错。

  • 3.3epoll优点

epoll在epoll_ctl就会将fd加入内核态,之后如果有事件发生,epoll_wait会从双向链表取出数据。而select和poll每次调用对应函数的时候,重新将fd从用户态拷贝到内核态。
可以参考epoll优点

  • 3.4完整代码

#include <bits/stdc++.h>

#include "yzz_server.h"

#include <sys/epoll.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

#define MAXFD 100

int Init(int port, std::string ip = "10.0.16.16"){///封装一个默认参数函数来完成申请socket、bind、listen这三步操作
    int listen_fd = socket(AF_INET,SOCK_STREAM,0);
    if(listen_fd <= 0){
        printf("apply socket error\n");
        return -1;
    }

    sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(ip.data());//指定ip地址,如果没有就用默认参数
    server_addr.sin_port = htons(port);

    if(bind(listen_fd, (sockaddr *)&server_addr, sizeof(server_addr)) != 0){
        close(listen_fd);
        printf("bind error\n");
        return -1;
    }

    if(listen(listen_fd, 10) != 0){
        close(listen_fd);
        printf("listen error");
        return -1;
    }

    return listen_fd;
}

int main(int argc,char* argv[]){
    int listen_fd = -1;
    if(argc == 2)listen_fd = Init(atoi(argv[1]));
    else if(argc == 3)listen_fd = Init(atoi(argv[2]), argv[1]);

    if(listen_fd <= 0){
        printf("Init error\n");
        return -1;
    }

    int epoll_fd = epoll_create(1);//创建epoll_fd
    epoll_event ev;//将listen_fd加入红黑树
    ev.data.fd = listen_fd;
    ev.events = EPOLLIN;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);

    epoll_event events[MAXFD];//接收事件的数组

    while(true){
        int event_num = epoll_wait(epoll_fd, events, MAXFD, 1000);
        if(event_num <= 0){//-1为错误,0为超时
            printf("epoll error\n");
            continue;
        }

        for(int event_id = 0; event_id < event_num; ++event_id){//0是标准输入,1是标准输出,2是标准错误,所以listen_fd至少是从3开始
            if(events[event_id].data.fd == listen_fd && events[event_id].events == EPOLLIN){//如果当前这个发生事件的socket是listen_fd,说明有客户端产生连接,我们要生成一个新的client_fd去处理这个客户端
                sockaddr_in client_addr;
                int socklen = sizeof(sockaddr_in);
                int client_fd = accept(listen_fd, (sockaddr *)&client_addr, (socklen_t *)&socklen);//这个时候不会阻塞,因为select已经阻塞监听到事件,这里accept是可以直接执行的
                if(client_fd <= 0){
                    printf("accept error\n");
                    continue;
                }
                printf("client %s has been connect\n",inet_ntoa(client_addr.sin_addr));//输出一下表示连接成功
                //添加新的client_fd进epoll
                memset(&ev, 0, sizeof(epoll_event));
                ev.data.fd = client_fd;
                ev.events = EPOLLIN;
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
            }else if(events[event_id].events == EPOLLIN){//到这里说明有客户端发生通信
                int client_fd = events[event_id].data.fd;//为了更清楚而写的
                char buffer[1024];
                std::string ret_buffer;
                memset(buffer,0,sizeof(buffer));
                if(recv(client_fd, buffer, sizeof(buffer), 0) <= 0){//接收失败,可能断开连接了
                    printf("recv error\n");
                    goto to_close_fd;
                }
                printf("%s\n",buffer);
                ret_buffer = "已收到";//补充一下,其实发送由于缓冲区不够大也可能阻塞,但基于现在硬件水平,多半不会发生
                ret_buffer += buffer;
                if(send(client_fd, ret_buffer.data(), ret_buffer.size(),0) <= 0){//发送失败,也可能断连了
                    printf("send error\n");
                    goto to_close_fd;
                }
                continue;//如果正常运行到这里就没错误,不需要走到CLOSE_FD部分
                to_close_fd:
                    /*
                    memset(&ev,0,sizeof(epoll_event));
                    ev.events = EPOLLIN;
                    ev.data.fd = client_fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, &ev);
                    */
                    close(client_fd);//如果保证当前fd的引用只有这一个,那么直接close,这个fd会被epoll自动剔除
            }
        }
    }
    close(listen_fd);
    close(epoll_fd);
    return 0;
}

其实也是在之前select和poll的代码上作一些修改即可。而且在我看来,epoll代码会更加简单。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值