定时器处理非活动连接(五)

news2024/10/6 2:26:29

1 基础知识

非活跃,是指客户端(这里是浏览器)与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。

非活跃,是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源。

定时器,是指利用结构体或其他形式,将多种定时事件进行封装起来。具体的,这里只涉及一种定时事件,即定期检测非活跃连接,这里将该定时事件与连接资源封装为一个结构体定时器。

定时器容器,是指使用某种容器类数据结构,将上述多个定时器组合起来,便于对定时事件统一管理。具体的,项目中使用升序链表将所有定时器串联组织起来。

1.1 整体概述

本项目中,服务器主循环为每一个连接创建一个定时器,并对每个连接进行定时。另外,利用升序时间链表容器将所有定时器串联起来,若主循环接收到定时通知,则在链表中依次执行定时任务。

Linux下提供了三种定时的方法:

  1. socket选项SO_RECVTIMEO和SO_SNDTIMEO
  2. SIGALRM信号
  3. I/O复用系统调用的超时参数

三种方法没有一劳永逸的应用场景,也没有绝对的优劣。由于项目中使用的是SIGALRM信号,这里仅对其进行介绍,另外两种方法可以查阅游双的Linux高性能服务器编程 第11章 定时器

具体的,利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。

从上面的简要描述中,可以看出定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计与定时任务的处理。

1.2 本文内容

本节将介绍定时方法与信号通知流程,具体的涉及到基础API、信号通知流程和代码实现。

基础API,描述sigaction结构体、sigaction函数、sigfillset函数、SIGALRM信号、SIGTERM信号、alarm函数、socketpair函数、send函数。

信号通知流程,介绍统一事件源和信号处理机制。

代码实现,结合代码对信号处理函数的设计与使用进行详解。

1.3 基础API

为了更好的源码阅读体验,这里提前对代码中使用的一些API进行简要介绍,更丰富的用法可以自行查阅资料。

sigaction结构体

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}
  1. sa_handler是一个函数指针,指向信号处理函数
  2. sa_sigaction同样是信号处理函数,有三个参数,可以获得关于信号更详细的信息
  3. sa_mask用来指定在信号处理函数执行期间需要被屏蔽的信号
  4. sa_flags用于指定信号处理的行为
    • SA_RESTART,使被信号打断的系统调用自动重新发起
    • SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
    • SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
    • SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
    • SA_RESETHAND,信号处理之后重新设置为默认的处理方式
    • SA_SIGINFO,使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
  5. sa_restorer一般不使用

sigaction函数

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  1. signum表示操作的信号。
  2. act表示对信号设置新的处理方式。
  3. oldact表示信号原来的处理方式。
  4. 返回值,0 表示成功,-1 表示有错误发生。

sigfillset函数

#include <signal.h>
int sigfillset(sigset_t *set);

用来将参数set信号集初始化,然后把所有的信号加入到此信号集里。

SIGALRM、SIGTERM信号

#define SIGALRM  14     //由alarm系统调用产生timer时钟信号
#define SIGTERM  15     //终端发送的终止信号

alarm函数

#include <unistd.h>;
unsigned int alarm(unsigned int seconds);

设置信号传送闹钟,即用来设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程。如果未设置信号SIGALRM的处理函数,那么alarm()默认处理终止进程.

socketpair函数

在linux下,使用socketpair函数能够创建一对套接字进行通信,项目中使用管道通信。

#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
  1. domain表示协议族,PF_UNIX或者AF_UNIX
  2. type表示协议,可以是SOCK_STREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP
  3. protocol表示类型,只能为0
  4. sv[2]表示套节字柄对,该两个句柄作用相同,均能进行读写双向操作
  5. 返回结果, 0为创建成功,-1为创建失败

send函数

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

当套接字发送缓冲区变满时,send通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回EAGAIN或者EWOULDBLOCK错误,此时可以调用select函数来监视何时可以发送数据。

1.4 信号通知流程

Linux下的信号采用的异步处理机制,信号处理函数和当前进程是两条不同的执行路线。具体的,当进程收到信号时,操作系统会中断进程当前的正常流程,转而进入信号处理函数执行操作,完成后再返回中断的地方继续执行。

为避免信号竞态现象发生,信号处理期间系统不会再次触发它。所以,为确保该信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕。

一般的信号处理函数需要处理该信号对应的逻辑,当该逻辑比较复杂时,信号处理函数执行时间过长,会导致信号屏蔽太久。

这里的解决方案是,信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码。

统一事件源

统一事件源,是指将信号事件与其他事件一样被处理。

具体的,信号处理函数使用管道将信号传递给主循环,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值,使用I/O复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll来监测,从而实现统一处理。

信号处理机制

每个进程之中,都有存着一个表,里面存着每种信号所代表的含义,内核通过设置表项中每一个位来标识对应的信号类型。

  1. 信号的接收
    • 接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。
  2. 信号的检测
    • 进程从内核态返回到用户态前进行信号检测
    • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
    • 进程陷入内核态后,有两种场景会对信号进行检测:
    • 当发现有新信号时,便会进入下一步,信号的处理。
  3. 信号的处理
    • 内核 )信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。
    • 用户 )接下来进程返回到用户态中,执行相应的信号处理函数。
    • 内核 )信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。
    • 用户 )如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。

至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。

1.5 代码分析

信号处理函数

自定义信号处理函数,创建sigaction结构体变量,设置信号函数。

//信号处理函数
void sig_handler(int sig)
{
    //为保证函数的可重入性,保留原来的errno
    //可重入性表示中断后再次进入该函数,环境变量与之前相同,不会丢失数据
    int save_errno = errno;
    int msg = sig;

    //将信号值从管道写端写入,传输字符类型,而非整型
    send(pipefd[1], (char *)&msg, 1, 0);

    //将原来的errno赋值为当前的errno
    errno = save_errno;
}

信号处理函数中仅仅通过管道发送信号值,不处理信号对应的逻辑,缩短异步执行时间,减少对主程序的影响。

//设置信号函数
void addsig(int sig, void(handler)(int), bool restart = true)
{
    //创建sigaction结构体变量     
    //创建sigaction结构体变量     struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));

    //信号处理函数中仅仅发送信号值,不做对应逻辑处理     sa
    //信号处理函数中仅仅发送信号值,不做对应逻辑处理     sa.sa_handler = handler;
    if (restart)
        sa.sa_flags |= SA_RESTART;
    //将所有信号添加到信号集中     
    //将所有信号添加到信号集中     sigfillset(&sa.sa_mask);

    //执行sigaction函数
    assert(sigaction(sig, &sa, NULL) != -1);
}

项目中设置信号函数,仅关注SIGTERM和SIGALRM两个信号。

信号通知逻辑

  1. 创建管道,其中管道写端写入信号值,管道读端通过I/O复用系统监测读事件
  2. 设置信号处理函数SIGALRM(时间到了触发)和SIGTERM(kill会触发,Ctrl+C)
    • 通过struct sigaction结构体和sigaction函数注册信号捕捉函数
    • 在结构体的handler参数设置信号处理函数,具体的,从管道写端写入信号的名字
  3. 利用I/O复用系统监听管道读端文件描述符的可读事件 信
  4. 利用I/O复用系统监听管道读端文件描述符的可读事件 信息值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码

代码分析

//创建管道套接字
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);

//设置管道写端为非阻塞,为什么写端要非阻塞?
setnonblocking(pipefd[1]);

//设置管道读端为ET非阻塞
addfd(epollfd, pipefd[0], false);

//传递给主循环的信号值,这里只关注SIGALRM和SIGTERM
addsig(SIGALRM, sig_handler, false);
addsig(SIGTERM, sig_handler, false);

//循环条件
bool stop_server = false;

//超时标志
bool timeout = false;

//每隔TIMESLOT时间触发SIGALRM信号
alarm(TIMESLOT);

while (!stop_server)
{
    //监测发生事件的文件描述符
    int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
    if (number < 0 && errno != EINTR)
    {
        break;
    }

    //轮询文件描述符
    for (int i = 0; i < number; i++)
    {
        int sockfd = events[i].data.fd;

    //管道读端对应文件描述符发生读事件
        if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
        {
            int sig;
            char signals[1024];

            //从管道读端读出信号值,成功返回字节数,失败返回-1
            //正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
            ret = recv(pipefd[0], signals, sizeof(signals), 0);
            if (ret == -1)
            {
                // handle the error
                continue;
            }
            else if (ret == 0)
            {
                continue;
            }
            else
            {
                //处理信号值对应的逻辑
                for (int i = 0; i < ret; ++i)
                {
                    //这里面明明是字符
                    switch (signals[i])
                    {
                    //这里是整型
                    case SIGALRM:
                    {
                        timeout = true;
                        break;
                    }
                    case SIGTERM:
                    {
                        stop_server = true;
                    }
                    }
                }
            }
        }
    }
}

为什么管道写端要非阻塞?

send是将信息发送给套接字缓冲区,如果缓冲区满了,则会阻塞,这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞。

没有对非阻塞返回值处理,如果阻塞是不是意味着这一次定时事件失效了?

是的,但定时事件是非必须立即处理的事件,可以允许这样的情况发生。

管道传递的是什么类型?switch-case的变量冲突?

信号本身是整型数值,管道中传递的是ASCII码表中整型数值对应的字符。

switch的变量一般为字符或整型,当switch的变量为字符时,case中可以是字符,也可以是字符对应的ASCII码。

2 定时器

定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计、定时任务的处理。

本篇对第二部分进行介绍,具体的涉及到定时器设计、容器设计、定时任务处理函数和使用定时器。

定时器设计,将连接资源和定时事件等封装起来,具体包括连接资源、超时时间和回调函数,这里的回调函数指向定时事件。

定时器容器设计,将多个定时器串联组织起来统一处理,具体包括升序链表设计。

定时任务处理函数,该函数封装在容器类中,具体的,函数遍历升序链表容器,根据超时时间,处理对应的定时器。

代码分析-使用定时器,通过代码分析,如何在项目中使用定时器。

2.1 定时器设计

项目中将连接资源、定时事件和超时时间封装为定时器类,具体的,

  1. 连接资源包括客户端套接字地址、文件描述符和定时器
  2. 定时事件为回调函数,将其封装起来由用户自定义,这里是删除非活动socket上的注册事件,并关闭
  3. 定时器超时时间 = 浏览器和服务器连接时刻 + 固定时间(TIMESLOT),可以看出,      定时器使用绝对时间作为超时值,这里alarm设置为5秒,连接超时为15秒。
//连接资源结构体成员需要用到定时器类
//需要前向声明
class util_timer;

//连接资源
struct client_data
{
    //客户端socket地址
    sockaddr_in address;

    //socket文件描述符
    int sockfd;

    //定时器
    util_timer* timer;
};

//定时器类
class util_timer
{
public:
    util_timer() : prev( NULL ), next( NULL ){}

public:
    //超时时间
    time_t expire; 
    //回调函数
    void (*cb_func)( client_data* );
    //连接资源
    client_data* user_data;
    //前向定时器
    util_timer* prev;
    //后继定时器
    util_timer* next;
};

定时事件,具体的,从内核事件表删除事件,关闭文件描述符,释放连接资源。

//定时器回调函数
void cb_func(client_data *user_data)
{
    //删除非活动连接在socket上的注册事件
    epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
    assert(user_data);

    //关闭文件描述符
    close(user_data->sockfd);

    //减少连接数
    http_conn::m_user_count--;
}

2.2 定时器容器设计

项目中的定时器容器为带头尾结点的升序双向链表,具体的为每个连接创建一个定时器,将其添加到链表中,并按照超时时间升序排列。执行定时任务时,将到期的定时器从链表中删除。

从实现上看,主要涉及双向链表的插入,删除操作,其中添加定时器的事件复杂度是O(n),删除定时器的事件复杂度是O(1)。

升序双向链表主要逻辑如下,具体的,

  1. 创建头尾节点,其中头尾节点没有意义,仅仅统一方便调整
    • add_timer函数,将目标定时器添加到链表中,添加时按照升序添加
    • 若当前链表中只有头尾节点,直接插入
    • 否则,将定时器按升序插入
  2. adjust_timer函数,当定时任务发生变化,调整对应定时器在链表中的位置
    • 客户端在设定时间内有数据收发,则当前时刻对该定时器重新设定时间,这里只是往后延长超时时间
    • 被调整的目标定时器在尾部,或定时器新的超时值仍然小于下一个定时器的超时,不用调整
    • 否则先将定时器从链表取出,重新插入链表
  3. del_timer函数将超时的定时器从链表中删除
    • 常规双向链表删除结点
//定时器容器类
class sort_timer_lst
{
public:
    sort_timer_lst() : head( NULL ), tail( NULL ) {}
    //常规销毁链表
    ~sort_timer_lst()
    {
        util_timer* tmp = head;
        while( tmp )
        {
            head = tmp->next;
            delete tmp;
            tmp = head;
        }
    }

    //添加定时器,内部调用私有成员add_timer
    void add_timer( util_timer* timer )
    {
        if( !timer )
        {
            return;
        }
        if( !head )
        {
            head = tail = timer;
            return; 
        }

        //如果新的定时器超时时间小于当前头部结点
        //直接将当前定时器结点作为头部结点
        if( timer->expire < head->expire )
        {
            timer->next = head;
            head->prev = timer;
            head = timer;
            return;
        }

        //否则调用私有成员,调整内部结点
        add_timer( timer, head );
    }

    //调整定时器,任务发生变化时,调整定时器在链表中的位置
    void adjust_timer( util_timer* timer )
    {
        if( !timer )
        {
            return;
        }
        util_timer* tmp = timer->next;

        //被调整的定时器在链表尾部
        //定时器超时值仍然小于下一个定时器超时值,不调整
        if( !tmp || ( timer->expire < tmp->expire ) )
        {
            return;
        }

        //被调整定时器是链表头结点,将定时器取出,重新插入
        if( timer == head )
        {
            head = head->next;
            head->prev = NULL;
            timer->next = NULL;
            add_timer( timer, head );
        }

        //被调整定时器在内部,将定时器取出,重新插入
        else
        {
            timer->prev->next = timer->next;
            timer->next->prev = timer->prev;
            add_timer( timer, timer->next );
        }
    }

    //删除定时器
    void del_timer( util_timer* timer )
    {
        if( !timer )
        {
            return;
        }

        //链表中只有一个定时器,需要删除该定时器
        if( ( timer == head ) && ( timer == tail ) )
        {
            delete timer;
            head = NULL;
            tail = NULL;
            return;
        }

        //被删除的定时器为头结点
        if( timer == head )
        {
            head = head->next;
            head->prev = NULL;
            delete timer;
            return;
        }

        //被删除的定时器为尾结点
        if( timer == tail )
        {
            tail = tail->prev;
            tail->next = NULL;
            delete timer;
            return;
        }

        //被删除的定时器在链表内部,常规链表结点删除
        timer->prev->next = timer->next;
        timer->next->prev = timer->prev;
        delete timer;
    }
    private:
    //私有成员,被公有成员add_timer和adjust_time调用
    //主要用于调整链表内部结点
    void add_timer( util_timer* timer, util_timer* lst_head )
    {
        util_timer* prev = lst_head;
        util_timer* tmp = prev->next;

        //遍历当前结点之后的链表,按照超时时间找到目标定时器对应的位置,常规双向链表插入操作
        while( tmp )
        {
            if( timer->expire < tmp->expire )
            {
                prev->next = timer;
                timer->next = tmp;
                tmp->prev = timer;
                timer->prev = prev;
                break;
            }
            prev = tmp;
            tmp = tmp->next;
        }

        //遍历完发现,目标定时器需要放到尾结点处
        if( !tmp )
        {
            prev->next = timer;
            timer->prev = prev;
            timer->next = NULL;
            tail = timer;
        }

    }
    private:
    //头尾结点
    util_timer* head;
    util_timer* tail;
};

2.3 定时任务处理函数

使用统一事件源,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。

具体的逻辑如下,

  1. 遍历定时器升序链表容器,从头结点开始依次处理每个定时器,直到遇到尚未到期的定时器
  2. 若当前时间小于定时器超时时间,跳出循环,即未找到到期的定时器
  3. 若当前时间大于定时器超时时间,即找到了到期的定时器,执行回调函数,然后将它从链表中删除,然后继续遍历
//定时任务处理函数
void tick()
{
    if( !head )
    {
        return;
    }

    //获取当前时间
    time_t cur = time( NULL );
    util_timer* tmp = head;

    //遍历定时器链表
    while( tmp )
    {
        //链表容器为升序排列
        //当前时间小于定时器的超时时间,后面的定时器也没有到期
        if( cur < tmp->expire )
        {
            break;
        }

        //当前定时器到期,则调用回调函数,执行定时事件
        tmp->cb_func( tmp->user_data );

        //将处理后的定时器从链表容器中删除,并重置头结点
        head = tmp->next;
        if( head )
        {
            head->prev = NULL;
        }
        delete tmp;
        tmp = head;
    }
}

2.4 代码分析-如何使用定时器

服务器首先创建定时器容器链表,然后用统一事件源将异常事件,读写事件和信号事件统一处理,根据不同事件的对应逻辑使用定时器。

具体的:

  1. 浏览器与服务器连接时,创建该连接对应的定时器,并将该定时器添加到链表上
  2. 处理异常事件时,执行定时事件,服务器关闭连接,从链表上移除对应定时器
  3. 处理定时信号时,将定时标志设置为true
  4. 处理读事件时,若某连接上发生读事件,将对应定时器向后移动,否则,执行定时事件
  5. 处理写事件时,若服务器通过某连接给浏览器发送数据,将对应定时器向后移动,否则,执行定时事件
//定时处理任务,重新定时以不断触发SIGALRM信号
void timer_handler()
{
    timer_lst.tick();
    alarm(TIMESLOT);
}

//创建定时器容器链表
static sort_timer_lst timer_lst;

//创建连接资源数组
client_data *users_timer = new client_data[MAX_FD];

//超时默认为False
bool timeout = false;

//alarm定时触发SIGALRM信号
alarm(TIMESLOT);

while (!stop_server)
{
    int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
    if (number < 0 && errno != EINTR)
    {
        break;
    }

    for (int i = 0; i < number; i++)
    {
        int sockfd = events[i].data.fd;

        //处理新到的客户连接
        if (sockfd == listenfd)
        {
            //初始化客户端连接地址
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address);
            //该连接分配的文件描述符
            int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);

            //初始化该连接对应的连接资源
            users_timer[connfd].address = client_address;
            users_timer[connfd].sockfd = connfd;

            //创建定时器临时变量
            util_timer *timer = new util_timer;
            //设置定时器对应的连接资源
            timer->user_data = &users_timer[connfd];
            //设置回调函数
            timer->cb_func = cb_func;

            time_t cur = time(NULL);
            //设置绝对超时时间
            timer->expire = cur + 3 * TIMESLOT;
            //创建该连接对应的定时器,初始化为前述临时变量
            users_timer[connfd].timer = timer;
            //将该定时器添加到链表中
            timer_lst.add_timer(timer);
        }
        //处理异常事件
        else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
        {
            //服务器端关闭连接,移除对应的定时器
            cb_func(&users_timer[sockfd]);

            util_timer *timer = users_timer[sockfd].timer;
            if (timer)
            {
                timer_lst.del_timer(timer);
            }
        }
        //处理定时器信号
        else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
        {
            //接收到SIGALRM信号,timeout设置为True
        }

        //处理客户连接上接收到的数据
        else if (events[i].events & EPOLLIN)
        {
            //创建定时器临时变量,将该连接对应的定时器取出来
            util_timer *timer = users_timer[sockfd].timer;
            if (users[sockfd].read_once())
            {
                //若监测到读事件,将该事件放入请求队列
                pool->append(users + sockfd);

                //若有数据传输,则将定时器往后延迟3个单位
                //对其在链表上的位置进行调整
                if (timer)
                {
                    time_t cur = time(NULL);
                    timer->expire = cur + 3 * TIMESLOT;
                    timer_lst.adjust_timer(timer);
                }
            }
            else
            {
                //服务器端关闭连接,移除对应的定时器
                cb_func(&users_timer[sockfd]);
                if (timer)
                {
                    timer_lst.del_timer(timer);
                }
            }
        }
       else if (events[i].events & EPOLLOUT)
       {
           util_timer *timer = users_timer[sockfd].timer;
           if (users[sockfd].write())
           {
                //若有数据传输,则将定时器往后延迟3个单位
                //并对新的定时器在链表上的位置进行调整
                if (timer)
                {
                    time_t cur = time(NULL);
                    timer->expire = cur + 3 * TIMESLOT;
                    timer_lst.adjust_timer(timer);
                }
            }
            else
            {
                //服务器端关闭连接,移除对应的定时器
                cb_func(&users_timer[sockfd]);
                if (timer)
                {
                    timer_lst.del_timer(timer);
                }
            }
       }
    }
    //处理定时器为非必须事件,收到信号并不是立马处理
    //完成读写事件后,再进行处理
    if (timeout)
    {
        timer_handler();
        timeout = false;
    }
}

连接资源中的address是不是有点鸡肋?

确实如此,项目中虽然对该变量赋值,但并没有用到。类似的,可以对比HTTP类中address属性,只在日志输出中用到。

但不能说这个变量没有用,因为我们可以找到客户端连接的ip地址,用它来做一些业务,比如通过ip来判断是否异地登录等等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/763198.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习率的选择

学会画图去看学习率是否符合 梯度下降算法收敛所需要的迭代次数根据模型的不同而不同&#xff0c;我们不能提前预知&#xff0c;我们可以绘制迭代次数和代价函数的值的图表来观测算法在何时趋于收敛。 -自动测试方法 也有一些自动测试是否收敛的方法&#xff0c;例如将代价函数…

python_day9_继承

继承 单继承 class Phone:id Noneproducer "HW"def call_4g(self):print("4g通话")class Phone2023(Phone):face_id "1001"def call_45g(self):print("4.5g")phone Phone2023() print(phone.producer) phone.call_4g() phone.ca…

Leetcode-每日一题【109.有序链表转换二叉搜索树】

题目 给定一个单链表的头节点 head &#xff0c;其中的元素 按升序排序 &#xff0c;将其转换为高度平衡的二叉搜索树。 本题中&#xff0c;一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 1。 示例 1: 输入: head [-10,-3,0,5,9]输出: [0,-3,9,-…

【Linux基础】gcc编译器

(꒪ꇴ꒪ ),hello我是祐言博客主页&#xff1a;C语言基础,Linux基础,软件配置领域博主&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff01;送给读者的一句鸡汤&#x1f914;&#xff1a;集中起来的意志可以击穿顽石!作者水平很有限&#xff0c;如果发现错误&#x…

【IMX6ULL - psplash应用】修改uboot启动阶段的背景颜色

文件路径&#xff1a;psplash/psplash-colors.h 修改框起来的这两个宏定义就可以改变底部颜色了&#xff0c;参数代表是RGB的16进制颜色

Linux 支持 U盘 NTFS 文件系统格式

Linux 支持 U盘 NTFS 文件系统格式 1、在线&#xff08;可以连接外网&#xff09;方式&#xff1a; 1.1、RHEL/CentOS/Anolis/openEuler yum install ntfs-3g1.2、Debian/Ubuntu/Deepin apt install ntfs-3g2、离线方式 下载 ntfs-3g 安装包&#xff0c;离线环境安装 ntfs…

算法通关村第一关--链表青铜挑战笔记

算法通关村第一关–链表青铜挑战笔记 开始时间&#xff1a;2023年7月16日20:45:26 链表 什么是链表&#xff0c;链表是一种通过指针串联在一起的线性结构&#xff0c;每一个节点由两部分组成&#xff0c;一个是数据域一个是指针域&#xff08;存放指向下一个节点的指针&#x…

解决通过request.getParam()方法获取到的值为null的问题~

我们想获取前端表单提交的数据&#xff0c;并在控制台输出&#xff0c;但如下所示控制台输出的值均为null 原因在于&#xff1a; 表单中的name与getParam方法中名称不一致的问题 如下所示为表单内容&#xff1a; 而在getParam方法中我们是根据username和password获取的&#…

【LLM】Langchain使用[四](评估、代理)

note 评估目的&#xff1a;检验LLM是否达到验收标准&#xff1b;分析改动对于LLM应用性能的影响 思路&#xff1a;利用语言模型和链&#xff0c;辅助评估 代理&#xff1a; 代理能方便地将LLM连接自己的信息来源&#xff08;数据&#xff09;、API等PythonREPLTool工具&#x…

WSL2安装CUDA

WSL2安装CUDA 在WSL里面不用安装显卡驱动&#xff0c;显卡驱动必须要提前在Windows系统里面安装好&#xff0c;安装好后相应库文件会自动映射到WSL里面 进入网站 找到自己需要的版本&#xff0c;我这里是CUDA 11.7.1&#xff0c;点击进入详细页面 依次选择 L i n u x → x 86…

IP扫描工具

什么是高级 IP 扫描仪 高级 IP 扫描程序是网络中必不可少的工具&#xff0c;使管理员能够跟踪网络地址空间。在提供要扫描的 IP 地址范围时&#xff0c;高级 IP 扫描程序会借助网络扫描协议按顺序检查该范围内的每个 IP&#xff0c;扫描后&#xff0c;高级 IP 扫描程序工具可查…

【MongoDB实战】数据备份与恢复

场景&#xff1a; 需求&#xff1a; 解决方案&#xff1a; 步骤&#xff1a; Stage 1&#xff1a;【生产环境】修改备份文件映射 Stage 2&#xff1a;【生产环境】重新构建mongodb Stage 3&#xff1a;【客户环境】修改备份文件映射&#xff0c;同 Stage 1 Stage 4&#…

python接口自动化(四十)- logger 日志 - 下(超详解)

简介 按照上一篇的计划&#xff0c;这一篇给小伙伴们讲解一下&#xff1a;&#xff08;1&#xff09;多模块使用logging&#xff0c;&#xff08;2&#xff09;通过文件配置logging模块&#xff0c;&#xff08;3&#xff09;自己封装一个日志&#xff08;logging&#xff09;类…

Stable Diffusion配置要求,显卡推荐

Stable Diffusion 是一款流行的人工智能图像生成器&#xff0c;您可以在自己的 PC 上运行。但是运行Stable Diffusion的最低规格是多少&#xff0c;哪些组件最重要&#xff1f; Stable Diffusion需要什么 PC 硬件&#xff1f; Stable Diffusion最关键的一个组件是显卡 (GPU)。…

LaTex 中的Beamer使用

LaTex 中的Beamer使用 Beamer beamer 中通过frame 来控制每一页的内容&#xff0c;其与编写常用的LaTex文稿基本没有区别 titlepage \title[Short Title]{My Presentation} \subtitle{An Introduction to LaTeX Beamer} \author{River Chandler} \institute{Sichuan Universi…

国科大杭州高等研究院

2021年底的我还没决定开始考研 &#xff0c;过完年才确定开始考研&#xff0c;开学以后才开始学习&#xff0c;此时距离22年底考试时间差不多十个月的样子。 2022年开始正式学习&#xff0c;2 2 408&#xff0c;对于我来说内容挺多挺难的&#xff0c;我英语不太好&#xff0c;以…

懒人自动化生成e2e测试文件:JSON => playwright

前言 本工具实现的是&#xff1a;使用简单的 JSON 配置&#xff0c;生成可执行的 playwright UI 测试文件。 然后通过项目内已经配置好的 playwright 配置实现 UI 测试。 工具工作流程&#xff1a; 期望达到的目的是&#xff1a; ✅ 基础页面&#xff08;进入页面&#xff0…

加密保SSL超安通配版

今天收到几家公司网络IT经理询问&#xff1a;“要买“加密保SSL超安通配版”问有没有&#xff1f;” 这里特别强调一下&#xff01; “加密保SSL超安通配版”这不是国产SSL证书&#xff0c;不是国产SSL证书&#xff0c;属于套牌PKI类型非CA机构官方产品&#xff01; 这是经销…

cuda_11.6.1_510.47.03_linux.run

cuda_11.6.1_510.47.03_linux.run Installing the latest CUDA toolkit cuda_11.6.1_510.47.03_linux.run Download Installer for Linux Ubuntu 20.04 x86_64 cuda_11.6.1_510.47.03_linux.run

【Spring】Spring更简单的读取和存储对象---使用注解

目录 1.Spring的存储对象------存储Bean对象 1.前置工作&#xff0c;配置扫描路径 2.添加注解存储Bean对象 1.Controller&#xff08;控制器存储&#xff09; 2.service&#xff08;服务存储&#xff09; 3.Repository&#xff08;仓库存储&#xff09; 4.Component&…