高级I/O

五种I/O模型

阻塞I/O
  • 概念:在内核将数据准备好之前,系统调用会一直等待,直到数据准备就绪并从内核拷贝到用户空间,所有的套接字,默认都是阻塞方式;
  • 优点:流程简单
  • 缺点:效率低下
    在这里插入图片描述
非阻塞I/O
  • 概念:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回 EWOULDBLOCK 错误码,非阻塞 I/O 往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询;
  • 优点:效率相较于阻塞 I/O 稍有提高
  • 缺点:需要循环操作,开销大且不够实时
    在这里插入图片描述
信号驱动I/O
  • 概念:应用程序建立 SIGIO 信号处理程序,当内核将数据准备好的时候,使用 SIGIO 信号通知应用程序进行 I/O 操作;
  • 优点:效率更高,实施性更强
  • 缺点:操作流程更复杂,需要自定义信号处理
    在这里插入图片描述
I/O多路转接
  • 概念:虽然从流程图上看起来和阻塞 I/O 类似,实际上最核心在于 I/O 多路转接能够同时等待多个文件描述符的就绪状态,只要其中一个描述符数据准备完毕,那么进程就可以进行处理了;
  • 作用:I/O 多路转接模型,就是针对大量的描述符进行 I/O 就绪事件监控,让进程仅仅针对已经就绪了 I/O 事件的描述符进行操作,避免了进程对未就绪的描述符进行操作所带来的性能损失或者阻塞;
  • 优点:能在众多阻塞等待的文件描述符中,及时处理已经已就绪的描述符
  • 缺点:操作更复杂
    在这里插入图片描述
异步I/O
  • 概念:由内核在数据拷贝完成时,通知应用程序直接可以进行处理了,而前面介绍的信号驱动是告诉应用程序何时可以开始拷贝数据,异步 I/O 属于一步到位;
  • 优点:对于资源利用率极高,效率更高
  • 缺点:流程是最复杂的
    在这里插入图片描述
简单概念
  • 从阻塞 I/O 到异步 I/O 是一个对资源利用率以及效率提高的过程,也是流程变得复杂的过程;
  • 阻塞:为了完成某个功能,发起一个调用,如果完成功能的条件不具备,则一直等待直到条件具备;
  • 非阻塞:为了完成某个功能,发起一个调用,如果完成功能的条件不具备,则直接报错返回;
  • 阻塞 VS 非阻塞:通常用于描述某个接口发起调用后是否能够立即返回;
  • 同步:所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了,换句话说,就是由调用者主动等待这个调用的结果,否则不能执行其他操作;
  • 异步:异步则是相反,调用在发出之后,这个调用就直接返回了,所以当前并没有结果返回,换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态 / 通知来通知调用者,或通过回调函数处理这个调用;
  • 异步 VS 同步:通常用于描述功能的完成流程,区别在于功能是否由自身完成,同步流程清晰简单,但是效率低一些,异步对资源利用率更高(褒义 / 贬义),效率更高,但是流程较复杂;
  • 异步阻塞:发起一个调用,让系统完成任务,进程一直等着系统完成任务;
  • 异步非阻塞:发起一个调用,让系统完成任务,进程继续做自己的事情;

非阻塞I/O

fctnl函数
  • 函数原型
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
  • 五种功能
    • 复制一个现有的描述符(cmd = F_DUPFD);
    • 获得 / 设置文件描述符标记(cmd = F_GETFD 或 F_SETFD);
    • 获得 / 设置文件状态标记(cmd = F_GETFL 或 F_SETFL);
    • 获得 / 设置异步 I/O 所有权(cmd = F_GETOWN 或 F_SETOWN);
    • 获得 / 设置记录锁(cmd = F_GETLK、F_SETLK 或 F_SETLKW);
实现SetNoBlock函数
  • 基于 fcntl,实现一个 SetNoBlock 函数,将文件描述符设置为非阻塞;
void SetNoBlock(int fd){
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0) {
		perror("fcntl");
		return;
	}
	//第三个参数最好是“原来的属性 | 非阻塞属性”,因为这是一个覆盖式写入,所以 | 操作可以保证原来的属性不会丢失
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
  • 使用 F_GETFL 将当前的文件描述符的属性取出来,这是一个位图;
  • 然后再使用 F_SETFL 将文件描述符设置回去,设置回去的同时,加上一个 O_NONBLOCK 参数;
轮询方式读取标准输入
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
void SetNoBlock(int fd) {
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0) {
		perror("fcntl");
		return;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main() {
	SetNoBlock(0);
	while (1) {
		char buf[1024] = {0};
		ssize_t read_size = read(0, buf, sizeof(buf) - 1);
		if (read_size < 0) {
			perror("read");
			sleep(1);
			continue;
		}
		printf("input:%s\n", buf);
	}
	return 0;
}

select模型

接口
  • fd_set rfds;——设置读描述符集合,fd_set wfds;——设置写描述符集合,fd_set efds;——设置异常描述符集合;
  • void FD_ZERO(fd_set* set);:清空集合;
  • void FD_SET(int fd, fd_set* set);:将描述符添加到集合中;
  • int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);:发起监控调用;
    • nfds:所有要监控集合中,描述符的最大值 + 1;
    • rdset / wrset / exset:分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
    • timeout:监控超时等待时间;
      • NULL——阻塞等待直到有事件就绪;
      • 0——非阻塞等待,仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
      • 特定的时间值:监控指定的时间,时间结束后,根据不同的结果返回相应的返回值,它有两个成员,tv_sec——设置秒数,tv_usec——设置微秒数;
    • 返回值:
      1. 执行成功则返回文件描述符状态已改变的个数;
      2. 如果返回 0 代表在描述符状态改变前已超过 timeout 时间;
      3. 当有错误发生时则返回 -1,此时参数 readfds、writefds、exceptfds 和 timeout 的值变成不可预测,错误原因存于 errno,常见错误值有:EBADF——文件描述符为无效的或该文件已关闭,EINTR——此调用被信号所中断,EINVAL——参数 n 为负值,ENOMEM——核心内存不足;
  • void FD_CLR(int fd, fd_set *set);:用来删除监控描述符集合中指定的描述符 fd;
  • int FD_ISSET(int fd, fd_set *set);:用来判断 fd 描述符是否仍存在描述符就绪集合中;
流程
  1. 定义指定 I/O 事件的描述符集合,添加要监控的描述符到对应集合中;
  2. 将发起监控调用,将集合拷贝到内核进行 I/O 就绪事件监控,就绪 / 超时后将返回描述符集合,在返回前会将集合中没有就绪的描述符移除;
  3. 轮询遍历判断哪个描述符还在集合中,以此来确定哪些描述符就绪了什么事件,进而进行对应的 I/O 操作;
特性
fd_set结构
  • fd_set 就是一个整数数组,更严格的说是一个位图,使用位图中对应的位来表示要监视的文件描述符,并提供了一组操作 fd_set 的接口,来比较方便的操作位图;
特点
  • select本质上是一个位图,可监控的文件描述符个数取决与宏sizeof(fd_set)的值,所以如果sizeof(fd_set) = 512,那么可监控的最大文件描述符是 512 * 8 = 4096;
  • 将 fd 加入 select 监控集的同时,尽量再使用一个数据结构 array 保存 select 监控集中的fd;
    • 一是用于在select返回后,array 作为源数据和就绪集合进行FD_ISSET判断;
    • 二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,此时如果需要再次监控刚才的那些描述符,则需要先FD_ZERO,然后从 array 获取 fd 逐一加入监控集合中;
  • 在向监控集合中添加描述符或者删除描述符时,需要维护一个 maxfd,这样可以很方便的为select函数设置第一个参数;
优点
  • 遵循 POSIX 标准,跨平台移植性高;
缺点
  • 每次调用select,都需要手动设置 fd 集合,从接口使用角度来说也非常不便;
  • 每次调用select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大;
  • select监控原理是在内核中进行轮询遍历,性能随着描述符的增多而下降;
  • select支持的文件描述符数量有限,虽然可以自己设置,但是比较麻烦;
  • select返回的是就绪集合但是无法直接提供就绪描述符,需要进行遍历才能判断哪些描述符已经就绪了;
代码
封装select
#include <iostream>    
#include <vector>    
#include <time.h>    
#include <sys/select.h>    
#include "tcpsocket.hpp"                                                                  
class Select{    
public:    
    Select()
    	:_max_fd(-1)
    {    
        FD_ZERO(&_rfds);    
    }    
    //添加监控,同时更新最大描述符
    bool Add(TcpSocket &sock){   
        int fd = sock.GetFd(); //获取要添加套接字的描述符    
        FD_SET(fd, &_rfds);    
        _max_fd = _max_fd > fd ? _max_fd : fd;    
        return true;    
    }    
    //移除监控,同时更新最大描述符
    bool Del(TcpSocket &sock){    
        int fd = sock.GetFd();    
        FD_CLR(fd, &_rfds);//从集合中移除描述符    
        //重新判断最大的描述符    
        //原理:从当前最大描述符开始递减,直到有一个描述符存在于集合中,则该描述符为最大描述符
        for (int i = _max_fd; i >= 0; i--) {    
            if(FD_ISSET(i, &_rfds)) {    
                _max_fd = i;    
                break;    
            }    
        }    
        return true;    
    }    
    //Wait接口对集合中所有描述符进行监控    
    //通过参数arry将所有就绪的描述符返回给外界    
    bool Wait(std::vector<TcpSocket> *arry) {    
        struct timeval tv;    
        tv.tv_sec = 3;    
        tv.tv_usec = 0;    
        fd_set tmp = _rfds;    
        //select(max+1, 读,写,异常,超时)    
        int ret;
        ret = select(_max_fd+1, &tmp, NULL, NULL, &tv);
        if (ret < 0) {
            perror("select error");
            return false;
        }else if (ret == 0) {
            arry->clear();
            return true;
        }
        //循环遍历:有哪些要监控的描述符仍存在就绪集合中
        for(int i = 0; i <= _max_fd; i++) {
            if (FD_ISSET(i, &tmp)) {
                TcpSocket sock;
                sock.SetFd(i);
                arry->push_back(sock);
            }
        }
        return true;
    }
private:
    fd_set _rfds;//需要监控的描述符集合-备份
    int _max_fd;//保存当前集合中最大的描述符
};                                                                 

poll模型

接口
  • int poll(struct pollfd* fds, nfds_t nfds, int timeout);
    • fds:要监控的描述符事件结构数组,数组每个元素都为pollfd结构体,每一个元素中包含了三部分内容:文件描述符、监听的事件集合、返回的事件集合;
    • nfds:数组中有效结点个数,或者是需要监控数组中前 nfds 个描述符;
    • timeout:超时时间,以毫秒为单位;
    • 返回值:等于 0 表示超时,小于 0 表示出错,有就绪事件则返回就绪个数;
struct pollfd{
	int fd;//要监控的描述符
	short events;//fd描述符要监控的时间:POLLIN--读 / POLLOUT--写
	short revents;//监控调用返回后,记录时间具体就绪了什么事件
}
  • struct pollfd中 events 和 revents 的取值:点此访问
流程
  1. 定义事件结构体数组,为每个需要监控的描述符定义事件结构——pollfd结构体,然后将该结构体加入数组中;
  2. 发起监控调用,将数组中有效结点拷贝到内核进行监控,超时 / 就绪则调用返回,返回前将描述符就绪的事件记录到结构体的 revents 成员中;
  3. 调用返回后,遍历事件数组,通过每个结点的 revents 成员确定对应节点描述符是否就绪了某个事件;
特性
  • 优点
    1. 不同与select使用三个位图来表示三个 fdset 的方式,poll使用一个pollfd的指针实现,pollfd结构包含了要监视的 event 和已就绪的 event,不再使用select"参数–值"传递的方式,接口使用比select更方便;
    2. poll并没有最大数量限制(但是数量过大后性能也是会下降);
  • 缺点
    1. select函数一样,poll返回后,需要轮询遍历pollfd来获取就绪的描述符;
    2. 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中,这个开销很大;
    3. 同时连接的大量客户端在同一时刻可能只有少数处于就绪状态,因此随着监视的描述符数量的增长,空遍历的次数增加,其效率会线性下降;
    4. 监控原理依然是轮询遍历,性能随着描述符增多而下降;
    5. 跨平台移植性较差;

epoll模型

概念
  • 地位:按照 man 手册的说法,epoll是为处理大批量句柄而作了改进的poll,它是在 2.5.44 内核中被引进的,它几乎具备了之前所说的一切优点,被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法;
接口
  • int epoll_create(int size);:在内核中创建一个epoll的句柄(一个eventpoll结构体实例),用完之后, 必须调用close()关闭;
    • size:监控的数量上限,在 Linux2.6 之后被忽略,只要大于 0 即可;
    • 返回值:成功返回epoll描述符,失败返回 -1;
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);:事件注册函数;
    • epfd:创建epoll后返回的句柄;
    • op:表示该注册函数要执行什么操作,具体有三个选项;
      • EPOLL_CTL_ADD:添加新的 fd 到 epfd 中;
      • EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
      • EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
    • fd:需要监控的描述符;
    • event:针对要监控的描述符所定义的事件结构体;
    • 返回值:成功返回 0,失败返回 -1;
struct epoll_event{
	//对描述符要监控的事件
	uint32_t events;
	//EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
	//EPOLLOUT : 表示对应的文件描述符可以写;
	//EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
	//EPOLLERR : 表示对应的文件描述符发生错误;
	//EPOLLHUP : 表示对应的文件描述符被挂断;
	//EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的,这个下面会详细讲解
	//EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
	
	//这是一个联合体,通常用于保存要监控的描述符,也就是第三个参数fd
	//当监控结束后,如果这个结构体所代表的事件就绪后,则会被返回给调用者,此时调用者通过data成员就可以很容易拿到描述符了,方便操作
	epoll_data_t data;
}__EPOLL_PACKED;

typedef union epoll_data{
	void* ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t;
  • int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);:监控函数;
    • epdf:创建epoll后返回的句柄;
    • eventsepoll_event类型的数组首地址,监控调用返回后,该数组中保存了所有就绪描述符所对应的事件结构;
    • maxevents:告诉内核这个 events 数组有多大;
    • timeout:超时时间,以毫秒为单位,置 0 会立即返回,置 -1 是阻塞等待;
    • 返回值:超时返回 0,出错返回 -1,有就绪描述符则返回具体个数;
流程
  1. 在内核中创建一个epoll句柄,每一个epoll对象都有一个独立的eventpoll结构体实例,该结构体中主要成员为一个红黑树(rbr)和一个队列(rdllist),其它的暂时不用关注,红黑树中存储需要监控的文件描述符,当红黑树中有文件描述符就绪,就将该描述符的事件结构加入队列中;
    在这里插入图片描述
  2. epoll中,对于每一个事件,都会建立一个epitem结构体:
struct epitem{
	struct rb_node rbn;//红黑树节点
	struct list_head rdllink;//双向链表节点
	struct epoll_filefd ffd; //事件句柄信息
	struct eventpoll *ep; //指向其所属的eventpoll对象
	struct epoll_event event; //期待发生的事件类型
}
  1. 向内核epoll句柄中添加要监控的描述符以及对应的事件结构,这些事件都会挂载在红黑树中,这样一来,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是 lg2n,其中 n 为树的高度);
  2. 所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,当某个描述符就绪时,就会调用相应的回调方法,这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中;
  3. 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可,如果rdlist不为空,则把发生的事件复制到epoll_wait函数第二个参数所传入的数组中,同时将事件数量返回给用户,这个操作的时间复杂度是 O(1);
  4. 监控调用返回后,只需要遍历 events 数组,逐个对节点中的描述符进行对应事件处理即可;
epoll事件触发方式
socket就绪条件
  • 读就绪
    • socket内核中,接收缓冲区中的字节数大于等于低水位标记SO_RCVLOWAT(一个基准值,默认为 1 字节);
    • socketTCP 通信中,对端关闭连接;
    • 监听socket上有新的连接请求;
    • socket上有未处理的错误;
  • 写就绪
    • socket内核中,发送缓冲区的空闲位置大小大于等于低水位标记SO_SNDLOWAT(一个基准值,默认为 1 字节);
    • socket的写操作被关闭(调用close()或者shutdown(write));
    • socket使用非阻塞connect连接成功或失败之后;
    • socket上有未读取的错误;
水平触发
  • 概念:EPOLLLT,是描述符被监控时的默认方式;
  • 特性:只要所监控的描述符就绪了,那么就触发事件;
  • 实例:我们把一个 tcpsocket添加到epoll描述符中,这个时候socket的另一端发送了 2KB 的数据,然后调用epoll_wait并且返回了该socket,这说明它已经准备好读取操作,调用read,读取了 1KB 的数据,此时缓冲区还剩 1KB 的数据,当再次调用epoll_wait时,epoll_wait会立刻返回并通知socket读事件就绪,直到缓冲区上所有的数据都被处理完,epoll_wait才不会立刻返回;
边缘触发
  • 概念:EPOLLET,该触发方式需要进行设置,在epoll_ctl()函数的第四个参数struct epoll_event结构体中的 events 成员加入该方式,则表示当前描述符的监控已被设置为边缘触发;
  • 特性:只有当所监控的描述符状态发生改变,才会触发事件;
  • 实例:我们把一个 tcpsocket添加到epoll描述符中,这个时候socket的另一端发送了 2KB 的数据,然后调用epoll_wait并且返回了该socket,这说明它已经准备好读取操作,调用read,读取了 1KB 的数据,此时缓冲区还剩 1KB 的数据,当再次调用epoll_wait时,epoll_wait不会再返回了,因为此种情况下,缓冲区的状态并没有改变,所以不会触发事件;
  • 优点:边缘触发可以减少epoll触发的次数,不过代价就是需要在一次响应就绪过程中就把
    所有的数据都处理完,否则可能会出现问题;
  • 要求:使用 EPOLLET 触发方式的话,需要将描述符设置为非阻塞,这个不是接口上的要求,而是工程实践上的要求,下面举一个实例来说明这个问题;
    1. 假设有这样一个场景:客户端向服务端发送一个 10k 的请求,服务端收到了这个请求,在将全部请求数据处理完后才会向客户端返回一个应答数据,而如果客户端收不到应答,就不会发送第下一个请求;
    2. 假设服务端所监控的描述符是阻塞式的read,第一次调用epoll_wait后,因为缓冲区中有 10k 数据,所以会返回描述符已就绪,此时我们只读出 1k 数据,那么剩下的 9k 数据就会待在缓冲区中;
    3. 如果服务端所监控的描述符被设置为边缘触发,那么在第一次读完 1k 数据后,虽然缓冲区中仍有 9k 数据,但是接下来再次调用epoll_wait后,并不会返回就绪,这 9k 数据将会一直待在缓冲区中,直到有下一次新数据到来,epoll_wait才会返回就绪;
    4. 那么问题来了:
      1、服务器只读到 1k 个数据,而它要读完 10k 才会给客户端返回响应数据;
      2、客户端要读到服务器的响应,才会发送下一个请求;
      3、客户端发送了下一个请求,epoll_wait才会返回,服务端才能去读缓冲区中剩余的数据;
    5. 这就形成了一个死循环,程序无法进行下去了,所以服务端在收到请求后,需要将所有数据全部读出,要想达成这个目的,我们需要循环将缓冲区中的内容取出,直到缓冲区中无数据,但是如果是阻塞读取,那么循环读取到缓冲区中没有数据时就会阻塞,因此必须设置为非阻塞,此时循环读取到缓冲区中没有数据时就会报错返回,这个错误为EAGAIN,我们通过这个错误码就能判断什么时候读取完毕;
特性
  • 优点
    1. 所能监控的描述符数量没有上限;
    2. 描述符以及事件结构只需要向内核拷贝一次;
    3. 监控原理采用异步阻塞,监控由系统完成,进程只需要判断返回的就绪链表是否为 NULL 即可,性能不会随着描述符增多而下降;
    4. 监控完毕返回的是就绪描述符集合,不需要循环判断,减少空遍历;
  • 缺点:跨平台移植性较差;
代码
封装epoll
/*                                                                                        
 *封装Epoll类,向外提供简单的接口即可完成对大量描述符的    
 *监控                                 
 *返回就绪的套接字                    
 */                                     
#include <iostream>                                             
#include <vector>                                
#include <cstdlib>                                
#include <sys/epoll.h>           
#include "tcpsocket.hpp"                                          
class Epoll{                                              
public:                                          
    Epoll():_epfd(-1){                              
        _epfd = epoll_create(1);       
        if (_epfd < 0) {    
            perror("epoll_create error");            
            exit(-1);                     
        }                                                      
    }                        
    bool Add(TcpSocket &sock) {               
        //epoll_ctl(epoll句柄,类型,描述符,事件)    
        //EPOLL_CTL_ADD   struct epoll_event    
        int fd = sock.GetFd();    
        struct epoll_event ev;    
        ev.data.fd = fd;                   
        //EPOLLET:将所监控的描述符设置为边缘触发    
        ev.events = EPOLLIN | EPOLLET;    
        int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);    
        if (ret < 0) {                    
            perror("epoll ctl add error");    
            return false;    
        }               
        return true;    
    }       
    bool Del(TcpSocket &sock) {                  
        int fd = sock.GetFd();    
        int ret;
        ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, NULL);
        if (ret < 0) {
            perror("epoll ctl del error");
            return false;
        }
        return true;
    }
    bool Wait(std::vector<TcpSocket> *arry) {
        //epoll_wait(句柄,数组,节点数量,超时)
        struct epoll_event evs[10];
        int ret;
        ret = epoll_wait(_epfd, evs, 10, 3000);
        if (ret < 0) {
            perror("epoll_wait");
            return false;
        }else if (ret == 0) {
            arry->clear();
            std::cout << "timeout\n";
            return true;
        }
        for (int i = 0; i < ret; i++) {
            if (evs[i].events & EPOLLIN) {
                TcpSocket sock;
                sock.SetFd(evs[i].data.fd);
                arry->push_back(sock);
            }
        }
        return true;
    }
private:
    int _epfd;
}; 
边缘触发下的数据接收
//从缓冲区中接收数据时,采用循环接收,直到缓冲区为空,为空会报 EAGAIN 错误返回
bool Recv(std::string *buf) {    
    //int recv(描述符,空间,数据长度,标志位)    
    //返回值:实际获取大小, 0-连接断开; -1-出错了    
    char tmp[4096] = {0};    
    int len = 4096;    
    int total = 0;    
    while(total < len){    
        int ret=recv(_sockfd, tmp + total, 5, 0);    
        if (ret < 0) {    
            if (errno == EAGAIN){    
                printf("所有数据都读完了\n");    
                break;    
            }    
            perror("recv error");    
            return false;    
        }else if (ret == 0) {    
            printf("peer shutdown");    
            return false;    
        }    
        total += ret;    
    }    
    buf->assign(tmp, total);                                                      
    return true;    
}    

服务端代码

  • 无论是基于select还是基于epoll,服务端的代码都十分类似,如果基于epoll所监控的描述符是水平触发,那么二者的代码一模一样,如果基于epoll所监控的描述符是边缘触发,那么在epoll的的代码中需要将所监控的描述符设置为非阻塞的,其他一模一样;
#include "tcpsocket.hpp"                                                                  
#include "select.hpp"    
#include "epoll.hpp"    
    
int main(int argc, char *argv[])    
{    
    if (argc != 3) {    
        printf("usage: ./tcp_src 192.168.2.2 9000\n");    
        return -1;    
    }    
    bool ret;    
    std::string srvip = argv[1];    
    uint16_t srvport = std::stoi(argv[2]);    
    TcpSocket lst_sock;//监听套接字    
    CHECK_RET(lst_sock.Socket());    
    lst_sock.SetNonBlock(); 
    //边缘触发模式下,需要将描述符设置为非阻塞   
    //lst_sock.SetSocketOpt();
    CHECK_RET(lst_sock.Bind(srvip, srvport));    
    CHECK_RET(lst_sock.Listen());    
    Epoll s;    
    s.Add(lst_sock);//将监听套接字添加监控    
    while(1) {    
        std::vector<TcpSocket> arry;    
        ret = s.Wait(&arry);//开始监控,arry返回就绪套接字    
        if (ret == false) {return 0;}    
        for (int i = 0; i < arry.size(); i++) {    
            if (arry[i].GetFd() == lst_sock.GetFd()){    
                TcpSocket clisock;    
                std::string ip; uint16_t port;    
                ret=lst_sock.Accept(&clisock,&ip,&port);    
                if (ret == false) {continue;}    
                std::cout<<"conn:"<<ip<<"-"<<port<<"\n";    
                //边缘触发模式下,需要将描述符设置为非阻塞   
                //clisock.SetNonBlock();    
                s.Add(clisock);//将通信套接字添加监控    
            }else {    
                std::string buf;    
                ret = arry[i].Recv(&buf);    
                if (ret == false) {
                    s.Del(arry[i]);
                    arry[i].Close();
                    continue;
                }
                std::cout<<"client say: "<<buf<<std::endl;
                buf.clear();
                std::cout << "server say: ";
                std::cin >> buf;
                ret = arry[i].Send(buf);
                if (ret == false) {
                    s.Del(arry[i]);
                    arry[i].Close();
                }
            }
        }
    }
    lst_sock.Close();
}                                          
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值