IM即时通讯系统[SpringBoot+Netty]——梳理(五)

news2024/11/20 11:37:44

文章目录

  • 十一、打造QQ在线状态功能之为你的应用增添色彩
    • 1、在线状态设计
    • 2、Netty网关用户状态变更通知、登录ack
    • 3、逻辑层处理用户上线下线
    • 4、在线状态订阅—临时订阅
    • 5、实现手动设置客户端状态接口
    • 6、推拉结合实现在线状态更新
  • 十二、IM扩展—能做的事情还有很多
    • 1、如何让陌生人只能发送几条消息
    • 2、如何实现消息的撤回
    • 3、如何设计亿级聊天记录存储方案

项目源代码

目录
IM即时通讯系统[SpringBoot+Netty]——梳理(一)
IM即时通讯系统[SpringBoot+Netty]——梳理(二)
IM即时通讯系统[SpringBoot+Netty]——梳理(三)
IM即时通讯系统[SpringBoot+Netty]——梳理(四)

十一、打造QQ在线状态功能之为你的应用增添色彩


       这个状态是指用户的状态,最先接触的也是qq,还有一种状态叫服务端状态,实现一套在线机制对服务端性能消耗是极大的,手机端在线离线的频率是很高的,微信切进来看几眼算上线,退出去又算离线,假设有100个好友一个操作就会裂变成100个操作,设计这个在线功能也还是从业务需求的角度来设计的

在这里插入图片描述

1、在线状态设计


需求一:需要实时的更新好友的状态,有一个标识可以辨别在线和离线,在线和离线可以实时得到感知,手动修改忙碌啥的状态可以实时通知到好友
需求二:打开群组等,可以获取到这一批人的在线状态,在线的会有一个在线的标识,和好友一样可以实时感知到用户下线了,可以实时的将在线修改为离线

改进

  1. 改进一:状态变更只推送给在线的用户
  2. 改进二:使用按需拉取、临时订阅的方式

2、Netty网关用户状态变更通知、登录ack


在tcp层通知逻辑层某个用户上线了

在这里插入图片描述
在这里插入图片描述

3、逻辑层处理用户上线下线


首先在user里面搞一个mq接收类去接收状态变更消息

在这里插入图片描述

然后当处理到了用户状态变更的命令就处理

@Override
public void processUserOnlineStatusNotify(UserStatusChangeNotifyContent content) {
    // 1、获取到该用户的所有 session,并将其设置到pack中
    List<UserSession> userSessions
            = userSessionUtils.getUserSession(content.getAppId(), content.getUserId());
    UserStatusChangeNotifyPack userStatusChangeNotifyPack = new UserStatusChangeNotifyPack();
    BeanUtils.copyProperties(content, userStatusChangeNotifyPack);
    userStatusChangeNotifyPack.setClient(userSessions);

    // TODO 发送给自己的同步端
    syncSender(userStatusChangeNotifyPack, content.getUserId(),
            UserEventCommand.USER_ONLINE_STATUS_CHANGE_NOTIFY_SYNC, content);

    // TODO 同步给好友和订阅了自己的人
    dispatcher(userStatusChangeNotifyPack, content.getUserId(), UserEventCommand.USER_ONLINE_STATUS_CHANGE_NOTIFY,
            content.getAppId());
}

// 同步自己端
    private void syncSender(Object pack, String userId, Command command, ClientInfo clientInfo){
        messageProducer.sendToUserExceptClient(userId, command,
                pack, clientInfo);
    }

// 同步对方端口
private void dispatcher(Object pack, String userId, Command command, Integer appId){

    // TODO 获取指定用户的所有好友id
    List<String> allFriendId = imFriendShipService.getAllFriendId(userId, appId);
    for (String fid : allFriendId) {
        messageProducer.sendToUser(fid, command,
                pack, appId);
    }

    // TODO 发送给临时订阅的人
    String key = appId+ ":" + Constants.RedisConstants.subscribe + ":" + userId;
    // 取出key中的所有key
    Set<Object> keys = stringRedisTemplate.opsForHash().keys(key);
    // 遍历
    for (Object k : keys) {
        String filed = (String)k;
        // 取出其中的过期时间
        Long expired = Long.valueOf((String) Objects.requireNonNull(stringRedisTemplate.opsForHash().get(key, filed)));
        // 如果没有过期,就要给他发送
        if(expired > 0 && expired > System.currentTimeMillis()){
            messageProducer.sendToUser(filed, UserEventCommand.USER_ONLINE_STATUS_CHANGE_NOTIFY,
            pack, appId);
        }else{
            stringRedisTemplate.opsForHash().delete(key, filed);
        }
    }
}

要通知自己其他端登录,还要通知好友和订阅了改用户的用户们

4、在线状态订阅—临时订阅


// 订阅用户状态
@Override
public void subscribeUserOnlineStatus(SubscribeUserOnlineStatusReq req) {
    Long subExpireTime = 0L;
    if(req != null && req.getSubTime() > 0){
        subExpireTime = System.currentTimeMillis() + req.getSubTime();
    }
    // 使用redis的hash结构存储订阅用户的状态
    for (String subUserId : req.getSubUserId()) {
        String key = req.getAppId() + ":" + Constants.RedisConstants.subscribe + ":" + subUserId;
        stringRedisTemplate.opsForHash().put(key, req.getOperater(), subExpireTime.toString());
    }
}

这其中涉及到了一个用户被多个用户订阅,所以要选择一种合适的redis的数据结构去存储,也就是使用hash不错

在这里插入图片描述

逻辑层处理用户上线下线的时候,同步给临时订阅的人消息的时候,也是通过获取这个redis中的数据去做的分发处理

5、实现手动设置客户端状态接口


// 设置客户端状态
@Override
public void setUserCustomerStatus(SetUserCustomerStatusReq req) {

    // 包
    UserCustomStatusChangeNotifyPack userCustomStatusChangeNotifyPack = new UserCustomStatusChangeNotifyPack();
    userCustomStatusChangeNotifyPack.setCustomStatus(req.getCustomStatus());
    userCustomStatusChangeNotifyPack.setCustomText(req.getCustomText());
    userCustomStatusChangeNotifyPack.setUserId(req.getUserId());

    // 将状态存储到redis中
    String key = req.getAppId() + ":" + Constants.RedisConstants.userCustomerStatus + ":" + req.getUserId();
    stringRedisTemplate.opsForValue().set(key, JSONObject.toJSONString(userCustomStatusChangeNotifyPack));

    syncSender(userCustomStatusChangeNotifyPack, req.getUserId()
            , UserEventCommand.USER_ONLINE_STATUS__SET_CHANGE_NOTIFY_SYNC,
            new ClientInfo(req.getAppId(), req.getClientType(), req.getImei()));

    dispatcher(userCustomStatusChangeNotifyPack, req.getUserId()
            , UserEventCommand.USER_ONLINE_STATUS__SET_CHANGE_NOTIFY, req.getAppId());
}

在这里插入图片描述

6、推拉结合实现在线状态更新


// 拉取指定用户的状态
@Override
public Map<String, UserOnlineStatusResp> queryUserOnlineStatus(PullUserOnlineStatusReq req) {
    return getUserOnlineStatus(req.getUserList(), req.getAppId());
}

// 拉取所有用户的状态
@Override
public Map<String, UserOnlineStatusResp> queryFriendOnlineStatus(PullFriendOnlineStatusReq req) {
    List<String> allFriendId = imFriendShipService.getAllFriendId(req.getOperater(), req.getAppId());
    return getUserOnlineStatus(allFriendId, req.getAppId());
}

// 拉取用户在线状态
private Map<String, UserOnlineStatusResp> getUserOnlineStatus(List<String> userId,Integer appId){
    // 返回类
    Map<String, UserOnlineStatusResp> res = new HashMap<>(userId.size());

    for (String uid : userId) {
        UserOnlineStatusResp resp = new UserOnlineStatusResp();
        // 拉取服务端的状态
        List<UserSession> userSession = userSessionUtils.getUserSession(appId, uid);
        resp.setSession(userSession);
        // 拉取客户端的状态
        String key = appId + ":" + Constants.RedisConstants.userCustomerStatus + ":" + uid;
        String s = stringRedisTemplate.opsForValue().get(key);
        if(StringUtils.isNotBlank(s)){
            JSONObject parse = (JSONObject) JSON.parse(s);
            resp.setCustomText(parse.getString("customText"));
            resp.setCustomStatus(parse.getInteger("customStatus"));
        }
        res.put(uid, resp);
    }
    return res;
}

拉取用户的状态信息的各种接口

在这里插入图片描述

十二、IM扩展—能做的事情还有很多


1、如何让陌生人只能发送几条消息


有一些软件是没有称为好友的话,只能发送3条呀几条的消息消息,这个功能要怎么实现呢

这里采用的方案是用回调,在回调前和回调后做一些逻辑上的判断,来决定下面的操作是否执行

在这里插入图片描述

在这里插入图片描述

2、如何实现消息的撤回


       只能由im系统撤回,是一个command指令

       撤回的本质就是将要撤回的那条消息变成,谁谁谁撤回了消息,将原消息变更称为一条新的消息,并且插入一条新的消息

逻辑:

  1. 修改历史消息的状态
  2. 修改离线消息的状态
  3. ack给发送方
  4. 发送给同步端
  5. 分发给消息的接收方
// 撤回消息
public void recallMessage(RecallMessageContent messageContent) {

    // 如果消息发送超过一定的时间就不可以撤回了
    Long messageTime = messageContent.getMessageTime();
    Long now = System.currentTimeMillis();
    RecallMessageNotifyPack pack = new RecallMessageNotifyPack();
    BeanUtils.copyProperties(messageContent, pack);
    if(120000L < now - messageTime){
        recallAck(pack, ResponseVO.errorResponse(MessageErrorCode.MESSAGE_RECALL_TIME_OUT), messageContent);
        return;
    }

    LambdaQueryWrapper<ImMessageBodyEntity> lqw = new LambdaQueryWrapper<>();
    lqw.eq(ImMessageBodyEntity::getAppId, messageContent.getAppId());
    lqw.eq(ImMessageBodyEntity::getMessageKey, messageContent.getMessageKey());
    ImMessageBodyEntity body = imMessageBodyMapper.selectOne(lqw);

    // 如果查不到该消息的话
    if(body == null){
        // TODO ack失败 不存在的消息体不能撤回
        recallAck(pack, ResponseVO.errorResponse(MessageErrorCode.MESSAGEBODY_IS_NOT_EXIST), messageContent);
        return;
    }

    // 如果该消息已经被撤回
    if(body.getDelFlag() == DelFlagEnum.DELETE.getCode()){
        recallAck(pack, ResponseVO.errorResponse(MessageErrorCode.MESSAGE_IS_RECALLED), messageContent);
        return;
    }

    // 经过上面的判断,这时候的该信息就是没有撤回且正常的消息,下面就该进行修改历史信息
    body.setDelFlag(DelFlagEnum.DELETE.getCode());
    imMessageBodyMapper.update(body, lqw);

    // 如果撤回的消息的单聊的话
    if(messageContent.getConversationType() == ConversationTypeEnum.P2P.getCode()){

        // fromId的队列
        String fromKey = messageContent.getAppId() + ":"
                + Constants.RedisConstants.OfflineMessage + ":" + messageContent.getFromId();

        // toId的队列
        String toKey = messageContent.getAppId() + ":"
                + Constants.RedisConstants.OfflineMessage + ":" + messageContent.getToId();

        // 构建离线消息体
        OfflineMessageContent offlineMessageContent = new OfflineMessageContent();
        BeanUtils.copyProperties(messageContent, offlineMessageContent);
        offlineMessageContent.setDelFlag(DelFlagEnum.DELETE.getCode());
        offlineMessageContent.setMessageKey(messageContent.getMessageKey());
        offlineMessageContent.setConversationType(ConversationTypeEnum.P2P.getCode());
        offlineMessageContent.setConversationId(conversationService.conversationConversationId(
                offlineMessageContent.getConversationType(), messageContent.getFromId(), messageContent.getToId()));
        offlineMessageContent.setMessageBody(body.getMessageBody());

        long seq = redisSeq.doGetSeq(messageContent.getAppId()
                + ":" + Constants.SeqConstants.Message
                + ":" + ConversationIdGenerate.generateP2PId(messageContent.getFromId(), messageContent.getToId()));
        offlineMessageContent.setMessageSequence(seq);

        long messageKey = SnowflakeIdWorker.nextId();

        redisTemplate.opsForZSet().add(fromKey, JSONObject.toJSONString(offlineMessageContent), messageKey);
        redisTemplate.opsForZSet().add(toKey, JSONObject.toJSONString(offlineMessageContent), messageKey);

        // ack
        recallAck(pack, ResponseVO.successResponse(), messageContent);

        // 分发给同步端
        messageProducer.sendToUserExceptClient(messageContent.getFromId(), MessageCommand.MSG_RECALL_NOTIFY,
                pack, messageContent);

        // 分发给对方
        messageProducer.sendToUser(messageContent.getToId(), MessageCommand.MSG_RECALL_NOTIFY,
                pack, messageContent);
    }else{
        List<String> groupMemberId
                = imGroupMemberService.getGroupMemberId(messageContent.getToId(), messageContent.getAppId());
        long seq = redisSeq.doGetSeq(messageContent.getAppId() + ":" + Constants.SeqConstants.Message
                + ":" + ConversationIdGenerate.generateP2PId(messageContent.getFromId(), messageContent.getToId()));
        // ack
        recallAck(pack, ResponseVO.successResponse(), messageContent);

        // 发送给同步端
        messageProducer.sendToUserExceptClient(messageContent.getFromId(), MessageCommand.MSG_RECALL_NOTIFY,
                pack, messageContent);

        // 同步给对方端
        for (String memberId : groupMemberId) {
            String toKey = messageContent.getAppId() + ":" + Constants.RedisConstants.OfflineMessage + ":"
                    + memberId;
            OfflineMessageContent offlineMessageContent = new OfflineMessageContent();
            offlineMessageContent.setDelFlag(DelFlagEnum.DELETE.getCode());
            BeanUtils.copyProperties(messageContent, offlineMessageContent);
            offlineMessageContent.setConversationType(ConversationTypeEnum.GROUP.getCode());
            offlineMessageContent.setConversationId(conversationService.conversationConversationId(
                    offlineMessageContent.getConversationType(), messageContent.getFromId(), messageContent.getToId()
            ));
            offlineMessageContent.setMessageBody(body.getMessageBody());
            offlineMessageContent.setMessageSequence(seq);
            redisTemplate.opsForZSet().add(toKey, JSONObject.toJSONString(offlineMessageContent), seq);

            groupMessageProducer.producer(messageContent.getFromId(), MessageCommand.MSG_RECALL_NOTIFY
                    ,pack, messageContent);
        }
    }
}

3、如何设计亿级聊天记录存储方案


       比较主流的qq、微信对于聊天记录的存储都是有期限的,也就是说我们查询的聊天记录,怎么存储的多怎么取的快

在这里插入图片描述
      对owner_id加上索引,这个查询就变成了一次索引查询一次,主键查询,这样就满足了查的快了


       但是如果只有一个应用的话没问题,但是如果有多个应用接入的情况下,全部的消息存储到这张表中的话,就有点难了

分库

       按照appId进行分库,这样的只适用于你已知多少多少应用接入的情况下,才能准确的进行分库,而且也不能部署太多的库,每一个应用都要有一个库的话,太繁琐了
在这里插入图片描述


数据迁移

       我们可以限定一个期限,超过的期限的记录就不允许用户去查了,但是后台可以查询,这些数据就被迁移到其他的地方,以供后台查询,而且后台慢的查也可以,原来的数据表中的数据就都没了,就变成一个新的表了,然后再放进去数据,查到的也就是最新的了

分表

       比较适合已知有多少个应用接入,基于owener_id做分表

在这里插入图片描述
       基于时间戳做分表,就不用考虑多少个应用接入,但是缺点就是一张表要固定存储一端时间的数据,有的时间段消息多,有的时间段消息少,可能不太好

public class MessageKeyGenerate {

    //标识从2020.1.1开始
    private static final long T202001010000 = 1577808000000L;

    //    private Lock lock = new ReentrantLock();
    AtomicReference<Thread> owner = new AtomicReference<>();

    private static volatile int rotateId = 0;
    private static int rotateIdWidth = 15;

    private static int rotateIdMask = 32767;
    private static volatile long timeId = 0;

    private int nodeId = 0;
    private static int nodeIdWidth = 6;
    private static int nodeIdMask = 63;

    public void setNodeId(int nodeId) {
        this.nodeId = nodeId;
    }

    public synchronized long generateId() throws Exception {

//        lock.lock();

        this.lock();

        rotateId = rotateId + 1;

        long id = System.currentTimeMillis() - T202001010000;

        //不同毫秒数生成的id要重置timeId和自选次数
        if (id > timeId) {
            timeId = id;
            rotateId = 1;
        } else if (id == timeId) {
            //表示是同一毫秒的请求
            if (rotateId == rotateIdMask) {
                //一毫秒只能发送32768到这里表示当前毫秒数已经超过了
                while (id <= timeId) {
                    //重新给id赋值
                    id = System.currentTimeMillis() - T202001010000;
                }
                this.unLock();
                return generateId();
            }
        }

        id <<= nodeIdWidth;
        id += (nodeId & nodeIdMask);


        id <<= rotateIdWidth;
        id += rotateId;

//        lock.unlock();
        this.unLock();
        return id;
    }

    public static int getSharding(long mid) {

        Calendar calendar = Calendar.getInstance();

        mid >>= nodeIdWidth;
        mid >>= rotateIdWidth;

        calendar.setTime(new Date(T202001010000 + mid));

        int month = calendar.get(Calendar.MONTH);
        int year = calendar.get(Calendar.YEAR);
        year %= 3;

        return (year * 12 + month);
    }

    public static long getMsgIdFromTimestamp(long timestamp) {
        long id = timestamp - T202001010000;

        id <<= rotateIdWidth;
        id <<= nodeIdWidth;

        return id;
    }

    public void lock() {
        Thread cur = Thread.currentThread();
        while (!owner.compareAndSet(null, cur)){
        }
    }
    public void unLock() {
        Thread cur = Thread.currentThread();
        owner.compareAndSet(cur, null);
    }

    public static void main(String[] args) throws Exception {

        MessageKeyGenerate messageKeyGenerate = new MessageKeyGenerate();
        for (int i = 0; i < 10; i++) {
            long l = messageKeyGenerate.generateId();
            System.out.println(l);
        }

        //im_message_history_12


        //10000  10001
        //0      1

        long msgIdFromTimestamp = getMsgIdFromTimestamp(1734529845000L);
        int sharding = getSharding(msgIdFromTimestamp);
        System.out.println(sharding);
    }
}

这个是优化后的基于时间戳生成message_id的策略

最后这点东西听不太懂了,就这样吧!

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

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

相关文章

​ NISP一级备考知识总结之信息安全概述、信息安全基础

参加每年的大学生网络安全精英赛通过初赛就可以嫖一张 nisp&#xff08;国家信息安全水平考试&#xff09; 一级证书&#xff0c;nisp 一级本身没啥考的价值&#xff0c;能白嫖自然很香 1.信息安全概述 信息与信息技术 信息概述 信息奠基人香农认为&#xff1a;信息是用来消…

ChatGPT插件:让你的 ChatGPT 与众不同!

这个 Chrome 浏览器插件是作者觉得原本的 ChatGPT 界面不太丰富&#xff0c;然后想着自己写一个插件把它变得更加好看一点 &#x1f92a;&#xff0c;因此把这个插件取名为 ChatGPT-theme&#xff0c;目前插件已经发布了是 1.0.1 版本的&#xff0c;因为 1.0.0 作者测了一下有些…

水电站泄洪监测预警系统解决方案

一、方案背景 每到汛期水库或电站泄洪时&#xff0c;下游各责任单位接到泄洪通知后&#xff0c;组织人员对下游河道进行巡查&#xff0c;耗费大量的人力物力&#xff0c;且信息传递效果不明显。巡查办法老套单一&#xff0c;信息传递速度慢、覆盖范围小&#xff0c; 无法让沿途…

【软考备战·四月模考】希赛网四月模考软件设计师上午题

文章目录 一、成绩报告二、错题总结第一题第二题第三题第四题第五题第六题第七题第八题第九题第十题第十一题第十二题第十三题第十四题第十五题第十六题第十七题第十八题第十九题第二十题第二十一题第二十二题 三、知识查缺 题目及解析来源&#xff1a;2023上半年软考-模考大赛…

【Linux Network】传输层协议——TCP

目录 TCP协议 TCP协议段格式 确认应答(ACK)机制 超时重传机制 连接管理机制 理解TIME_WAIT状态 解决TIME_WAIT状态引起的bind失败的方法 理解 CLOSE_WAIT 状态 滑动窗口 流量控制 拥塞控制 延迟应答 捎带应答 面向字节流 粘包问题 TCP异常情况 TCP小结 基于TCP应用层协议 TCP/U…

torch.nn.Module

它是所有的神经网络的根父类&#xff01; 你的神经网络必然要继承 可以看一下这篇文章

《机器学习》习题 第 4 章

4.1 试证明对于不含冲突数据 (即特征向量完全相同但标记不同) 的训练集, 必存在与训练集一致 (即训练误差为 0)的决策树. 答案&#xff1a; 假设不存在与训练集一致的决策树&#xff0c;那么训练集训练得到的决策树至少有一个节点上存在无法划分的多个数据&#xff08;若节点…

Linux速通 常用基本命令

大部分摘自《Linux 命令行与shell脚本编程大全》该书&#xff0c;少部分参考自csdn博客 目录 一、基本的bash shell 命令 1、文件和目录列表 基本列表功能 修改输出信息 过滤输出列表 2、处理文件 3、处理目录 4、查看文件内容 查看整个文件 查看部分文件 二、更多的…

makefile 学习(1):C/C++ 编译过程

1. GCC 介绍 1.1 介绍 GCC 官方文档 https://gcc.gnu.org/onlinedocs/ 官方文档是最权威的&#xff0c;网上所有的答案都来自官方文档国内论坛参差不齐&#xff0c;找到好的答案比较花时间&#xff0c;并且很容易被错误的文档误导。所以推荐看官方文档靠谱点&#xff0c;并且…

mongodb设置用户名和密码

docker run --name mongodb -p 27017:27017 -v /opt/mongodb/data:/data/db -v /opt/mongodb/backup:/data/backup -d mongo --auth进入容器&#xff1a; docker -it exec 容器id /bin/bash进入mongo的控制台 mongosh设置用户名及密码 use admin db.createUser({ user: &…

dwg格式转换pdf,教大家几个简单方法

dwg格式转换pdf&#xff0c;今天教大家几个简单方法吧。因为有很多小伙伴私信小编&#xff0c;询问关于CAD格式转换的问题。我们知道&#xff0c;dwg是CAD格式的一种&#xff0c;只能使用CAD软件进行打开&#xff0c;这非常不方便。特别是在需要在手机或其他平台查看时&#xf…

IT项目管理小题计算总结【太原理工大学】

计算题小题应该就这些了吧&#xff0c;祝大家都高过&#xff01;>_< 目录 1. 求投资回收期 2. 求投资收益率 3. 求功能点 4. 成本预期值 5. 成本加固定 6. 期望时间及概率 7. 项目进度计算 8. 完工尚需估算 9. 合格率计算 10. 合同总价 11. 压缩工期 1. 求投资…

Hugging Face Transformers Agent

&#x1f917;Hugging Face Transformers Agent 就在两天前&#xff0c;&#x1f917;Hugging Face 发布了 Transformers Agent——一种利用自然语言从精选工具集合中选择工具并完成各种任务的代理。听着是不是似曾相识&#xff1f; 没错&#xff0c;Hugging Face Transformer…

APP软件的测试方法和工具

手机APP的使用已经非常普及&#xff0c;使用方便&#xff0c;因此越来越多的企业通过APP对外管理客户及产品&#xff0c;对内管理工作流程。这些APP有的是自研&#xff0c;有的是找专业的APP外包公司开发完成&#xff0c;开发完成后需要做详细的测试&#xff0c;今天和大家分享…

java版企业电子招投标系统源码 招采系统源码 spring boot+mybatis+前后端分离实现电子招投标系统

spring bootmybatis前后端分离实现电子招投标系统 电子招投标系统解决方案 招标面向的对象为供应商库中所有符合招标要求的供应商&#xff0c;当库中的供应商有一定积累的时候&#xff0c;会节省大量引入新供应商的时间。系统自动从供应商库中筛选符合招标要求的供应商&#x…

干货!12个程序员证书​,含金量超高

近来IT行业成为了发展前景好高薪资的大热门&#xff0c;越来越多的人选择参加各种各样的计算机考试&#xff0c;就是为了拿含金量高的证书&#xff0c;提升自己的职场竞争力。 那么程序员有哪些含金量高的证书可以考&#xff1f;下面云学姐将详细介绍一下含金量高的IT证书&…

游戏网站JS加密限制,用python来突破限制,进行逆向解密~

回来了回来了 好久没更新了 不过好像没啥人看文章了 难不成都去看视频学习了吗 今天线的无聊来分享分享如果用python来突破JS加密限制&#xff0c;进行逆向解密&#xff0c;来实现自动登录~ 逆向目标 目标&#xff1a;某 7 网游登录主页&#xff1a;aHR0cHM6Ly93d3cuMzcuY29…

【pytest】执行环境切换的两种解决方案

一、痛点分析 在实际企业的项目中&#xff0c;自动化测试的代码往往需要在不同的环境中进行切换&#xff0c;比如多套测试环境、预上线环境、UAT环境、线上环境等等&#xff0c;并且在DevOps理念中&#xff0c;往往自动化都会与Jenkins进行CI/CD&#xff0c;不论是定时执行策略…

深度解析Linux kernel同步机制(上篇)

在现代操作系统里&#xff0c;同一时间可能有多个内核执行流在执行&#xff0c;因此内核其实像多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问&#xff0c;尤其是在多处理器系统上&#xff0c;更需要一些同步机制来同步不同处理器上的执行单元对共享…

开源之夏 2023 | 欢迎报名openEuler sig-eBPF开发任务

开源之夏是中国科学院软件研究所联合openEuler发起的开源软件供应链点亮计划系列暑期活动&#xff0c;旨在鼓励在校学生积极参与开源软件的开发维护&#xff0c;促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区&#xff0c;针对重要开源软件的开发与维护提供项目&#x…