TinyWebserver学习(6)-线程监听函数eventListen()

六、线程监听函数eventListen()

一、相关知识总结

1、setsockopt()函数
setsockopt 是用于设置套接字(socket)选项的系统调用,允许应用程序对套接字的行为进行更细粒度的控制。它通常用于配置网络通信的参数,例如超时、缓冲区大小、地址复用等。以下是详细的解析

#include 
#include 

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd :已经创建的套接字描述符(通过 socket() 返回)。
level :选项所在的协议层,常见的取值:
	SOL_SOCKET:通用套接字选项(如 SO_REUSEADDR)。
	IPPROTO_TCP:TCP 协议层选项(如 TCP_NODELAY)。
	IPPROTO_IP:IP 协议层选项(如 IP_MULTICAST_TTL)。
optname :要设置的选项名称(具体含义依赖 level)。
optval :指向选项值的指针。
optlen :选项值的长度(以字节为单位)。

Level与对应的选项名称:

  1. 通用套接字选项
选项名 类型 作用
SO_REUSEADDR int 端口复用
SO_KEEPALIVE int 启动保活机制
SO_RCVBUF/SO_SNDBUF int 设置接收/发送缓冲区大小
SO_RCVTIMEO/SO_SNDTIMEO int 设置接收/发送超时时间
SO_LINGER struct 是否优雅关闭

列如在该项目中用于设置优雅关闭的代码

if (0 == m_OPT_LINGER)
    {
        struct linger tmp = {0, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }
    else if (1 == m_OPT_LINGER)
    {
        struct linger tmp = {1, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }

当然TCP以及IP选项也有对应的选项名称,因为项目不涉及,所以这里就不提了,大家有兴趣请自行查略。

2、Epoll多路复用

多路复用是为了解决高并发场景下 ,主线程或进程不阻塞于某一个具体的IO操作,如我们的电脑既要读取键盘输入也要读取鼠标输入。
常用的多路复用函数有select、poll以及epoll,其中epoll是前两种的优化版本,具有更高的高并发场景下的性能瓶颈,其无需像前两者每次都轮询所有的文件描述符,从而大大减少内核和用户空间的交互开销,是高并发网络编程的基石之一。
常用函数:

struct epoll_event event;//用于储存就绪事件的结构体

//1.创建epoll实例
int epfd = epoll_create1(0);  // 创建 epoll 实例

//2.向epoll实例中添加、删除对象
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
	//op:操作类型
		//EPOLL_CTL_ADD:添加监听
		//EPOLL_CTL_MOD:修改监听
		//EPOLL_CTL_DEL:移除监听
	//fd:需要监听的文件描述符
	//event:监听的事件类型(如EPOLLIN、EPOLLOUT)和附加数据(EPOLL_data_t)
	
//3.等待事件发生,返回就绪的
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
	//events:用于储存就绪的事件
	//maxevents:最多返回的事件
	//timeout:最多等待的时间
//返回值:返回已就绪时间的个数

事件触发模式:
(1)水平触发(Level Triggered, LT)

  • 默认模式 :只要文件描述符处于就绪状态,就会持续触发通知,直到事件被处理。
  • 特点 :安全性高,适合新手使用,但可能频繁触发。

(2)边缘触发(Edge Triggered, ET)

  • 仅当状态变化时触发一次 (如从不可读变为可读)。

  • 特点 :性能更高,但需确保一次性处理完所有数据,否则可能遗漏事件。

    使用条件 :

  • 文件描述符必须设置为 非阻塞模式 。

  • 需循环读取直到返回 EAGAIN 或 EWOULDBLOCK。

使用注意事项

  1. 非阻塞 I/O 配合 ET 模式:在 ET 模式下,必须将文件描述符设为非阻塞,否则可能因未读完数据导致线程阻塞。
  2. 线程安全:epoll 本身是线程安全的(Linux 2.6.27+),但多个线程同时调用 epoll_wait 时需注意同步。
  3. 连接管理:当连接关闭或出错时,需及时从 epoll 中删除对应的文件描述符。
    使用 epoll_data_t 保存连接的上下文信息(如 client_data)。
  4. 惊群问题(Thundering Herd)
    多进程/线程调用 epoll_wait 时,若有多个线程同时被唤醒,可能导致资源竞争。可通过以下方式缓解:使用互斥锁(mutex)、采用单线程 epoll_wait + 多线程任务队列的架构。

二、源码分析

void WebServer::eventListen()
{
    //网络编程基础步骤
    m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(m_listenfd >= 0);

    //优雅关闭连接
    if (0 == m_OPT_LINGER)
    {
        struct linger tmp = {0, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }
    else if (1 == m_OPT_LINGER)
    {
        struct linger tmp = {1, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }

    int ret = 0;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    address.sin_port = htons(m_port);

    int flag = 1;
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
    assert(ret >= 0);
    ret = listen(m_listenfd, 5);
    assert(ret >= 0);

    utils.init(TIMESLOT);

    //epoll创建内核事件表
    epoll_event events[MAX_EVENT_NUMBER];
    m_epollfd = epoll_create(5);
    assert(m_epollfd != -1);

    utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
    http_conn::m_epollfd = m_epollfd;

    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
    assert(ret != -1);
    utils.setnonblocking(m_pipefd[1]);
    utils.addfd(m_epollfd, m_pipefd[0], false, 0);

    utils.addsig(SIGPIPE, SIG_IGN);
    utils.addsig(SIGALRM, utils.sig_handler, false);
    utils.addsig(SIGTERM, utils.sig_handler, false);

    alarm(TIMESLOT);

    //工具类,信号和描述符基础操作
    Utils::u_pipefd = m_pipefd;
    Utils::u_epollfd = m_epollfd;
}

eventlisten函数的主要作用包含两个,首先是对套接字socket的设置,包括创造、绑定、相关属性设置等;其次,是对epoll多路复用的设置,比如添加事件等,在源码中没有直接使用相关的系统接口,而是对该系统调用进行了包装。

addfd()函数
主要作用就是添加事件到epoll中,主要用到的函数是epoll_ctr()

//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
    epoll_event event;
    event.data.fd = fd;

    if (1 == TRIGMode)
        event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    else
        event.events = EPOLLIN | EPOLLRDHUP;

    if (one_shot)
        event.events |= EPOLLONESHOT;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

addsig()函数
主要作用是为信号设置信号处理函数,主要用到的函数时sigaction()

//设置信号函数
void Utils::addsig(int sig, void(handler)(int), bool restart)
{
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = handler;
    if (restart)
        sa.sa_flags |= SA_RESTART;
    sigfillset(&sa.sa_mask);
    assert(sigaction(sig, &sa, NULL) != -1);
}

这几段代码的逻辑还是比较简单的,大家自行观看

你可能感兴趣的:(c++)