当前位置: 首页 > news >正文

Netty综合案例(下)

本文为个人学习笔记整理,仅供交流参考,非专业教学资料,内容请自行甄别

文章目录

  • 实验内容
  • 五、报文加密解密
    • 4.1、加密编码器
    • 4.2、解密编码器
  • 六、应用层的握手
  • 七、心跳机制
  • 八、整体流程


实验内容

  实验内容(本篇从第5条开始):

  1. 消息实体的定义
  2. 客户端,服务端的定义
  3. 客户端,服务端Handler责任链的设计
  4. 半包,粘包的框架解决方案
  5. 应用层的握手,授权认证,报文加密解密
  6. 心跳检测(TCP有keep alive 为什么应用层还要有心跳?TCP的保活机制2小时,TCP的保活机制只能保证客户端和服务端的通信链路是通的,不能保证客户端或服务端的进程还是活着的)
  7. 业务数据的通信

五、报文加密解密

  报文加密解密在实际项目中运用广泛,本案例中使用SM2加密的方式,使用SM2加密,首先需要生成一对公钥和私钥,公钥用于加密,私钥用于解密。私钥选择存储在Redis中。
  Redis工具类:

/*** 双检锁的单例*/
public class JedisClient {private static volatile Jedis JEDIS_CLIENT = null;public static Jedis getJedisClient(){if (JEDIS_CLIENT == null){synchronized (new Object()){if (JEDIS_CLIENT == null){JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();jedisPoolConfig.setMaxTotal(20);jedisPoolConfig.setMaxIdle(10);jedisPoolConfig.setMinIdle(5);// timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数try (JedisPool jedisPool = new JedisPool(jedisPoolConfig, "localhost", 6379, 3000, null)) {return jedisPool.getResource();}}}}return JEDIS_CLIENT;}
}

4.1、加密编码器

  报文的加密,会经过以下的流程: MessageEntity ➝ JSON ➝ SM2加密 ➝ 字节数组 ➝ 输出到 ByteBuf。
  选择将生成的私钥存入Redis,Redis需要一个唯一的key,对应每一条消息,使用uuid生成,同时将Key放在密文之前,在解密编码器中读取消息的前36个字节,转换为privateKeyId,从而从redis中获取私钥。

        +---------------------+--------------------------+| 36字节的 privateKeyId |        SM2 加密的密文数据     |+---------------------+--------------------------+

  因为客户端和服务端处于两个不同的进程中,有各自的Pipeline,不能使用传统的方式进行上下文传递:

AttributeKey<String> key = AttributeKey.valueOf("privateKeyId");
channelHandlerContext.channel().attr(key).set(privateKeyId);
public class SM2EncodeHandler extends MessageToByteEncoder<MessageEntity> {private final Jedis jedisClient = JedisClient.getJedisClient();@Overrideprotected void encode(ChannelHandlerContext channelHandlerContext, MessageEntity messageEntity, ByteBuf byteBuf) throws Exception {System.out.println("SM2EncodeHandler执行" + Thread.currentThread().getName());// 生成SM2密钥对 公钥加密,私钥解密Map<String, String> stringStringMap = Sm2Util.generateKey();String publicKey = stringStringMap.get("publicKey");String privateKey = stringStringMap.get("privateKey");// 生成一个唯一标识String privateKeyId = UUID.randomUUID().toString();// 存入 Redis(设置过期时间)jedisClient.setex(RedisConstants.PRIVATE_KEY_PREFIX + privateKeyId, 10 * 60, privateKey); // 10分钟有效期//将 MessageEntity ➝ JSONString messageJSON = JSON.toJSONString(messageEntity);//将 JSON-> 加密String encrypt = Sm2Util.encrypt(publicKey, messageJSON);//SM2加密 -> 字节数组byte[] encryptedData = encrypt.getBytes(CharsetUtil.UTF_8);//写入密文byte[] privateKeyIdBytes = privateKeyId.getBytes(StandardCharsets.UTF_8);/*将私钥的唯一标识放在密文之前:+---------------------+--------------------------+| 36字节的 privateKeyId |        SM2 加密的密文数据     |+---------------------+--------------------------+*/byteBuf.writeBytes(privateKeyIdBytes); // UUID 通常是 36 字节byteBuf.writeBytes(encryptedData);}
}

4.2、解密编码器

  报文的解密,会经过以下的流程:ByteBuf ➝ JSON -> SM2解密 ➝ 反序列化为 MessageEntity
  首先需要配读取密文之前的36 字节的privateKeyId,从Redis中取出该条消息对应的私钥,用于进行解密。

public class SM2DecodeHandler extends ByteToMessageDecoder {private final Jedis jedisClient = JedisClient.getJedisClient();@Overrideprotected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {System.out.println("SM2DecodeHandler执行" + Thread.currentThread().getName());// 读取 密文之前 的36 字节的 privateKeyIdbyte[] privateKeyIdBytes = new byte[36];byteBuf.readBytes(privateKeyIdBytes);String privateKeyId = new String(privateKeyIdBytes, StandardCharsets.UTF_8);//读取密文byte[] encryptedBytes = new byte[byteBuf.readableBytes()];byteBuf.readBytes(encryptedBytes);//拿到保存的私钥的key//根据私钥标识,从redis中拿到真正的私钥String privateKey;try {privateKey = jedisClient.get(RedisConstants.PRIVATE_KEY_PREFIX + privateKeyId);} finally {jedisClient.del(RedisConstants.PRIVATE_KEY_PREFIX + privateKeyId);}if (privateKey == null) {throw new RuntimeException("解密失败:未找到私钥 privateKey");}//转换为字符串String jsonStr = new String(encryptedBytes, CharsetUtil.UTF_8);//执行解密String jsonDecrypt = Sm2Util.decrypt(privateKey, jsonStr);//转换为消息对象MessageEntity messageEntity = JSON.parseObject(jsonDecrypt, MessageEntity.class);list.add(messageEntity);}
}

六、应用层的握手

  当连接建立完成后,会触发客户端ClientLoginHandlerchannelActive事件,在该事件中向服务端发起登录请求(具体代码详见附件)

    /*** 连接建立** @param ctx* @throws Exception*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {LOG.info("ClientLoginHandler.channelActive开始执行");//向服务端发送登录请求MessageEntity req = NettyUtil.makeMessage("请求登录", MessageTypeEnum.LOGIN_REQ.getValue(), null);ctx.writeAndFlush(req);}

  服务端的LoginPreCheckHandler在接收到客户端的请求后,主要会做三件事:

  1. 要检查是不是登录认证请求
  2. 要检查同一IP是否重复登陆
  3. 要检查用户是否在白名单中

  检查通过,将用户放入缓存,并且向客户端发送响应。
  客户端会在ClientLoginHandler中接收到响应。如果登录成功,可以将当前的ClientLoginHandler从责任链中移除。因为在一次通信的生命周期中,只需要登录一次。

 channelHandlerContext.pipeline().remove(this);

七、心跳机制

  客户端的责任链中,放入了IdleStateHandlerReadTimeoutHandler,而服务端的责任链中,也放入了ReadTimeoutHandler,这些处理器是心跳机制实现的关键。
  一般是由客户端主动向服务端发送心跳,在客户端构造IdleStateHandler时,传入了三个参数
在这里插入图片描述
  当指定的时间段内没有进行对应的读或写操作,则会触发事件,我们这里设置的是5s内没有执行过写操作,则触发写空闲事件。
在这里插入图片描述
  客户端的HeartBeatReqHandler中,userEventTriggered会进行监听,向服务端发送心跳请求,

    @Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {LOG.info("userEventTriggered触发,事件类型: {}", evt.getClass().getName());//用于发心跳包if (evt == IdleStateEvent.WRITER_IDLE_STATE_EVENT) {MessageEntity req = NettyUtil.makeMessage("客户端发送心跳", MessageTypeEnum.HEARTBEAT_REQ.getValue(),null);ctx.writeAndFlush(req);}//如果你重写了 userEventTriggered() 但不调用 super.userEventTriggered(),事件就不会继续传播。super.userEventTriggered(ctx, evt);}

  当心跳请求在一定的时间间隔内正常发送,就不会触发ReadTimeoutHandler 的超时关闭连接。

八、整体流程

  

http://www.dtcms.com/a/299393.html

相关文章:

  • 人工智能与能源:智慧能源的高效与可持续
  • 2025年入局苹果Vision Pro开发:从零到发布的完整路线图
  • uniapp+vue3——通知栏标题纵向滚动切换
  • 去除视频字幕 4 : 下一步,打算研究 Video Inpainting (视频修复):
  • MongoDB数据库高并发商业实践优化·运行优化之不可使用root账户进行MongoDB运行-优雅草卓伊凡
  • 【LeetCode 热题 100】79. 单词搜索——回溯
  • 《用于几何广义断层触觉传感的图结构超分辨率:在仿人面部的应用》论文解读
  • Linux随记(二十一)
  • COZE官方文档基础知识解读第六期 ——数据库和知识库
  • 【算法】前缀和经典例题
  • RU 19.28安装
  • ubuntu22.04系统 算力4090服务器 病毒防护 查杀等 运维入门(三)clamAV工具离线查杀
  • C 语言详解:特性、应用与发展
  • UniappDay03
  • 港股历史逐笔十档分钟级订单簿行情数据分析
  • ChibiOS深度解析:硬实时内核的全景剖析与FreeRTOS实战对比
  • 前端组件梳理
  • dify前端应用相关
  • Linux进程:系统运行的核心机制
  • Claude Code PowerShell 安装 MCPs 方法:以 Puppeteer 为例
  • 基于深度学习的食管癌右喉返神经旁淋巴结预测系统研究
  • Effective C++ 条款4:确定对象被使用前已先被初始化
  • Java-数构二叉树
  • 学习嵌入式的第三十一天-数据结构-(2025.7.23)网络协议封装
  • epoll_create1函数含义和使用案例
  • 深度解析【JVM】三大核心架构:运行时数据区、类加载与垃圾回收机制
  • Java大厂经典面试题
  • 复杂产品系统集成协同研发平台的研究与实现
  • @PathVariable与@RequestParam的区别
  • k8s的nodeport和ingress