Netty实战应用学习笔记-中级拓展篇(九)

「深入浅出Netty」不愧是阿里架构师,这篇Netty实战文档太实用 前言 什么是Netty? Netty是由JBOSS提供的一个java开源框架。 Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty是一个基于NIO的客户,服务器端编程框架,使用Netty可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。 作为当前最流行的NIO框架,Netty在互联... 阅读详情

🌐 项目九:集群部署实现跨服务端通信

项目概述

项目名称:itstack-demo-netty-2-09
核心功能:使用Redis Pub/Sub实现Netty集群间的跨服务器通信
技术要点:Redis发布订阅、两级缓存、消息路由、Spring Boot集成

为什么需要这个项目?

实际问题:

  • 单台服务器无法支撑大量用户连接
  • 集群部署后,用户A和用户B可能连接到不同服务器
  • 不同服务器的用户之间无法直接通信
  • Netty服务器之间直连,连接数指数增长

解决方案:

  • ✅ Redis Pub/Sub:作为消息中转站,解耦服务器
  • ✅ 两级缓存:本地内存+Redis,提高查询效率
  • ✅ 消息路由:先查本地,再查全局
  • ✅ 动态扩展:新增服务器无需修改现有服务器

适用场景:

  • IM即时通讯系统(微信、QQ的集群架构)
  • 游戏服务器(跨服务器组队、交易)
  • 物联网平台(设备跨网关通信)
  • 分布式推送系统

核心问题

集群部署的通信难题

当Netty以集群方式部署时:

  • 用户A连接到服务器X
  • 用户B连接到服务器Y
  • A和B不在同一个服务器,如何实现通信?

两种解决方案对比:

方案Netty服务器直连Redis消息中间件
连接数N×(N-1) 指数增长N 线性增长
服务发现需要额外的注册中心Redis本身就是注册中心
消息路由需要自己实现Redis Pub/Sub自动分发
扩展性新增服务器需修改所有服务器新增服务器无需修改其他服务器
复杂度高低

为什么选择Redis?

Netty直连方案:
服务器A ←→ 服务器B
    ↕         ↕
服务器C ←→ 服务器D
(需要维护6个连接)

Redis方案:
服务器A → Redis ← 服务器B
服务器C → Redis ← 服务器D
(只需要4个连接)

核心知识点

1. 系统架构设计
┌─────────────┐                    ┌─────────────┐
│  用户A       │                    │  用户B       │
└──────┬──────┘                    └──────┬──────┘
       │                                  │
       │ Netty连接                        │ Netty连接
       ↓                                  ↓
┌─────────────┐                    ┌─────────────┐
│ Netty服务器X │                    │ Netty服务器Y │
└──────┬──────┘                    └──────┬──────┘
       │                                  │
       │ 发布消息                         │ 订阅消息
       ↓                                  ↓
       └──────────→ Redis ←───────────────┘
                   Pub/Sub

通信流程:

1. 用户A发送消息给用户B
2. 服务器X接收消息,发现用户B不在本服务器
3. 服务器X通过Redis发布消息
4. Redis广播消息到所有订阅者(服务器X、Y、Z...)
5. 服务器Y接收到订阅消息
6. 服务器Y从本地缓存查找用户B的Channel
7. 服务器Y将消息发送给用户B
2. 核心领域对象

MsgAgreement.java - 消息协议

public class MsgAgreement {
    private String toChannelId;  // 接收者的channelId
    private String content;      // 消息内容
}

UserChannelInfo.java - 用户连接信息

public class UserChannelInfo {
    private String ip;        // 服务器IP
    private int port;         // 服务器端口
    private String channelId; // 用户channelId
    private Date linkDate;    // 连接时间
}
3. 两级缓存机制

为什么需要两级缓存?

本地内存缓存(CacheUtil):
- 快速判断用户是否在本服务器
- 避免每次都查Redis
- 存储本服务器的Channel对象

Redis缓存(RedisUtil):
- 全局共享用户分布信息
- 方便管理和监控
- 存储所有服务器的用户信息

CacheUtil.java - 本地缓存

public class CacheUtil {
    // 缓存本服务器的Channel
    public static Map<String, Channel> cacheChannel = 
        Collections.synchronizedMap(new HashMap<>());
    
    // 缓存服务器信息
    public static Map<Integer, ServerInfo> serverInfoMap = 
        Collections.synchronizedMap(new HashMap<>());
    
    // 缓存Netty服务器实例
    public static Map<Integer, NettyServer> serverMap = 
        Collections.synchronizedMap(new HashMap<>());
}

RedisUtil.java - Redis缓存

@Service("redisUtil")
public class RedisUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 保存用户信息到Redis
    public void pushObj(UserChannelInfo userChannelInfo) {
        redisTemplate.opsForHash().put(
            "itstack-demo-netty-2-09-user", 
            userChannelInfo.getChannelId(), 
            JSON.toJSONString(userChannelInfo)
        );
    }
    
    // 查询所有用户信息
    public List<UserChannelInfo> popList() {
        List<Object> values = redisTemplate.opsForHash()
            .values("itstack-demo-netty-2-09-user");
        List<UserChannelInfo> userChannelInfoList = new ArrayList<>();
        for (Object strJson : values) {
            userChannelInfoList.add(
                JSON.parseObject(strJson.toString(), UserChannelInfo.class)
            );
        }
        return userChannelInfoList;
    }
    
    // 移除用户信息
    public void remove(String channelId) {
        redisTemplate.opsForHash().delete(
            "itstack-demo-netty-2-09-user", channelId
        );
    }
}
4. Redis发布订阅机制

发布者配置(PublisherConfig.java)

@Configuration
public class PublisherConfig {
    @Bean
    public RedisTemplate<String, Object> redisMessageTemplate(
        RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setDefaultSerializer(
            new FastJsonRedisSerializer<>(Object.class)
        );
        return template;
    }
}

订阅者配置(ReceiverConfig.java)

@Configuration
public class ReceiverConfig {
    @Bean
    public RedisMessageListenerContainer container(
        RedisConnectionFactory connectionFactory, 
        MessageListenerAdapter msgAgreementListenerAdapter) {
        
        RedisMessageListenerContainer container = 
            new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        
        // 订阅主题:itstack-demo-netty-push-msgAgreement
        container.addMessageListener(
            msgAgreementListenerAdapter, 
            new PatternTopic("itstack-demo-netty-push-msgAgreement")
        );
        return container;
    }
    
    @Bean
    public MessageListenerAdapter msgAgreementListenerAdapter(
        MsgAgreementReceiver receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }
}

发布消息(Publisher.java)

@Service
public class Publisher {
    private final RedisTemplate<String, Object> redisMessageTemplate;
    
    public void pushMessage(String topic, MsgAgreement message) {
        // 向指定主题发布消息
        redisMessageTemplate.convertAndSend(topic, message);
    }
}

接收消息(MsgAgreementReceiver.java)

@Service
public class MsgAgreementReceiver extends AbstractReceiver {
    @Override
    public void receiveMessage(Object message) {
        logger.info("接收到PUSH消息:{}", message);
        
        // 解析消息
        MsgAgreement msgAgreement = JSON.parseObject(
            message.toString(), MsgAgreement.class
        );
        String toChannelId = msgAgreement.getToChannelId();
        
        // 从本地缓存查找目标用户的Channel
        Channel channel = CacheUtil.cacheChannel.get(toChannelId);
        if (null == channel) return;
        
        // 发送消息给目标用户
        channel.writeAndFlush(MsgUtil.obj2Json(msgAgreement));
    }
}
5. 消息路由逻辑

MyServerHandler.java

@Override
public void channelActive(ChannelHandlerContext ctx) {
    SocketChannel channel = (SocketChannel) ctx.channel();
    
    // 1. 保存用户信息到Redis(全局共享)
    UserChannelInfo userChannelInfo = new UserChannelInfo(
        channel.localAddress().getHostString(),
        channel.localAddress().getPort(),
        channel.id().toString(),
        new Date()
    );
    extServerService.getRedisUtil().pushObj(userChannelInfo);
    
    // 2. 保存到本地缓存(快速查找)
    CacheUtil.cacheChannel.put(channel.id().toString(), channel);
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object objMsgJsonStr) {
    MsgAgreement msgAgreement = MsgUtil.json2Obj(objMsgJsonStr.toString());
    String toChannelId = msgAgreement.getToChannelId();
    
    // 1. 先查找本地缓存,判断接收者是否在本服务器
    Channel channel = CacheUtil.cacheChannel.get(toChannelId);
    if (null != channel) {
        // 接收者在本服务器,直接发送
        channel.writeAndFlush(MsgUtil.obj2Json(msgAgreement));
        return;
    }
    
    // 2. 接收者不在本服务器,通过Redis发布消息
    logger.info("接收消息的用户不在本服务端,PUSH!");
    extServerService.push(msgAgreement);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
    // 连接断开,清理缓存
    extServerService.getRedisUtil().remove(ctx.channel().id().toString());
    CacheUtil.cacheChannel.remove(ctx.channel().id().toString());
}

ExtServerService.java - 扩展服务

@Service("extServerService")
public class ExtServerService {
    @Resource
    private Publisher publisher;
    @Resource
    private RedisUtil redisUtil;
    
    // 发布消息到Redis
    public void push(MsgAgreement msgAgreement) {
        publisher.pushMessage(
            "itstack-demo-netty-push-msgAgreement", 
            msgAgreement
        );
    }
    
    public RedisUtil getRedisUtil() {
        return redisUtil;
    }
}
6. Spring Boot集成

NettyController.java - HTTP接口管理

@Controller
public class NettyController {
    @Autowired
    private ExtServerService extServerService;
    @Resource
    private RedisUtil redisUtil;
    
    // 启动Netty服务
    @RequestMapping("/openNettyServer")
    @ResponseBody
    public EasyResult openNettyServer() {
        int port = NetUtil.getPort();  // 动态获取可用端口
        nettyServer = new NettyServer(
            new InetSocketAddress(port), extServerService
        );
        Future<Channel> future = executorService.submit(nettyServer);
        Channel channel = future.get();
        
        // 缓存服务器信息
        CacheUtil.serverInfoMap.put(port, 
            new ServerInfo(NetUtil.getHost(), port, new Date())
        );
        CacheUtil.serverMap.put(port, nettyServer);
        
        return EasyResult.buildSuccessResult();
    }
    
    // 查询用户列表
    @RequestMapping("/queryUserChannelInfoList")
    @ResponseBody
    public List<UserChannelInfo> queryUserChannelInfoList() {
        return redisUtil.popList();
    }
}

项目总结

核心技术:

  • ✅ Redis Pub/Sub:实现跨服务器消息传递
  • ✅ 两级缓存:本地内存+Redis,提高查询效率
  • ✅ 消息路由:先查本地,再查全局
  • ✅ Spring Boot集成:简化配置和管理

架构优势:

  1. 解耦服务器:服务器之间不需要直接连接
  2. 动态扩展:新增服务器无需修改现有服务器
  3. 连接数线性增长:N台服务器只需N个Redis连接
  4. 统一管理:通过Redis集中管理用户分布

适用场景:

  • IM即时通讯系统(微信、QQ)
  • 游戏服务器(跨服务器组队、交易)
  • 物联网平台(设备跨网关通信)
  • 分布式推送系统

注意事项:

  • ⚠️ Redis单点故障问题(建议使用Redis Cluster)
  • ⚠️ 消息可靠性(当前实现没有ACK确认)
  • ⚠️ 离线消息处理(需要额外存储)

优化建议:

// 1. 定向发送(避免广播)
String targetServer = redisUtil.getUserServer(toChannelId);
publisher.pushMessage("server-" + targetServer, msgAgreement);

// 2. 消息确认机制
public void sendWithAck(MsgAgreement msg) {
    String ackId = UUID.randomUUID().toString();
    msg.setAckId(ackId);
    publisher.pushMessage(topic, msg);
    // 等待ACK或超时重发
}

// 3. 离线消息存储
if (channel == null) {
    redisUtil.saveOfflineMsg(toChannelId, msgAgreement);
}

🎯 三大项目核心对比

技术选型对比

项目核心技术解决问题应用场景
2-07Future + CountDownLatch异步转同步RPC框架、同步调用
2-08IdleStateHandler + 重连连接保活长连接系统、IM
2-09Redis Pub/Sub + 集群跨服务器通信分布式IM、游戏

核心知识点总结

1. 同步通信(项目2-07)

核心原理:

发送请求 → 创建Future → 阻塞等待 → 收到响应 → 唤醒线程

关键技术:

  • CountDownLatch:线程阻塞和唤醒
  • 请求ID映射:准确匹配请求和响应
  • 超时控制:避免无限等待

类比理解:

  • 像打电话:拨号后等待对方接听,接听后才能继续
2. 心跳重连(项目2-08)

核心原理:

空闲检测 → 触发事件 → 发送心跳/关闭连接 → 自动重连

关键技术:

  • IdleStateHandler:自动检测空闲状态
  • ChannelFutureListener:监听连接结果
  • EventLoop.schedule:延迟重连

类比理解:

  • 像微信聊天:定期检测网络,断线后自动重连
3. 集群通信(项目2-09)

核心原理:

本地查找 → 未找到 → Redis发布 → 其他服务器订阅 → 转发消息

关键技术:

  • Redis Pub/Sub:消息中转站
  • 两级缓存:本地+Redis
  • 消息路由:智能转发

类比理解:

  • 像快递系统:不是站点直连,而是通过分拨中心转发

bugstack虫洞栈 | netty4.1案例文章列表汇总,跟着案例学Netty itstack-demo-netty netty4.x案例 微信公众号:bugstack虫洞栈,欢迎您的关注&获取更多案例与源码!博客栈:https://bugstack.cn 本专题针对netty4.x分为从;基础入门篇(九篇)、中级拓展篇(八篇)、高级应用篇(三章),由浅入深实现netty服务功能,介绍netty如何从上手到实战。 基础入门篇(九篇) netty案例,netty4.1... 阅读详情

相关推荐

netty案例,netty4.1中级拓展篇一《Netty与SpringBoot整合》

前言介绍 在实际的开发中,我们需要对netty服务进行更多的操作,包括;获取它的状态信息、启动/停止、对客户端用户强制下线等等,为此我们需要把netty服务加入到web系统中,那么本章节介绍如何将Netty与SpringBoot整合。 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使...

bugstack虫洞栈 2642

Netty实战应用学习笔记-中级拓展篇(一)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 1072

netty案例,netty4.1中级拓展篇九《Netty集群部署实现跨服务端通信的落地方案》

前言介绍 Netty的性能非常好,在一些小型用户体量的socket服务内,仅部署单台机器就可以满足业务需求。但当遇到一些中大型用户体量的服务时,就需要考虑讲Netty按照集群方式部署,以更好的满足业务诉求。但Netty部署集群后都会遇到跨服务端怎么通信,也就是有集群服务X和Y,用户A链接服务X,用户B链接服务Y,那么他们都不在一个服务内怎么通信?本章节将介绍一种实现方式案例,以满足跨服务之间的用...

bugstack虫洞栈 5318

Netty实战应用学习笔记-中级拓展篇(六)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 630

netty案例,netty4.1中级拓展篇五《基于Netty搭建WebSocket,模仿微信聊天页面》

前言介绍 本章节我们模仿微信聊天页面,开发一个基于Netty搭建WebSocket通信案例。Netty的应用方面非常广;聊天、MQ、RPC、数据等等,在5G到来的时候更加需要大量数据传输,Netty的应用也会更加广阔。 1、这个案例使用SpringBoot+Netty+WebSocket搭建功能。 2、使用Netty提供的HttpServerCodec、HttpObjectAggregator、...

bugstack虫洞栈 3072

netty案例,netty4.1中级拓展篇七《Netty请求响应同步通信》

前言介绍 在我们实现开发RPC框架的时候,需要选择socket的通信方式。而我们知道一般情况下socket通信类似与qq聊天,发过去消息,什么时候回复都可以。但是我们RPC框架通信,从感觉上类似http调用,需要在一定时间内返回,否则就会发生超时断开。这里我们选择netty作为我们的socket框架,采用future方式进行通信。 Dubbo:国内最早开源的 RPC 框架,由阿里巴巴公司开发并于...

bugstack虫洞栈 2265

Netty实战应用学习笔记-中级拓展篇(十二)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 1087

netty案例,netty4.1中级拓展篇三《Netty传输Java对象》

前言介绍 Netty在实际应用级开发中,有时候某些特定场景下会需要使用Java对象类型进行传输,但是如果使用Java本身序列化进行传输,那么对性能的损耗比较大。为此我们需要借助protostuff-core的工具包将对象以二进制形式传输并做编码解码处理。与直接使用protobuf二进制传输方式不同,这里不需要定义proto文件,而是需要实现对象类型编码解码器,用以传输自定义Java对象。 pro...

bugstack虫洞栈 2142

Netty实战应用学习笔记-中级拓展篇(八)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 640

netty案例,netty4.1中级拓展篇十二《Netty流量整形数据流速率控制分析与实战》

作者:付政委 | 关注微信公众号:bugstack虫洞栈,获取专题案例源码 前言介绍 你是否使用过某盘,在前几年我们使用的时候速度飞快,上传下载嗖嗖嗖。但是近年来只要不办会员,速度慢的像蜗牛,当然人家也得赚钱我们能理解。那么这样的限速是怎么实现的呢,我们这个案例使用Netty的流量整形进行限速传输,测试过程中当你把流量整形功能去掉后你就是年费VIP。 流量整形(Traffic Shaping...

bugstack虫洞栈 3696

netty案例,netty4.1中级拓展篇二《Netty使用Protobuf传输数据》

前言介绍 在netty数据传输过程中可以有很多选择,比如;字符串、json、xml、java对象,但为了保证传输的数据具备;良好的通用性、方便的操作性和传输的高性能,我们可以选择protobuf作为我们的数据传输格式。目前protobuf可以支持;C++、C#、Dart、Go、Java、Python等,也可以在JS里使用。知识点;ProtobufDecoder、ProtobufEncoder、Pr...

bugstack虫洞栈 2080

Netty实战应用学习笔记-中级拓展篇(十三)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 570

netty案例,netty4.1中级拓展篇八《Netty心跳服务与断线重连》

前言介绍 在我们使用netty中,需要监测服务是否稳定以及在网络异常链接断开时候可以自动重连。需要实现监听;f.addListener(new MyChannelFutureListener()) 环境准备 1、jdk1.8【jdk1.7以下只能部分支持netty】 2、Netty4.1.36.Final【netty3.x 4.x 5每次的变化较大,接口类名也随着变化】 代码示例 itstack-...

bugstack虫洞栈 2069

Netty实战应用学习笔记-中级拓展篇(五)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 636

netty案例,netty4.1中级拓展篇四《Netty传输文件、分片发送、断点续传》

前言介绍 1、在实际应用中我们经常使用到网盘服务,他们可以高效的上传下载较大文件。那么这些高性能文件传输服务,都需要实现的分片发送、断点续传功能。 2、在Java文件操作中有RandomAccessFile类,他可以支持文件的定位读取和写入,这样就满足了我们对文件分片的最基础需求。 3、Netty服务端启动后,可以向客户端发送文件传输指令;允许接收文件、控制读取位点、记录传输标记、文件接收完成。 ...

bugstack虫洞栈 3229

RK3588+TRL8367s四网口千兆交换机硬件调试与DTS配置实战

本文详细介绍了基于RK3588处理器与RTL8367S交换机芯片构建四网口千兆交换设备的硬件调试与DTS配置实战。文章深入对比了MDIO与SMI两种通讯方式的配置差异,提供了详尽的设备树代码示例,并总结了常见问题的排查思路,旨在帮助开发者高效完成嵌入式网络设备的软硬件集成与调试工作。

weixin_28308325的博客 470

Netty实战应用学习笔记-中级拓展篇(七)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 322

5步搞定ABB机器人RobotStudio与S7-1500PLC的Profinet通讯(附PLCSIM调试技巧)

本文详细介绍了如何利用RobotStudio与TIA Portal软件,结合PLCSIM Advanced仿真器,在零硬件条件下实现ABB机器人与西门子S7-1500 PLC之间的Profinet通讯虚拟调试。通过五个核心步骤,从环境搭建、网络配置、信号映射到联合仿真与高级调试,帮助工程师在项目前期高效验证逻辑、压缩现场调试周期,并掌握实用的PLCSIM调试技巧。

weixin_29188043的博客 506

Netty实战应用学习笔记-中级拓展篇(三)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 306

Vivado图形化界面烧写MCS文件到SPI FLASH全流程(附常见报错解决方案)

本文详细介绍了使用Vivado图形化界面将MCS文件烧录至SPI FLASH的完整操作流程。内容涵盖从环境准备、MCS文件生成到硬件连接与烧写的每一步,并针对SPI总线宽度报错、FLASH型号识别失败等常见问题提供了深度解决方案,帮助用户高效完成FPGA配置,摆脱对TCL命令行的依赖。

s8t9u0v1w的博客 536

Vue - 在纯 HTML 普通项目中实现组件化,让原生 html 项目支持引入 *.vue 组件 / 组件与组件间的互相引用等,完美解决了引入组件出现的跨域问题(保姆级详细教程,完整示例源码及插件)

html中使用vue组件,在HTML页面中引入vue组件,HTML界面利用http-vue-loader.js以组件方式引入vue文件,使用http-vue-loader引入组件出现报错,在html页面使用http-vue-loader插件引入组件报跨域错误,如何解决http-vue-loader引用组件时出现跨域问题,纯html原生web项目引入vue后如何实现组件引入,使用http-vue-loader插件控制台报错跨域,http-vue-loader跨域怎么办官方文档,html页面如何引入.vu

🏆 前端领域优质创作者 3909

Netty实战应用学习笔记-中级拓展篇(二)

Netty实战应用学习笔记-中级拓展篇

java_rutu的博客 301

UDF学习(四)FLUENT UDF-DEFINE_PROFILE宏

利用DEFINE_PROFLILE可以指定以下边界或计算域信息:DEFINE_PROFILE(name,t,i)//既可以用。

weixin_44386973的博客 1806
上一篇: Netty实战应用学习笔记-中级拓展篇(八)
下一篇: Netty实战应用学习笔记-中级拓展篇(十)
不早睡不改名@
博客等级 码龄5年 536粉丝 306原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值