面试官:介绍一下自己对 Netty 的认识吧!小伙子。
我:好的!那我就简单用 3 点来概括一下 Netty 吧!
Netty 是一个 基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。
它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
用官方的总结就是:Netty 成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。
除了上面介绍的之外,很多开源项目比如我们常用的RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
面试官:为什么要用 Netty 呢?能不能说一下自己的看法。
我:因为 Netty 具有下面这些优点,并且相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用。
1、并发高。使用了NIO(同步非阻塞IO)
2、传输快。即NIO的一个特性——零拷贝(直接在Buffer中进行操作,无需从流拷贝到Buffer)
3、封装好
4、简单而强大的线程模型。
5、自带编解码器解决 TCP 粘包/拆包问题。
6、比直接使用 Java 核心 API ,有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
7、成熟稳定。经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty, 比如 Dubbo、RocketMQ、网络游戏关联用户 等。
NIO是一种同步非阻塞
的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,主要由 Channel , Selector,Buffer
这三个部分组成。
IO操作主要分为两个步骤,即发起IO请求和实际IO操作,同步IO与异步IO的区别就在于第二个步骤是否阻塞。
同步IO:请求进程需要等待或者轮询查看IO操作是否就绪。
异步IO:若实际IO操作并不阻塞请求进程,而是由操作系统来进行实际IO操作并将结果返回
总结:谁返回结果
IO操作主要分为两个步骤,即发起IO请求和实际IO操作,阻塞IO与非阻塞IO的区别就在于第一个步骤是否阻塞。
若发起IO请求后请求线程一直等待实际IO操作完成,则为阻塞IO。
若发起IO请求后请求线程返回而不会一直等待,即为非阻塞IO。
总结:等待否
类别 | IO类型~~~~~~~~~ | 含义 |
---|---|---|
BIO | 同步阻塞 | 服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 |
NIO | 同步非阻塞 | 服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。 |
AIO | 异步非阻塞 | 服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成IO操作后再通知服务器应用来启动线程进行处理。 |
应用场景:
BIO
适用于连接数目比较小且固定的架构
,该方式对服务器资源要求比较高,JDK 1.4以前的唯一选择。
NIO
适用于连接数目多且连接比较短(轻操作)的架构
,如聊天服务器,编程复杂,JDK 1.4开始支持,如Netty框架。
AIO
适用于连接数目多且连接比较长(重操作)的架构
,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持。
备注:在大多数场景下,不建议直接使用JDK的NIO类库(门槛很高),除非精通NIO编程或者有特殊的需求。
在绝大多数的业务场景中,可以使用NIO框架Netty来进行NIO编程,其既可以作为客户端也可以作为服务端,
且支持UDP和异步文件传输,功能非常强大。
问:谈一谈对同步IO和与异步IO的理解?
同步是指用户进程触发IO操作并等待或轮询查看IO操作是否就绪。
异步是指用户进程触发IO操作后便开始做其他事情,当IO操作完成时用户进程会得到相应的通知。
问:谈一谈对阻塞与非阻塞的理解(针对IO操作)?
在阻塞状态下,如果没有东西可读或不可写,读写函数将进入等待状态,直到有东西可读或可写再返回。
非阻塞状态下,如果没有东西可读或不可写,读写函数马上返回,而并不会等待。
如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。
NIO 包含下面几个核心的组件:
Buffer(缓冲区)
Channel(通道)
Selector(选择器)
IO流是阻塞的,NIO流是不阻塞的。
主要区别如下:
可简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
面向流的I/O 系统一次一个字节地处理数据。
一个面向块(缓冲区)的I/O系统以块的形式处理数据。
Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
Java IO的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
NIO 包含下面几个核心的组件:
Buffer(缓冲区)
Channel(通道)
Selector(选择器)
1、Buffer(缓冲区)
IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。
Bufer顾名思义,它是一个缓冲区,实际上是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer
。
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作
。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。其中ByteBuffer是用得最多的实现类(在管道中读写字节数据)。
jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存,这样做有两个好处:
1、 通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
2、DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响
具体过程见 https://blog.csdn.net/qq_28666081/article/details/82315086
Buffer缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该模块内存。为了理解Buffer的工作原理,需要熟悉它的三个属性:capacity(容量)、position(位置)和limit(界限)。
缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)
缓冲区里的数据的总数,代表了当前缓冲区中一共有多少数据。
下一个要被读或写的元素的位置。Position会自动由相应的 get( )和 put( )函数更新。
2、Channel (通道)
NIO 通过Channel(通道) 进行读写。
Channel是一个双向通道,与传统IO操作只允许单向的读写不同的是,NIO的Channel允许在一个通道上进行读和写的操作。
在NIO中以buffer缓冲区和Channel管道配合使用来处理数据。
简单理解一下:
Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)
而我们的NIO就是通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理!
要时刻记住:
Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区
Channel----->运输
Buffer------>数据
channel分类:
3、Selectors(选择器)
NIO有选择器,而IO没有。
作用:可以检测多个NIO channel,看看读或者写事件是否就绪
。
Selector可以监听Channel的四种状态(Connect、Accept、Read、Write
),当监听到某一Channel的某个状态时,才允许对Channel进行相应的操作。
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
Selector提供选择已经就绪的任务的能力:
1、Selector轮询注册在其上的Channel,如果某个Channel发生读写请求并且Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。(同步)
2、 一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。(非阻塞)
这种方式在连接数比较少的时候还是可以接受的,当并发连接超过10000时,开销会明显增加。此外,每一个线程都有一个默认的堆栈内存分配了128K和1M之间的空间。考虑到整体的内存和操作系统需要处理更多的并发连接资源,所以这似乎不是一个理想的解决方案。
selector——实现Java的无阻塞I/O实现的关键,工作流程如下:
最终由Selector决定哪一组注册的socket准备执行I/O。通过通知,一个线程可以同时处理多个并发连接(一个Selector通常由一个线程处理,但具体实施可以使用多个线程)因此,每次读或写操作执行能立即检查完成。该模型可以用较少的线程处理更多连接,避免了多线程之间的上下文切换,当没有I/O处理时,线程可以被重定向到其他任务上。
从这两图可以看出,NIO的单线程能处理连接的数量比BIO要高出很多,而为什么单线程能处理更多的连接呢?原因就是图二中出现的Selector。
当一个连接建立之后,他有两个步骤要做,第一步是接收完客户端发过来的全部数据,第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。
在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。
而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是不阻塞的,这样就能让一个Thread处理更多的请求了。
1、NIO的类库和API繁杂,使用麻烦
2、需要具备其他额外技能:java多线程编程、网络编程等等
3、开发工作量和难度都较大:网络异常、失败缓存
4、Epoll Bug会导致Selector空轮询,最终导致CPU 100%
Netty和Tomcat最大的区别就在于通信协议。
Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器。
Netty能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流。其本质是一个易于使用的API的客户端/服务器框架
友情链接
参考1
参考2
参考3
netty其实是基于TCP的。TCP 是以流的方式来处理数据,所以会导致粘包 / 拆包
。
拆包:一个完整的包可能会被 TCP 拆分成多个包进行发送。
粘包:也可能把小的封装成一个大的数据包发送。
产生粘包和拆包问题的主要原因:操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。
应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象。而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象。
在 Netty 中,提供了多个 Decoder 解析类,如下:
FixedLengthFrameDecoder
,基于固定长度消息进行粘包拆包处理的。LengthFieldBasedFrameDecoder
,基于消息头指定消息长度进行粘包拆包处理的。LineBasedFrameDecoder
,基于换行符(\n或者\r\n)来进行消息粘包拆包处理的。(发送端发送数据包的时候,每个数据包之间以换行符作为分隔。工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。)DelimiterBasedFrameDecoder
,基于指定消息边界方式进行粘包拆包处理的。实际上,上述四个 FrameDecoder 实现可以进行规整:
① 是
② 的特例,固定长度是消息头指定消息长度的一种形式
。
③ 是
④ 的特例,换行是于指定消息边界方式的一种形式。
在生产中是通过自定义协议处理的,在协议的开始有startTag字段,结束有endTag字段。
使用方法
一、FixedLengthFrameDecoder
参考链接!!!
对于使用固定长度的粘包和拆包场景,可以使用FixedLengthFrameDecoder,该解码一器会每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足。 其使用也比较简单,只需要在构造函数中指定每个消息的长度即可。下面的示例中展示了如何使用FixedLengthFrameDecoder来进行粘包和拆包处理:
public class EchoServer {
public void bind(int port) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 这里将FixedLengthFrameDecoder添加到pipeline中,指定长度为20······················
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder());
// 这里FixedLengthFrameEncoder是我们自定义的,用于将长度不足20的消息进行补全空格
ch.pipeline().addLast(new FixedLengthFrameEncoder(20));
// 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoServer().bind(8080);
}
}
上面的pipeline中,对于入栈数据,这里主要添加了FixedLengthFrameDecoder和StringDecoder,前面一个用于处理固定长度的消息的粘包和拆包问题,第二个则是将处理之后的消息转换为字符串。最后由EchoServerHandler处理最终得到的数据,处理完成后,将处理得到的数据交由FixedLengthFrameEncoder处理,该编码器是我们自定义的实现,主要作用是将长度不足20的消息进行空格补全。下面是FixedLengthFrameEncoder的实现代码:
public class FixedLengthFrameEncoder extends MessageToByteEncoder<String> {
private int length;
public FixedLengthFrameEncoder(int length) {
this.length = length;
}
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out)
throws Exception {
// 对于超过指定长度的消息,这里直接抛出异常
if (msg.length() > length) {
throw new UnsupportedOperationException(
"message length is too large, it's limited " + length);
}
// 如果长度不足,则进行补全
if (msg.length() < length) {
msg = addSpace(msg);
}
ctx.writeAndFlush(Unpooled.wrappedBuffer(msg.getBytes()));
}
// 进行空格补全
private String addSpace(String msg) {
StringBuilder builder = new StringBuilder(msg);
for (int i = 0; i < length - msg.length(); i++) {
builder.append(" ");
}
return builder.toString();
}
}
这里FixedLengthFrameEncoder实现了encode()方法,在该方法中,主要是将消息长度不足20的消息进行空格补全。
二、LineBasedFrameDecoder与DelimiterBasedFrameDecoder
对于通过分隔符进行粘包和拆包问题的处理,Netty提供了两个编解码的类,LineBasedFrameDecoder和DelimiterBasedFrameDecoder。这里LineBasedFrameDecoder的作用主要是通过换行符,即\n或者\r\n对数据进行处理;而DelimiterBasedFrameDecoder的作用则是通过用户指定的分隔符对数据进行粘包和拆包处理。同样的,这两个类都是解码一器类,而对于数据的编码,也即在每个数据包最后添加换行符或者指定分割符的部分需要用户自行进行处理。这里以DelimiterBasedFrameDecoder为例进行讲解,如下是EchoServer中使用该类的代码片段,其余部分与前面的例子中的完全一致:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
String delimiter = "_$";
// 将delimiter设置到DelimiterBasedFrameDecoder中,经过该解码一器进行处理之后,源数据将会
// 被按照_$进行分隔,这里1024指的是分隔的最大长度,即当读取到1024个字节的数据之后,若还是未
// 读取到分隔符,则舍弃当前数据段,因为其很有可能是由于码流紊乱造成的
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串数据
ch.pipeline().addLast(new StringDecoder());
// 这是我们自定义的一个编码器,主要作用是在返回的响应数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder(delimiter));
// 最终处理数据并且返回响应的handler
ch.pipeline().addLast(new EchoServerHandler());
}
三、LengthFieldBasedFrameDecoder
关于LengthFieldBasedFrameDecoder,这里需要对其构造函数参数进行介绍:
划重点: 参照一个公式写,肯定没问题:
公式: 发送数据包长度 = 长度域的值 + lengthFieldOffset + lengthFieldLength + lengthAdjustment
//ChannelHandler用于处理Channel对应的事件
ChannelInitializer<SocketChannel> initializer = new ChannelInitializer<SocketChannel>() {
/**
*
* @param channel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 4,
2, 0,0,true));
channel.pipeline().addLast(new ModbusTcpDecoder(modBusBeanMap));
channel.pipeline().addLast(new ModbusTcpHandler(modBusBeanMap));
channel.pipeline().addLast(new ModbusTcpEncoder(modBusBeanMap));
}
};
public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
----------
}
public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
----------
}
只要是tcp协议的一定会进行三次握手,但是netty对这部分进行了优化,取消了三次握手。
netty -> Java Runtime Socket (io、nio、nio2) -> OS Socket -> TCP (当然也可以是UDP、SCTP);
既然连操作系统层的Socket都必须做三次握手(仅对TCP而言),Netty当然无法跳过,只不过它对用户屏蔽了三次握手(当然还有四次挥手)的部分细节。
TCP为了保证可靠传输并减少额外的开销(每次发包都要验证),采用了基于流的传输,基于流的传输不认为消息是一条一条的,是无保护消息边界的(保护消息边界:指传输协议把数据当做一条独立的消息在网上传输,接收端一次只能接受一条独立的消息)。
UDP则是面向消息传输的,是有保护消息边界的,接收方一次只接受一条独立的信息,所以不存在粘包问题。
举个例子:有三个数据包,大小分别为2k、4k、6k,如果采用UDP发送的话,不管接受方的接收缓存有多大,我们必须要进行至少三次以上的发送才能把数据包发送完,但是使用TCP协议发送的话,我们只需要接受方的接收缓存有12k的大小,就可以一次把这3个数据包全部发送完毕。
在计算机网络二进制传输的过程中,字节存在两种序列化顺序:高位字节序和低位字节序。
高位字节序:高位字节在前,低位字节在后(内存地址低位在前,高位地址在后)。
低位字节序:低位字节在前,高位字节在后(内存地址低位在前,高位地址在后)。
netty中默认字节序是大端字节序,即字节高位在前,低位在后 ,符合人类的书写习惯。
在操作ByteBuffer中,也可以使用 ByteBuffer.order() 进行设置
order的参数
ByteBuf byteBuf = Unpooled.buffer(4, 4);
设置大端
byteBuf.order(ByteOrder.BIG_ENDIAN);
设置小端
byteBuf.order(ByteOrder.LITTLE_ENDIAN);
Bootstrap / ServerBootstrap :建立连接 。Netty引导组件,简化NIO的开发步骤,是一个Netty程序的开始,作用是配置和串联各个组件。其中 Bootstrap
类是客户端
程序的启动引导类,ServerBootstrap
是服务端
启动引导类
Future、ChannelFuture :监听 。Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件
Channel:网络通信 的组件,能够用于执行网络 I/O 操作。如 bind、connect、read、write 等。
Selector:通过 Selector 一个线程可以监听多个连接的 Channel 事件
ChannelHandler:一个接口, 处理I/O 事件或拦截 I/O 操作 。比如可以是连接、数据接收、异常、数据转换等。
ChannelHandlerContext: 事 件 处理 器 上 下 文对 象 , Pipeline 链 中 的 实际 处 理 节 点。 每 个 ChannelHandlerContext 中 包 含 一 个 ChannelHandler , 同 时也绑定了对应的 pipeline 和 Channel 的信息,便于 ChannelHandler进行调用。
ChannelPipeline : 是 保存 ChannelHandler 的 集合 (List)
EventLoop:事件循环 。循环服务Channel,用来处理连接的生命周期中所发生的事情,可以包含多个Channel。
EventLoopGroup:事件循环组 。是EventLoop组合,可以包含多个EventLoop。创建一个EventLoopGroup的时候,内部包含的方法就会创建一个子对象EventLoop。
具体一定要细看!!!!!!!!&&&&&&&&&&&&&&*********************¥¥¥¥¥¥¥
NioEventLoop 中包含有一个 Selector
,一个 taskQueueSelector 上可以注册监听多个 NioChannel
NioChannel 只会绑定在唯一的 NioEventLoop
上NioChannel 都绑定有一个自己的 ChannelPipeline
Bootstrap 类是客户端程序的启动引导类
,ServerBootstrap 是服务端启动引导类
常见方法:
方法 | 描述 |
---|---|
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) |
用于服务器端,用来设置两个EventLoop(boss和work) |
public ServerBootstrap childHandler(ChannelHandler childHandler) |
设置业务处理类(自定义的 handler) |
public ChannelFuture bind(String inetHost, int inetPort) |
服务器端,用来设置占用的ip、端口号 |
public ChannelFuture connect(String inetHost, int inetPort) |
客户端,用来连接服务器 |
常见方法:
方法 | 描述 |
---|---|
Channel channel() |
返回当前正在进行 IO 操作的通道 |
ChannelFuture sync() |
等待异步操作执行完毕,相当于将阻塞在当前。 |
网络 I/O 操作
。通道状态
配置参数
(例如接收缓冲区大小)常用的 Channel
类型
名称 | 介绍 |
---|---|
NioServerSocketChannel | 异步的服务器端 TCP Socket 连接 |
NioSocketChannel | 异步的客户端 TCP Socket 连接。 |
NioDatagramChannel | 异步的 UDP 连接 |
NioSctpChannel | 异步的客户端 Sctp 连接。 |
NioSctpServerChannel | 异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。 |
Selector 一个线程可以监听多个连接的 Channel 事件。
ChannelHandler 是一个接口
,处理 I/O 事件或拦截 I/O 操作
,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类
我们经常需要自定义一个 Handler 类去继承 ChannelInboundHandlerAdapter,然后通过重写相应方法实现业务逻辑
ChannelInboundHandler
用于处理入站 I/O 事件。ChannelOutboundHandler
用于处理出站 I/O 操作。适配器
ChannelInboundHandlerAdapter
用于处理入站 I/O 事件。ChannelOutboundHandlerAdapter
用于处理出站 I/O 操作。ChannelDuplexHandler
用于处理入站和出站事件。本例采用的是
1、ModbusTcpHandler extends SimpleChannelInboundHandler
2、public abstract class SimpleChannelInboundHandler extends ChannelInboundHandlerAdapter
3、public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler
你的应用程序会利用一个 ChannelHandler 来接收解码消息,并对该数据应用业务逻辑。 这种情况下,你只需要扩展基类 SimpleChannelInboundHandler,其中 T 是你要处理的消息的 Java 类型。
在这种类型的 ChannelHandler 中, 最重要的方法是 channelRead0
(ChannelHandlerContext,T)。除了要求不要阻塞当前的 I/O 线程之外,其具体实现完全取决于业务需要。
每个 Channel 都有且仅有一个 ChannelPipeline
与之对应,它们的组成关系如下一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler
入站事件和出站事件在一个双向链表
中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰
EventLoopGroup是一组 EventLoop的抽象,Netty为了更好的利用多核 CPU资源,一般会有多个 EventLoop同时工作,每个 EventLoop维护着一个 Selector 实例。
EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个 EventLoop来处理任务。在 Netty 服务器端编程中,我们一般都需要提供两个 EventLoopGroup,例如:BossEventLoopGroup 和WorkerEventLoopGroup。
需要注意的是,引导一个客户端只需要一个 EventLoopGroup,但是一个ServerBootstrap 则需要两个(也可以是同一个实例)
如果要bind 两个端口,boosgroup估计需要不同设定 不过这种情况比较少
通常一个服务端口即一个 ServerSocketChannel对应一个Selector 和一个EventLoop线程
。BossEventLoop 负责接收客户端的连接并将 SocketChannel 交给 WorkerEventLoopGroup 来进行 IO 处理,如下图所示
// 创建监听线程组,监听客户端请求,负责与客户端创建连接,并把连接注册到workerGroup的Selector中
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 创建工作线程组,处理每一个与客户端连接的数据通讯
//处理hadnler的工作线程,其实也就是处理IO读写 。
EventLoopGroup workerGroup = new NioEventLoopGroup();
原因:对于Server端,如果仅由一个EventLoopGroup处理所有请求和连接的话,在并发量很大的情况下,这个EventLoopGroup有可能会忙于处理已经接收到的连接而不能及时处理新的连接请求,用两个的话,会有专门的线程来处理连接请求,不会导致请求超时的情况,大大提高了并发处理能力
通常是 OP_ACCEPT 事件,然后将接收到的 SocketChannel 交给 WorkerEventLoopGroup
WorkerEventLoopGroup 会由 next 选择其中一个 EventLoop来将这个 SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理
Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。
ChannelOption 参数如下:
对应 TCP/IP 协议 listen 函数中的 backlog 参数,用来初始化服务器可连接队列大小。
服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定了队列的大小。
一直保持连接活动状态
EventLoopGroup bossGroup = new NioEventLoopGroup();
boss线程和work线程,有两个一个负责监听监听连接事件一个负责IO事件
如果没有设置程序启动参数,那么默认情况下线程的个数为cpu的核数乘以2
参考链接
readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)。
writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)。
allIdleTime:所有类型的超时时间。
IO 线程模型:同步非阻塞,用最少的资源做更多的事。
内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
串形化处理读写:避免使用锁带来的性能开销。
高性能序列化协议:支持 protobuf 等高性能序列化协议。
高效并发编程的体现:volatile的大量、正确使用;CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。
根据Reactor的数量和处理资源池线程的数量不同,分为以下三种线程模型:
1、单Reactor 单线程模型,
2、单Reactor 多线程模型
,
3、主从 Reactor 多线程模型
。
Netty是基于主从 Reactor 多线程模型
做了一定的改进。
Reactor三种模式形象比喻
餐厅一般有接待员和服务员,接待员负责在门口接待顾客,服务员负责全程服务顾客
Reactor的三种线程模型可以用接待员和服务员类比
单Reactor单线程模型:接待员和服务员是同一个人,一直为顾客服务。客流量较少适合
单Reactor多线程模型:一个接待员,多个服务员。客流量大,一个人忙不过来,由专门的接待员在门口接待顾客,然后安排好桌子后,由一个服务员一直服务,一般每个服务员负责一片中的几张桌子
多Reactor多线程模型:多个接待员,多个服务员。这种就是客流量太大了,一个接待员忙不过来了
所有操作都在同一个NIO线程处理,在这个单线程中要负责接收请求,处理IO,编解码所有操作,相当于一个饭馆只有一个人,同时负责前台和后台服务,效率低。
多线程的优点在于有单独的一个线程去处理请求,另外有一个线程池创建多个NIO线程去处理IO。相当于一个饭馆有一个前台负责接待,有很多服务员去做后面的工作,这样效率就比单线程模型提高很多。
缺点:
缺点在于并发量很高的情况下,只有一个Reactor单线程去处理是来不及的,就像饭馆只有一个前台接待很多客人也是不够的。为此需要使用主从线程模型。
主从线程模型:一组线程池接收请求,一组线程池处理IO。
这种模型使各个模块职责单一,降低耦合度,性能和稳定性都有提高。
在 Netty 主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 。
我们实现服务端的时候,一般会初始化两个线程组:
bossGroup :接收连接。
workerGroup :负责具体的处理,交由对应的 Handler 处理。
具体流程见下图:
串行无锁化设计:即消息的处理尽可能在同一个线程内完成,期间不进行线程切换
,这样就避免了多线程竞争和同步锁。
原理:
调用inEventLoop()来判断 当前线程 和 之前创建NioEventLoop时绑定的那个IO线程 是否一样?如果是一样的,说明此线程就是绑定的IO线程, 直接执行读写操作;如果不一样,就说明是其他线程,把读写操作封装成任务放在任务队列中,,由绑定的那个IO线程去执行.。
优势:
表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程
并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
https://blog.csdn.net/ai_xiangjuan/article/details/75808152
Netty的接收和发送ByteBuffer使用直接内存
进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用JVM的堆内存进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于使用直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
CompositeByteBuf
类——Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
Netty的文件传输采用了transferTo
方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
https://www.jianshu.com/p/a199ca28e80d
服务器端:
1~2:首先创建两个NioEventLoopGroup实例,它是一个由Netty封装好的包含NIO的线程组。bossGroup 是用于接收客户端的连接,原理就是一个实现的Selector的Reactor线程。而workerGroup用于进行SocketChannel的网络读写。
3:创建一个ServerBootstrap对象,作为服务端启动对象。
4:使用bootstrap.group(bossGroup , workGroup )
将两个NioEventLoopGroup实例绑定到ServerBootstrap对象中。
5:使用bootstrap..channel(NioServerSocketChannel.class)
创建Channel。(典型的channel有NioSocketChannel,NioServerSocketChannel,OioSocketChannel,OioServerSocketChannel,EpollSocketChannel,EpollServerSocketChannel),这里创建的是NIOserverSocketChannel,它的功能可以理解为当接受到客户端的连接请求的时候,完成TCP三次握手,TCP物理链路建立成功。并将该“通道”与workerGroup线程组的某个线程相关联。
6:使用bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
; 设置参数,保持活动连接状态
7:使用bootstrap.childHandler(initializer)
给workGroup的某一个eventLoop对应的Channel设置对应的处理器。就是我们接受数据后的具体操作,例如:记录日志,对信息解码编码等。
8:使用bootstrap.bind(localIp, modbusServerPort).sync( )
绑定端口,同步
等待成功。这里会生成一个ChannelFuture对象得到连接结果。
9:等待服务端监听端口关闭
客户端:
1:区别于服务端,我们在客户端只创建了一个NioEventLoopGroup实例
,因为客户端你并不需要使用I/O多路复用模型,需要有一个Reactor来接受请求。只需要单纯的读写数据即可
2:区别于服务端,我们在客户端只需要创建一个Bootstrap对象
,它是客户端辅助启动类,功能类似于ServerBootstrap。
3:将NioEventLoopGroup实例绑定到Bootstrap对象中。
4:创建Channel(典型的channel有NioSocketChannel,NioServerSocketChannel,OioSocketChannel,OioServerSocketChannel,EpollSocketChannel,EpollServerSocketChannel),区别与服务端,这里创建的是NIOSocketChannel
.
5:设置参数,这里设置的TCP_NODELAY为true,意思是关闭延迟发送,一有消息就立即发送,默认为false。
6:建立连接后的具体Handler。注意这里区别与服务端,使用的是handler()而不是childHandler()。handler和childHandler的区别在于,handler是接受或发送之前的执行器;childHandler为建立连接之后的执行器。
7:发起异步连接操作
8:当代客户端链路关闭
在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入 心跳机制。
心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性.
TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。
但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler
。
在 Netty 中, 实现心跳机制的关键是 IdleStateHandler, 那么这个 Handler 如何使用呢? 先看下它的构造器:
public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
}
这里解释下三个参数的含义:
readerIdleTimeSeconds
: 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE
的 IdleStateEvent 事件。如果要禁用它的话,就设置readerIdleTime=0。writerIdleTimeSeconds
: 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE
的 IdleStateEvent 事件。如果要禁用它的话,设置其writerIdleTime=0即可。allIdleTimeSeconds
: 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE
的 IdleStateEvent 事件。如果要禁用它的话,就设置allIdleTime=0。注意
1、这三个参数默认的时间单位是秒。若需要指定其他时间单位,可以使用另一个构造方法:IdleStateHandler(boolean observeOutput, long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit)
2、在使用的时候,我们只需要关注以上的三个参数就行。对于服务器来说,重点关注readIdleTimeSecond,指定时间内没有读到Socket缓冲区的数据,就认为异常。对于客户端来说,可以关注readIdleTimeSecond和writeIdleTimeSecond,或者可以指定allIdleTimeSeconds参数即可。
服务端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理;
1、IdleStateHandler这个类主要也是一个ChannelHandler,也需要被载入到ChannelPipeline中,加入我们在服务器端的ChannelInitializer中加入如下的代码。 设定IdleStateHandler心跳检测每五秒进行一次读检测,如果五秒内ChannelRead()方法未被调用则触发一次userEventTrigger()方法
我们在channel链中加入了IdleSateHandler,第一个参数是5,单位是秒,那么这样做的意思就是:在服务器端会每隔5秒来检查一下channelRead方法被调用的情况,如果在5秒内该链上的channelRead
方法都没有被触发,就会调用userEventTriggered
方法:
2、自定义处理类Handler继承ChannlInboundHandlerAdapter,实现其userEventTriggered( ) 方法,在出现超时事件时会被触发,包括读空闲超时或者写空闲超时;
我们分别实现了读写方法(channelRead)
、异常机制(exceptionCaught)
、超时处理(userEventTriggered)
以及客户端退出通知事件(channelInactive)
。
在上面的方法中,我们重点关注超时机制处理。在进入这个userEventTriggered方法后,我们可以根据事件类型进行相应的处理。我们这里服务端重点关注读超时。这里我们将其 channel关闭。
在实际业务场景中,我们可能采用3次或者n次超时就断开客户端channel。这里我们加了一个变量lossConnectCount进行统计。
其中,如果配置了写超时,则可以修改如下,试着重连客户端
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("已经5秒未收到客户端的消息了!");
if (evt instanceof IdleStateEvent){
//将evt转型成IdleStateEvent
IdleStateEvent event = (IdleStateEvent)evt;
if (event.state()== IdleState.READER_IDLE){
lossConnectCount++;
if (lossConnectCount>2){
System.out.println("关闭这个不活跃通道!");
ctx.channel().close();
}else if(event.state().equals(IdleState.WRITER_IDLE)) {
//当写空闲时,就发送ping消息给对端!!!!!!!!!!!!!!!!!!!!
ctx.writeAndFlush("ping");
}
}
}else {
super.userEventTriggered(ctx,evt);
}
}
1、客户端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理;
2、设定IdleStateHandler心跳检测每四秒进行一次写检测,如果四秒内write()方法未被调用则触发一次userEventTrigger()方法,实现客户端每四秒向服务端发送一次消息;
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new IdleStateHandler(0,4,0, TimeUnit.SECONDS));
socketChannel.pipeline().addLast(new StringEncoder());
socketChannel.pipeline().addLast(new HeartBeatClientHandler());
}
});
3、自定义处理类Handler继承ChannlInboundHandlerAdapter,实现自定义userEventTrigger()方法,如果出现超时时间就会被触发,包括读空闲超时或者写空闲超时;
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("客户端循环心跳监测发送: "+new Date());
if (evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent)evt;
if (event.state()== IdleState.WRITER_IDLE){
if (curTime<beatTime){
curTime++;
ctx.writeAndFlush("biubiu");
}
}
}
}
今天研究的是,心跳和重连,虽然这次是大神写的代码,但是万变不离其宗,我们先回顾一下Netty应用心跳和重连的整个过程:
1)客户端连接服务端
2)在客户端的的ChannelPipeline中加入一个比较特殊的IdleStateHandler,设置一下客户端的写空闲时间,例如5s
3)当客户端的所有ChannelHandler中4s内没有write事件,则会触发userEventTriggered方法(上文介绍过)
4)我们在客户端的userEventTriggered中对应的触发事件下发送一个心跳包给服务端,检测服务端是否还存活,防止服务端已经宕机,客户端还不知道
5)同样,服务端要对心跳包做出响应,其实给客户端最好的回复就是“不回复”,这样可以服务端的压力,假如有10w个空闲Idle的连接,那么服务端光发送心跳回复,则也是费事的事情,那么怎么才能告诉客户端它还活着呢,其实很简单,因为5s服务端都会收到来自客户端的心跳信息,那么如果10秒内收不到,服务端可以认为客户端挂了,可以close链路
6)加入服务端因为什么因素导致宕机的话,就会关闭所有的链路链接,所以作为客户端要做的事情就是短线重连
因为断线重连是客户端的工作,所以只需对客户端代码进行修改
客户端需要关注点的地方:channelActive
、channelInactive
这两个方法
channelActive
:每次连接成功是被调用。将重连次数置为0;
channelInactive
:每次连接关闭的时候被调用,进行循环的断线重连操作,直至超过重连次数上限。
/**
* channel链路每次active的时候,将其连接的次数重新☞ 0
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("当前链路已经激活了,重连尝试次数重新置为0");
attempts = 0;
ctx.fireChannelActive();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("链接关闭");
if(reconnect){
System.out.println("链接关闭,将进行重连");
if (attempts < 12) {
attempts++;
//重连的间隔时间会越来越长
int timeout = 2 << attempts;
timer.newTimeout(this, timeout, TimeUnit.MILLISECONDS);
}
}
ctx.fireChannelInactive();
}
————————————————
版权声明:本文为CSDN博主「BazingaLyncc」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/linuu/article/details/51509847
https://blog.csdn.net/u013967175/article/details/78591810
https://baijiahao.baidu.com/s?id=1669639041722396699&wfr=spider&for=pc
https://www.jianshu.com/p/1a28e48edd92
https://baijiahao.baidu.com/s?id=1669639041722396699&wfr=spider&for=pc
https://blog.csdn.net/qq_35751014/article/details/104524889
https://blog.csdn.net/eric_sunah/article/details/80424381
https://blog.csdn.net/linuu/article/details/51509847
https://www.jianshu.com/p/1a28e48edd92