Netty实战 IM即时通讯系统(五)客户端启动流程
程序员文章站
2022-03-02 15:25:24
...
##
零、 目录
- IM系统简介
- Netty 简介
- Netty 环境配置
- 服务端启动流程
- 实战: 客户端和服务端双向通信
- 数据传输载体ByteBuf介绍
- 客户端与服务端通信协议编解码
- 实现客户端登录
- 实现客户端与服务端收发消息
- pipeline与channelHandler
- 构建客户端与服务端pipeline
- 拆包粘包理论与解决方案
- channelHandler的生命周期
- 使用channelHandler的热插拔实现客户端身份校验
- 客户端互聊原理与实现
- 群聊的发起与通知
- 群聊的成员管理(加入与退出,获取成员列表)
- 群聊消息的收发及Netty性能优化
- 心跳与空闲检测
- 总结
- 扩展
五、 客户端启动流程
-
客户端启动demo
/** * 客户端启动流程 * */ public class Test_05_客户端启动流程 { public static void main(String[] args) { NioEventLoopGroup workerGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap(); bootstrap // 指定线程模型 .group(workerGroup) // 指定IO 模型 .channel(NioSocketChannel.class) // 指定业务处理逻辑 .handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { } }); // 建立连接 bootstrap.connect("127.0.0.1" , 8000) .addListener(future ->{ if(future.isSuccess()) { System.out.println("连接成功"); }else { System.out.println("连接失败"); } }); } }
- 从上面的代码可以看出 , 客户端的启动引导类是BootStrap , 负责启动客户端以及连接服务端 , 而上面一小节我们在描述服务端启动的时候, 这个引导类是 ServerBootStrap , 引导类创建完成之后我们描述一下客户端启动流程:
- 首先和服务端的启动流程一样 , 我们需要给特指定线程模型, 驱动着连接的数据读写
- 然后我们指定IO 模型为 NioSocketChannel , 表示IO模型为 NIO
- 接着, 我们给引导类指定一个handler , 这里主要就是定义连接的业务处理逻辑 , 不理解没有关系 , 我们在后面会详解
- 配置完线程模型 , IO 模型 , 业务处理逻辑之后 , 调用connect() 方法进行连接 , 可以看到connect() 方法有两个参数 , 第一个参数可以填写IP或域名 ,第二个参数填写的是端口号 , 由于connect() 方法返回的是一个Future , 也就是说这个方法是异步的 , 我们通过addListener方法可以监听到连接是否成功 , 进而打印连接状态
- 到这里一个客户端的demo 就完成了 , 其实只要和客户端Socket 编程模型对应起来 , 这里的三个概念就会显得非常简单
- 从上面的代码可以看出 , 客户端的启动引导类是BootStrap , 负责启动客户端以及连接服务端 , 而上面一小节我们在描述服务端启动的时候, 这个引导类是 ServerBootStrap , 引导类创建完成之后我们描述一下客户端启动流程:
-
失败重连
-
在网络差的情况下 , 客户端第一次连接可能会失败 , 这个时候我们可能会尝试重新连接 , 重新连接的逻辑写在连接失败的逻辑块里
// 建立连接 bootstrap.connect("127.0.0.1" , 8000) .addListener(future ->{ if(future.isSuccess()) { System.out.println("连接成功"); }else { System.out.println("连接失败"); //TODO: 重新连接逻辑 } });
-
重新连接时依然是调用相同的逻辑 , 所以我们把连接的代码抽取出来, 实现代码复用 , 在连接失败的情况下使用递归的方法 实现重连
public static void connect(Bootstrap bootstrap, String IP, int port) { // 建立连接 bootstrap.connect(IP, port).addListener(future -> { if (future.isSuccess()) { System.out.println("连接成功"); } else { System.out.println("连接失败,执行重连"); connect(bootstrap, IP, port); } }); }
-
以上代码就实现了重连机制 , 但是在通常情况下连接失败不会立即重连 , 而是通过一个指数退避的方式 , 比如 每隔1秒、2秒、4秒、8秒 , 以2的次幂来实现建立连接 , 然后到达一定次数之后就放弃重连
connect(bootstrap , "127.0.0.1" , 8000 , 5); public static void connect(Bootstrap bootstrap, String IP, int port ,int maxRetry , int... retryIndex) { // 建立连接 bootstrap.connect(IP, port).addListener(future -> { // 由于闭包特性 不能修改外部的变量 所有需要在闭包内定义一个相同的变量 拷贝外部变量的值 int[] finalRetryIndex ; if (future.isSuccess()) { System.out.println("连接成功"); } else if(maxRetry == 0){ System.out.println("到达重试最大次数,放弃重连"); }else { // 初始化 重试计数 if(retryIndex.length == 0) { finalRetryIndex = new int[] {0}; }else { finalRetryIndex = retryIndex; } //计算时间间隔 int delay = 1 << finalRetryIndex[0]; // 执行重试 System.out.println(new Date()+"连接失败,剩余重连次数:"+maxRetry+","+delay+"秒后执行第"+(finalRetryIndex[0]+1)+"次重连..."); bootstrap.config().group().schedule(()->{ connect(bootstrap, IP, port , maxRetry-1 , finalRetryIndex[0]+1); }, delay, TimeUnit.SECONDS); } }); } 执行结果: Thu Dec 27 11:04:19 CST 2018连接失败,剩余重连次数:5,1秒后执行第1次重连... Thu Dec 27 11:04:21 CST 2018连接失败,剩余重连次数:4,2秒后执行第2次重连... Thu Dec 27 11:04:24 CST 2018连接失败,剩余重连次数:3,4秒后执行第3次重连... Thu Dec 27 11:04:29 CST 2018连接失败,剩余重连次数:2,8秒后执行第4次重连... Thu Dec 27 11:04:38 CST 2018连接失败,剩余重连次数:1,16秒后执行第5次重连... 到达重试最大次数,放弃重连
- 从上面的代码中我们可以看到 , 通过判断是否连接成功以及剩余重试次数 , 分别执行不同的逻辑
- 如果连接成功则打印连接成功的消息
- 如果连接失败 , 但是重试次数已经用完则放弃连接
- 如果连接失败 , 但是连接没有用完则计算下一次重试时间间隔 , 然后定时重连
- 从上面代码中我们可以看到 , 定时任务是调用bootstrap.config().group().schedule() , 其中bootStrap.config()这个方法返回的是BootStrapConfig , 他是对BootStrap 参数配置的抽象 , 然后ootstrap.config().group() 返回的就是我们一开始设置的线程模型workerGroup , 最后调用schedule() 方法就可以实现定时任务逻辑了。
- 从上面的代码中我们可以看到 , 通过判断是否连接成功以及剩余重试次数 , 分别执行不同的逻辑
-
-
-
客户端启动其他方法
-
attr(): attr()方法可以给客户端channel也就是NioSocketChannel绑定自定义属性 , 然后我们通过channel.attr()取出属性。 说白了就是给NioSocketChannel维护一个Map 而已
//设置属性 bootstrap.attr(AttributeKey.newInstance("clientName"), "NettyClient"); //业务逻辑 bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { // 取出属性 Attribute<Object> attr = ch.attr(AttributeKey.valueOf("clientName")); System.out.println("客户端名称:"+attr.get()); } });
-
option(): option()可以给连接设置一些TCP底层的相关属性 : (ChannelOption相关参数详解在 上一节《服务端启动流程》中有连接地址)
// 设置TCP 相关的属性 // 设置连接超时时间 bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000); // 开启TCP 心跳机制 bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
-
-
总结:
- 在本节中我们学习了Netty客户端启动的流程 , 一句话来说就是: 创建一个引导类 , 然后给他指定线程 , IO模型 , 指定业务逻辑 ,连接特定的IP:port 客户端就启动起来了
- 然后我们学习到 connect()方法时异步的 , 我们可以通过异步回调机制来实现指数退避重连机制。
- 最后我们讨论了Netty客户端启动的额外参数 , 只要包括给客户端Channel 绑定自定义属性 , 设置TCP底层参数。
-
疑问:
- 客户端Channel设置的attr是否会被服务端接收到,并且以此进行必要的参数传递?
- 答: 客户端Channel 会被服务端接收到。
- 客户端Channel设置的attr是否会被服务端接收到,并且以此进行必要的参数传递?
上一篇: 多对多双向关联
下一篇: JPA OneToMany双向关联