netty第一篇--初识netty
本文是建立在你有一点的java Nio基础,才展开的netty学习,不然你会比较吃力。
netty的起源和历史
2004年6月Netty2发布
2004年6月Netty2的1.0版本发布,这是在java社区中第一个基于事件驱动的应用网络框架。
Maven中最后版本:1.9.2 :net.gleamynode:netty2:1.9.2
2005年5月Mina发布
2005年5月官方发布了第一个版本mina 0.7.1,并在ApacheDS 项目中使用。
2006年10月Mina发布1.0.0版本。
2010年9月Mina发布2.0.0版本。
2008年Netty3发布
2008年10月jboss发布Netty3.0.0版本。
2013年Netty4发布
2013年7月Netty(netty.io)发布4.0.0版本。
Netty和Mina是Java世界非常知名的通讯框架。它们都出自同一个作者,Mina属于Apache基金会,而Netty开始在Jboss名下,后来出来自立门户netty.io。
netty的现状及欢迎程度
- 高并发 Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高 。
- 传输快 Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。
- 封装好 Netty封装了NIO操作的很多细节,提供易于使用的API,还有心跳、重连机制、拆包粘包方案等特性,使开发者能能够快速高效的构建一个稳健的高并发应用。
为什么Netty使用NIO而不是AIO?
- Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用epoll,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化
- Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱
- Linux上AIO不够成熟,处理回调结果速度跟不到处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)
netty的整体架构--reactor模型
Reactor具体分为三种线程模型
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程
单Reactor单线程
在单线程模型中Reactor和Acceptor,以及执行任务的线程都在一个线程,当线程在执行耗时的业务处理时,这时的链=连接请求或者其他的业务不能及时的处理,当请求并发量比较低的时候还是可以抗住的,一旦高并发,将不堪重负,而且单线程也没有充分利用多核cpu的特点,浪费了资源
单Reactor多线程
在单Reactor多线程模型下,连接请求及acceptor仍然在一个线程,io的read和send操作也是在Reactor线程,但是将业务逻辑处理交给线程池(多线程),此时不管业务操作多耗时,线程里有多个线程来处理任务,任务之间不会有大的延迟处理的影响,但如果并发太高,Reactor线程也来不及连接请求和读写数据,于是主从Reactor多线程模型产生
主从Reactor多线程模型
在这个模型中,主要有一个mainReactor,多个subReactor,以及每个subReactor都附带一个线程池,这个模型大大的分解了各项处理,mainReactor主要负责监听来自客户端的请求(即selector.select()),当有连接请求到达,将会调用accept函数(位于mainReactor线程中),并将产生的socketChannel注册到一个subReactor的selector中,而且绑定该socketChannel感兴趣的事件,当有该socketChannel感兴趣的事件发生时,该subReactor就会调用selector.select()方法,并将需要完成的任务扔到绑定该subReactor的线程池来执行任务,任务结果返回subReactor,然后由subReactor回复客户端.
netty快速上手:
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.53.Final</version> </dependency>
建立netty服务端:
1 import io.netty.bootstrap.ServerBootstrap; 2 import io.netty.channel.*; 3 import io.netty.channel.nio.NioEventLoopGroup; 4 import io.netty.channel.socket.nio.NioServerSocketChannel; 5 import io.netty.channel.socket.nio.NioSocketChannel; 6 import io.netty.handler.codec.string.StringDecoder; 7 8 public class NettyServerDemo { 9 10 public static void main(String[] args) { 11 try { 12 new NettyServerDemo().run(); 13 } catch (Exception e) { 14 e.printStackTrace(); 15 } 16 } 17 18 public void run() throws Exception{ 19 // ServerBootstrap负责建立服务端,可以直接使用Channel去建立服务端但是一般使用ServerBootstrap 20 // ServerBootstrap 是服务端启动引导类。 21 ServerBootstrap serverBootstrap = new ServerBootstrap(); 22 23 // boos接收传入连接,因为boos仅接收客户端连接,不做复杂的逻辑处理,为了尽可能减少资源的占用,取值越小越好 24 EventLoopGroup boss = new NioEventLoopGroup(1); 25 // 用于真正读写事件的group socketChannel 26 EventLoopGroup worker = new NioEventLoopGroup(); 27 28 serverBootstrap.group(boss, worker) 29 // 指定使用NioServerSocketChannel产生一个Channel用来接收连接 30 // 设置通道实现方式 31 .channel(NioServerSocketChannel.class) 32 // ChannelInitializer用于配置一个新的Channel 33 // 用于向你的Channel当中添加ChannelInboundHandler的实现 34 // 该方法用来设置业务处理类(自定义handler) 35 36 //一个channel 只能分配给一个EventLoop,但是一个EventLoop可以同时维护多个channel 🌟🌟🌟 37 .childHandler(new ChannelInitializer<NioSocketChannel>() { 38 39 protected void initChannel(NioSocketChannel ch) { 40 41 ChannelPipeline pipeline = ch.pipeline(); 42 //解码器 43 pipeline.addLast(new StringDecoder()); 44 45 //因为你的 Echo 服务器会响应传入的消息,所以它需要实现 ChannelInboundHandler 接口,用 来定义响应入站事件的方法。 46 // 这个简单的应用程序只需要用到少量的这些方法,所以继承 Channel- InboundHandlerAdapter 类也就足够了,它提供了 ChannelInboundHandler 的默认实现。 47 ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { 48 49 //对于每个传入的消息都要调用 channelRead方法 50 @Override 51 protected void channelRead0(ChannelHandlerContext ctx, String msg) { 52 System.out.println(msg); 53 } 54 }); 55 56 } 57 58 }) 59 .option(ChannelOption.SO_BACKLOG, 128) 60 // 是否启用心跳保活机制。在双方TCP套接字建立连接后(即都进入ESTABLISHED状态)并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活。 61 // childOption是用来给父级ServerChannel之下的Channels设置参数的 62 .childOption(ChannelOption.SO_KEEPALIVE, true) 63 // 绑定端口号 64 .bind(8000); 65 66 } 67 68 }
代码解读:
首先创建了 ServerBootstrap 启动类,构建了两个 EventLoopGroup 对象分别为 boss 和 worker 对应着上述的线程模型的 mainReactor 和 subReactor ,然后调用 ServerBootstrap 的group方法,channel方法,childHandler方法,option方法,childOption方法,bind方法。
其中最重要的就是childHandler方法,也是消息处理的方法。
看代码,我们大概可以猜到,里面做了哪些工作,通过 initChannel 方法构建了一个 NioSocketChannel 对象,取出其中的 ChannelPipeline ,然后又往 ChannelPipeline 中通过 addLast 方法添加 handler 对象。
1 public class NettyClient { 2 3 public static void main(String[] args) throws InterruptedException { 4 //客户端配置和启动的类 5 Bootstrap bootstrap = new Bootstrap(); 6 //接受新连接线程,主要负责创建新连接 7 EventLoopGroup group = new NioEventLoopGroup(); 8 //指定使用NioServerSocketChannel产生一个Channel用来接收连接 9 bootstrap.group(group).channel(NioSocketChannel.class) 10 .handler(new ChannelInitializer<Channel>() { 11 @Override 12 protected void initChannel(Channel ch) { 13 ch.pipeline().addLast(new StringEncoder()); 14 } 15 }); 16 //连接服务端 17 Channel channel = bootstrap.connect("127.0.0.1", 8000).channel(); 18 19 while (true) { 20 //写数据 21 channel.writeAndFlush(new Date() + ": hello world!"); 22 Thread.sleep(2000); 23 } 24 } 25 }
netty关键类解析
ServerBootstrap, EventLoop,EventLoopGroup,ChannelPipeline,ChannelHandler,Channel,ChannelHandlerContext
我们结合上述的reactor模型进行分析,简单了解下它们各自处于什么位置:
逐个解释:
Channel 接口 (通道):
channel是一个通道,在Nio网络编程模型中, 服务端和客户端进行IO数据交互(得到彼此推送的信息)的媒介就是Channel;
根据服务端和客户端,Channel可以分成两类:
- 服务端:
NioServerSocketChannel
- 客户端:
NioSocketChannel
Netty对Jdk原生的ServerSocketChannel
进行了封装和增强封装成了NioXXXChannel
EventLoop 接口:
EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
一个 EventLoopGroup 包含一个或者多个 EventLoop;
- 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
- 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
- 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
- 一个 EventLoop 可能会被分配给一个或多个 Channel。
-
注意,在这种设计中,一个给定 Channel 的 I/O 操作都是由相同的 Thread 执行的,实际上消除了对于同步的需要。
EventLoopGroup 是存放 EventLoop 的集合,与EventLoop的关系类似于 线程池和线程。
ChannelHandler:
Netty 的主要组件是 ChannelHandler,就是一个事件处理接口,数据的读写最终会在ChannelHandler里进行处理。
ChannelHandler通常会分两种实现:
ChannelInboundHandler :入站数据的处理实现
ChannelOutboundHandler :出站数据的处理实现
ChannelPipeline,ChannelHandlerContext,ChannelHandler三者的关系:
一个channel注册到EventLoop上后,会生成一个ChannelPipeline,为ChannelHandler的执行编排顺序, ChannelPipeline就是一个由 ChannelHandlerContext 组成的双向链表,之所以是链表结构,是因为一个Channel可能拥有多个ChannelHandler,而又为何双向,是因为ChannelInboundHandler和ChannelOutboundHandler 的执行顺序是相反的。
ChannelHandlerContext就是用来包裹ChannelHandler的,与后者是一对一的关系,主要功能是管理它所关联的 ChannelHandler 和在 同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。
ByteBuf类--netty的数据容器
ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从 ByteBuf 读取时, 它的 readerIndex 将会被递增已经被读取的字节数。同样地,当你写入 ByteBuf 时,它的 writerIndex 也会被递增。
下图展示了一个空 ByteBuf 的布局结构和状态。
要了解这些索引两两之间的关系,请考虑一下,如果打算读取字节直到 readerIndex 达到 和 writerIndex 同样的值时会发生什么。在那时,你将会到达“可以读取的”数据的末尾。就 如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个 IndexOutOfBoundsException。
名称以 read 或者 write 开头的 ByteBuf 方法,将会推进其对应的索引,而名称以 set 或 者 get 开头的操作则不会。后面的这些方法将在作为一个参数传入的一个相对索引上执行操作。 可以指定 ByteBuf 的最大容量。试图移动写索引(即 writerIndex)超过这个值将会触发异常。(默认的限制是 Integer.MAX_VALUE)
netty如何解决 TCP粘包和半包问题的?
什么是粘包和半包?
由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;
分包产生的原因就简单的多:可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。
通常解决粘包半包问题的办法
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。
(1)在包尾增加分割符,比如回车换行符进行分割,例如FTP协议;linebase包和delimiter包下,分别使用LineBasedFrameDecoder和DelimiterBasedFrameDecoder,如果超过规定字节长度,会报错。
(2)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;fixed包下,使用FixedLengthFrameDecoder
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第 一个字段使用int32来表示消息的总长度,LengthFieldBasedFrameDecoder;。