参考专栏:https://blog.csdn.net/qq_37909508/category_8983741.html
原理主要参照:https://www.jianshu.com/p/40a2004a531b
零拷贝:http://www.360doc.com/content/19/0528/13/99071_838741319.shtml
1 Netty用途
1.1 服务器
- 有了netty,你就可以实现自己的http服务器,FTP服务器,UDP服务器,RPC服务器,WebSocket服务器,Redis的Proxy服务器,MySQL的Proxy服务器等等。
我们回顾一下传统的HTTP服务器的原理
1)创建一个ServerSocket,监听并绑定一个端口
2)客户端请求端口
3)服务使用Accept,获取客户端的socket连接对象
4)启动一个线程处理
4.1 读Socket,得到字节流
4.2 解码协议,得到Http请求对象
4.3 处理请求,处理后封装成HttpResponse对象
4.4 编码协议,将结果序列化字节流,写入Socket,将字节流发送给客户端
5) 继续循环步骤3,4
HTTP服务器之所以称为HTTP服务器,是因为编码解码协议是HTTP协议,如果协议是Redis协议,那它就成了Redis服务器,如果协议是WebSocket,那它就成了WebSocket服务器,等等。 使用Netty你就可以定制编解码协议,实现自己的特定协议的服务器。
2 传统BIO服务器到NIO服务器
上面是一个传统处理http的服务器,但是在高并发的环境下,线程数量会比较多,System load也会比较高,于是就有了NIO。
- NIO简介: 他并不是Java独有的概念,NIO代表的一个词汇叫着IO多路复用。它是由操作系统提供的系统调用,早期这个操作系统调用的名字是select,但是性能低下,后来渐渐演化成了Linux下的epoll和Mac里的kqueue。我们一般就说是epoll,因为没有人拿苹果电脑作为服务器使用对外提供服务。而Netty就是基于Java NIO技术封装的一套框架。为什么要封装,因为原生的Java NIO使用起来没那么方便,而且还有臭名昭著的bug,Netty把它封装之后,提供了一个易于操作的使用模式和接口,用户使用起来也就便捷多了。
说NIO之前先说一下BIO(Blocking IO),如何理解这个Blocking呢?
- 客户端监听(Listen)时,Accept是阻塞的,只有新连接来了,Accept才会返回,主线程才能继续
- 读写socket时,Read是阻塞的,只有请求消息来了,Read才能返回,子线程才能继续处理
- 读写socket时,Write是阻塞的,只有客户端把消息收了,Write才能返回,子线程才能继续读取下一个请求
传统的BIO模式下,从头到尾的所有线程都是阻塞的,这些线程就干等着,占用系统的资源,什么事也不干。
那么NIO是怎么做到非阻塞的呢。它用的是事件机制。它可以用一个线程把Accept,读写操作,请求处理的逻辑全干了。如果什么事都没得做,它也不会死循环,它会将线程休眠起来,直到下一个事件来了再继续干活,这样的一个线程称之为NIO线程。用伪代码表示:
while true {
events = takeEvents(fds) // 获取事件,如果没有事件,线程就休眠
for event in events {
if event.isAcceptable {
doAccept() // 新链接来了
} elif event.isReadable {
request = doRead() // 读消息
if request.isComplete() {
doProcess()
}
} elif event.isWriteable {
doWrite() // 写消息
}
}
}
3 Reactor线程模式
建议参考:1. https://blog.csdn.net/quxing10086/article/details/80296245
2. https://www.jianshu.com/p/5d18fc614ade
Netty能基于以下三种线程模型来处理连接,具体按实际使用
-
单线程模型(较少用)
-
多线程模型
简介:一个线程/进程接收连接、一组线程/进程处理IO读写事件。
一般的多线程模型(使用线程池)
netty4的多线程模型(读写事件使用使用多个线程)
实际reactor的多线程模型,并不需要将已连接套接字绑定在某个线程上,也可以统一放在连接池中,由多个IOWork线程从池中取连接进行轮询并处理,但这样会复杂很多,而且容易出问题,比如说不同线程从同一个channel收到了write事件,这就类似惊群问题了;并且多线程并发操作同一个channel,后续很可能需要你讲IO事件进行同步,与其如此,不如直接将channel绑定到一个线程,让channel上触发与处理IO事件逻辑上同步。netty3中channel(已连接套接字)入站事件由固定线程处理,出站事件由触发的线程处理,netty4中修改了设计,将channel绑定到固定的eventloop(线程)。
- 主从线程模型
简介:一组线程/进程接收连接、一组线程/进程处理IO读写事件。它与多线程模型的主要区别在于其使用一组线程或进程在一个共享的监听套接字上accept连接,而不是一个线程。这么做的原因是为了应付单个线程/进程不足以快速处理内核中监听套接字的已连接套接字队列(并发量极大)的情况。如下
主从多线程模型,有可能引起惊群效应。不过这个问题已经渐渐被规避,内核可以保证连接只被唯一一个accept调用所获取,其余对此连接的accept调用将失败。
- 总结
1)Netty是建立在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象。
2)在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。
3)Accept连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的高性能并发模型。
4 为什么选择Netty
如果不用netty,使用原生JDK的话,有如下问题:
1、API复杂
2、对多线程很熟悉:因为NIO涉及到Reactor模式
3、高可用的话:需要出路断连重连、半包读写、失败缓存等问题
4、JDK NIO的bug
而Netty来说,他的api简单、性能高而且社区活跃(dubbo、rocketmq等都使用了它)
解决TCP粘包/拆包
- 粘包是怎么样的我们来尝试一下
public class FirstClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 1000; i++) {
ByteBuf buffer = getByteBuf(ctx);
ctx.channel().writeAndFlush(buffer);
}
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
byte[] bytes = "你好,我的名字是1234567!".getBytes(Charset.forName("utf-8"));
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(bytes);
return buffer;
}
}
读取到的数据
2. 从服务端的控制台输出可以看出,存在三种类型的输出
一种是正常的字符串输出。
一种是多个字符串“粘”在了一起,我们定义这种 ByteBuf 为粘包。
一种是一个字符串被“拆”开,形成一个破碎的包,我们定义这种 ByteBuf 为半包。
- 为什么会发生TCP粘包、拆包?
发生TCP粘包、拆包主要是由于下面一些原因:
- 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
- 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
- 进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。
- 接收方法不及时读取套接字缓冲区数据,这将发生粘包。
- 粘包、拆包解决办法
TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
- netty解决方法
在Netty中,已经造好了许多类型的拆包器,我们直接用就好
5 零拷贝
- 传统的拷贝
是在发送数据的时候,传统的实现方式是:
- File.read(bytes)
- Socket.send(bytes)
这种方式需要四次数据拷贝和四次上下文切换:
- 数据从磁盘读取到内核的read buffer
- 数据从内核缓冲区拷贝到用户缓冲区
- 数据从用户缓冲区拷贝到内核的socket buffer
?4. 数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区
- 零拷贝
- 概念:明显上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)
- 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
- 接着DMA从内核read buffer将数据拷贝到网卡接口buffer
上面的两次操作都不需要CPU参与,所以就达到了零拷贝。
Netty中的零拷贝
主要体现在三个方面:
1、bytebuffer
Netty发送和接收消息主要使用bytebuffer,bytebuffer使用对外内存(DirectMemory)直接进行Socket读写。
原因:如果使用传统的堆内存进行Socket读写,JVM会将堆内存buffer拷贝一份到直接内存中然后再写入socket,多了一次缓冲区的内存拷贝。DirectMemory中可以直接通过DMA发送到网卡接口
2、Composite Buffers
传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
3、对于FileChannel.transferTo的使用
Netty中使用了FileChannel的transferTo方法,该方法依赖于操作系统实现零拷贝。
Netty 内部执行流程
服务端:
1、创建ServerBootStrap实例
2、设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel
3、设置并绑定服务端的channel
4、5、创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证
6、绑定并启动监听端口
7、当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler
客户端
Channel、EventLoop和ChannelFuture详解
参考文章:https://blog.csdn.net/q1532440739/article/details/81448119
Netty可以说是有Channel、EventLoop、ChannelFuture聚合起来的一个网络抽象代表
- Channel——Socket;
- EventLoop——控制流、多线程处理、并发
- ChannelFuture——异步通知
Channel接口
基本的I/O操作(bing()、connect()、read()、和write())依赖于底层网络传输所提供的原始。在基于Java的网络编程中,其基本的构造是class Socket。Netty的Channel接口所提供的API,大大地降低了直接使用Socket类的复杂性。此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:
EmbeddedChannel; LocalServerChannel; NioDatagramChannel; NioSctpChannel; NioSocketChannel;
EventLoop接口(简单解释,具体建议百度)
EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。
-
一个EventLoopGroup包含一个或者多个EventLoop;
-
一个EventLoop在它的生命周期内只和一个Thread绑定;
-
所有由EventLoop处理得I/O事件都将在它专有的Thread上处理
-
一个Channel在它的生命周期内只注册一个EventLoop; - 一个EventLoop可能会被分配给一个或多个Channel。
注意,一个给定的Channel的I/O操作都是由相同的Thread执行的,实际上消除了对于同步的需要
ChannelFuture接口
Netty所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后得某个时间点确定其结果的方法。为此,Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
读写检测和长连接和断线重连(心跳检测)
参考解决代码:https://blog.csdn.net/hzf1993/article/details/82841043
大概解决方法:
- 读写检测
- 服务端和客户端的管道列表都添加相应的处理器
pipeline.addLast(new IdleStateHandler(40,0,0,TimeUnit.SECONDS));- 在handler中写相应的处理,当检测到断线事件时,做反应
重写userEventTriggered()
- 断线重连
channelInactive 该方法中也实现了断线重连的功能,以防止在运行过程中突然断线。
userEventTriggered:该方法中实现了如果30s内客户端没有向服务端写入任何消息,该方法就会触发向服务端发送心跳信息,从而保持客户端与服务端的长连接。
- 保持长连接(选择option:https://blog.csdn.net/smallcatbaby/article/details/89877617)
b…childOption(ChannelOption.SO_KEEPALIVE,true)
//保持长连接,Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
future.channel().closeFuture().sync()作用
https://blog.csdn.net/m0_45406092/article/details/104394617
让线程进入wait状态,也就是main线程暂时不会执行到finally里面,nettyserver也持续运行,如果监听到关闭事件,可以优雅的关闭通道和nettyserver,虽然这个例子中,永远不会监听到关闭事件。也就是说这个例子是仅仅为了展示存在api shutdownGracefully,可以优雅的关闭
ChannelFuture
https://www.jianshu.com/p/8a35a7bc4751