前面介绍了基本的网络模型以及IO与NIO,那么有了NIO来开发非阻塞服务器,大家就满足了吗?有了技术支持,就回去追求效率,因此就产生了很多NIO的框架对NIO进行封装——这就是大名鼎鼎的Netty。

前几篇的内容,可以参考:

  1. 网络IO的基本知识与概念
  2. 普通IO以及BIO服务器
  3. NIO的使用与服务器Hello world
  4. Netty的使用与服务器Hello world

为什么要使用开源框架?

这个问题几乎可以当做废话,框架肯定要比一些原生的API封装了更多地功能,重复造轮子在追求效率的情况并不是明智之举。那么先来说说NIO有什么缺点吧:

  1. NIO的类库和API还是有点复杂,比如Buffer的使用
  2. Selector编写复杂,如果对某个事件注册后,业务代码过于耦合
  3. 需要了解很多多线程的知识,熟悉网络编程
  4. 面对断连重连、保丢失、粘包等,处理复杂
  5. NIO存在BUG,根据网上言论说是selector空轮训导致CPU飙升,具体有兴趣的可以看看JDK的官网

那么有了这些问题,就急需一些大牛们开发通用框架来方便劳苦大众了。最致命的NIO框架就是MINA和Netty了,这里不得不说个小插曲:

先来看看MINA的主要贡献者:

再来看看NETYY的主要贡献者:

总结起来,有这么几点:

  1. MINA和Netty的主要贡献者都是同一个人——Trustin lee,韩国Line公司的。
  2. MINA于2006年开发,到14、15年左右,基本停止维护
  3. Nety开始于2009年,目前仍由苹果公司的norman maurer在主要维护。
  4. Norman Maurer是《Netty in Action》一书的作者

因此,如果让你选择你应该知道选择谁了吧。另外,MINA对底层系统要求功底更深,且国内Netty的氛围更好,有李林峰等人在大力宣传(《Netty权威指南》的作者)。

讲了一大堆的废话之后,总结来说就是——Netty有前途,学它准没错。

Netty介绍

按照定义来说,Netty是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架。主要的优点有:

  1. 框架设计优雅,底层模型随意切换适应不同的网络协议要求
  2. 提供很多标准的协议、安全、编码解码的支持
  3. 解决了很多NIO不易用的问题
  4. 社区更为活跃,在很多开源框架中使用,如Dubbo、RocketMQ、Spark等

主要支持的功能或者特性有:

  1. 底层核心有:Zero-Copy-Capable Buffer,非常易用的灵拷贝Buffer(这个内容很有意思,稍后专门来说);统一的API;标准可扩展的时间模型
  2. 传输方面的支持有:管道通信(具体不知道干啥的,还请老司机指教);Http隧道;TCP与UDP
  3. 协议方面的支持有:基于原始文本和二进制的协议;解压缩;大文件传输;流媒体传输;protobuf编解码;安全认证;http和websocket

总之提供了很多现成的功能可以直接供开发者使用。

Netty服务器小例子

基于Netty的服务器编程可以看做是Reactor模型:



即包含一个接收连接的线程池(也有可能是单个线程,boss线程池)以及一个处理连接的线程池(worker线程池)。boss负责接收连接,并进行IO监听;worker负责后续的处理。为了便于理解Netty,直接看看代码:

  1. package cn.xingoo.book.netty.chap04;
  2. import io.netty.bootstrap.ServerBootstrap;
  3. import io.netty.buffer.ByteBuf;
  4. import io.netty.buffer.Unpooled;
  5. import io.netty.channel.*;
  6. import io.netty.channel.nio.NioEventLoopGroup;
  7. import io.netty.channel.socket.SocketChannel;
  8. import io.netty.channel.socket.nio.NioServerSocketChannel;
  9. import java.net.InetSocketAddress;
  10. import java.nio.charset.Charset;
  11. public class NettyNioServer {
  12. public void serve(int port) throws InterruptedException {
  13. final ByteBuf buffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi\r\n", Charset.forName("UTF-8")));
  14. // 第一步,创建线程池
  15. EventLoopGroup bossGroup = new NioEventLoopGroup(1);
  16. EventLoopGroup workerGroup = new NioEventLoopGroup();
  17. try{
  18. // 第二步,创建启动类
  19. ServerBootstrap b = new ServerBootstrap();
  20. // 第三步,配置各组件
  21. b.group(bossGroup, workerGroup)
  22. .channel(NioServerSocketChannel.class)
  23. .localAddress(new InetSocketAddress(port))
  24. .childHandler(new ChannelInitializer<SocketChannel>() {
  25. @Override
  26. protected void initChannel(SocketChannel socketChannel) throws Exception {
  27. socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
  28. @Override
  29. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  30. ctx.writeAndFlush(buffer.duplicate()).addListener(ChannelFutureListener.CLOSE);
  31. }
  32. });
  33. }
  34. });
  35. // 第四步,开启监听
  36. ChannelFuture f = b.bind().sync();
  37. f.channel().closeFuture().sync();
  38. } finally {
  39. bossGroup.shutdownGracefully().sync();
  40. workerGroup.shutdownGracefully().sync();
  41. }
  42. }
  43. public static void main(String[] args) throws InterruptedException {
  44. NettyNioServer server = new NettyNioServer();
  45. server.serve(5555);
  46. }
  47. }

代码非常少,而且想要换成阻塞IO,只需要替换Channel里面的工厂类即可:

  1. public class NettyOioServer {
  2. public void serve(int port) throws InterruptedException {
  3. final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi\r\b", Charset.forName("UTF-8")));
  4. EventLoopGroup bossGroup = new OioEventLoopGroup(1);
  5. EventLoopGroup workerGroup = new OioEventLoopGroup();
  6. try{
  7. ServerBootstrap b = new ServerBootstrap();
  8. b.group(bossGroup, workerGroup)//配置boss和worker
  9. .channel(OioServerSocketChannel.class) // 使用阻塞的SocketChannel
  10. ....

概括来说,在Netty中包含下面几个主要的组件:

  • Bootstrap:netty的组件容器,用于把其他各个部分连接起来;如果是TCP的Server端,则为ServerBootstrap.
  • Channel:代表一个Socket的连接
  • EventLoopGroup:一个Group包含多个EventLoop,可以理解为线程池
  • EventLoop:处理具体的Channel,一个EventLoop可以处理多个Channel
  • ChannelPipeline:每个Channel绑定一个pipeline,在上面注册处理逻辑handler
  • Handler:具体的对消息或连接的处理,有两种类型,Inbound和Outbound。分别代表消息接收的处理和消息发送的处理。
  • ChannelFuture:注解回调方法

了解上面的基本组件后,就看一下几个重要的内容。

Netty的Buffer和零拷贝

在Unix操作系统中,系统底层可以基于mmap实现内核空间和用户空间的内存映射。但是在Netty中并不是这个意思,它主要来自于下面几个功能:

  1. 通过Composite和slice实现逻辑上的Buffer的组合和拆分,重新维护索引,避免内存拷贝过程。
  2. 通过DirectBuffer申请堆外内存,避免用户空间的拷贝。不过堆外内存的申请和释放都很麻烦,推荐小心使用。关于堆外内存的一些研究,还可以参考执勤的分享:Java堆外内存之突破JVM枷锁 以及 Java直接内存与非直接内存性能测试
  3. 通过FileRegion包装FileChannel,直接实现channel到channel的传输。

另外,Netty自己封装实现了ByteBuf,相比于Nio原生的ByteBuffer,API上更易用了;同时支持容量的动态扩容;另外还支持Buffer的池化,高效复用Buffer。

  1. public class ByteBufTest {
  2. public static void main(String[] args) {
  3. //创建bytebuf
  4. ByteBuf buf = Unpooled.copiedBuffer("hello".getBytes());
  5. System.out.println(buf);
  6. // 读取一个字节
  7. buf.readByte();
  8. System.out.println(buf);
  9. // 读取一个字节
  10. buf.readByte();
  11. System.out.println(buf);
  12. // 丢弃无用数据
  13. buf.discardReadBytes();
  14. System.out.println(buf);
  15. // 清空
  16. buf.clear();
  17. System.out.println(buf);
  18. // 写入
  19. buf.writeBytes("123".getBytes());
  20. System.out.println(buf);
  21. buf.markReaderIndex();
  22. System.out.println("mark:"+buf);
  23. buf.readByte();
  24. buf.readByte();
  25. System.out.println("read:"+buf);
  26. buf.resetReaderIndex();
  27. System.out.println("reset:"+buf);
  28. }
  29. }

输出为:

  1. UnpooledHeapByteBuf(ridx: 0, widx: 5, cap: 5/5)
  2. UnpooledHeapByteBuf(ridx: 1, widx: 5, cap: 5/5)
  3. UnpooledHeapByteBuf(ridx: 2, widx: 5, cap: 5/5)
  4. UnpooledHeapByteBuf(ridx: 0, widx: 3, cap: 5/5)
  5. UnpooledHeapByteBuf(ridx: 0, widx: 0, cap: 5/5)
  6. UnpooledHeapByteBuf(ridx: 0, widx: 3, cap: 5/5)
  7. mark:UnpooledHeapByteBuf(ridx: 0, widx: 3, cap: 5/5)
  8. read:UnpooledHeapByteBuf(ridx: 2, widx: 3, cap: 5/5)
  9. reset:UnpooledHeapByteBuf(ridx: 0, widx: 3, cap: 5/5)

有兴趣的可以看一下上一篇分享的ByteBuffer,对比一下,就能发现在Netty中通过独立的读写索引维护,避免读写模式的切换,更加方便了。

Handler的使用

前面介绍了Handler包含了Inbound和Outbound两种,他们统一放在一个双向链表中:

当接收消息的时候,会从链表的表头开始遍历,如果是inbound就调用对应的方法;如果发送消息则从链表的尾巴开始遍历。那么上面途中的例子,接收消息就会输出:

  1. InboundA --> InboundB --> InboundC

输出消息,则会输出:

  1. OutboundC --> OutboundB --> OutboundA

这里有段代码,可以直接复制下来,试试看:

  1. package cn.xingoo.book.netty.pipeline;
  2. import io.netty.bootstrap.ServerBootstrap;
  3. import io.netty.buffer.ByteBuf;
  4. import io.netty.buffer.Unpooled;
  5. import io.netty.channel.*;
  6. import io.netty.channel.nio.NioEventLoopGroup;
  7. import io.netty.channel.socket.SocketChannel;
  8. import io.netty.channel.socket.nio.NioServerSocketChannel;
  9. import java.net.InetSocketAddress;
  10. import java.net.SocketAddress;
  11. import java.nio.charset.Charset;
  12. /**
  13. * 注意:
  14. *
  15. * 1 ChannelOutboundHandler要在最后一个Inbound之前
  16. *
  17. */
  18. public class NettyNioServerHandlerTest {
  19. final static ByteBuf buffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi\r\n", Charset.forName("UTF-8")));
  20. public void serve(int port) throws InterruptedException {
  21. EventLoopGroup bossGroup = new NioEventLoopGroup(1);
  22. EventLoopGroup workerGroup = new NioEventLoopGroup();
  23. try{
  24. ServerBootstrap b = new ServerBootstrap();
  25. b.group(bossGroup, workerGroup)
  26. .channel(NioServerSocketChannel.class)
  27. .localAddress(new InetSocketAddress(port))
  28. .childHandler(new ChannelInitializer<SocketChannel>() {
  29. @Override
  30. protected void initChannel(SocketChannel socketChannel) throws Exception {
  31. ChannelPipeline pipeline = socketChannel.pipeline();
  32. pipeline.addLast("1",new InboundA());
  33. pipeline.addLast("2",new OutboundA());
  34. pipeline.addLast("3",new InboundB());
  35. pipeline.addLast("4",new OutboundB());
  36. pipeline.addLast("5",new OutboundC());
  37. pipeline.addLast("6",new InboundC());
  38. }
  39. });
  40. ChannelFuture f = b.bind().sync();
  41. f.channel().closeFuture().sync();
  42. } finally {
  43. bossGroup.shutdownGracefully().sync();
  44. workerGroup.shutdownGracefully().sync();
  45. }
  46. }
  47. public static void main(String[] args) throws InterruptedException {
  48. NettyNioServerHandlerTest server = new NettyNioServerHandlerTest();
  49. server.serve(5555);
  50. }
  51. private static class InboundA extends ChannelInboundHandlerAdapter {
  52. @Override
  53. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  54. ByteBuf buf = (ByteBuf)msg;
  55. System.out.println("InboundA read"+buf.toString(Charset.forName("UTF-8")));
  56. super.channelRead(ctx, msg);
  57. }
  58. }
  59. private static class InboundB extends ChannelInboundHandlerAdapter {
  60. @Override
  61. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  62. ByteBuf buf = (ByteBuf)msg;
  63. System.out.println("InboundB read"+buf.toString(Charset.forName("UTF-8")));
  64. super.channelRead(ctx, msg);
  65. // 从pipeline的尾巴开始找outbound
  66. ctx.channel().writeAndFlush(buffer);
  67. }
  68. }
  69. private static class InboundC extends ChannelInboundHandlerAdapter {
  70. @Override
  71. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  72. ByteBuf buf = (ByteBuf)msg;
  73. System.out.println("InboundC read"+buf.toString(Charset.forName("UTF-8")));
  74. super.channelRead(ctx, msg);
  75. // 这样会从当前的handler向前找outbound
  76. //ctx.writeAndFlush(buffer);
  77. }
  78. }
  79. private static class OutboundA extends ChannelOutboundHandlerAdapter {
  80. @Override
  81. public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
  82. System.out.println("OutboundA write");
  83. super.write(ctx, msg, promise);
  84. }
  85. }
  86. private static class OutboundB extends ChannelOutboundHandlerAdapter {
  87. @Override
  88. public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
  89. System.out.println("OutboundB write");
  90. super.write(ctx, msg, promise);
  91. }
  92. }
  93. private static class OutboundC extends ChannelOutboundHandlerAdapter {
  94. @Override
  95. public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
  96. System.out.println("OutboundC write");
  97. super.write(ctx, msg, promise);
  98. }
  99. }
  100. }

最后有一个TCP粘包的例子,有兴趣的也可以自己试一下,代码就不贴上来了,可以参考最后面的Github连接。

参考

  1. 《Netty实战》
  2. 《Netty权威指南》
  3. github代码链接

漫谈Java IO之 Netty与NIO服务器的更多相关文章

  1. 漫谈Java IO之普通IO流与BIO服务器

    今天来复习一下基础IO,也就是最普通的IO. 网络IO的基本知识与概念 普通IO以及BIO服务器 NIO的使用与服务器Hello world Netty的使用与服务器Hello world 输入流与输 ...

  2. 漫谈Java IO之 NIO那些事儿

    前面一篇中已经介绍了基本IO的使用以及最简单的阻塞服务器的例子,本篇就来介绍下NIO的相关内容,前面的分享可以参考目录: 网络IO的基本知识与概念 普通IO以及BIO服务器 NIO的使用与服务器Hel ...

  3. 漫谈Java IO之基础篇

    Java的网络编程如果不是专门搞服务器性能开发或者消息分发,几乎可能涉及不到.但是它却是面试找工作必问的一个知识点,涵盖的知识体系也非常广泛,从Java底层IO原理到操作系统内核组成,再到网络TCP. ...

  4. Java IO系列之四:NIO通信模型

    分布式rpc框架有很多,比如dubbo,netty,还有很多其他的产品.但他们大部分都是基于nio的, nio是非阻塞的io,那么它的内部机制是怎么实现的呢. 1.由一个专门的线程处理所有IO事件,并 ...

  5. 高级Java工程师必备 ----- 深入分析 Java IO (二)NIO

    接着上一篇文章 高级Java工程师必备 ----- 深入分析 Java IO (一)BIO,我们来讲讲NIO 多路复用IO模型 场景描述 一个餐厅同时有100位客人到店,当然到店后第一件要做的事情就是 ...

  6. Java IO系列之三:NIO VS IO

    NIO VS IO NIO: 面向缓存: 非阻塞的: selector IO: 面向流:    阻塞的:     无 JAVA IO Java IO: Reading data from a bloc ...

  7. 深入分析JAVA IO(BIO、NIO、AIO)

    IO的基本常识 1.同步 用户进程触发IO操作并等待或者轮询的去查看IO操作是否完成 2.异步 用户触发IO操作以后,可以干别的事,IO操作完成以后再通知当前线程继续处理 3.阻塞 当一个线程调用 r ...

  8. 【转】深入分析JAVA IO(BIO、NIO、AIO)

    IO的基本常识 1.同步 用户进程触发IO操作并等待或者轮询的去查看IO操作是否完成 2.异步 用户触发IO操作以后,可以干别的事,IO操作完成以后再通知当前线程继续处理 3.阻塞 当一个线程调用 r ...

  9. java io 网络编程 高性能NIO

    很久没写了,一是觉得这后台不怎么方便,二是 写的时候突然觉得没兴趣了. 还好,今天突然想记一下,那就随便写吧.  1.一开始还是放几个连接.  什么是 同步,异步,阻塞,非阻塞 : http://bl ...

随机推荐

  1. poj1417菜鸡的详细题解(希望能帮助到和我一样陷于本题的新手)

    题意太麻烦就偷懒转载他人的题意..... 题意转载自http://blog.csdn.net/acm_cxlove/article/details/7854526       by---cxlove ...

  2. jsonp的案例

    一,转发一 首先基于安全的原因,浏览器是存在同源策略这个机制的,同源策略阻止从一个源加载的文档或脚本获取或设置另一个源加载的文档的属性.看起来不知道什么意思,实践一下就知道了. 1.随便建两个网页 一 ...

  3. springboot--如何优雅的使用mybatis

    这两天启动了一个新项目因为项目组成员一直都使用的是mybatis,虽然个人比较喜欢jpa这种极简的模式,但是为了项目保持统一性技术选型还是定了 mybatis.到网上找了一下关于spring boot ...

  4. mybatis快速入门(六)

    前面mybatis的入门程序基本上都写完了,就看大家的灵活运用了,今天来吧前面的整合一下封装一个公共的BaseDao 只需要把前面的改造下然后创建一个BaseDao的接口,其它的继承BaseDao接口 ...

  5. yyb要填的各种总结的坑

    已经写好啦的 莫比乌斯反演 杜教筛 动态点分治 斜率优化 Splay 莫队 凸包 旋转卡壳 Manacher算法 Trie树 AC自动机 高斯消元 KMP算法 SA后缀数组 SAM后缀自动机 回文树 ...

  6. 【BZOJ3130】费用流(最大流,二分)

    [BZOJ3130]费用流(最大流,二分) 题面 Description Alice和Bob在图论课程上学习了最大流和最小费用最大流的相关知识. 最大流问题:给定一张有向图表示运输网络,一个源点S和一 ...

  7. [LOJ2230][BJOI2014]大融合

    题面戳我 sol LCT维护子树size. 开一个数组\(sz_i\)表示一个节点的所有虚儿子的size和,\(sum_i\)表示以一个节点为根的子树的\(size\)和,可见\(sz_u=\sum_ ...

  8. checkBox半选中状态

    checkbox 可以半选中,这个特性,很多浏览器都支持 // 用 input.indeterminate 这个属性来获取或者设置半选中状态,必须要用 js 添加属性,才有效果. input.inde ...

  9. Eslint报错整理与解决方法(持续整理)

    1.'Unexpected tab character' 字面意思理解呢就是意想不到的制表符,当时出现的时候就是我习惯的使用Tab键去打空格,但是eslint默认不认可Tab,所以解决方法很简单: 在 ...

  10. redis基本类型和使用

    redis存储数据的基本类型有:string(字符串类型).hash(散列类型).list(列表类型).set(集合类型).zset(有序集合类型). 依次做一些练习.redis命令不区分大小写. k ...