tinyWebServer 学习笔记——三、定时器处理非活跃链接

news2024/12/1 0:26:25

文章目录

  • 一、基础知识
    • 1. 概念
    • 2. API
    • 3. 信号处理机制
  • 二、代码解析
    • 1. 信号处理函数
    • 2. 信号通知逻辑
    • 3. 定时器
    • 4. 定时器容器
    • 5. 定时任务处理函数
    • 6. 使用定时器
  • 参考文献

一、基础知识

1. 概念

  • 非活跃:指客户端与服务器建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费;
  • 定时事件:指固定一段时间之后释放某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源;
  • 定时器:指利用结构体或其他形式,将多种定时器事件进行封装,一个定时器用于处理一个事件;
  • 定时器容器:使用某种容器将多个定时器组合起来,便于对定时事件统一管理,本项目中使用升序链表作为容器。
  • SIGALRM 信号:Linux 中提供了三种定时方法,本项目中使用 SIGALRM 信号,即利用 alarm 函数周期性地触发 SIGALRM 信号,信号处理函数利用管道通知主循环,主循环接收到信号后对升序链表上的定时器进行处理,若该段时间内没有数据交换,则关闭连接,释放资源;
  • 信号通知:Linux 下的信号采用异步处理机制,信号处理函数和当前进程是两条不同的执行路线,即当进程收到信号时,操作系统会中断进程,转而进入信号处理函数,完成后再关闭中断:
    • 为避免信号竞态发生,信号处理期间将不会再次触发它,为使信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕;
    • 当信号处理逻辑较复杂时,信号处理函数仅仅发送信号通知程序主循环,将信号对应的逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码,这样不会导致信号屏蔽太久;
  • 统一事件源:指将信号与其他事件一样被处理,即信号处理函数利用管道将信号传递给主循环,这样信号事件与其他文件描述符都可以通过 epoll 来监测;

2. API

  • sigaction 结构体:记录信号的处理方式,成员如下:
    • void (*sa_handler)(int); :指向信号处理函数的函数指针;
    • void (*sa_sigaction)(int, siginfo_t*, void*); :指向信号处理函数的函数指针,三个参数能够获得更详细的信息;
    • sigset_t sa_mask; :指定信号处理函数执行期间需要屏蔽的信号;
    • int sa_flags; :指定信号处理的行为:
      • SA_RESTART :使被信号打断的系统调用自动重新发起;
      • SA_NOCLDSTOP :使父进程在它子进程暂停或继续运行时不会受到 SIGCHLD 信号;
      • SA_NOCLDWAIT :使父进程在它子进程退出时不会受到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程;
      • SA_NODEFER :使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号;
      • SA_RESETHAND :信号处理之后重新设置为默认的处理方式;
      • SA_SIGINFO :使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数;
    • void (*sa_restorer)(void); :一般不使用;
  • int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    • signum :标识操作的信号;
    • act :标识对信号设置新的处理方式;
    • oldact :标识信号原来的处理方式;
    • 返回值 :0 表示成功,-1 表示错误;
  • int sigfillset(sigset_t *set); :用来将参数 set 信号集初始化,然后把所有信号加入到此信号集里;
  • #define SIGALRM 14 :由 alarm 系统调用产生 timer 时钟信号;
  • #define SIGTERM 15 :终端发送的终止信号;
  • unsigned int alarm(unsigned int seconds); :设置信号传送闹钟,即用来设置信号 SIGALRM 在经过参数 seconds 秒数后发送给目前的进程。如果未设置信号 SIGALRM 的处理函数,那么 alarm() 默认处理终止进程;
  • int socketpair(int dimain, int type, int protocol, int sv[2]); :创建双向管道;
  • ssize_t send(int sockfd, const void *buf, size_t len, int flags); :当套接字发送缓冲区变满时,send 通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回 EAGAIN 或者 EWOULDBLOCK 错误,此时可以调用 select 函数来监视何时可以发送数据;

3. 信号处理机制

1

流程图 [1]
  • 信号接收:由内核代理接收信号,并将其放入对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。此时信号还在队列中,进程尚不知道有信号到来;
  • 信号检测:当发现有新的信号后,对其进行处理:
    • 进程从内核态返回到用户态前进行信号检测;
    • 进程在内核态中,从睡眠状态被唤醒时进行信号检测;
    • 进程陷入内核态后,有两种情况会对信号进行检测;
  • 信号处理:
    • 调用处理函数之前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器,将其指向信号处理函数;
    • 进程返回到用户态,执行相应的信号处理函数;
    • 信号处理函数执行完毕后,进程返回内核态,检查是否还有其他信号未处理;
    • 如果所有信号都处理完成,将恢复内核栈(从用户栈拷贝回来),同时修改指令寄存器,将其指向中断前的运行位置,最后回到用户态继续执行进程;

二、代码解析

1. 信号处理函数

// 信号处理函数
void Utils::sig_handler(int sig)
{
    // 为保证函数的可重入性,保留原来的errno
    // 可重入性表示中断后再次进入该函数,环境变量与之前相同,不会丢失数据
    int save_errno = errno;
    int msg = sig;
    // 将信号值从管道写端写入,传输字符类型,而非整型
    send(u_pipefd[1], (char *)&msg, 1, 0);
    // 将原来的errno赋值为当前的errno
    errno = save_errno;
}

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

    // 信号处理函数中仅仅发送信号值,不做对应逻辑处理
    sa.sa_handler = handler;
    if (restart)
        sa.sa_flags |= SA_RESTART;

    // 将所有信号添加到信号集中
    sigfillset(&sa.sa_mask);

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

2. 信号通知逻辑

  • 创建管道;
  • 设置信号处理函数 SIGALRM 和 SIGTERM;
  • 利用 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)
                {
                    //传递字符,即ASCII码
                    switch (signals[i])
                    {
                    //这里是整型,对应ASCII码
                    case SIGALRM:
                    {
                        timeout = true;
                        break;
                    }
                    case SIGTERM:
                    {
                        stop_server = true;
                    }
                    }
                }
            }
        }
    }
}

3. 定时器

// 用户数据
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(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
    assert(user_data);

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

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

4. 定时器容器

// 升序计时器
class sort_timer_lst
{
public:
    sort_timer_lst();  // 初始化链表
    ~sort_timer_lst(); // 回收链表空间

    // 添加定时器,内部调用私有成员add_timer
    void add_timer(util_timer *timer);

    // 调整定时器,任务发生变化时,调整定时器在链表中的位置
    void adjust_timer(util_timer *timer);

    // 删除计时器
    void del_timer(util_timer *timer);

    // 定时任务处理函数
    void tick();

private:
    // 私有成员,被公有成员add_timer和adjust_time调用
    // 主要用于调整链表内部结点
    void add_timer(util_timer *timer, util_timer *lst_head);
    // 头尾结点
    util_timer *head;
    util_timer *tail;
};

// 初始化链表
sort_timer_lst::sort_timer_lst()
{
    head = NULL;
    tail = NULL;
}

// 常规销毁链表
sort_timer_lst::~sort_timer_lst()
{
    util_timer *tmp = head;
    while (tmp)
    {
        head = tmp->next;
        delete tmp;
        tmp = head;
    }
}

// 添加定时器,内部调用私有成员add_timer
void sort_timer_lst::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 sort_timer_lst::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 sort_timer_lst::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;
}

// 循环查找合适的插入位置
void sort_timer_lst::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;
    }
}

5. 定时任务处理函数

// 计时,并处理到期的计时器
void sort_timer_lst::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;
    }
}

6. 使用定时器

  • 浏览器与服务器连接时,创建连接对应的定时器,并将该定时器添加到链表上;
  • 处理异常事件时,执行定时事件,服务器关闭连接,并从链表上移除对应的定时器;
  • 处理定时信号时,将定时标志设置为 true ;
  • 处理读事件时,若某连接上发生读事件,将对应定时器向后移动,否则执行定时事件;
  • 处理写事件时,若服务器通过某连接给浏览器发送数据,将对应定时器向后移动,否则执行定时事件。
//定时处理任务,重新定时以不断触发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;
    }
}

参考文献

[1] 最新版Web服务器项目详解 - 07 定时器处理非活动连接(上)
[2] 最新版Web服务器项目详解 - 08 定时器处理非活动连接(下)

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

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

相关文章

第二章 数据的表示和运算

1.进位计数制 其他进制转十进制 二进制<——> 八进制&#xff0c;十六进制 (注意&#xff1a;小数部分也是从右往左算十进制——>任意进制&#xff08;整数部分&#xff09; 十进制——>任意进制&#xff08;小数部分&#xff09; 十进制转二进制&#xff08;拼凑…

【gitee流水线实现自动化部署】

首先进入自己的gitee仓库 创建流水线 配置基本信息 名称标识 事件监听 -----触发条件 主要是任务排编内 vue前端则选择node构建 这些就是字面意思 若无特殊需求 按照默认的即可 构建完之后添加新任务 主机部署 选择部署 主机部署 添加主机组 新建主机组 自主导入 之后配…

配置Git

1.安装Git git官网 2.配置Git 在点击桌面上的Git Bash快捷图标中输入&#xff1a; 配置用户名&#xff1a; git config --global user.name "username" //&#xff08; "username"是自己的账户名&#xff0c;&#xff09; 配置邮箱&#xff1a; git…

Mac终端主题配置

如果你不想安装item2这类第三方终端&#xff0c;可以试试我下面的步骤&#xff0c;先上效果图&#xff0c;如果感觉还符合你的胃口&#xff0c;可以继续读下去啦!!! 1.下载item2的主题安装包 https://github.com/mbadolato/iTerm2-Color-Schemes 2.解压缩&#xff0c;打开…

电脑断电文件丢失如何找回?给你支几招!

电脑断电文件丢失如何找回&#xff1f;我好不容易熬夜加班做的活动方案&#xff0c;正当将U盘文件转移到笔记本电脑的时候&#xff0c;没有注意笔记本的电量&#xff0c;在转移数据的过程中突然断电了。我的电脑一下子就“熄”了&#xff0c;方案都没来得及保存。这真是一个悲剧…

06. git关联远程仓库

大家好&#xff0c;前面几节&#xff0c;我们用很长的篇幅介绍了git本地使用过程中的一些基本命令&#xff0c;本节开始&#xff0c;我们介绍通过远程仓库多人协作的时候&#xff0c;基本操作以及遇见的问题。 本节内容预告&#xff1a; 1、github 与gitlab简介 2、git本地连接…

【软考高项】项目范围管理中的需求跟踪矩阵说明

文章目录 需求跟踪矩阵的创建角色步骤 需求跟踪矩阵变更角色步骤 需求跟踪矩阵是把产品需求从其来源连接到能满足需求的可交付成果的一种表格。使用需求跟踪矩阵&#xff0c;把每个需求与业务目标或项目目标联系起来&#xff0c;有助于确保每个需求都具有业务价值。 需求跟踪矩…

从零开始Vue3+Element Plus的后台管理系统(三)——按需自动引入组件和unplugin-vue-components

按需导入Element Plus遇到页面卡顿问题 本项目使用Element Plus的方式是按需自动导入 首先安装unplugin-vue-components 和 unplugin-auto-import这两款插件 npm install -D unplugin-vue-components unplugin-auto-import然后把下列代码插入到你的 Vite 配置文件中 Vite# …

Salesforce Experience Cloud_体验云顾问认证考试-备考攻略 (内含模拟练习题)

Salesforce Experience Cloud顾问认证专为具有Experiences应用程序实施和咨询经验的顾问设计的&#xff0c;适用于使用Experience平台的声明性自定义功能展示其在设计、配置、构建和实施Salesforce Experience应用程序方面的技能和知识的备考者。 备考者需要有6个月的Experien…

周赛345(模拟、分类讨论、DFS求完全联通分量)

文章目录 周赛345[2682. 找出转圈游戏输家](https://leetcode.cn/problems/find-the-losers-of-the-circular-game/)模拟 [2683. 相邻值的按位异或](https://leetcode.cn/problems/neighboring-bitwise-xor/)方法一&#xff1a;分类讨论&#xff08;反向思考&#xff09;方法二…

Android 调用TTS语音引擎过程及问题记录

Android 调用TTS引擎过程及问题记录 前言 背景是需要在华为平板上部署一个能够进行相关中文语音提示的APP&#xff0c;华为系统为鸿蒙3.0&#xff0c;对应Android API 12. Android 调用TTS引擎 调用TTS引擎之前&#xff0c;首先要确认自己的设备中是否安装了相关的文本转语音…

从《水浒传》看项目管理

水浒传和项目管理&#xff0c;这两个看似毫不相关的话题&#xff0c;其实有着惊人的相似之处。你没听错&#xff0c;就是水浒传&#xff01;这个充满了江湖义气和刀光剑影的故事&#xff0c;竟然能给我们现代人提供一些关于项目管理的启示。别怀疑&#xff0c;跟我一起来看看吧…

nginx liunx最新版本安装flask部署

一、nginx安装 1.进入Nginx官网的资源下载页&#xff1a;http://nginx.org/en/download.html 2.下载nginx-1.22.1.tar.gz&#xff0c; 3解压&#xff1a; tar -zxvf nginx-1.22.1.tar.gz解压完成后会在当前目录下得到一个新的nginx文件夹 4.终端进入nginx文件夹目录&#x…

C++ -- AVL树插入实现和测试

文章目录 1. AVL树概念2. AVL树满足性质3. AVL节点结构4. 插入操作5. 检测6. 完整代码 1. AVL树概念 AVL树就是自平衡二叉查找树&#xff0c;为了解决二叉树退化为单链表&#xff0c;使得增删查改时间度为O(N)&#xff0c;这里采用控制平衡策略达到效率是O(logN)。 2. AVL树满足…

Golang结构体入门

目录 结构体基础 结构体示例 为结构体定义方法 组合结构体 嵌套结构体 指针结构体 匿名字段 面向对象 封装 继承 多态 结构体基础 1.结构体是值类型&#xff1a;在Go语言中&#xff0c;结构体是一种值类型&#xff0c;与数组和基本数据类型一样。当结构体被赋值给一…

【MySQL】MySQL批量插入测试数据的几种方式

文章目录 前言一、表二、使用函数生成设置允许创建函数产生随机字符串产生随机数字 三、创建存储过程插入角色表插入用户表 四、执行存储过程小结五、使用 Navicat自带的数据生成 前言 在开发过程中我们不管是用来测试性能还是在生产环境中页面展示好看一点, 又或者学习验证某…

企业为什么要数字化转型?

数字化转型是使用数字技术从根本上改变企业运营方式并为客户创造价值的过程。企业进行数字化转型&#xff0c;常见因素包括&#xff1a; 提高效率&#xff1a;数字化转型可以简化流程并自动执行重复性任务&#xff0c;从而减少执行这些任务所需的时间和精力。可以节省成本并提高…

网页三剑客之 Javascript

JavaScript (简称 JS) 是世界上最流行的编程语言之一是一个脚本语言, 通过解释器运行主要在客户端(浏览器)上运行, 现在也可以基于 node.js 在服务器端运行 其实&#xff0c;语言都是触类旁通的&#xff0c;我们学过了C语言和JavaSE&#xff0c;那么学起其他任何语言都不难&a…

CSS 中的常用属性(图文详解版)

CSS 中的常用属性 &#x1f50e;引入方式&#x1f50e;CSS 选择器&#x1f50e;字体&#x1f50e;文本&#x1f50e;背景&#x1f50e;圆角矩形&#x1f50e;元素的显示模式&#x1f50e;CSS 盒模型&#x1f50e;弹性布局&#x1f50e;结尾 CSS 中的属性有很多 本文列举了一些较…

Spark大数据处理讲课笔记4.4 Spark SQL数据源 - JSON数据集

文章目录 零、本讲学习目标一、读取JSON文件概述二、读取JSON文件案例演示&#xff08;一&#xff09;创建JSON文件并上传到HDFS&#xff08;二&#xff09;读取JSON文件&#xff0c;创建临时表&#xff0c;进行关联查询1、读取user.json文件&#xff0c;创建临时表t_user2、读…