为什么要用Netty
- 虽然JAVA NIO 框架提供了多路复用IO 的支持,但是并没有提供上层“信息格式”的良好封装。
- NIO 的类库和API 相当复杂,使用它来开发,需要非常熟练地掌握Selector、ByteBuffer、ServerSocketChannel、SocketChannel 等,需要很多额外的编程技能来辅助使用NIO。
- 要编写一个可靠的、易维护的、高性能的NIO 服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理很多上层特有服务。
- JAVA NIO 框架存在一个poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能block 意味着CPU 的使用率会变成100%
为什么Netty 使用NIO 而不是AIO?
Netty 不看重Windows 上的使用,在Linux 系统上,AIO 的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK 封装了一层不容易深度优化。
AIO 还有个缺点是接收数据需要预先分配缓存, 而不是NIO 那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多。
Netty 程序
Channel
Channel 是Java NIO 的一个基本构造。
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O 操作的程序组件)的开放连接,如读操作和写操作。
目前,可以把Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
回调和Future
一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。
Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个interface-ChannelHandler 的实现处理。
Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
ChannelFuture 提供了几种额外的方法,这些方法使得我们能够注册一个或者多ChannelFutureListener 实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用。然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我们可以检索产生的Throwable。简而言之,由ChannelFutureListener 提供的通知机制消除了手动检查对应的操作是否完成的必要。
每个Netty 的出站I/O 操作都将返回一个ChannelFuture。
事件和ChannelHandler
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。
可以认为每个ChannelHandler 的实例都类似于一种为了响应特定事件而被执行的回调。
Netty 提供了大量预定义的可以开箱即用的ChannelHandler 实现,包括用于各种协议(如HTTP 和SSL/TLS)的ChannelHandler。
Netty组件
Channel 的生命周期状态
ChannelUnregistered :Channel 已经被创建,但还未注册到EventLoop
ChannelRegistered :Channel 已经被注册到了EventLoop
ChannelActive :Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
ChannelInactive :Channel 没有连接到远程节点
最重要Channel 的方法
eventLoop: 返回分配给Channel 的EventLoop
pipeline: 返回分配给Channel 的ChannelPipeline
isActive: 如果Channel 是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket 传输一旦连接到了远程节点便是活动的,而一个Datagram 传输一旦被打开便是活动的。
localAddress: 返回本地的SokcetAddress
remoteAddress: 返回远程的SocketAddresswrite: 将数据写到远程节点。这个数据将被传递给ChannelPipeline,并且排队直到它被冲刷
flush: 将之前已写的数据冲刷到底层传输,如一个Socket
writeAndFlush: 一个简便的方法,等同于调用write()并接着调用flush()
线程的分配
服务于Channel 的I/O 和事件的EventLoop 则包含在EventLoopGroup 中。
EventLoop 的分配方式对ThreadLocal 的使用的影响。因为一个EventLoop 通常会被用于支撑多个Channel,所以对于所有相关联的Channel 来说,ThreadLocal 都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而,在一些无状态的上下文中,它仍然可以被用于在多个Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件。
ChannelFuture 接口
Netty 中所有的I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法
解决粘包/半包问题
(1)服务端分两次读取到了两个独立的数据包,分别是D1 和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1 和D2 粘合在一起,被称为TCP 粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1 包和D2 包的部分内容,第二次读取到了D2 包的剩余内容,这被称为TCP 拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1 包的部分内容D1_1,第二次读取到了D1 包的剩余内容D1_2 和D2 包的整包。
解决粘包半包问题
(1) 在包尾增加分割符,比如回车换行符进行分割。
(2)消息定长,例如每个报文的大小为固定长度200 字节,如果不够,空位补空格。
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32 来表示消息的总长度,LengthFieldBasedFrameDecoder。