Linux网络编程—五种IO模型与非阻塞IO

第一章:五种IO模型

阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。

阻塞IO是最常见的IO模型。

非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。

非阻塞IO往往需要程序员以循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用。

信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作

IO多路转接:虽然从流程图上看起来和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。

小结

任何IO过程中,都包含两个步骤。第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让IO更高效,最核心的办法就是让等待的时间尽量少。

第二章:高级IO重要概念

在这里,我们要强调几个概念

同步通信 vs 异步通信(synchronous communication/ asynchronous communication)

同步和异步关注的是消息通信机制。

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果;
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

另外,我们回忆在讲多进程多线程的时候,也提到同步和互斥。这里的同步通信和进程之间的同步是完全不相干的概念。

  • 进程/线程同步也是进程/线程之间直接的制约关系
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。尤其是在访问临界资源的时候。

同学们以后在看到"同步"这个词,一定要先搞清楚大背景是什么。这个同步,是同步通信异步通信的同步,还是同步与互斥的同步。

阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

其他高级IO

非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。

我们此处重点讨论的是I/O多路转接

第三章:非阻塞IO

fcntl

一个文件描述符,默认都是阻塞IO。

函数原型如下。

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

fcntl函数有5种功能:

  • 复制一个现有的描述符(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函数,将文件描述符设置为非阻塞。

cpp 复制代码
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参数。

轮询方式读取标准输入

cpp 复制代码
#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;
}

第四章:I/O多路转接之select

初识select

系统提供select函数来实现多路复用输入/输出模型。

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

select函数原型

select的函数原型如下:

cpp 复制代码
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,
	struct timeval* timeout);

参数解释:

  • 参数nfds是需要监视的最大的文件描述符值+1;
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间。

参数timeout取值:

  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

关于fd_set结构

其实这个结构就是一个整数数组,更严格的说,是一个"位图"。使用位图中对应的位来表示要监视的文件描述符。

提供了一组操作fd_set的接口,来比较方便的操作位图。

cpp 复制代码
void FD_CLR(int fd, fd_set* set);	// 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set* set);	// 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set* set);	// 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set* set);			// 用来清除描述词组set的全部位

关于timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

函数返回值:

  • 执行成功则返回文件描述符状态已改变的个数
  • 如果返回0代表在描述符状态改变前已超过timeout时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。

错误值可能为:

  • EBADF 文件描述符为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n 为负值。
  • ENOMEM 核心内存不足

理解select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

  • (1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
  • (2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
  • (3)若再加入fd=2,fd=1,则set变为0001,0011
  • (4)执行select(6,&set,0,0,0)阻塞等待
  • (5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

socket就绪条件

读就绪

  • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT。此时可以无阻塞的读该文件描述符,并且返回值大于0;
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪

  • socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

异常就绪(选学)

  • socket上收到带外数据。关于带外数据,和TCP紧急模式相关(回忆TCP协议头中,有一个紧急指针的字段),同学们课后自己收集相关资料。

select的特点

  • 可监控的文件描述符个数取决于sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
    • 一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。
    • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

备注:

fd_set的大小可以调整,可能涉及到重新编译内核。感兴趣的同学可以自己去收集相关资料。

select缺点

  • 每次调用select,都需要手动设置fd集合,从接口使用角度来说也非常不便。
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

select使用示例: 检测标准输入输出

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main() {
	fd_set read_fds;
	FD_ZERO(&read_fds);
	FD_SET(0, &read_fds);//将标准输入(文件描述符 0)加入集合,用于监听键盘输入。
	for (;;) {
		printf("> ");
		fflush(stdout);//确保提示符立即显示(因为通常标准输出是行缓冲的)。
		int ret = select(1, &read_fds, NULL, NULL, NULL);//select() 会阻塞直到标准输入有数据可读。
		if (ret < 0) {
			perror("select");
			continue;
		}
		if (FD_ISSET(0, &read_fds)) {
			char buf[1024] = { 0 };
			read(0, buf, sizeof(buf) - 1);
			printf("input: %s", buf);
		}
		else {
			printf("error! invaild fd\n");
			continue;
		}
		//重新初始化文件描述符集合(因为 select() 会修改传入的集合),为下一次循环做准备。
		FD_ZERO(&read_fds);
		FD_SET(0, &read_fds);
	}
	return 0;
}

select使用示例

SelectServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = sizeof(fd_set) * 8;
int defaultfd = -1;


class SelectServer {
public:
    SelectServer(uint16_t port = defaultport) 
        :_port(port) {
        for (int i = 0; i < fd_num_max; ++i) {
            fd_array[i] = defaultfd;//将数组全部初始化为无效的文件描述符(即-1)
            // cout << "fd_array[" << i << "]" << " : " << fd_array[i] << endl;
        }
    }

    bool Init() {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();
        return true;
    }

    void Accepter() {
        string clientip;
        uint16_t clientport = 0;    
        int sock = _listensock.Accepet(&clientip, &clientport);
        if (sock < 0) return;
        lg(Info, "accept success, %s:%d, sock fd:%d", clientip.c_str(), clientport, sock);

        //获取的连接不能直接读取(缓冲区可能还没数据),可能会阻塞,
        //所以要交给select去等,交给select就要交给rfds,交给rfds就要交给fd_array
        int pos = 1;
        for (; pos < fd_num_max; pos++) {
            if (fd_array[pos] != defaultfd) continue;//跳过无效文件描述符
            else break;
        }
        if (pos == fd_num_max) {
            lg(Warning, "server is full, close %d now!", sock);
            close(sock);
        }
        else {
            fd_array[pos] = sock;
            PrintFd();
        }
    }

    void Recver(int fd, int pos) {
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);//不能保证是一个完整报文
        if (n > 0) {
            buffer[n] = 0;
            cout << "get a message: " << buffer << endl;
        }
        else if (n == 0) {
            lg(Warning, "client quit, me too. close fd is:%d", fd);
            close(fd);
            fd_array[pos] = defaultfd;
        }
        else {
            lg(Warning, "recv error, fd is:%d", fd);
            close(fd);
            fd_array[pos] = defaultfd;                       
        }
    }

    void Dispatcher(fd_set& rfds) { //事件派发器
        for (int i = 0; i < fd_num_max; ++i) {
            int fd = fd_array[i];
            if (fd == defaultfd) continue;
            if (FD_ISSET(fd, &rfds)) {
                if (fd == _listensock.Fd())  //连接事件就绪
                    Accepter();//连接管理器
                else  //读事件就绪
                    Recver(fd, i);   
            }
        }
    }
    void Start() {
        int listensock = _listensock.Fd();
        fd_array[0] = listensock;
        // struct timeval timeout = {5, 0};//如果放在这里就只设置了一次
        for(;;) {
            //每次for循环可能会有新连接到来,或者一些原有的连接断开,导致fd变化,所以每次都要重新设置rfds
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = fd_array[0];//select第一个参数是最大fd+1
            for(int i = 0; i < fd_num_max; ++i) {
                if (fd_array[i] == defaultfd) continue;
                FD_SET(fd_array[i], &rfds);
                if (maxfd < fd_array[i]) {
                    maxfd = fd_array[i];
                    lg(Info, "max fd update, max fd:%d", maxfd);
                }
            }
            //accept?不能直接accept
            //accept检测listensock是否有连接事件(三次握手后才有连接),
            //如果直接accept且没有连接,那么阻塞了。新连接到来等价于读事件就绪

            //输入输出,可能要进行周期的重复设置
            //如果设置为{0, 0},就是非阻塞轮询,没有就绪立刻返回
            struct timeval timeout = {1, 0}; 

            //如果事件就绪,上层不处理,select会一直通知;select告知就绪,下一次读取fd不会被阻塞。
            //select最后时间参数设置为nullptr,意味着一直阻塞,直到事件就绪
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/nullptr);
            switch (n) {
            case 0:
                cout << "time out, timeout:" << timeout.tv_sec << "." << timeout.tv_usec << endl;
                break;
            case -1:
                cerr << "select error" << endl;
                break;
            default:
                //有事件就绪了
                cout << "get a new link" << endl;
                Dispatcher(rfds);//连接就绪和读取就绪怎么区分
                break;
            }
        }
    }
    void PrintFd() {
        cout << "online fd list: ";
        for(int i = 0; i < fd_num_max; ++i) {
            if (fd_array[i] == defaultfd) continue;
            cout << fd_array[i] << " ";
        }
        cout << endl;
    }
    //无辅助数组版本
    // void Start() {
    //     int listensock = _listensock.Fd();
    //     // struct timeval timeout = {5, 0};//如果放在这里就只设置了一次
    //     for(;;) {
    //         //accept?不能直接accept
    //         //accept检测listensock是否有连接事件(三次握手后才有连接),
    //         //如果直接accept且没有连接,那么阻塞了。新连接到来等价于读事件就绪
    //         fd_set rfds;
    //         FD_ZERO(&rfds);
    //         FD_SET(listensock, &rfds);
    //         //输入输出,可能要进行周期的重复设置
    //         //如果设置为{0, 0},就是非阻塞轮询,没有就绪立刻返回
    //         struct timeval timeout = {1, 0}; 

    //         //如果事件就绪,上层不处理,select会一直通知;select告知就绪,下一次读取fd不会被阻塞。
    //         //select最后时间参数设置为nullptr,意味着一直阻塞,直到事件就绪
    //         int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
    //         switch (n) {
    //         case 0:
    //             cout << "time out, timeout:" << timeout.tv_sec << "." << timeout.tv_usec << endl;
    //             break;
    //         case -1:
    //             cerr << "select error" << endl;
    //             break;
    //         default:
    //             //有事件就绪了
    //             cout << "get a new link" << endl;
    //             HandlerEvent(rfds);
    //             break;
    //         }
    //     }
    // }

    ~SelectServer() { _listensock.Close(); }
private:
    Sock _listensock;
    uint16_t _port;
    int fd_array[fd_num_max];//数组,用户维护
    //设置辅助数组的原因
    //rfds:输入输出型参数,假设要关心的fd是0~7(即1111 1111)。没有就绪的,返回0000 0000。全部清零了。
    //所以每次都要对rfds重新设(辅助数组fd_array的作用,及为什么全部初始化为-1)
    //select第一个参数是maxfd+1,连接越来越多,所以这个参数应该动态变化
};

Main.cc

cpp 复制代码
#include "SelectServer.hpp"
#include <memory>

int main() {
    // std::cout << "fd_set bits num:" << sizeof(fd_set) * 8 << std::endl;//1024

    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Init();
    svr->Start();
    return 0;
}

第五章:I/O多路转接之poll

poll函数接口

cpp 复制代码
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
	int fd; /* file descriptor */
	short events; /* requested events */
	short revents; /* returned events */
};

参数说明

  • fds是一个poll函数监听的结构列表。每一个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合。
  • nfds表示fds数组的长度。
  • timeout表示poll函数的超时时间,单位是毫秒(ms)。

events和revents的取值:

返回结果

  • 返回值小于0,表示出错;
  • 返回值等于0,表示poll函数等待超时;
  • 返回值大于0,表示poll由于监听的文件描述符就绪而返回。

socket就绪条件

同select

poll的优点

不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。

  • pollfd结构包含了要监视的event和发生的event,不再使用select"参数-值"传递的方式。接口使用比select更方便。
  • poll并没有最大数量限制 (但是数量过大后性能也是会下降)。

poll的缺点

poll中监听的文件描述符数目增多时

  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

PollServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <poll.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = 64;
int defaultfd = -1;
int non_event = 0;

class PollServer {
public:
    PollServer(uint16_t port = defaultport) 
        :_port(port) {
        for (int i = 0; i < fd_num_max; ++i) {
            _event_fds[i].fd = defaultfd;
            _event_fds[i].events = non_event;//一开始没有要关心的事件,设为空
            _event_fds[i].revents = non_event;
            // cout << "fd_array[" << i << "]" << " : " << fd_array[i] << endl;
        }
    }

    bool Init() {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();
        return true;
    }

    void Accepter() {
        string clientip;
        uint16_t clientport = 0;    
        int sock = _listensock.Accepet(&clientip, &clientport);
        if (sock < 0) return;
        lg(Info, "accept success, %s:%d, sock fd:%d", clientip.c_str(), clientport, sock);

        //获取的连接不能直接读取(缓冲区可能还没数据),可能会阻塞,
        //所以要交给select去等,交给select就要交给rfds,交给rfds就要交给fd_array
        int pos = 1;
        for (; pos < fd_num_max; pos++) {
            if (_event_fds[pos].fd != defaultfd) continue;
            else break;
        }
        if (pos == fd_num_max) {
            lg(Warning, "server is full, close %d now!", sock);
            close(sock);
            //将struct pollfd _event_fds[fd_num_max] 改为 struct pollfd* _event_fds
            //可以扩容,而不是报告服务器满载
        }
        else {
            _event_fds[pos].fd = sock;
            _event_fds[pos].events = POLLIN;//关心连接套接字是否可读
            _event_fds[pos].revents = non_event;
            PrintFd();
        }
    }

    void Recver(int fd, int pos) {
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);//不能保证是一个完整报文
        if (n > 0) {
            buffer[n] = 0;
            cout << "get a message: " << buffer << endl;
        }
        else if (n == 0) {
            lg(Warning, "client quit, me too. close fd is:%d", fd);
            close(fd);
            _event_fds[pos].fd = defaultfd;
        }
        else {
            lg(Warning, "recv error, fd is:%d", fd);
            close(fd);
            _event_fds[pos].fd = defaultfd;                       
        }
    }

    void Dispatcher() { //事件派发器
        for (int i = 0; i < fd_num_max; ++i) {
            int fd = _event_fds[i].fd;
            if (fd == defaultfd) continue;
            if (_event_fds[i].revents & POLLIN) {
                if (fd == _listensock.Fd())  //连接事件就绪
                    Accepter();//连接管理器
                else  //读事件就绪
                    Recver(fd, i);   
            }
        }
    }
    void Start() {
        _event_fds[0].fd = _listensock.Fd();
        _event_fds[0].events = POLLIN;//是否有新连接也可以理解为是否可以读取新连接
        int timeout = 1000;
        for(;;) {
            int n = poll(_event_fds, fd_num_max, timeout);
            switch (n) {
            case 0:
                cout << "time out..." << endl;
                break;
            case -1:
                cerr << "poll error" << endl;
                break;
            default:
                //有事件就绪了
                cout << "get a new link" << endl;
                Dispatcher();//往对应的fd派发事件
                break;
            }
        }
    }
    void PrintFd() {
        cout << "online fd list: ";
        for(int i = 0; i < fd_num_max; ++i) {
            if (_event_fds[i].fd == defaultfd) continue;
            cout << _event_fds[i].fd << " ";
        }
        cout << endl;
    }

     ~PollServer() { _listensock.Close(); }
private:
    Sock _listensock;
    uint16_t _port;
    struct pollfd _event_fds[fd_num_max];//数组,用户维护
};

Main.cc

cpp 复制代码
#include "PollServer.hpp"
#include <memory>

int main() {
    // std::cout << "fd_set bits num:" << sizeof(fd_set) * 8 << std::endl;//1024

    std::unique_ptr<PollServer> svr(new PollServer());
    svr->Init();
    svr->Start();
    return 0;
}

第六章:I/O多路转接之epoll

epoll初识

按照man手册的说法:是为处理大批量句柄而作了改进的poll。

它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)

它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

epoll的相关系统调用

epoll有3个相关的系统调用。

epoll_create

cpp 复制代码
int epoll_create(int size);

创建一个epoll的句柄。

  • 自从linux2.6.8之后,size参数是被忽略的。
  • 用完之后,必须调用close()关闭。

epoll_ctl

cpp 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件注册函数。

  • 它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
  • 第一个参数是epoll_create()的返回值(epoll的句柄)。
  • 第二个参数表示动作,用三个宏来表示。
  • 第三个参数是需要监听的fd。
  • 第四个参数是告诉内核需要监听什么事。

第二个参数的取值:

  • EPOLL_CTL_ADD :注册新的fd到epfd中;
  • EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL :从epfd中删除一个fd;

struct epoll_event结构如下:

events可以是以下几个宏的集合:

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL红黑树里。

epoll_wait

cpp 复制代码
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, 
int timeout);

收集在epoll监控的事件中已经发送的事件。

  • 参数events是分配好的epoll_event结构体数组。
  • epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
  • maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
  • 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞)。
  • 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败。

epoll工作原理

  • 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。
cpp 复制代码
struct eventpoll {
	....
	/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
	struct rb_root rbr;
	/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
	struct list_head rdlist;
	....
};
  • 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。
  • 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。
  • 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
  • 在epoll中,对于每一个事件,都会建立一个epitem结构体。
cpp 复制代码
struct epitem {
	struct rb_node rbn;//红黑树节点
	struct list_head rdllink; //双向链表节点
	struct epoll_filefd ffd; //事件句柄信息
	struct eventpoll* ep; //指向其所属的eventpoll对象
	struct epoll_event event; //期待发生的事件类型
}
  • 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。
  • 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。这个操作的时间复杂度是O(1)。

总结一下, epoll的使用过程就是三部曲:

  • 调用epoll_create创建一个epoll句柄;
  • 调用epoll_ctl, 将要监控的文件描述符进行注册;
  • 调用epoll_wait, 等待文件描述符就绪;

epoll的优点(和 select 的缺点对应)

  • 接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效。不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开。
  • 数据拷贝轻量:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)。
  • 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作时间复杂度O(1)。即使文件描述符数目很多,效率也不会受到影响。
  • 没有数量限制:文件描述符数目无上限。

注意!!

网上有些博客说, epoll中使用了内存映射机制

  • 内存映射机制:内核直接将就绪队列通过mmap的方式映射到用户态。避免了拷贝内存这样的额外性能开销。

这种说法是不准确的。我们定义的struct epoll_event是我们在用户空间中分配好的内存。势必还是需要将内核的数据拷贝到这个用户空间的内存中的。

请同学们对比总结select, poll, epoll之间的优点和缺点(重要,面试中常见)。

epoll工作方式

bash 复制代码
你正在吃鸡,眼看进入了决赛圈,你妈饭做好了,喊你吃饭的时候有两种方式:
1. 如果你妈喊你一次,你没动,那么你妈会继续喊你第二次,第三次。。。。(亲妈,水平触发)
2. 如果你妈喊你一次,你没动,你妈就不管你了(后妈,边缘触发)

epoll有2种工作方式-水平触发(LT)和边缘触发(ET)

假如有这样一个例子:

  • 我们已经把一个tcp socket添加到epoll描述符
  • 这个时候socket的另一端被写入了2KB的数据
  • 调用epoll_wait,并且它会返回。说明它已经准备好读取操作
  • 然后调用read,只读取了1KB的数据
  • 继续调用epoll_wait.....

水平触发Level Triggered 工作模式

epoll默认状态下就是LT工作模式。

  • 当epoll检测到socket上事件就绪的时候,可以不立刻进行处理。或者只处理一部分。
  • 如上边的例子,由于只读了1K数据,缓冲区中还剩1K数据,在第二次调用 epoll_wait 时,epoll_wait 仍然会立刻返回并通知socket读事件就绪。
  • 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回。
  • 支持阻塞读写和非阻塞读写

边缘触发Edge Triggered工作模式

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式。

  • 当epoll检测到socket上事件就绪时,必须立刻处理。
  • 如上边的例子,虽然只读了1K的数据,缓冲区还剩1K的数据,在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了。
  • 也就是说, ET模式下,文件描述符上的事件就绪后,只有一次处理机会。
  • ET的性能比LT性能更高( epoll_wait 返回的次数少了很多)。Nginx默认采用ET模式使用epoll。
  • 只支持非阻塞的读写。

select和poll其实也是工作在LT模式下。epoll既可以支持LT,也可以支持ET。

对比LT和ET

LT是 epoll 的默认行为。

使用 ET 能够减少 epoll 触发的次数。但是代价就是强逼着程序员一次响应就绪过程中就把所有的数据都处理完。

相当于一个文件描述符就绪之后,不会反复被提示就绪,看起来就比 LT 更高效一些。但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理,不让这个就绪被重复提示的话,其实性能也是一样的。

另一方面, ET 的代码复杂程度更高了。

理解ET模式和非阻塞文件描述符

使用 ET 模式的 epoll,需要将文件描述设置为非阻塞。这个不是接口上的要求,而是"工程实践"上的要求。

假设这样的场景:服务器接受到一个10k的请求,会向客户端返回一个应答数据。如果客户端收不到应答,不会发送第二个10k请求。

如果服务端写的代码是阻塞式的read,并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,参考 man 手册的说明,可能被信号打断),剩下的9k数据就会待在缓冲区中。

此时由于 epoll 是ET模式,并不会认为文件描述符读就绪。epoll_wait 就不会再次返回。剩下的 9k数据会一直在缓冲区中。直到下一次客户端再给服务器写数据。epoll_wait 才能返回

但是问题来了。

  • 服务器只读到1k个数据,要10k读完才会给客户端返回响应数据。
  • 客户端要读到服务器的响应,才会发送下一个请求
  • 客户端发送了下一个请求,epoll_wait 才会返回,才能去读缓冲区中剩余的数据。

所以,为了解决上述问题(阻塞read不一定能一下把完整的请求读完),于是就可以使用非阻塞轮询的方式来读缓冲区,保证一定能把完整的请求都读出来。

而如果是LT没这个问题。只要缓冲区中的数据没读完,就能够让 epoll_wait 返回文件描述符读就绪。

epoll的使用场景

epoll的高性能,是有一定的特定场景的。如果场景选择的不适宜,epoll的性能可能适得其反。

  • 对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll。

例如,典型的一个需要处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适合epoll。

如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用epoll就并不合适。具体要根据需求和场景特点来决定使用哪种IO模型。

epoll中的惊群问题(选学)

惊群问题有些面试官可能会问到。建议同学们课后自己查阅资料了解一下问题的解决方案。

参考 http://blog.csdn.net/fsmiy/article/details/36873357

epoll示例:epoll服务器(LT模式)

CMakeLists.txt

bash 复制代码
cmake_minimum_required(VERSION 2.8.12.2)
project(EpollServer)
add_compile_options(-std=c++11)
add_executable(epoll_server Main.cc)

Socket.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

enum {
    SocketErr = 2,
    BindErr,
    ListenErr
};

const int backlog = 10;

class Sock {
public:
    Sock() {}
    ~Sock() {}

public:
    void Socket() {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0) {
            lg(Fatal, "socket error, %s:%d", strerror(errno), errno);
            exit(SocketErr);
        }
        int opt = 1; //几乎所有 socket 选项都是:0 → 关闭,非 0 → 打开,这里 1 表示 开启对应的 socket 行为。
        setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
        //SOL_SOCKET:表示设置的是 socket 层级 的选项(不是 TCP、IP 层)

        //SO_REUSEADDR:端口快速复用。TCP 服务器关闭后,端口通常会进入:TIME_WAIT。
        //在这个状态下:端口还没真正释放,你立刻 bind 同一个端口 → 失败。
        //SO_REUSEADDR 解决什么:端口处于 TIME_WAIT 状态时仍然可以 bind。
        //服务器快速重启(非常重要),对服务器程序来说,几乎是必开选项。
        
        //SO_REUSEPORT:端口共享(进阶)
        //它比 SO_REUSEADDR 更激进,用途也更"高级"。
        //允许:多个 socket 同时 bind 到同一个 IP + 端口。内核会把新连接 负载均衡 地分发给这些 socket
    }

    void Bind(uint16_t port) {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {
            lg(Fatal, "bind error, %s:%d", strerror(errno), errno);
            exit(BindErr);           
        }
    }

    void Listen() {
        if (listen(sockfd_, backlog) < 0) {
            lg(Fatal, "listen error, %s:%d", strerror(errno), errno);
            exit(ListenErr);             
        }
    }

    int Accepet(std::string* clientip, uint16_t* clientport) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
        if (newfd < 0) {
            lg(Warning, "accept error, %s:%d", strerror(errno), errno);
            return -1;             
        }
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);
        return newfd;
    }

    bool Connect(const std::string& ip, const uint16_t& port) {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);

        int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
        if (n == -1) {
            std::cerr << "Connect to " << ip << ":" << port << " error" << std::endl;
            return false;
        }
        return true;
    }

    void Close() { close(sockfd_); }
    int Fd() { return sockfd_; }

private:
    int sockfd_;
};

nocopy.hpp

cpp 复制代码
#pragma once

class nocopy {
public:
    nocopy(const nocopy&) = delete;
    nocopy& operator=(const nocopy&) = delete;
};

Epoller.hpp

cpp 复制代码
#pragma once

#include "nocopy.hpp"
#include "Log.hpp"
#include <cerrno>
#include <cstring>
#include <sys/epoll.h>

class Epoller : public nocopy {
    static const int size = 128;
public:
    Epoller() {
        _epfd = epoll_create(size);
        if (_epfd < 0) 
            lg(Error, "epoll_create error: %s", strerror(errno));
        else 
            lg(Info, "epoll_create success: %d", _epfd);
    }

    int EpollerWait(struct epoll_event revents[], int num) {
        int n = epoll_wait(_epfd, revents, num, /*_timeout*/-1);//_timeout为0是非阻塞模式;为1时阻塞模式
        return n;
    }

    int EpollerUpdate(int oper, int sock, uint32_t event) {
        int n = 0;
        if (oper == EPOLL_CTL_DEL) {
            n = epoll_ctl(_epfd, oper, sock, nullptr);
            if (n != 0) 
                lg(Error, "epoll_ctl delete error");
        }
        else {
            struct epoll_event ev;
            ev.events = event;
            ev.data.fd = sock;//方便后期得知是哪个fd就绪
            n = epoll_ctl(_epfd, oper, sock, &ev);
            if (n != 0) 
                lg(Error, "epoll_ctl error");
        }
        return n;
    }

    ~Epoller() {
        if (_epfd >= 0) close(_epfd);
    }
private:
    int _epfd;
    int _timeout{1000};
};

EpollSever.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "Log.hpp"
#include "nocopy.hpp"

uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);

class EpollSever : public nocopy {
    static const int num = 64;//如果就绪的事件超过这个数,那么下次会再返回
public:
    EpollSever(uint16_t port)
        :_port(port), 
        _listensocket_ptr(new Sock()),
        _epoller_ptr(new Epoller())
        {}

    void Init() {
        _listensocket_ptr->Socket();
        _listensocket_ptr->Bind(_port);
        _listensocket_ptr->Listen();
        lg(Info, "create listen socket success:%d", _listensocket_ptr->Fd());
    }

    void Accepeter() {
        std::string clientip;
        uint16_t clientport;
        int sock = _listensocket_ptr->Accepet(&clientip, &clientport);
        if (sock > 0) {
            //不能直接读取
            _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
            lg(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);
        }
    }

    void Recver(int fd) {
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);//不能保证是一个完整报文
        if (n > 0) {
            buffer[n] = 0;
            std::cout << "get a message: " << buffer << std::endl;

            //write
            std::string echo_str = std::string("server echo @ ") + buffer;//使用 std::string 构造函数
            write(fd, echo_str.c_str(), echo_str.size());
        }
        else if (n == 0) { //客户端断开连接
            lg(Warning, "client quit, me too. close fd is:%d", fd);
            _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);//因为断开连接,不需要在关心,所以从epoll中移除该fd
            close(fd);//先移除再关闭,因为移除的fd要合法
        }
        else { //读取错误
            lg(Warning, "recv error, fd is:%d", fd);
            _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
            close(fd);              
        }
    }

    void Dispatcher(struct epoll_event revs[], int num) {
        for (int i = 0; i < num; ++i) {
            uint32_t event = revs[i].events;
            int fd = revs[i].data.fd;
            if (event & EVENT_IN) { //读事件就绪
                if (fd == _listensocket_ptr->Fd()) //监听套接字读就绪,即可以获取新连接
                    Accepeter();//连接管理器
                else  //其他fd的读事件就绪
                    Recver(fd);//事件管理器
            }
            else if (event & EVENT_OUT) { //写事件就绪

            }
            else {

            }
        }
    }
    void Start() {
        //将listensocket添加到epoll中 -> listensocket和它关心的事件添加到内核epoll模型的rb_tree
        _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listensocket_ptr->Fd(), EVENT_IN);
        struct epoll_event revs[num];
        for(;;) {
            int n = _epoller_ptr->EpollerWait(revs, num);
            if (n > 0) {
                //有事件就绪
                lg(Debug, "event happened, fd is:%d", revs[0].data.fd);
                Dispatcher(revs, n);
            }
            else if (n == 0) {
                lg(Info, "time out...");
            }
            else {
                lg(Error, "epoll wait error");
            }
        }
    }

    ~EpollSever() { _listensocket_ptr->Close(); }
private:
    std::shared_ptr<Sock> _listensocket_ptr;
    std::shared_ptr<Epoller> _epoller_ptr;
    uint16_t _port;
};

Main.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "EpollSever.hpp"


int main() {
    std::unique_ptr<EpollSever> epoll_svr(new EpollSever(8888));
    epoll_svr->Init();
    epoll_svr->Start();
    
    return 0;
}

第七章:Reactor反应堆模式

Reactor 模式是一种事件驱动的编程模式,用于高效地处理并发 I/O 操作。它通过一个或多个事件循环(Event Loop)来监听和处理各种事件(如网络请求、定时器事件等),从而实现高效的并发处理,而无需为每个连接创建一个线程或进程。

Protocol.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

//json版本
// #define MySelf 1

const std::string blank_space_sep = " "; 
const std::string protocol_sep = "\n";

//"x op y" => "len"\n"x op y"\n
std::string Encode(std::string& content) { 
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;
    return package;
}

//"len"\n"x op y"\n => "x op y"
bool Decode(std::string& package, std::string* content) {
    //取第一个\n之前的len,并转为整形
    size_t pos = package.find(protocol_sep);
    if (pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);//报头字符串
    size_t len = std::stoi(len_str);//报头表示有效载荷的长度
    //检查整个package的长度。 package = len_str + content_str + 2 (2个\n)
    size_t total_len = len_str.size() + len + 2;
    if (package.size() < total_len) return false;
    
    *content = package.substr(pos + 1, len);
    //从流中获取完一条完整报文后就要移除它,erase 移除报文 
    package.erase(0, total_len);
    return true;
}

class Request {
public:
    Request(int data1, int data2, char oper)
        :x(data1), y(data2), op(oper) {}
    Request() {}
public:
    //struct => string,  "x op y"
    bool Serialize(std::string* out) {
#ifdef MySelf
        //构建报文的有效载荷
        std::string s = std::to_string(x);
        s += blank_space_sep;
        s += op;
        s += blank_space_sep;
        s += std::to_string(y);
        *out = s;
        return true;
#else
        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["op"] = op;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif
    }

    // string => struct 
    bool Deserialize(const std::string& in) { 
#ifdef MySelf
        // 索引: 0 1 2 3 4
        // 字符: 3   +   4
        size_t left = in.find(blank_space_sep);//left是1
        if (left == std::string::npos) return false;
        std::string part_x = in.substr(0, left);

        size_t right = in.rfind(blank_space_sep);//right是3
        if (right == std::string::npos) return false;
        std::string part_y = in.substr(right + 1);

        if (left + 2 != right) return false;
        op = in[left + 1];
        x = std::stoi(part_x);
        y = std::stoi(part_y);
        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);
        x = root["x"].asInt();
        y = root["y"].asInt();
        op = root["op"].asInt();
        return true;
#endif        
    }

    void DebugPrint() {
        printf("请求构建完成:%d %c %d = ?\n", x, op, y);
    }
public:
    int x;
    int y;
    char op;
};


class Response {
public:
    Response(int res, int c)
        :result(res), code(c) {}
    Response() {}
public:
    //struct => string  "result code"
    bool Serialize(std::string* out) {
#ifdef MySelf        
        //构建有效载荷
        std::string s = std::to_string(result);
        s += blank_space_sep;
        s += std::to_string(code);
        *out = s;
        return true;
#else
        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif  
    }

    //string => struct
    bool Deserialize(const std::string& in) {
#ifdef MySelf        
        size_t pos = in.find(blank_space_sep);//left是1
        if (pos == std::string::npos) return false;
        std::string left = in.substr(0, pos);
        std::string right = in.substr(pos + 1);
        result = std::stoi(left);
        code = std::stoi(right);       
        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);
        result = root["result"].asInt();
        code = root["code"].asInt();
        return true;
#endif
    }

    void DebugPrint() {
        std::cout << "结果响应完成, result:" << result << ", code:" << code << std::endl << std::endl; 
    }
public:
    int result;
    int code;//0可信;!0是几,表明对应的错误原因
};



//手写序列化和反序列化
const std::string blank_space_sep = " "; 
const std::string protocol_sep = "\n";

//"x op y" => "len"\n"x op y"\n
std::string Encode(std::string& content) { 
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;
    return package;
}

//"len"\n"x op y"\n => "x op y"
bool Decode(std::string& package, std::string* content) {
    //取第一个\n之前的len,并转为整形
    size_t pos = package.find(protocol_sep);
    if (pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);//报头字符串
    size_t len = std::stoi(len_str);//报头表示有效载荷的长度
    //检查整个package的长度。 package = len_str + content_str + 2 (2个\n)
    size_t total_len = len_str.size() + len + 2;
    if (package.size() < total_len) return false;
    
    *content = package.substr(pos + 1, len);
    //从流中获取完一条完整报文后就要移除它,erase 移除报文 
    package.erase(0, total_len);
    return true;
}

class Request {
public:
    Request(int data1, int data2, char oper)
        :x(data1), y(data2), op(oper) {}
    Request() {}
public:
    //struct => string,  "x op y"
    bool Serialize(std::string* out) {
        //构建报文的有效载荷
        std::string s = std::to_string(x);
        s += blank_space_sep;
        s += op;
        s += blank_space_sep;
        s += std::to_string(y);
        *out = s;
        return true;
    }

    // string => struct 
    bool Deserialize(const std::string& in) { 
        // 索引: 0 1 2 3 4
        // 字符: 3   +   4
        size_t left = in.find(blank_space_sep);//left是1
        if (left == std::string::npos) return false;
        std::string part_x = in.substr(0, left);

        size_t right = in.rfind(blank_space_sep);//right是3
        if (right == std::string::npos) return false;
        std::string part_y = in.substr(right + 1);

        if (left + 2 != right) return false;
        op = in[left + 1];
        x = std::stoi(part_x);
        y = std::stoi(part_y);
        return true;
    }

    void DebugPrint() {
        printf("请求构建完成:%d %c %d = ?\n", x, op, y);
    }
public:
    int x;
    int y;
    char op;
};


class Response {
public:
    Response(int res, int c)
        :result(res), code(c) {}
    Response() {}
public:
    //struct => string  "result code"
    bool Serialize(std::string* out) {
        //构建有效载荷
        std::string s = std::to_string(result);
        s += blank_space_sep;
        s += std::to_string(code);
        *out = s;
        return true;
    }

    //string => struct
    bool Deserialize(const std::string& in) {
        size_t pos = in.find(blank_space_sep);//left是1
        if (pos == std::string::npos) return false;
        std::string left = in.substr(0, pos);
        std::string right = in.substr(pos + 1);
        result = std::stoi(left);
        code = std::stoi(right);       
        return true;
    }

    void DebugPrint() {
        std::cout << "结果响应完成, result:" << result << ", code:" << code << std::endl << std::endl; 
    }
public:
    int result;
    int code;//0可信;!0是几,表明对应的错误原因
};

Calculator.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include "Protocol.hpp"

enum {
    Div_Zero = 1,
    Mod_Zero,
    Other_oper
};

class Calculator {
public:
    Calculator() {}

    Response CalculatorHelper(const Request& req) {
        Response resp(0, 0);
        switch (req.op) {
            case '+':
                resp.result = req.x + req.y;
                break;
            case '-':
                resp.result = req.x - req.y;
                break;
            case '*':
                resp.result = req.x * req.y;
                break;
            case '/': 
            {
                if (req.y == 0) resp.code = Div_Zero;
                else resp.result = req.x / req.y;
            }
                break;
            case '%': 
            {
                if (req.y == 0) resp.code = Mod_Zero;
                else resp.result = req.x % req.y;
            }
                break;           
            default:
                resp.code = Other_oper;
                break;
        }
        return resp;
    }
    std::string Handler(std::string& package) {
        std::string content;
        bool r = Decode(package, &content);
        if (!r) return "";
        Request req;
        r = req.Deserialize(content);
        if (!r) return "";

        content = "";
        Response resp = CalculatorHelper(req);

        resp.Serialize(&content);
        content = Encode(content);
        return content;
    }

    ~Calculator() {}
};

Socket.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

enum {
    SocketErr = 2,
    BindErr,
    ListenErr
};

const int backlog = 10;

class Sock {
public:
    Sock() {}
    ~Sock() {}

public:
    void Socket() {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0) {
            lg(Fatal, "socket error, %s:%d", strerror(errno), errno);
            exit(SocketErr);
        }
        int opt = 1; //几乎所有 socket 选项都是:0 → 关闭,非 0 → 打开,这里 1 表示 开启对应的 socket 行为。
        setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
        //SOL_SOCKET:表示设置的是 socket 层级 的选项(不是 TCP、IP 层)

        //SO_REUSEADDR:端口快速复用。TCP 服务器关闭后,端口通常会进入:TIME_WAIT。
        //在这个状态下:端口还没真正释放,你立刻 bind 同一个端口 → 失败。
        //SO_REUSEADDR 解决什么:端口处于 TIME_WAIT 状态时仍然可以 bind。
        //服务器快速重启(非常重要),对服务器程序来说,几乎是必开选项。
        
        //SO_REUSEPORT:端口共享(进阶)
        //它比 SO_REUSEADDR 更激进,用途也更"高级"。
        //允许:多个 socket 同时 bind 到同一个 IP + 端口。内核会把新连接 负载均衡 地分发给这些 socket
    }

    void Bind(uint16_t port) {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {
            lg(Fatal, "bind error, %s:%d", strerror(errno), errno);
            exit(BindErr);           
        }
    }

    void Listen() {
        if (listen(sockfd_, backlog) < 0) {
            lg(Fatal, "listen error, %s:%d", strerror(errno), errno);
            exit(ListenErr);             
        }
    }

    int Accepet(std::string* clientip, uint16_t* clientport) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
        if (newfd < 0) {
            lg(Warning, "accept error, %s:%d", strerror(errno), errno);
            return -1;             
        }
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);
        return newfd;
    }

    bool Connect(const std::string& ip, const uint16_t& port) {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);

        int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
        if (n == -1) {
            std::cerr << "Connect to " << ip << ":" << port << " error" << std::endl;
            return false;
        }
        return true;
    }

    void Close() { close(sockfd_); }
    int Fd() { return sockfd_; }

private:
    int sockfd_;
};

Comm.hpp

cpp 复制代码
#pragma once

#include <unistd.h>
#include <fcntl.h>
#include "Socket.hpp"

void SetNonBlockOrDie(int sock) {
    int fl = fcntl(sock, F_GETFL);
    if (fl < 0) exit(NonBlockErr);
    fcntl(sock, F_SETFL, fl | O_NONBLOCK);
}

nocopy.hpp

cpp 复制代码
#pragma once

class nocopy {
public:
    nocopy() {};//由于显式声明了拷贝构造函数(虽然是 = delete),编译器不会自动生成默认构造函数。
    nocopy(const nocopy&) = delete;
    nocopy& operator=(const nocopy&) = delete;
};

Epoller.hpp

cpp 复制代码
#pragma once

#include "nocopy.hpp"
#include "Log.hpp"
#include <cerrno>
#include <cstring>
#include <sys/epoll.h>

class Epoller : public nocopy {
    static const int size = 128;
public:
    Epoller() {
        _epfd = epoll_create(size);
        if (_epfd < 0) 
            lg(Error, "epoll_create error: %s", strerror(errno));
        else 
            lg(Info, "epoll_create success: %d", _epfd);
    }

    int EpollerWait(struct epoll_event revents[], int num, int timeout) {
        int n = epoll_wait(_epfd, revents, num, timeout);//_timeout为0是非阻塞模式;为1时阻塞模式
        return n;
    }

    int EpollerUpdate(int oper, int sock, uint32_t event) {
        int n = 0;
        if (oper == EPOLL_CTL_DEL) {
            n = epoll_ctl(_epfd, oper, sock, nullptr);
            if (n != 0) 
                lg(Error, "epoll_ctl delete error, sockfd:%d", sock);
        }
        else {
            struct epoll_event ev;
            ev.events = event;
            ev.data.fd = sock;//方便后期得知是哪个fd就绪
            n = epoll_ctl(_epfd, oper, sock, &ev);
            if (n != 0) 
                lg(Error, "epoll_ctl error");
        }
        return n;
    }

    ~Epoller() {
        if (_epfd >= 0) close(_epfd);
    }
private:
    int _epfd;
    int _timeout{1000};
};

TcpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <cerrno>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Epoller.hpp"
#include "Socket.hpp"
#include "Comm.hpp"

class Connection;
class TcpServer;

//using关键字创建类型别名
using func_t = std::function<void(std::shared_ptr<Connection>)>;
// using except_func = std::function<void(std::weak_ptr<Connection>)>;


//表示一个客户端与服务器之间的连接对象,封装了与该连接相关的所有数据和操作。
//逻辑:每个客户端连接对应一个 Connection 对象,用于管理该连接的生命周期、数据缓冲和事件回调。
class Connection {
public:
    // Connection(int sock, std::shared_ptr<TcpServer> tcp_server_ptr) 
    //     : _sock(sock), _tcp_server_ptr(tcp_server_ptr) {}

    Connection(int sock, std::weak_ptr<TcpServer> tcp_server_ptr) 
        : _sock(sock), _tcp_server_ptr(tcp_server_ptr) {}
    void SetHandler(func_t recv_cb, func_t send_cb, func_t except_cb) {
        _recv_cb = recv_cb;
        _send_cb = send_cb;
        _except_cb = except_cb;
    }
    int Sockfd() { return _sock; }
    void AppendInBuffer(const std::string& info) { _inbuffer += info; }
    void AppendOutBuffer(const std::string& info) { _outbuffer += info; }
    std::string& InBuffer() { return _inbuffer; } // for debug
    std::string& OutBuffer() { return _outbuffer; }
    ~Connection() {}
private:
    int _sock;
    std::string _inbuffer;//接收缓冲区 string不能处理二进制流
    std::string _outbuffer;//发送缓冲区

public:
    //三个回调函数,用于处理读、写、异常事件。
    func_t _recv_cb;//读回调
    func_t _send_cb;//写回调
    func_t _except_cb;
    // except_func _except_cb;

    //添加一个回指TcpServer的回指指针
    //指向所属 TcpServer 实例的智能指针,用于回指服务器对象,方便在 Connection 中访问服务器资源。
    // std::shared_ptr<TcpServer> _tcp_server_ptr;
    std::weak_ptr<TcpServer> _tcp_server_ptr;

    std::string _ip;
    uint16_t _port;
};



uint32_t EVENT_IN = (EPOLLIN | EPOLLET);
uint32_t EVENT_OUT = (EPOLLOUT | EPOLLET);
static const int g_buffer_size = 128;

class TcpServer : public nocopy, 
                  public std::enable_shared_from_this<TcpServer> {
                  //让一个"已经被 shared_ptr 管理的对象",
                  //在它的成员函数里,安全地拿到指向自己的 shared_ptr。
// class TcpServer : public nocopy {
    static const int num = 64;
public:
    TcpServer(uint16_t port, func_t OnMessage)
        :_port(port),
         _OnMessage(OnMessage),
         _quit(true),
         _epoller_ptr(new Epoller()), 
         _listensock_ptr(new Sock()) 
    {}

    void Init() {
        _listensock_ptr->Socket();
        SetNonBlockOrDie(_listensock_ptr->Fd());
        _listensock_ptr->Bind(_port);
        _listensock_ptr->Listen();
        lg(Info, "create listen socket success:%d", _listensock_ptr->Fd());
        AddConnection(_listensock_ptr->Fd(), EVENT_IN, 
        std::bind(&TcpServer::Accepeter, this, std::placeholders::_1), nullptr, nullptr);
    }

    //Connection 是连接实体,负责存储连接状态和数据。
    //_connections 是连接管理器,负责存储和索引所有活跃连接。
    //AddConnection() 是连接初始化入口,用于将监听套接字加入事件循环,
    //后续应为每个新连接的客户端套接字创建 Connection 对象并注册到 _connections 中,从而实现事件驱动的高并发处理模型。
    void AddConnection(int sock, uint32_t event, func_t recv_cb, func_t send_cb, func_t except_cb,
                      const std::string& ip = "0.0.0.0", uint16_t port = 0) {
        //1. 给sock建立connection对象,并将sock的connection对象添加到unordered_map _connections
        // std::shared_ptr<Connection> new_connection = std::make_shared<Connection>(sock, std::shared_ptr<TcpServer>(this));
        std::shared_ptr<Connection> new_connection = std::make_shared<Connection>(sock, shared_from_this());
        new_connection->SetHandler(recv_cb, send_cb, except_cb);
        new_connection->_ip = ip;
        new_connection->_port = port;
        //2. 添加到unordered_map
        _connections.insert(std::make_pair(sock, new_connection));

        //3. 将要关心的fd和对应的event添加到内核
        _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, event);
        lg(Debug, "add a new connection success, sock fd is:%d", sock);
    }

    void Accepeter(std::shared_ptr<Connection> connection) { //连接管理器
        while (true) {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = ::accept(connection->Sockfd(), (struct sockaddr*)&peer, &len);
            if (sock > 0) {
                uint16_t peerport = ntohs(peer.sin_port);
                char ipbuf[128];
                inet_ntop(AF_INET, &peer.sin_addr.s_addr, ipbuf, sizeof(ipbuf));
                lg(Debug, "get a new link, get info->[%s:%d], sockfd:%d", ipbuf, peerport, sock);
                
                connection->_ip = ipbuf;
                connection->_port = peerport;
                
                SetNonBlockOrDie(sock);
                //listensock只需要设置_recv_cb,而其他sock,读,写,异常
                AddConnection(sock, EVENT_IN, 
                             std::bind(&TcpServer::Recver, this, std::placeholders::_1),
                             std::bind(&TcpServer::Sender, this, std::placeholders::_1),
                             std::bind(&TcpServer::Excepter, this, std::placeholders::_1),
                             ipbuf, peerport);
            }
            else {
                if (errno == EWOULDBLOCK) break; //内核还未将数据准备好
                else if (errno == EINTR) continue; //被信号中断
                else break; //读出错
            }
        }
    }

    //事件管理器
    //不需要关心数据的格式。服务器只需要IO数据,至于是否读完、报文的格式细节不用管。
    void Recver(std::shared_ptr<Connection> connection) {
        // std::cout << "haha, got you, sockfd:" << connection->Sockfd() << std::endl;
        char buffer[g_buffer_size];
        memset(buffer, 0, sizeof(buffer));
        int sock = connection->Sockfd();
        while (true) {
            //设置0是阻塞,但前面已改为非阻塞
            ssize_t n = recv(connection->Sockfd(), buffer, sizeof(buffer) - 1, 0);
            if (n > 0) {
                connection->AppendInBuffer(buffer);
            }
            else if (n == 0) {
                lg(Info, "sockfd:%d, client info %s:%d quit...", sock, connection->_ip.c_str(), connection->_port);
                connection->_except_cb(connection);
            }
            else {
                //EWOULDBLOCK 是一个错误码(宏定义),用于指示一个操作(如读或写)在当前状态下无法完成,
                //但操作可能在稍后完成。这通常出现在非阻塞 I/O 操作中。
                if (errno == EWOULDBLOCK) break; //接收缓冲区没数据
                else if (errno == EINTR) continue;//被信号中断
                else {
                    lg(Info, "sockfd:%d, client info %s:%d recv error...", sock, connection->_ip.c_str(), connection->_port);  
                    connection->_except_cb(connection);   
                    return; 
                }
            }
        }
        //有数据,但不一定完整。 1.检测  2.如果有完整报文,就处理
        _OnMessage(connection);//当前sock读到的所有数据在connection对象中
    }
    void Sender(std::shared_ptr<Connection> connection) {
        auto& outbuffer = connection->OutBuffer();
        while (true) {
            ssize_t n = send(connection->Sockfd(), outbuffer.c_str(), outbuffer.size(), 0);
            if (n > 0) {
                outbuffer.erase(0, n);
                if (outbuffer.empty()) return;
            }
            else if (n == 0) {
                return;
            }
            else {
                if (errno == EWOULDBLOCK) break; //发送缓冲区不足
                else if (errno == EINTR) continue;
                else {
                    lg(Info, "sockfd:%d, client info %s:%d send error...", connection->Sockfd(), connection->_ip.c_str(), connection->_port);  
                    connection->_except_cb(connection);   
                    return; 
                }               
            }
        }

        //1. 发完了  2. 数据没发完,但发送缓冲区满了,
        if (!outbuffer.empty()) //开启对写事件的关心
            EnableEvent(connection->Sockfd(), true, true);//读写开启
        else //关闭对写事件的关心
            EnableEvent(connection->Sockfd(), true, false);//写关闭
    }
    void Excepter(std::weak_ptr<Connection> connection) {
        if (connection.expired()) return;
        auto coon = connection.lock();
        int fd = coon->Sockfd();
        lg(Info, "Excepter Hander sockfd:%d, client info %s:%d excepter handler", fd, coon->_ip.c_str(), coon->_port);
        //1. 移除对特定fd的关心
        _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
        //2. 关闭异常的文件描述符
        lg(Debug, "close %d done...", fd);
        close(fd);
        //3. 从unorderder_map中移除该连接
        lg(Debug, "remove %d from _connections...", fd);
        // _connections[fd].reset();
        _connections.erase(fd);
    }

    void EnableEvent(int sock, bool readable, bool writeable) {
        uint32_t events = 0;
        events |= (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET;
        _epoller_ptr->EpollerUpdate(EPOLL_CTL_MOD, sock, events);
    }

    bool IsConnectionSafe(int fd) {
        auto iter = _connections.find(fd);
        if (iter == _connections.end()) return false;
        else return true;
    }

    void Dispatcher(int timeout) {
        int n = _epoller_ptr->EpollerWait(revs, num, timeout);
        for (int i = 0; i < n; ++i) {
            uint32_t events = revs[i].events;
            int sock = revs[i].data.fd;
            //统一把事件异常转换成读写问题
            if (events & EPOLLERR) events |= (EPOLLIN | EPOLLOUT);
            if (events & EPOLLHUP) events |= (EPOLLIN | EPOLLOUT);
            //只需要处理EPOLLIN  EPOLLOUT
            if ((events & EPOLLIN) && IsConnectionSafe(sock))
                if (_connections[sock]->_recv_cb)
                    _connections[sock]->_recv_cb(_connections[sock]);
            
            if ((events & EPOLLOUT) && IsConnectionSafe(sock))
                if (_connections[sock]->_send_cb)
                    _connections[sock]->_send_cb(_connections[sock]);
        }
    }

    void Loop() {
        _quit = false;

        // AddConnection();
        while (!_quit) {
            // Dispatcher(1000);
            Dispatcher(-1);
            PrintConnection();
        }
        _quit = true;
    }

    void PrintConnection() {
        std::cout << "_connections fd list:";
        for (auto& connection : _connections) {
            std::cout << connection.second->Sockfd() << ", ";
            std::cout << "inbuffer:" << connection.second->InBuffer();
        }
        std::cout << std::endl;
    }
    
    ~TcpServer() {}
private:
    // std::shared_ptr<Epoller> _epoller_ptr;
    // std::shared_ptr<Sock> _listensock_ptr;
    std::unique_ptr<Epoller> _epoller_ptr;
    std::unique_ptr<Sock>    _listensock_ptr;
    std::unordered_map<int, std::shared_ptr<Connection>> _connections;//用于管理所有活跃的客户端连接。
    struct epoll_event revs[num];
    uint16_t _port;
    bool _quit;

    //让上层处理信息
    func_t _OnMessage;
};

Main.cc

cpp 复制代码
#include <iostream>
#include <functional>
#include <memory>
#include "Log.hpp"
#include "TcpServer.hpp" //处理IO
#include "Calculator.hpp" //处理业务

Calculator calculator;

//for debug
void DefaultOnMessage(std::shared_ptr<Connection> connection_ptr) {
    std::cout << "上层得到了数据:" << connection_ptr->InBuffer() << std::endl;
    std::string response_str = calculator.Handler(connection_ptr->InBuffer());
    if (response_str.empty()) return; //如果没有完整报文,这里不处理,继续读取
    lg(Debug, "%s", response_str.c_str());
    //response_str 发送
    connection_ptr->AppendOutBuffer(response_str);
    // connection_ptr->_tcp_server_ptr->Sender(connection_ptr);
    auto server = connection_ptr->_tcp_server_ptr.lock();
    if (!server) return;

    server->Sender(connection_ptr);
}

int main() {
    // std::unique_ptr<TcpServer> tcp_svr(new TcpServer(8888, DefaultOnMessage));
    auto tcp_svr = std::make_shared<TcpServer>(8888, DefaultOnMessage);
    tcp_svr->Init();
    tcp_svr->Loop();
    
    return 0;
}

ClientCal.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"

static void Usage(const std::string& proc) {
    std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);    
    Sock sockfd;
    sockfd.Socket();
    bool r = sockfd.Connect(serverip, serverport);
    if (!r) return 1;

    srand(time(nullptr) ^ getpid());
    int cnt = 1;
    const std::string opers = "+-*/%=-=&^";
    std::string inbuff_stream;//报文
    while (cnt <= 10) {
        std::cout << "第" << cnt << "次测试" << std::endl;
        int x = rand() % 100 + 1;
        usleep(1234);
        int y = rand() % 100;
        usleep(1234);
        char op = opers[rand() % opers.size()];
        Request req(x, y, op);//初始化请求
        req.DebugPrint();

        std::string package;
        req.Serialize(&package);
        package = Encode(package);
        std::cout << "最新发出的请求:\n" << package;
        write(sockfd.Fd(), package.c_str(), package.size());
        // write(sockfd.Fd(), package.c_str(), package.size());
        // write(sockfd.Fd(), package.c_str(), package.size());
        // write(sockfd.Fd(), package.c_str(), package.size());

        char buffer[128];
        int n = read(sockfd.Fd(), buffer, sizeof(buffer));
        if (n > 0) {
            buffer[n] = 0;
            inbuff_stream += buffer;//报文
            std::cout << inbuff_stream << std::endl;
            
            std::string content;//有效载荷
            bool r = Decode(inbuff_stream, &content);
            assert(r);

            Response resp;
            r = resp.Deserialize(content);
            assert(r);

            resp.DebugPrint();
            cnt++;
        }
        // usleep(500000);
        sleep(1);
    }

    sockfd.Close();
    return 0;
}

bind问题

Lambda问题

智能指针1

智能指针2

作业

1. 以下关于异步IO说法不正确的是()

A.System V的异步IO信号是SIGPOLL

B.POSIX异步IO接口使用AIO控制块来描述IO操作

C.当使用异步IO的时候,由内核在数据拷贝完成时, 通知应用程序

D.当使用异步IO的时候,由内核告诉应用程序何时可以开始拷贝数据

答案:D

答案解析:

A正确:与SIGIO同义,表示文件描述符准备就绪, 可以开始进行输入 / 输出操作

B正确:POSIX中定义了异步 IO 应用程序接口(AIO API)

C正确:内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据),也就是说信号驱动是告诉进程什么时候可以进行IO, 而异步IO是告诉进程什么时候IO完成了。

D错误

2. 以下关于IO复用说法不正确的是()

A.IO多路转接能够同时等待多个文件描述符的就绪状态

B.在所有的POSIX兼容的平台上,select函数使我们可以执行IO多路转接

C.select的参数会告诉内核我们所关心的描述符

D.以上说法都不正确

答案:D

答案解析:

IO复用也是指的是IO多路转接,可以同时监听多个文件描述符的状态,多路转接模型有:select,poll,epoll,其中select遵循posix标准,因此POSIX平台可以使用select

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

可以通过setect函数的readfds, writefds,告诉系统需要监控的描述符以及对应的监控状态

综上:ABC都是正确的描述

3. 以下关于信号驱动IO说法不正确的是()

A.信号驱动IO是指内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作

B.信号驱动IO需要注册一个信号处理函数

C.当使用信号驱动IO的时候,线程可以做其他事情,并不需要一直去关注数据是否已经准备好

D.当使用信号驱动IO的时候,调用者需要配合循环来时不时的关注是否可以进行IO操作

答案:D

答案解析:

D错误:当时用信号驱动IO的时候,由于是注册了信号处理函数,当内核将数据准备好,通过SIGIO来通知用户进程,进程这时候再进行IO操作,所以不需要配合循环来关注IO操作

4. 以下关于非阻塞IO说法不正确的是()

A.非阻塞调用是指发生调用之后,当不能得到结果时,直接返回

B.非阻塞IO需要搭配循环来使用

C.当非阻塞IO模型时,用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果

D.在使用非阻塞IO的时候CPU使用率一定很高

答案:D

答案解析:

非阻塞指的是如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码,但是这时候并没有获取到数据,因此也就意味着,非阻塞的调用需要搭配循环来使用。

换个实际的场景,也就是意味着,当用户发起一个read操作之后,如果内核没有准备好数据,则read返回,返回 - 1,并重置errno为EWOULDBLOCK 表示数据未就绪

但是非阻塞IO虽然需要配合循环来使用,但是并不一定CPU使用率就很高,CPU使用率很高,意味着CPU在做大量的运算(逻辑运算或者算术运算),所以不一定CPU使用率会高,因为返回后不一定会立即进行没有就绪的IO操作

5. 以下是关于阻塞IO说法不正确的是()

A.典型的阻塞IO的例子为:data = socket.read();

B.用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态

C.当调用socket.read()后,如果内核数据没有就绪,当前调用者的进程中所有线程都会阻塞

D.阻塞调用是指调用结果返回之前,当前线程会被挂起

答案:C

C错误:如果在内核数据没有就绪的时候进行read调用,则只会阻塞调用线程这一个执行流,并不会阻塞所有线程

6. select的优缺点

优点:

  • select模型是Windows sockets中最常见的IO模型。它利用select函数实现IO 管理。通过对select函数的调用,应用程序可以判断套接字是否存在数据、能否向该套接字写入据
  • 可以等待多个套接字

缺点:

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小.

7. poll的优缺点

优点:

  • 不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现.
  • pollfd结构包含了要监视的event和发生的event,不再使用select"参数 - 值"传递的方式.接口使用比select更方便.
  • poll并没有最大数量限制(但是数量过大后性能也是会下降).

缺点:

  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

8. epoll的优缺点

  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效.不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量 : 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select / poll都是每次循环都要进行拷贝)
  • 事件回调机制 : 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪.这个操作时间复杂度O(1).即使文件描述符数目很多, 效率也不会受到影响.
  • 没有数量限制 : 文件描述符数目无上限.

9. 以下说法不正确的是()

A.ET事件发生仅通知一次的原因是只被添加到rdlist中一次,而LT可以有多次添加的机会

B.当时用ET模式的时候描述符最好设置为非阻塞模式

C.epoll理论上而言可以高效的监视无限多的文件描述符

D.LT模式也被称之为边沿触发

答案:D

ET模式叫边缘触发:表示描述符状态发生变化时触发一次事件,在没有新的状态变化时不会通知第二次,对于IO读事件来说缓冲区有新的数据到来的时候才会触发一次事件,不管是否处理都只会触发一次,因此尽可能的一次事件处理中循环将自己需要处理的数据处理完,为了避免在循环读取数据中因为没有数据而阻塞,因此最好将描述符设置为非阻塞。

LT模式叫水平触发:表示描述符状态发生变化,但是没有被处理,就会触发事件,对于IO读写操作来说,只要接收缓冲区中有数据或者发送缓冲区有剩余空间就会触发事件,不断通知用户

D错误:LT是水平触发

10. 以下关于事件放入epoll等待队列说法不正确的是()

A.当LT模式下,有新数据到来才会加入到epoll等待队列中

B.有老数据,并且通过epoll_ctl设置EPOLL_CTL_MOD(ET模式)

C.数据可写,并且通过epoll_ctl设置EPOLL_CTL_MOD(ET模式)

D.以上说法中都不正确

答案:D

A错误:LT水平触发,对于读是接收缓冲区中有数据可读,也就是有数据就加入等待队列, 对于写是发送缓冲区有剩余空间

B错误:ET边缘触发,有新数据到来的时候才会触发事件,放入EPOLL等待队列

C错误;ET边缘触发,对于写来说是状态从不可写变为可写时才会触发事件,放入等待队列

11. 以下关于LT模式说法错误的是()

A.通常情况下ET模式效率比LT模式高

B.LT模式下,当epoll_wait检测到fd上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件

C.客户端发送数据,I / O函数会提醒描述符fd有数据---- > recv读数据,若一次没有读完,I / O函数会一直提醒服务端fd上有数据,直到recv缓冲区里的数据读完

D.LT模式读取激活事件后,如果还有未处理的数据。事件不会放入EPOLL等待队列

答案:D

A正确:在ET模式下,可以减少epoll的系统调用次数,并且减少每次返回的就绪事件信息,因此能够一定量的提高部分效率。

B正确:LT模式下,若不处理触发的事件,则下次监控依然会触发事件,因此可以不立即处理该事件。

C正确:LT模式下,对于IO读操作来说,就是缓冲区中只要有数据就会一直触发事件,直到缓冲区数据被读完。

D错误:LT模式下,如果还有未处理的数据,事件会再次被触发,并放入EPOLL等待队列。

12. 以下关于ET模式说法错误的是()

A.epoll_wait只有在客户端每次发数据时才会返回, 除此以外即使接收缓冲区里还有数据也不会触发事件返回

B.在使用ET模式的时候描述符必须设置为阻塞模式

C.使用ET模式的时候最好使用循环读取,将自己需要处理的数据全部处理完毕。

D.以上说法中A, B是正确的,C是错误的

答案:B

ET模式指的是边缘触发模式,表示只有描述符状态发生改变的时候才会触发一次事件,对于读事件来说指的是只有新数据到来时触发一次,后续不管这次数据是否处理都不会触发第二次事件,直到有下次新数据到来。

因为边缘触发是只有新数据到来才会触发一次事件,因此使用ET模式的时候最好使用循环读取,将自己需要处理的数据全部处理,避免因为没有新数据到来而导致不触发新事件,使剩下的数据因为无法触发事件而得不到处理

B错误:在边缘模式下,通常是尽量设置为非阻塞操作,而并非阻塞操作

13. 以下关于ET模式和LT模式说法错误的是()

A.ET模式是边缘触发,LT模式是水平触发

B.ET模式是epoll的缺省工作模式

C.ET模式每当状态变化时,触发一个事件

D.LT模式只要满足条件,就触发一个事件,即只要有数据没有被获取,内核就不断通知用户

答案:B

ET模式叫边缘触发:表示描述符状态发生变化时触发一次事件,在没有新的状态变化时不会通知第二次,对于IO读事件来说缓冲区有新的数据到来的时候才会触发一次事件,不管是否处理都只会触发一次

LT模式叫水平触发:表示描述符状态发生变化,但是没有被处理,就会触发事件,对于IO读写操作来说,只要接收缓冲区中有数据或者发送缓冲区有剩余空间就会触发事件,不断通知用户

epoll的缺省工作模式是LT模式,也就是水平触发模式

相关推荐
未来之窗软件服务2 小时前
幽冥大陆(六十五) PHP6.x SSL 文字解密—东方仙盟古法结界
网络·数据库·ssl·加解密·仙盟创梦ide·东方仙盟
小鹏linux2 小时前
【linux】进程与服务管理命令 - pkill
linux·运维·服务器
ChenXinBest2 小时前
一次firewalld和docker冲突问题排查
linux·docker
Henry Zhu1232 小时前
VPP中DHCP插件源码深度解析第二篇:DHCPv4客户端实现详解(下)
服务器·c语言·网络·计算机网络·云原生
墨白曦煜2 小时前
计算机组成原理:大端序与小端序的原理与权衡
linux·windows
写代码的橘子n2 小时前
IPv6协议深入学习指南(从易到难)
网络·计算机网络·ipv6
Knight_AL2 小时前
HTTP 状态码一览:理解 2xx、3xx、4xx 和 5xx 分类
网络·网络协议·http
网硕互联的小客服2 小时前
人工智能服务器是什么,人工智能服务器的有什么用?
运维·服务器·网络·安全
深圳市恒讯科技2 小时前
美国云服务器和美国物理服务器怎么选?
运维·服务器