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

##

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() {
     
     				@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() {
      
       	@Override
       	protected void initChannel(NioSocketChannel ch) throws Exception {
       		// 取出属性
       		Attribute 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);
      
    3. 总结:

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

      1. 客户端Channel设置的attr是否会被服务端接收到,并且以此进行必要的参数传递?
        1. 答: 客户端Channel 会被服务端接收到。
    5. 你可能感兴趣的:(Netty)