Netty实战 IM即时通讯系统(二)Netty简介

##

Netty实战 IM即时通讯系统(二)Netty简介

零、 目录

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

二、 Netty简介

  1. 回顾IO编程
    1. 场景: 客户端每隔两秒发送一个带有时间戳的“hello world”给服务端 , 服务端收到之后打印。

    2. 代码:

       IOServer.java
       /**
        * @author 闪电侠
        */
       public class IOServer {
           public static void main(String[] args) throws Exception {
       
               ServerSocket serverSocket = new ServerSocket(8000);
       
               // (1) 接收新连接线程
               new Thread(() -> {
                   while (true) {
                       try {
                           // (1) 阻塞方法获取新的连接
                           Socket socket = serverSocket.accept();
       
                           // (2) 每一个新的连接都创建一个线程,负责读取数据
                           new Thread(() -> {
                               try {
                                   int len;
                                   byte[] data = new byte[1024];
                                   InputStream inputStream = socket.getInputStream();
                                   // (3) 按字节流方式读取数据
                                   while ((len = inputStream.read(data)) != -1) {
                                       System.out.println(new String(data, 0, len));
                                   }
                               } catch (IOException e) {
                               }
                           }).start();
       
                       } catch (IOException e) {
                       }
       
                   }
               }).start();
           }
       }
      
      
      
      
       IOClient.java
       /**
        * @author 闪电侠
        */
       public class IOClient {
       
           public static void main(String[] args) {
               new Thread(() -> {
                   try {
                       Socket socket = new Socket("127.0.0.1", 8000);
                       while (true) {
                           try {
                               socket.getOutputStream().write((new Date() + ": hello world").getBytes());
                               Thread.sleep(2000);
                           } catch (Exception e) {
                           }
                       }
                   } catch (IOException e) {
                   }
               }).start();
           }
       }
      
    3. IO编程,模型在客户端较少的场景下运行良好 , 但是客户端比较多的业务来说 , 单机服务端可能需要支撑成千上万的连接, IO模型可能就不太合适了 , 原因:

      1. 在传统的IO模型中 , 每一个连接创建成功之后都需要一个线程来维护 , 每个线程包含一个while死循环, 那么1W个连接就对应1W个线程 , 继而1W个死循环。
      2. 线程资源受限: 线程是操作系统中非常宝贵的资源 , 同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统开销太大。
      3. 线程切换效率低下: 单机CPU核数固定 , 线程爆炸之后操作系统频繁的进行线程切换 , 应用性能几句下降
      4. IO编程中 , 数据读写是以字节流为单位。
    4. 为了解决这些问题 , JDK1.4之后提出了NIO

  2. NIO 编程
    1. NIO 是如何解决一下三个问题。

      1. 线程资源受限
        1. NIO编程模型中 , 新来一个连接不再创建一个新的线程, 而是可以把这条连接直接绑定在某个固定的线程 , 然后这条连接所有的读写都由这个线程来负责 , 那么他是怎么做到的? Netty实战 IM即时通讯系统(二)Netty简介_第1张图片
          1. 如上图所示,IO 模型中,一个连接来了,会创建一个线程,对应一个 while 死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w 个连接里面同一时刻只有少量的连接有数据可读,因此,很多个 while 死循环都白白浪费掉了,因为读不出啥数据。
          2. 而在 NIO 模型中,他把这么多 while 死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个 while 死循环就能监测1w个连接是否有数据可读的呢? 这就是 NIO 模型中 selector 的作用,一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,而是直接把这条连接注册到 selector 上,然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明 IO 与 NIO 的区别。
          3. 在一家幼儿园里,小朋友有上厕所的需求,小朋友都太小以至于你要问他要不要上厕所,他才会告诉你。幼儿园一共有 100 个小朋友,有两种方案可以解决小朋友上厕所的问题:
            1. 每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100 个小朋友就需要 100 个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。
            2. 所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是 NIO 模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。
          4. 这就是 NIO 模型解决线程资源受限的方案,实际开发过程中,我们会开多个线程,每个线程都管理着一批连接,相对于 IO 模型中一个线程管理一条连接,消耗的线程资源大幅减少
      2. 线程切换效率低下
        1. 由于NIO模型中线程数量大大降低 , 线程切换的效率也因此大幅度提高
      3. IO读写面向流
        1. IO读写是面向流的 , 一次性只能从流中读取一个或多个字节 , 并且读完之后无法再次读取 , 你需要自己缓存数据 , 而NIO的读写是面向Buffer的 , 你可以随意读取里面的任何一个字节数据 , 不需要你自己缓存数据 , 这一切只需要移动读写指针即可。
    2. 原生NIO 实现

      /**
       * 服务端
       * */
      class NIO_server_test_01{
      	
      	public static void start () throws IOException {
      		Selector serverSelect = Selector.open();
      		Selector clientSelect = Selector.open();
      		
      		new Thread(() -> {
      			try {
      				ServerSocketChannel socketChannel = ServerSocketChannel.open();
      				socketChannel.socket().bind(new InetSocketAddress(8000)); // 监听端口
      				socketChannel.configureBlocking(false); // 是否阻塞
      				socketChannel.register(serverSelect, SelectionKey.OP_ACCEPT);
      				
      				while ( true ) {
      					// 检测是否有新的连接
      					if(serverSelect.select(1) > 0) {  // 1 是超时时间     select 方法返回当前连接数量
      						Set set = serverSelect.selectedKeys();
      						
      						set.stream()
      							.filter(key -> key.isAcceptable())
      							.collect(Collectors.toList())
      							.forEach(key ->{
      								try {
      									// 每次来一个新的连接, 不需要创建新的线程 , 而是注册到clientSelector
      									SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
      									clientChannel.configureBlocking(false);
      									clientChannel.register(serverSelect, SelectionKey.OP_ACCEPT);
      								}catch(Exception e) {
      									e.printStackTrace();
      								}finally {
      									set.iterator().remove();
      								}
      							});
      					}
      				}
      			}catch (Exception e) {
      				e.printStackTrace();
      			}
      		}).start();
      		
      		
      		new Thread(() -> {
      			try {
      				// 批量轮询  有哪些连接有数据可读
      				while ( true ) {
      					if(clientSelect.select(1) > 0) {
      						clientSelect.selectedKeys().stream()
      							.filter(key -> key.isReadable())
      							.collect(Collectors.toList())
      							.forEach(key -> {
      								try {
      									SocketChannel clientChannl = (SocketChannel) key.channel();
      									ByteBuffer bf = ByteBuffer.allocate(1024);
      									// 面向byteBuffer
      									clientChannl.read(bf);
      									bf.flip();
      									System.out.println(Charset.defaultCharset().newDecoder().decode(bf).toString());
      								}catch ( Exception e) {
      									e.printStackTrace();
      								}finally {
      									clientSelect.selectedKeys().iterator().remove();
      									key.interestOps(SelectionKey.OP_READ);
      								}
      								
      							});
      					}
      				}
      			}catch (Exception e) {
      				e.printStackTrace();
      			}
      		}).start();
      	}
      	
      }   
      
      1. 通常NIO 模型中会有两个线程每个线程中绑定一个轮询器selector , 在我们的例子中serverSelector负责轮询是否有新的连接 , clientSelector 负责轮询连接中是否有数据可读。
      2. 服务端检测到新的连接之后 , 不在创建一个新的线程 , 而是直接将连接注册到clientSelector中
      3. clientorSelector 被一个while死循环抱着 , 如果在某一时刻有多个连接数据可读 ,数据将会被clientSelector.select() 方法轮询出来。 进而批量处理 。
      4. 数据的读写面向buffer 而不是面向流。
    3. 原生NIO 进行网络开发的缺点:

      1. JDK 的NIO 编程需要了解很多概念, 编程复杂 , 对NIO 入门很不友好 , 编程模型不友好 , ByteBuffer的API简直反人类 (这是书里这么说的 , 不要喷我)。
      2. 对NIO 编程来说 , 一个比较适合的线程模型能充分发挥它的优势 , 而JDK没有给你实现 , 你需要自己实现 , 就连简单的协议拆包都要自己实现 (我感觉这样才根据创造力呀 )
      3. JDK NIO 底层由epoll 实现 , 该实现饱受诟病的空轮训bug会导致cpu 飙升100%
      4. 项目庞大之后 , 自己实现的NIO 很容易出现各类BUG , 维护成本高 (作者怎么把自己的过推向JDK haha~)
      5. 正因为如此 , 我连客户端的代码都懒得给你写了 (这作者可真够懒的) , 你可以直接使用IOClient 和NIO_Server 通信
    4. JDK 的NIO 犹如带刺的玫瑰 , 虽然美好 , 让人向往 , 但是使用不当会让你抓耳挠腮 , 痛不欲生 , 正因为如此 , Netty横空出世!(作者这才华 啧啧啧~)

  3. Netty 编程
    1. Netty到底是何方神圣(被作者吹上天了都) , 用依据简单的话来说就是: Netty 封装了JDK 的NIO , 让你使用更加干爽 (干爽???) , 你不用在写一大堆复杂的代码了 , 用官方的话来说就是: Netty是一个异步事件驱动的网络应用框架 , 用于快速开发可维护的高性能服务器和客户端。
    2. Netty 相比 JDK 原生NIO 的优点 :
      1. 使用NIO 需要了解太多概念, 编程复杂 , 一不小心 BUG 横飞
      2. Netty 底层IO模型随意切换 , 而这一切只需要小小的改动 , 改改参数 , Netty乐意直接从NIO模型转换为IO 模型 。
      3. Netty 自带的拆包解包 , 异常检测可以让你从NIO 的繁重细节中脱离出来 , 让你只关心业务逻辑 。
      4. Netty 解决了JDK 的很多包括空轮训在内的BUG
      5. Netty社区活跃 , 遇到问题可以轻松解决
      6. Netty 已经经历各大RPC 框架 , 消息中间价 , 分布式通信中间件线上的广泛验证 , 健壮性无比强大
    3. 代码实例
      1. maven 依赖

        
            io.netty
            netty-all
            4.1.6.Final
        
        
      2. NettyServer

         /**
          * @author outman
          * */
         class Netty_server_02 {
         	public void start () {
         		ServerBootstrap serverBootstrap = new ServerBootstrap();
         		
         		NioEventLoopGroup boss = new NioEventLoopGroup();
         		NioEventLoopGroup woker = new NioEventLoopGroup();
         		
         		serverBootstrap.group(boss ,woker)
         			.channel(NioServerSocketChannel.class)
         			.childHandler(new ChannelInitializer() {
         
         				@Override
         				protected void initChannel(NioSocketChannel ch) throws Exception {
         					ch.pipeline().addLast(new StringDecoder());
         					ch.pipeline().addLast(new SimpleChannelInboundHandler() {
         
         						@Override
         						protected void channelRead0(ChannelHandlerContext cxt, String msg) throws Exception {
         							System.out.println(msg);
         							
         						}
         					});
         					
         				}
         				
         			}).bind(8000);
         	}
         }
        
        1. 这么一小段代码就实现了我们前面NIO 编程中所有的功能 , 包括服务端启动 , 接收新连接 , 打印客户端传来的数据。
        2. 将NIO 中的概念与IO模型结合起来理解:
          1. boss 对应 IOServer 中接收新连接创建线程 , 主要负责创建新连接
          2. worker 对应 IOServer 中负责读取数据的线程 , 主要用于数据读取语句业务逻辑处理 。
          3. 详细逻辑会在后续深入讨论
      3. NettyClient

        /**
        * @author outman
        * */
        class Netty_client_02 {

           public static void main(String[] args) throws InterruptedException {
           	Bootstrap bootstrap = new Bootstrap();
           	NioEventLoopGroup group = new NioEventLoopGroup();
        
           	bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer() {
           		@Override
           		protected void initChannel(Channel ch) {
           			ch.pipeline().addLast(new StringEncoder());
           		}
           	});
        
           	Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
        
           	while (true) {
           		channel.writeAndFlush(new Date() + ": hello world!");
           		Thread.sleep(2000);
           	}
           }
        

        }

      4. 在客户端程序中 , group 对应了我们IOClient 中 新起的线程。

      5. 剩下的逻辑 我们在后文中详细分析 , 现在你可以把 Netty_server_02 和Netty_client_02 复制到 你的IDE 中 运行起来 感受世界 的美好 (注意 先启动 服务端 再启动客户端 )

      6. 使用Netty 之后 整个世界都美好了, 一方面 Netty 对NIO 封装的如此完美 , 另一方面 , 使用Netty 之后 , 网络通信这块的性能问题几乎不用操心 , 尽情的让Netty 榨干你的CPU 吧~~

你可能感兴趣的:(Netty)