深入理解Reactor模型的原理与应用

1、什么是Reactor模型

        Reactor意思是“反应堆”,是一种事件驱动机制。

        和普通函数调用的不同之处在于:应用程序不是主动的调用某个 API 完成处理,而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor 上,如果相应的时间发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。

        对于刚开始接触这个机制,个人感觉翻译成“感应器”可能会更好理解一点,因为注册在Reactor上的函数就像感应器一样,只要有事件到达,就会触发它开始工作。

        Reactor 模式是编写高性能网络服务器的必备技术之一。


2、Reactor模型的优点

  • 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的;
  • 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
  • 可扩展性强,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源;
  • 可复用性高,reactor 框架本身与具体事件处理逻辑无关,具有很高的复用性;
           Reactor 模型开发效率上比起直接使用 IO 复用要高,它通常是单线程的,设计目标是希望单线程使用一颗 CPU 的全部资源。
            优点即每个事件处理中很多时候可以不考虑共享资源的互斥访问。可是缺点也是明显的,现在的硬件发展,已经不再遵循摩尔定律,CPU 的频率受制于材料的限制不再有大的提升,而改为是从核数的增加上提升能力,当程序需要使用多核资源时,Reactor 模型就会悲剧 , 为什么呢?
            如果程序业务很简单,例如只是简单的访问一些提供了并发访问的服务,就可以直接开启多个反应堆,每个反应堆对应一颗 CPU 核心,这些反应堆上跑的请求互不相关,这是完全可以利用多核的。例如 Nginx 这样的 http 静态服务器。

3、通过对网络编程(epoll)代码的优化,深入理解Reactor模型

1、epoll的普通版本,根据fd类型(listen_fd和client_fd)分为两大类处理。

        如果是listen_fd,调用accept处理连接请求;

        如果是client_fd,调用recv或者send处理数据。

         代码实现:


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#include <errno.h>

int main(int argc, char* argv[])
{
    if (argc < 2)
        return -1;

    int port = atoi(argv[1]);   //字符串转换为整型
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        return -1;

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));   //新申请的空间一定要置零

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);    //转换成网络字节序
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0)
        return -2;

    if (listen(sockfd, 5) < 0)
        return -3;

    //epoll

    int epfd = epoll_create(1); //创建epoll,相当于红黑树的根节点
    struct epoll_event ev, events[1024] = {0};  //events相当于就绪队列,一次性可以处理的集合
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;

    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);    //将ev节点加入到epoll,此处的sockfd参数随便添加没有意义,需要操作系统索引和它有对应的句柄

    while (1)
    {
        int nready = epoll_wait(epfd, events, 1024, -1);    //第四个参数-1表示一直等待,有事件才返回
        if (nready < 1) //没有事件触发,nready代表触发事件的个数
            break;

        int i = 0;
        for (i = 0; i < nready; i++)    //epoll_wait带出的就绪fd包括两大类:1、处理连接的listen_fd,2、处理数据的send和recv
        {
            if (events[i].data.fd == sockfd) //如果是listenfd,就将它加入到epoll
            {
                struct sockaddr_in client_addr;
                memset(&client_addr, 0, sizeof(struct sockaddr_in));
                socklen_t client_len = sizeof(client_addr);
                
                int client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
                if (client_fd <= 0)
                    continue;

                char str[INET_ADDRSTRLEN] = {0};
                printf("recv from IP = %s ,at Port= %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)), ntohs(client_addr.sin_port));

                ev.events = EPOLLIN | EPOLLET;  //epoll默认是LT模式
                ev.data.fd = client_fd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
            }
            else    //fd进行读写操作
            {

               //对fd的读写操作没有分开
                int client_fd = events[i].data.fd;

                char buf[1024] = {0};
                int ret = recv(client_fd, buf, 1024, 0);
                if (ret < 0)
                {
                    if (errno == EAGAIN || errno == EWOULDBLOCK)
                    {
                        //
                    }
                    else
                    {
                        //
                    }

                    printf("ret < 0,断开连接:%d\n", client_fd);

                    close(client_fd);
                    ev.events = EPOLLIN;
                    ev.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev);
                }
                else if (ret == 0)  //接收到了客户端发来的断开连接请求FIN后,没有及时调用close函数,进入了CLOSE _WAIT状态
                {
                    printf("ret = 0,断开连接:%d\n", client_fd);

                    close(client_fd);
                    ev.events = EPOLLIN;
                    ev.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev); //close关闭连接后要将它既是从epoll中删除
                }
                else
                {
                    printf("Recv: %s, %d Bytes\n", buf, ret);
                }

                //区分fd的读写操作,即recv和send
                if (events[i].events & EPOLLIN)
                {
                    int client_fd = events[i].data.fd;

                    char buf[1024] = {0};
                    int ret = recv(client_fd, buf, 1024, 0);
                    if (ret < 0)
                    {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                        {
                            //...
                        }
                        else
                        {
                            //...
                        }

                        printf("ret < 0,断开连接:%d\n", client_fd);

                        close(client_fd);
                        ev.events = EPOLLIN;
                        ev.data.fd = client_fd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev);
                    }
                    else if (ret == 0)  //接收到了客户端发来的断开连接请求FIN后,没有及时调用close函数,进入了CLOSE _WAIT状态
                    {
                        printf("ret = 0,断开连接:%d\n", client_fd);

                        close(client_fd);
                        ev.events = EPOLLIN;
                        ev.data.fd = client_fd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev); //close关闭连接后要将它既是从epoll中删除
                    }
                    else
                    {
                        printf("Recv: %s, %d Bytes\n", buf, ret);
                    }
                }
                if (events[i].events & EPOLLOUT)    //为什么需要判断EPOLLOUT,而不是直接else?因为一个fd有可能同时存在可读和可写事件的
                {
                    int client_fd = events[i].data.fd;

                    char buf[1024] = {0};
                    send(client_fd, buf, sizeof(buf), 0);
                }

            }
        }
    }

    return 0;
}

 

2、epoll的优化版本,根据事件类型(读和写)分为两大类处理。

         代码实现:

        for (i = 0; i < nready; i++)    //epoll_wait带出的就绪fd包括两大类:1、处理连接的listen_fd,2、处理数据的send和recv
        {
            //区分fd的读写操作
            if (events[i].events & EPOLLIN)
            {
                if (events[i].data.fd == sockfd) //如果是listenfd,就将它加入到epoll
                {
                    struct sockaddr_in client_addr;
                    memset(&client_addr, 0, sizeof(struct sockaddr_in));
                    socklen_t client_len = sizeof(client_addr);
                    
                    int client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
                    if (client_fd <= 0)
                        continue;

                    char str[INET_ADDRSTRLEN] = {0};
                    printf("recv from IP = %s ,at Port= %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)), ntohs(client_addr.sin_port));

                    ev.events = EPOLLIN | EPOLLET;  //epoll默认是LT模式
                    ev.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
                }
                else 
                {
                    int client_fd = events[i].data.fd;

                    char buf[1024] = {0};
                    int ret = recv(client_fd, buf, 1024, 0);
                    if (ret < 0)
                    {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                        {
                            //...
                        }
                        else
                        {
                            //...
                        }

                        printf("ret < 0,断开连接:%d\n", client_fd);

                        close(client_fd);
                        ev.events = EPOLLIN;
                        ev.data.fd = client_fd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev);
                    }
                    else if (ret == 0)  //接收到了客户端发来的断开连接请求FIN后,没有及时调用close函数,进入了CLOSE _WAIT状态
                    {
                        printf("ret = 0,断开连接:%d\n", client_fd);

                        close(client_fd);
                        ev.events = EPOLLIN;
                        ev.data.fd = client_fd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev); //close关闭连接后要将它既是从epoll中删除
                    }
                    else
                    {
                        printf("Recv: %s, %d Bytes\n", buf, ret);
                    }
                }
            }
            //为什么需要判断EPOLLOUT,而不是直接else?因为一个fd有可能同时存在可读和可写事件的
            if (events[i].events & EPOLLOUT)    
            {
                int client_fd = events[i].data.fd;

                char buf[1024] = {0};
                send(client_fd, buf, sizeof(buf), 0);
            }

        }

 

3、epoll的Reactor模式, epoll由以前的对网络io(fd)进行管理,转变成对events事件进行管理。

         代码实现:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#include <errno.h>


//每个fd所对应的信息
struct sockitem
{
    int sockfd;
    int (*callback)(int fd, int events, void*arg);
    char sendbuf[1024];
    char recvbuf[1024];
};

//每个epoll所对应的信息
struct epollitem
{
    int epfd;
    struct epoll_event events[1024];    //events相当于就绪队列,一次性可以处理的集合
};

struct epollitem *eventloop = NULL;

int recv_cb(int fd, int events, void*arg);
int send_cb(int fd, int events, void*arg);

int accept_cb(int fd, int events, void*arg)
{
    printf("---accept_cb(int fd, int events, void*arg)---\n");

    struct sockaddr_in client_addr;
    memset(&client_addr, 0, sizeof(struct sockaddr_in));
    socklen_t client_len = sizeof(client_addr);
    
    int client_fd = accept(fd, (struct sockaddr *)&client_addr, &client_len);
    if (client_fd <= 0)
        return -1;

    char str[INET_ADDRSTRLEN] = {0};
    printf("recv from IP = %s ,at Port= %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)), ntohs(client_addr.sin_port));

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;  //epoll默认是LT模式

    struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
    si->sockfd = client_fd;
    si->callback = recv_cb;

    ev.data.ptr = si;

    epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, client_fd, &ev);

    return client_fd;
}

int recv_cb(int fd, int events, void*arg)
{
    printf("---recv_cb(int fd, int events, void*arg)---\n");

    struct epoll_event ev;
    struct sockitem *sit = (struct sockitem*)arg;

    int ret = recv(fd, sit->recvbuf, 1024, 0);
    if (ret < 0)
    {
        if (errno == EAGAIN || errno == EWOULDBLOCK)
        {
            //...
        }
        else
        {
            //...
        }

        printf("ret < 0,断开连接:%d\n", fd);

        ev.events = EPOLLIN;
        epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);    //close关闭连接后要将它既是从epoll中删除


        close(fd);
        free(sit);  //连接关闭后释放内存
    }
    else if (ret == 0)  //接收到了客户端发来的断开连接请求FIN后,没有及时调用close函数,进入了CLOSE _WAIT状态
    {
        printf("ret = 0,断开连接:%d\n", fd);
        
        ev.events = EPOLLIN;
        epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev); 

        close(fd);
        free(sit);
    }
    else
    {
        printf("Recv from recvbuf:  %s, %d Bytes\n", sit->recvbuf, ret);

        ev.events = EPOLLIN | EPOLLOUT;  //
        sit->sockfd = fd;
        sit->callback = send_cb;
        ev.data.ptr = sit;

        epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);
    }

    return ret;
}

int send_cb(int fd, int events, void*arg)
{
    struct epoll_event ev;
    struct sockitem *sit = (struct sockitem*)arg;

    strncpy(sit->sendbuf, sit->recvbuf, sizeof(sit->recvbuf) + 1);
    send(fd, sit->sendbuf, sizeof(sit->recvbuf) + 1, 0);

    ev.events = EPOLLIN | EPOLLET;  //
    sit->sockfd = fd;
    sit->callback = recv_cb;
    ev.data.ptr = sit;

    epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);

    return fd;
}

int main(int argc, char* argv[])
{
    if (argc < 2)
        return -1;

    int port = atoi(argv[1]);   //字符串转换为整型
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        return -1;

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));   //新申请的空间一定要置零

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);    //转换成网络字节序
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0)
        return -2;

    if (listen(sockfd, 5) < 0)
        return -3;

    //epoll

    eventloop = (struct epollitem *)malloc(sizeof(struct epollitem));
    eventloop->epfd = epoll_create(1); //创建epoll,相当于红黑树的根节点

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;

    struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
    si->sockfd = sockfd;
    si->callback = accept_cb;

    ev.data.ptr = si;   //将fd和对应的回调函数绑定一起带进epoll

    epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, sockfd, &ev);    //将ev节点加入到epoll,此处的sockfd参数随便添加没有意义,需要操作系统索引和它有对应的句柄

    while (1)
    {
        int nready = epoll_wait(eventloop->epfd, eventloop->events, 1024, -1);    //第四个参数-1表示一直等待,有事件才返回
        if (nready < 1) //没有事件触发,nready代表触发事件的个数
            break;

        int i = 0;
        for (i = 0; i < nready; i++)
        {
            //区分fd的读写操作
            if (eventloop->events[i].events & EPOLLIN)
            {
                struct sockitem *sit = (struct sockitem*)eventloop->events[i].data.ptr;
                sit->callback(sit->sockfd, eventloop->events[i].events, sit);    //不用区分listen_fd和recv_fd,相应的fd都会调用他们所对应的callback
                
            }
            //为什么需要判断EPOLLOUT,而不是直接else?因为一个fd有可能同时存在可读和可写事件的
            if (eventloop->events[i].events & EPOLLOUT)    
            {
                struct sockitem *sit = (struct sockitem*)eventloop->events[i].data.ptr;
                sit->callback(sit->sockfd, eventloop->events[i].events, sit);
            }
        }
    }

    return 0;
}

4、Reactor模型的应用 

        1、单线程模式的Reactor,参考libevent、redis;

        2、多线程模式的Reactor,参考memcached;

        3、多进程模式的Reactor,参考nginx。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【力扣每日一题】2023.8.26 汇总区间

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一个有序数组&#xff0c;让我们把数组内的元素汇总区间&#xff0c;也就是说有一串数字是连续的&#xff0c;比如是 1 2 3 4…

leetcode359周赛

2828. 判别首字母缩略词 核心思想:枚举。只需要枚举首字母和s是否一一对应即可。 2829. k-avoiding 数组的最小总和 核心思想&#xff1a;自己的方法就是哈希表&#xff0c;枚举i的时候&#xff0c;将k-i统计起来&#xff0c;如果出现了那么就跳过。灵神的方法是数学法&#…

PCB设计常见问题

Fill Mode中存在3个选项 Solid&#xff08;Copper Regions&#xff09; Hatched&#xff08;Tracks/arcs&#xff09; None&#xff08;outlines&#xff09; 区别Solid&#xff08;Copper Regions&#xff09;过大电流的能力更强&#xff0c;且对于电路板存在的分布电容的干扰…

山西电力市场日前价格预测【2023-08-28】

日前价格预测 预测明日&#xff08;2023-08-28&#xff09;山西电力市场全天平均日前电价为319.70元/MWh。其中&#xff0c;最高日前电价为371.80元/MWh&#xff0c;预计出现在19: 15。最低日前电价为278.59元/MWh&#xff0c;预计出现在13: 00。 价差方向预测 1&#xff1a; …

Android 基础知识

一、Activity 1、onSaveInstanceState(),onRestoreInstanceState的调用时机 onSaveInstanceState 调用时机 从最近应用中选择运行其他程序时 但用户按下Home键时 屏幕方向切换时 按下电源案件时 从当前activity启动一个新的activity时 onRestorInstanceState调用时机 只…

HCIP-HCS华为私有云

1、概述 HCS&#xff08;HuaweiCoudStack&#xff09;华为私有云&#xff1a;6.3 之前叫FusionSphere OpenStack&#xff0c;6.3.1 版本开始叫FusionCloud&#xff0c;6.5.1 版本开始叫HuaweiCloud Stack (HCS)华为私有云软件。 开源openstack&#xff0c;发放云主机的流程&am…

如何从“监控”到“可观测性”?

什么是可观测性&#xff1f; 可观测性&#xff08;Observability&#xff09;是一种通过系统产生的输出数据&#xff08;如日志、指标和链路追踪&#xff09;来衡量当前系统运行状态的能力&#xff0c;其源于现代应用系统的复杂性和分布式架构&#xff0c;这些应用系统往往由大…

Unity编辑器扩展:提高效率与创造力的关键

Unity编辑器扩展&#xff1a;提高效率与创造力的关键 前言 一、理解Unity编辑器二、扩展Unity编辑器的意义三、扩展Unity编辑器的必要性四、Unity编辑器的扩展方式五、扩展Unity编辑器的步骤六、Unity编辑器扩展的应用案例七、总结 前言 Unity是一款广泛使用的游戏开发引擎&am…

LangChain-Chatchat:基于LangChain和ChatGLM2-6B构建本地离线私有化知识库

如果你对这篇文章感兴趣&#xff0c;而且你想要了解更多关于AI领域的实战技巧&#xff0c;可以关注「技术狂潮AI」公众号。在这里&#xff0c;你可以看到最新最热的AIGC领域的干货文章和案例实战教程。 一、前言 自从去年GPT模型火爆以来&#xff0c;降低了很多个人和企业进入…

shell 06(shell内置命令)

一、内置命令介绍 shell 内置命令&#xff0c;就是由 Bash shell 自身提供的命令&#xff0c;而不是文件系统中的可执行文件 使用type 来确定一个命令是否是内置命令: type 命令 通常来说&#xff0c;内置命令会比外部命令执行得更快: 执行外部命令时不但会触发磁盘 I/0&am…

云计算服务体系-架构真题(十四)

云计算服务体系结构SaaS、PaaS、IaaS相对应分别&#xff08;&#xff09;。 答案。应用层、平台层、基础设施层 (2022)给定关系模式R(U,F)&#xff0c;其中U为属性集&#xff0c;F是U的一组函数依赖&#xff0c;那么函数依赖的公理系统(Armstrong)中分解规则是指&#xff08;&…

Protobuf在IDEA中的插件安装教程

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

《JVM修仙之路》初入JVM世界

《JVM修仙之路》初入JVM世界 博主目前正在学习JVM的相关知识&#xff0c;想以一种不同的方式记录下&#xff0c;娱乐一下 清晨&#xff0c;你睁开双眼&#xff0c;看到刺眼的阳光&#xff0c;你第一反应就是完了完了&#xff0c;又要迟到了。刚准备起床穿衣的你突然意识到不对&…

【mq】如何保证消息可靠性

文章目录 mq由哪几部分组成rocketmqkafka 为什么需要这几部分nameserver/zookeeper可靠性 broker可靠性 生产者消费者 mq由哪几部分组成 rocketmq kafka 这里先不讨论Kafka Raft模式 比较一下&#xff0c;kafka的结构和rocketmq的机构基本上一样&#xff0c;都需要一个注册…

首席执行官Adam Selipsky解读“亚马逊云科技的技术产品差异化”

迄今为止&#xff0c;亚马逊云科技已经参与了21世纪几乎所有的大型计算变革&#xff0c;亚马逊云科技是一个很传奇的故事&#xff0c;它始于大约20年前的一项实验&#xff0c;当时亚马逊试图出售其过剩的服务器。人们确实对此表示怀疑。为什么在线书店试图销售云服务&#xff1…

区分什么是Java内存模型(JMM)和 JVM运行时数据区

文章目录 一、概念区分1、什么是内存模型&#xff1f;什么是&#xff08;内存区域&#xff09;运行时数据区&#xff1f;2、为什么要有Java内存模型&#xff1f;2.1、硬件的效率与一致性2.2、 CPU和缓存的一致性2.2.1、为什么需要CPU cache&#xff1f;2.2.2、三级缓存&#xf…

如何在 Linux 中设置 SSH 无密码登录

SSH&#xff08;Secure SHELL&#xff09;是一种开源且可信的网络协议&#xff0c;用于登录远程服务器以执行命令和程序。 它还用于使用安全复制 (SCP) 命令和 rsync 命令通过网络将文件从一台计算机传输到另一台计算机。 在本文[1]中&#xff0c;我们将向您展示如何在基于 RHE…

基于Java+SpringBoot+vue前后端分离在线问卷调查系统设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

推荐系统峰会:图与推荐系统

文章目录 图机器学习在京东视频召回中的应用提纲背景图召回架构图业务特色图召回总结 图算法在蚂蚁集团营销推荐场景的应用目录背景基金推荐长尾推荐 图模型在百度推荐系统的实践与思考目录图背景介绍常用算法Feed流图模型演进历程 GNN跨域推荐在微信业务上的应用目录GNN跨域遇…

登录校验-JWT令牌-登陆后下发令牌

目录 思路 接口文档 令牌生成和下发 步骤 具体代码如下 工具类 控制类 测试 前后端联调 思路 令牌生成&#xff1a;登陆成功后&#xff0c;生成JWT令牌&#xff0c;并返回给前端令牌校验&#xff1a;在请求到达服务端后&#xff0c;对令牌进行统一拦截、校验 接口文档…