SpringBoot-心跳机制+redis实现网站实时在线人数统计

news2024/11/14 18:20:38

在社交网站中,通常需要实时统计某个网站的在线人数,通过该指标来实时帮助运营人员更好的维护网站业务:

先说一下目前在市面上主流的做法再加上我自己查阅的资料总结:

  • 创建一个session监听器,在用户登录时即创建一个session,监听器记录下来并且把count加一
  • 用户点击注销时把session给remove掉,count减一

说一下上面这种做法的弊端:

  • 当用户关闭浏览器时并不会触发session监听,当下一次登录时仍然会让count加一
  • 或者在session过期时,session监听并不能做一个实时的响应去将在线数减一
  • 当用户在次登陆,由于cookie中含有的session_id不同而导致session监听器记录下session创建,而使count加一。
  • 对服务器性能影响较大,用户每次访问网站时,服务端都会创建一个session,并将该session与用户关联起来,这样会增加服务器的负担,特别是在高并发的时候,导致服务器压力过大
  • 容易被恶意攻击,攻击者不断发送ddox请求大量创建肉鸡用户,从而大量占据服务器资源,从而崩坏
  • 分布式环境下不好操作

在网上找了很多博客看,发现好多都是在瞎几把写,没看到什么好一点的方案,经过查阅资料,总结如下一个方案算是比较好的:

使用用户登录凭证:token机制+心跳机制实现

用户登录机制时序图如下

在这里插入图片描述

实现思路:
根据时序图的这套方案,用户如果60s内没有任何操作(不调用接口去传递token)则判定该用户为下线状态,当用户重新登陆或者再次操作网站则判定为在线状态,对用户的token进行续期。这其实是心跳机制思想的一种实现,类似于Redis集群中的哨兵对Master主观下线的过程:每10s对Master发送一个心跳包,10s内没有响应则说明Master已经下线了。这里采用的是60s作为一个生存值,如果60s内该用户没有在此页面(如果在此页面,前端会间隔10s发送一次心跳包对Token进行续期+60s过期时间)上执行任何操作,也就不会携带Token发送请求到后端接口中,那么就无法给map中的token过期时间续期,所以该用户就处于过期状态。

代码实现:

1.新建sp项目,导入如下pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.cd</groupId>
    <artifactId>springboot-Comprehensive business</artifactId>
    <version>1.0-SNAPSHOT</version>


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>


        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.6.3</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.19</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

</project>

2.编写配置文件

server.port=9999

spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.84.135:3307/resource-manage?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
mybatis.type-aliases-package=com.cd.pojo

#配置redis
spring.redis.database=11
spring.redis.host=127.0.0.1
spring.redis.port=6379

3.定义一个类,用户统计用户的在线人数等操作

@Component
public class OnlineCounter {

    /**
     * 每次打开此类是该属性只初始化一次
     */
    private static Map<String,Object> countMap = new ConcurrentHashMap<>();


    /**
     * 当一个用户登录时,就往map中构建一个k-v键值对
     * k- 用户名,v 当前时间+过期时间间隔,这里以60s为例子
     * 如果用户在过期时间间隔内频繁对网站进行操作,那摩对应
     * 她的登录凭证token的有效期也会一直续期,因此这里使用用户名作为k可以覆盖之前
     * 用户登录的旧值,从而不会出现重复统计的情况
     */
    public void insertToken(String userName){
        long currentTime = System.currentTimeMillis();
        countMap.put(userName,currentTime+60*1000);
    }

    /**
     * 当用户注销登录时,将移除map中对应的键值对
     * 避免当用户下线时,该计数器还错误的将该用户当作
     * 在线用户进行统计
     * @param userName
     */
    public void deleteToken(String userName){
        countMap.remove(userName);
    }

    /**
     * 统计用户在线的人数
     * @return
     */
    public Integer getOnlineCount(){
        int onlineCount = 0;
        Set<String> nameList = countMap.keySet();
        long currentTime = System.currentTimeMillis();
        for (String name : nameList) {
            Long value = (Long) countMap.get(name);
            if (value > currentTime){
                // 说明该用户登录的令牌还没有过期
                onlineCount++;
            }
        }
        return onlineCount;
    }
}

4.一般在前后分离项目中,都是有统一返回数据格式的,以及一些项目通用配置

/**
 * 统一响应结果
 * @param <T>
 */
public class ResponseResult<T> {

    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提示信息,如果有错误时,前端可以获取该字段进行提示
     */
    private String msg;
    /**
     * 查询到的结果数据,
     */
    private T data;

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

}

redis序列化配置

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // key 序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // value 序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // hash 类型 key序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // hash 类型 value序列化方式
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 让设置生效
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

全局异常配置

package com.cd.exception;

import com.cd.common.ResponseResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(RuntimeException.class)
    public ResponseResult handleRuntimeException(RuntimeException e) {
        logger.error(e.toString(), e);
        return new ResponseResult(400,e.getMessage());
    }
}

线程隔离工具类

package com.cd.util;

import com.cd.pojo.User;
import org.springframework.stereotype.Component;

/**
 * 线程隔离,用于替代session
 */
@Component
public class HostHolder {

    private ThreadLocal<User> users = new ThreadLocal<>();

    public void setUser(User user) {
        users.set(user);
    }

    public User getUser() {
        return users.get();
    }

    public void clear() {
        users.remove();
    }


}

jwt工具类

package com.cd.util;

import cn.hutool.core.lang.UUID;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;

public class JwtUtil {
    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "sangeng";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("sg")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    public static void main(String[] args) throws Exception {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
        Claims claims = parseJWT(token);
        System.out.println(claims);
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

}

有时候我们需要在响应流中设置返回数据,因此有如下工具类

package com.cd.util;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class WebUtils {
    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }

}

  1. 我们这里可以使用springboot的拦截器来拦截需要登录后才能操作的接口,操作这些接口就代表的当前用户属于登录状态,因此需要给用户的登录凭证也就是token续期,对应的往map中添加用户的过期时间来进行覆盖之前的,这样就不会出现同一个用户出现重复统计的情况

配置拦截器

@Component
public class LoginInteceptor implements HandlerInterceptor {

    @Autowired
    private OnlineCounter onlineCounter;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private HostHolder hostHolder;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
          // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StringUtils.isEmpty(token)){
            ResponseResult responseResult = new ResponseResult(400,"未携带请求头信息,不合法");
            String jsonStr = JSONUtil.toJsonStr(responseResult);
            WebUtils.renderString(response,jsonStr);
            return false;
        }
        User user =(User) redisTemplate.opsForValue().get(token);
        if (Objects.isNull(user)){
            ResponseResult responseResult = new ResponseResult(403,"token过期,请重新登录");
            String jsonStr = JSONUtil.toJsonStr(responseResult);
            WebUtils.renderString(response,jsonStr);
            return false;
        }

        // 当请求执行到此处,说明当前token是有效的,对token续期
        redisTemplate.opsForValue().set(token,user,60, TimeUnit.SECONDS);
        // 在本次请求中持有当前用户,方便业务使用
        hostHolder.setUser(user);
        // 覆盖之前的map统计时间,使用最新的token有效期时长
        onlineCounter.insertToken(user.getName());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
         // 释放前挡用户,防止内存泄露
         hostHolder.clear();
    }

}

使拦截器生效

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {


    @Autowired
    private LoginInteceptor loginInteceptor;

    /**
     * 配置拦截哪些请求
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInteceptor)
                .excludePathPatterns("/login","/online"); // 不拦截这些资源
    }
}

数据库创建两个用户,这里直接展示类,数据库字段就不展示了,对象关系映射即可:
在这里插入图片描述

对应接口层如下:

@RestController
public class HelloController {

    @Autowired
    private Userservice userservice;


    /**
     * 该接口需要登录后才能操作
     * @return
     */
    @RequestMapping("/user/list")
    public ResponseResult hello(){
        return userservice.selectUserList();
    }


    /**
     * 登录
     * @param loginParam
     * @return
     */
    @PostMapping("/login")
    public ResponseResult login(@RequestBody LoginParam loginParam){
        return userservice.login(loginParam);
    }


    /**
     * 退出登录
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public ResponseResult logout(HttpServletRequest request){
        return userservice.logout(request);
    }


    /**
     * 获取当前在线人数
     * 这个就相当于一个心跳检查机制
     * 前端每间隔一定时间就请求一下该接口达到在线人数
     * @return
     */
    @PostMapping("/online")
    public ResponseResult getOnLineCount(){
        return userservice.getOnLineCount();
    }



}

对应业务层

@Service
public class UserviceImpl implements Userservice {

    private static final Logger logger = LoggerFactory.getLogger(UserviceImpl.class);


    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private OnlineCounter onlineCounter;

    /**
     * 用户登录
     * @param loginParam
     * @return
     */
    @Override
    public ResponseResult login(LoginParam loginParam) {

        String name = loginParam.getName();
        User user = userMapper.selectByName(name);
        if (Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码不正确");
        }
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        logger.info("当前账号对应的token是: {}",token);
        redisTemplate.opsForValue().set(token,user,60, TimeUnit.SECONDS);
        // 往map中添加一条用户记录
        onlineCounter.insertToken(name);
        return new ResponseResult(200,"登录成功");
    }


    /**
     * 退出登录
     * 需要先有登录才能有退出
     * @return
     */
    @Override
    public ResponseResult logout(HttpServletRequest request) {
        String authorization = request.getHeader("authorization");
        User user = (User) redisTemplate.opsForValue().get(authorization);
        redisTemplate.delete(authorization);
        onlineCounter.deleteToken(user.getName());
        return new ResponseResult(200,"退出成功");
    }


    /**
     * 需要登录才能操作
     * 获取所有用户列表
     * @return
     */
    @Override
    public ResponseResult selectUserList() {
        List<User> userList = userMapper.selectList();
        return new ResponseResult(200,"获取列表成功",userList);
    }


    /**
     * 不需登录
     * 获取当前在线人数
     * @return
     */
    @Override
    public ResponseResult getOnLineCount() {
        Integer onlineCount = onlineCounter.getOnlineCount();
        return new ResponseResult(200,"ok",onlineCount);
    }


}

测试:

未登录时去操作需要登录的接口或者token过期了:
在这里插入图片描述
这个时候网站的在线人数:
在这里插入图片描述
登录后:
在这里插入图片描述
这时候再去请求需要登录才能访问的接口:
在这里插入图片描述
可以看到成功访问了,并且该用户的token会一直续期

获取当前在线人数:

在这里插入图片描述

大功告成

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

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

相关文章

WEB集群之反向代理,动静分离,NFS,mysql,MHA高可用

目录 第一章实验架构 1.1.实验图谱架构 1.2.实验前环境部署 第二章实验步骤 2.1.在ha01&#xff0c;ha02上部署keeplived,lvs-dr 2.2.Slave01,slave02安装nginx 2.3.LVS负载均衡 2.4.搭建动态网页 2.5.nginx反向代理 2.6.部署NFS 2.7.安装mysql 2.8.安装mha 2.9.主…

教你安装 CodeWhisperer: 一款个人免费的类似GitHubCopilot能代码补全的 AI 编程助手

1、官网 AI Code Generator - Amazon CodeWhisperer - AWS 官方扩展安装教程 2、安装VSCode 下载安装VSCode 3、VSCode安装CodeWhisperer插件 安装VSCode插件 - AWS Toolkit主侧栏&#xff0c;点击AWS &#xff0c;展开CodeWhisperer&#xff0c;点击Start 在下拉菜单中点…

洗浴中心管理系统【GUI/Swing+MySQL】(Java课设)

系统类型 Swing窗口类型Mysql数据库存储数据 使用范围 适合作为Java课设&#xff01;&#xff01;&#xff01; 部署环境 jdk1.8Mysql8.0Idea或eclipsejdbc 运行效果 本系统源码地址&#xff1a; 更多系统资源库地址&#xff1a;骚戴的博客_CSDN_更多系统资源 更多系统…

煤矿井下人员精准定位系统,煤矿应急救援高效应用

煤矿行业的安全发展&#xff0c;事关数人民群众的生命财产安全。自2020年起&#xff0c;国家连续三年出台煤矿智能化建设及重大风险防控的相关指导政策&#xff0c;其中&#xff0c;2022年出台的《煤矿及重点非煤矿山重大灾害风险防控建设工作总体方案》中提到&#xff0c;要充…

52 openEuler搭建PostgreSQL数据库服务器-管理数据库角色

文章目录52 openEuler搭建PostgreSQL数据库服务器-管理数据库角色52.1 创建角色创建角色示例52.2 查看角色查看角色示例52.3 修改角色52.3.1 修改用户名52.3.2 修改用户示例52.3.3 修改用户密码52.3.4 修改角色密码示例52.4 删除角色删除角色示例52.5 角色授权角色授权示例52.6…

Python旅游好帮手:提前15天准备五一旅游景点详细数据

人生苦短&#xff0c;我用python 虽然还是有15天才放五一的假&#xff0c; 但是我的心早已经在旅游的路上了~ 本文源码&#xff1a;点击此处跳转文末名片获取 趁现在&#xff0c;先来用python做一个旅游攻略 知识点&#xff1a; requests parsel csv 第三方库&#x…

python查看时间序列数据的季节规律matplotlib画时间(10分钟为间隔)序列坐标

目录0 问题描述1. 案例12. 案例2参考资料0 问题描述 将多个时间序列数据&#xff0c;绘制到一张图上&#xff0c;每段时间序列数据一般只有几个月&#xff0c;少则 1 个月左右&#xff0c;想看它们的季节规律&#xff0c;需要去除年份&#xff0c;只看月份。 也就是横轴是1月…

44.CSS Grid布局概述

什么是CSS Grid&#xff1f; ● CSS Grid 是一组用于构建二维布局的 CSS 属性 ● CSS Grid 背后的主要思想是我们将容器元素划分为行和列&#xff0c;这些行和列可以填充是子元素。 ● 在两段式语境中&#xff0c;CSS网格允许我们写出更少的嵌套HTML和更容易阅读的CSS ● CSS …

二分查找原理及使用场景

建议使用左闭右开区间[l, r)查找。二分查找的最后&#xff0c;索引l&#xff0c;r会落到右区间第一个元素位置。因此但凡是能够见数组分成左右两个区间的都能应用二分查找法。 1、普通查值 常见问题方式&#xff1a;寻找含重复值的有序数组 [...,a, tar, tar, tar,.b....]&am…

AI制药 - RCSB PDB 数据集的多维度分析与整理 (1)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/130089781 整体&#xff1a; RCSB PDB 数据集是一个收集了蛋白质的三维结构信息的数据库&#xff0c;是世界蛋白质数据库&#xff08;…

SQL SERVER调Web Service时候权限错误的解决

日期 2023/4/15 18:00:00 日志 作业历史记录 (AIPACS) 步骤 ID 1 服务器 GOOGLE 作业名称 AIPACS 步骤名称 RUNWS 持续时间 00:00:00 SQL 严重性 16 SQL 消息 ID 15281 已通过电子邮件通知的操作员 已通过…

MATLAB 基于空间格网的点云抽稀 (3)

MATLAB 基于空间格网的点云抽稀 (3) 一、实现效果二、原理步骤三、代码实现四、重点函数与对象的解释说明4.1 indices= pcbin(incloud,[rowNum colNum LayerNum]);4.2 occupancyGrid = cellfun(@(c) ~isempty(c), indices);4.3 outpointIndex = [];4.4 outpointIndex(end+1) …

基于ubuntu18.04上搭建OpenWRT-rtd1619环境

下载OpwnWRT的源码 下载路径&#xff1a;https://gitee.com/yangquan3_admin/rtd1619 您需要以下工具来编译 OpenWrt&#xff0c;包名称因发行版而异。 在 Build System Setup 文档中可以找到包含特定于发行版的软件包的完整列表。 binutils bzip2 diff find flex gawk gcc-6…

【Linux进阶篇】系统网络附加存储

目录 &#x1f341;NFS &#x1f342;软件安装 &#x1f342;服务端配置 &#x1f342;客户端配置 &#x1f342;访问浏览器测试 &#x1f341;iscsi &#x1f342;服务器端安装软件 &#x1f342;服务器端配置iscsi &#x1f342;客户端软件安装配置 &#x1f341;常用的端口号…

这6个免费去水印工具,一定要码住!

现在很多平台会在用户保存图片/视频的时候自动给视频添加一个平台的水印&#xff0c;这在一定程度上影响了它的美观和使用。 下面我来分享几个图片/视频一键去水印方法&#xff0c;操作简单还不会损坏画质哦&#xff01; 1. Magic Eraser 这是一个魔术橡皮擦在线网站&#x…

一文了解API接口自动化测试:让你在人才市场上无往不利

目录&#xff1a;导读 引言 架构 接口测试 API自动化测试 前后端分离的开发模式 测试工作&#xff1a; 协议 网络分层 三次握手的设计(很重要) 问题&#xff1a; URL:统一资源定位符 HTTP协议 &#xff08;重点&#xff09;HTTP的完整请求流程&#xff1a; 通信模…

springboot项目集成JWT实现身份认证(权鉴)

一、什么是JWT JSON Web Token (JWT)&#xff0c;它是目前最流行的跨域身份验证解决方案。现在的项目开发一般都是前端端分离&#xff0c;这就涉及到跨域和权鉴问题。 二、JWT组成 由三部分组成&#xff1a;头部(Header)、载荷(Payload)与签名(signature) 头部&#xff08;Head…

[测试新人必看] 测试报告如何编写? 掌握这五十个测试报告模板

作为一个曾经是测试萌新的我&#xff0c;在首次接收到一个任务时总有一种忐忑慌张激动紧张期望的复杂情绪~~ 忐忑慌张紧张是怕自己做不好&#xff0c;得不到领导的赏识&#xff1b;激动期望是哇塞&#xff0c;我有任务了耶&#xff0c;终于有我的用武之地了~~~ 就好比今天的主题…

Android 实现控件对称布局(约束布局和线性布局)

画界面时会遇到很多界面上的布局&#xff0c;虽然很简单&#xff0c;但是每次做起来不熟练&#xff0c;总结一下一些日常的 一.实现界面上的两个空间对称布局 方法一、用约束布局的guideLine.适用于两个控件不确定宽高&#xff0c;且约束条件较多 Guideline是只能用在Constra…

linux安装并发送邮件

linux安装、配置并发送邮件&#xff08;以CentOS7.9为例&#xff09; 1、安装邮箱软件 yum install mailx -y2、 配置邮箱&#xff08;以qq邮箱为例&#xff09; 2.1 网页访问并登录&#xff1a;https://mail.qq.com/ 2.2 选择“设置->账户” 2.3 在账户下面找到POP3/IMAP/…