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

零、 目录

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

二、 Netty简介

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

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(); } }

  1. IO编程,模型在客户端较少的场景下运行良好 , 但是客户端比较多的业务来说 , 单机服务端可能需要支撑成千上万的连接, IO模型可能就不太合适了 , 原因:
  2. 在传统的IO模型中 , 每一个连接创建成功之后都需要一个线程来维护 , 每个线程包含一个while死循环, 那么1W个连接就对应1W个线程 , 继而1W个死循环。
  3. 线程资源受限: 线程是操作系统中非常宝贵的资源 , 同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统开销太大。
  4. 线程切换效率低下: 单机CPU核数固定 , 线程爆炸之后操作系统频繁的进行线程切换 , 应用性能几句下降
  5. IO编程中 , 数据读写是以字节流为单位。
  6. 为了解决这些问题 , JDK1.4之后提出了NIO
  7. NIO 编程
  8. NIO 是如何解决一下三个问题。
  9. 线程资源受限
  10. NIO编程模型中 , 新来一个连接不再创建一个新的线程, 而是可以把这条连接直接绑定在某个固定的线程 , 然后这条连接所有的读写都由这个线程来负责 , 那么他是怎么做到的?

netty原理详解(Netty实战IM即时通讯系统)(1)

  1. 如上图所示,IO 模型中,一个连接来了,会创建一个线程,对应一个 while 死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w 个连接里面同一时刻只有少量的连接有数据可读,因此,很多个 while 死循环都白白浪费掉了,因为读不出啥数据。
  2. 而在 NIO 模型中,他把这么多 while 死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个 while 死循环就能监测1w个连接是否有数据可读的呢? 这就是 NIO 模型中 selector 的作用,一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,而是直接把这条连接注册到 selector 上,然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明 IO 与 NIO 的区别。
  3. 在一家幼儿园里,小朋友有上厕所的需求,小朋友都太小以至于你要问他要不要上厕所,他才会告诉你。幼儿园一共有 100 个小朋友,有两种方案可以解决小朋友上厕所的问题:
  4. 每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100 个小朋友就需要 100 个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。
  5. 所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是 NIO 模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。
  6. 这就是 NIO 模型解决线程资源受限的方案,实际开发过程中,我们会开多个线程,每个线程都管理着一批连接,相对于 IO 模型中一个线程管理一条连接,消耗的线程资源大幅减少
  7. 线程切换效率低下
  8. 由于NIO模型中线程数量大大降低 , 线程切换的效率也因此大幅度提高
  9. IO读写面向流
  10. IO读写是面向流的 , 一次性只能从流中读取一个或多个字节 , 并且读完之后无法再次读取 , 你需要自己缓存数据 , 而NIO的读写是面向Buffer的 , 你可以随意读取里面的任何一个字节数据 , 不需要你自己缓存数据 , 这一切只需要移动读写指针即可。
  11. 原生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<SelectionKey> 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 而不是面向流。
  5. 原生NIO 进行网络开发的缺点:
  6. JDK 的NIO 编程需要了解很多概念, 编程复杂 , 对NIO 入门很不友好 , 编程模型不友好 , ByteBuffer的API简直反人类 (这是书里这么说的 , 不要喷我)。
  7. 对NIO 编程来说 , 一个比较适合的线程模型能充分发挥它的优势 , 而JDK没有给你实现 , 你需要自己实现 , 就连简单的协议拆包都要自己实现 (我感觉这样才根据创造力呀 )
  8. JDK NIO 底层由epoll 实现 , 该实现饱受诟病的空轮训bug会导致cpu 飙升100%
  9. 项目庞大之后 , 自己实现的NIO 很容易出现各类BUG , 维护成本高 (作者怎么把自己的过推向JDK haha~)
  10. 正因为如此 , 我连客户端的代码都懒得给你写了 (这作者可真够懒的) , 你可以直接使用IOClient 和NIO_Server 通信
  11. JDK 的NIO 犹如带刺的玫瑰 , 虽然美好 , 让人向往 , 但是使用不当会让你抓耳挠腮 , 痛不欲生 , 正因为如此 , Netty横空出世!(作者这才华 啧啧啧~)
  12. Netty 编程
  13. Netty到底是何方神圣(被作者吹上天了都) , 用依据简单的话来说就是: Netty 封装了JDK 的NIO , 让你使用更加干爽 (干爽???) , 你不用在写一大堆复杂的代码了 , 用官方的话来说就是: Netty是一个异步事件驱动的网络应用框架 , 用于快速开发可维护的高性能服务器和客户端。
  14. Netty 相比 JDK 原生NIO 的优点 :
  15. 使用NIO 需要了解太多概念, 编程复杂 , 一不小心 BUG 横飞
  16. Netty 底层IO模型随意切换 , 而这一切只需要小小的改动 , 改改参数 , Netty乐意直接从NIO模型转换为IO 模型 。
  17. Netty 自带的拆包解包 , 异常检测可以让你从NIO 的繁重细节中脱离出来 , 让你只关心业务逻辑 。
  18. Netty 解决了JDK 的很多包括空轮训在内的BUG
  19. Netty社区活跃 , 遇到问题可以轻松解决
  20. Netty 已经经历各大RPC 框架 , 消息中间价 , 分布式通信中间件线上的广泛验证 , 健壮性无比强大
  21. 代码实例
  22. maven 依赖

<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.6.Final</version> </dependency>

  1. 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<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext cxt, String msg) throws Exception { System.out.println(msg); } }); } }).bind(8000); } }

  1. 这么一小段代码就实现了我们前面NIO 编程中所有的功能 , 包括服务端启动 , 接收新连接 , 打印客户端传来的数据。
  2. 将NIO 中的概念与IO模型结合起来理解:
  3. boss 对应 IOServer 中接收新连接创建线程 , 主要负责创建新连接
  4. worker 对应 IOServer 中负责读取数据的线程 , 主要用于数据读取语句业务逻辑处理 。
  5. 详细逻辑会在后续深入讨论
  6. 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<Channel>() { @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); } } }

  1. 在客户端程序中 , group 对应了我们IOClient 中 新起的线程。
  2. 剩下的逻辑 我们在后文中详细分析 , 现在你可以把 Netty_server_02 和Netty_client_02 复制到 你的IDE 中 运行起来 感受世界 的美好 (注意 先启动 服务端 再启动客户端 )
  3. 使用Netty 之后 整个世界都美好了, 一方面 Netty 对NIO 封装的如此完美 , 另一方面 , 使用Netty 之后 , 网络通信这块的性能问题几乎不用操心 , 尽情的让Netty 榨干你的CPU 吧~~

netty原理详解(Netty实战IM即时通讯系统)(2)

后记

感谢各位客官的阅读 , 如果文章中有错误或剖析的不够透彻还请您能够给不吝赐教在评论中告诉小编 , 以便小编能够及时调整文章内容 , 为大家带来更加优质的文章

netty原理详解(Netty实战IM即时通讯系统)(3)

,