欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Netty实战 IM即时通讯系统(五)客户端启动流程

程序员文章站 2022-03-02 15:25:24
...

##

Netty实战 IM即时通讯系统(五)客户端启动流程

零、 目录

  1. IM系统简介
  • Netty 简介
  • Netty 环境配置
  • 服务端启动流程
  • 实战: 客户端和服务端双向通信
  • 数据传输载体ByteBuf介绍
  • 客户端与服务端通信协议编解码
  • 实现客户端登录
  • 实现客户端与服务端收发消息
  • pipeline与channelHandler
  • 构建客户端与服务端pipeline
  • 拆包粘包理论与解决方案
  • channelHandler的生命周期
  • 使用channelHandler的热插拔实现客户端身份校验
  • 客户端互聊原理与实现
  • 群聊的发起与通知
  • 群聊的成员管理(加入与退出,获取成员列表)
  • 群聊消息的收发及Netty性能优化
  • 心跳与空闲检测
  • 总结
  • 扩展

五、 客户端启动流程

  1. 客户端启动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("连接失败");
     				}
     			});
     	}
     
     }
    
    1. 从上面的代码可以看出 , 客户端的启动引导类是BootStrap , 负责启动客户端以及连接服务端 , 而上面一小节我们在描述服务端启动的时候, 这个引导类是 ServerBootStrap , 引导类创建完成之后我们描述一下客户端启动流程:
      1. 首先和服务端的启动流程一样 , 我们需要给特指定线程模型, 驱动着连接的数据读写
      2. 然后我们指定IO 模型为 NioSocketChannel , 表示IO模型为 NIO
      3. 接着, 我们给引导类指定一个handler , 这里主要就是定义连接的业务处理逻辑 , 不理解没有关系 , 我们在后面会详解
      4. 配置完线程模型 , IO 模型 , 业务处理逻辑之后 , 调用connect() 方法进行连接 , 可以看到connect() 方法有两个参数 , 第一个参数可以填写IP或域名 ,第二个参数填写的是端口号 , 由于connect() 方法返回的是一个Future , 也就是说这个方法是异步的 , 我们通过addListener方法可以监听到连接是否成功 , 进而打印连接状态
    2. 到这里一个客户端的demo 就完成了 , 其实只要和客户端Socket 编程模型对应起来 , 这里的三个概念就会显得非常简单
  2. 失败重连

    1. 在网络差的情况下 , 客户端第一次连接可能会失败 , 这个时候我们可能会尝试重新连接 , 重新连接的逻辑写在连接失败的逻辑块里

       // 建立连接
       bootstrap.connect("127.0.0.1" , 8000)
       	.addListener(future ->{
       		if(future.isSuccess()) {
       			System.out.println("连接成功");
       		}else {
       			System.out.println("连接失败");
       			
       			//TODO: 重新连接逻辑
       		}
       	});
      
    2. 重新连接时依然是调用相同的逻辑 , 所以我们把连接的代码抽取出来, 实现代码复用 , 在连接失败的情况下使用递归的方法 实现重连

       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. 以上代码就实现了重连机制 , 但是在通常情况下连接失败不会立即重连 , 而是通过一个指数退避的方式 , 比如 每隔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次重连...
         到达重试最大次数,放弃重连
        
        1. 从上面的代码中我们可以看到 , 通过判断是否连接成功以及剩余重试次数 , 分别执行不同的逻辑
          1. 如果连接成功则打印连接成功的消息
          2. 如果连接失败 , 但是重试次数已经用完则放弃连接
          3. 如果连接失败 , 但是连接没有用完则计算下一次重试时间间隔 , 然后定时重连
        2. 从上面代码中我们可以看到 , 定时任务是调用bootstrap.config().group().schedule() , 其中bootStrap.config()这个方法返回的是BootStrapConfig , 他是对BootStrap 参数配置的抽象 , 然后ootstrap.config().group() 返回的就是我们一开始设置的线程模型workerGroup , 最后调用schedule() 方法就可以实现定时任务逻辑了。
  3. 客户端启动其他方法

    1. 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());
       	}
       });
      
    2. option(): option()可以给连接设置一些TCP底层的相关属性 : (ChannelOption相关参数详解在 上一节《服务端启动流程》中有连接地址)

       // 设置TCP 相关的属性
       // 设置连接超时时间
       bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
       // 开启TCP 心跳机制
       bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
      
  4. 总结:

    1. 在本节中我们学习了Netty客户端启动的流程 , 一句话来说就是: 创建一个引导类 , 然后给他指定线程 , IO模型 , 指定业务逻辑 ,连接特定的IP:port 客户端就启动起来了
    2. 然后我们学习到 connect()方法时异步的 , 我们可以通过异步回调机制来实现指数退避重连机制。
    3. 最后我们讨论了Netty客户端启动的额外参数 , 只要包括给客户端Channel 绑定自定义属性 , 设置TCP底层参数。
  5. 疑问:

    1. 客户端Channel设置的attr是否会被服务端接收到,并且以此进行必要的参数传递?
      1. 答: 客户端Channel 会被服务端接收到。
相关标签: Netty