今天给大家讲讲家喻户晓的netty!
netty的诞生
Netty的创始人是韩国人trustin lee,他现在韩国line公司工作。netty 目前的项目leader 是德国人Norman maurer,也是https://twitter.com/normanmaurerhttps://twitter.com/trustin
补充一点 ,Norman maurer 之前在redhat工作时,是全职开发Netty的,那时候netty项目属于redhat旗下,相当于redhat付工资让Norman全职开发Netty ,这比题主说的那种全职是不是更爽一点?)
java另一款网络编程框架mina跟netty都源自韩国这位大佬之手,膜拜!!!!!!!
什么是netty
Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award,见https://www.java.net/dukeschoice/2011)。它活跃和成长于用户社区,像大型公司 Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。Netty 是一个基于 JAVA NIO(Nonblocking I/O,非阻塞同步IO)类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。对比于BIO(Blocking I/O,阻塞同步IO),他的并发性能得到了很大提高,两张图让你了解BIO和NIO的区别:
BIO网络模型
对于同一个socket采用单线程出处理,处理整个读写以及
NIO网络模型
从这两图可以看出,NIO的单线程能处理连接的数量比BIO要高出很多,而为什么单线程能处理更多的连接呢?原因就是图二中出现的Selector。
当一个连接建立之后,他有两个步骤要做,第一步是接收完客户端发过来的全部数据,第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是不阻塞的,这样就能让一个Thread处理更多的请求了。然而Bio通常使用线程池的方法去接入更多的链接,工作效率相对较低。
Netty为什么高性能
零拷贝是网络编程的关键,很多性能优化都离不开零拷贝。在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。
Java 中传统的 IO 和网络编程:
传统意义NIO的文件写入socket拷贝
java中应用:
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图:
java中应用:
// 从标准输入获取数据
Scanner sc = new Scanner(System.in);
System.out.println("请输入:");
String str = sc.nextLine();
byte[] bytes = str.getBytes();
RandomAccessFile raf = new RandomAccessFile("map.txt", "rw");
FileChannel channel = raf.getChannel();
// 获取内存映射缓冲区,并向缓冲区写入数据
MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_WRITE, 0, bytes.length);
mappedBuffer.put(bytes);
raf.close();
raf.close();
// 再次打开刚刚的文件,读取其中的内容
raf = new RandomAccessFile("map.txt", "rw");
channel = raf.getChannel();
System.out.println("\n文件内容:")
System.out.println(readChannel(channel));
raf.close();
raf.close();
Linux 2.1 版本提供了 sendFile 函数,其基本原理是:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了一次上下文切换。
在java中FileChannel的transferTo方法使用的就是sendFile函数,需要操作系统的支持
java中应用:
public class ZeroCopyClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String filename = "E:\\a.pdf";
FileChannel fileChannel = new FileInputStream(filename).getChannel();
long startTime = System.currentTimeMillis();
// 在 linux 下一个 transferTo 方法就可以完成传输,但 windows 下调用一次 transferTo 只能发送 8m,就需要分段传输而且要注意传输的位置
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总字节数:" + transferCount + ",耗时:" + (System.currentTimeMillis() - startTime));
fileChannel.close();
Thread.sleep(1000);
}
}
Netty针对这种情况,使用了nio的零拷贝机制,而在Netty中还有另一种形式的零拷贝,即Netty允许我们将多段数据合并为一整段虚拟数据供用户使用,而过程中不需要对数据进行拷贝操作,这也是我们今天要讲的重点关于ByteBuffer,关于ByteBuffer,Netty提供了两个接口:
- ByteBuf
- ByteBufHolder
对于ByteBuf,Netty提供了多种实现:
- Heap ByteBuf:直接在堆内存分配
- Direct ByteBuf:直接在内存区域分配而不是堆内存
- CompositeByteBuf:组合Buffer
Netty核心概念讲解
首先看下如何启动一个netty服务
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup work = new NioEventLoopGroup(2);
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, work).channel(Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.handler(new ServerFatherHandler())
.childHandler(new NettyServerHandlerInitializer())
.option(ChannelOption.SO_BACKLOG, 1024) //用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.TCP_NODELAY, true) //禁用nagle算法
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.SO_KEEPALIVE, true) //连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
.childOption(ChannelOption.SO_RCVBUF, 1024 * 100)
.childOption(ChannelOption.SO_SNDBUF, 1024 * 100);//设置链接的channel的option
if (Epoll.isAvailable()) {
serverBootstrap.option(EpollChannelOption.EPOLL_MODE, EpollMode.EDGE_TRIGGERED)
.option(EpollChannelOption.TCP_QUICKACK, Boolean.TRUE);
}
ChannelFuture future = serverBootstrap.bind().sync();
logger.info("nettyServer服务端启动成功=====");
future.channel().closeFuture().sync();
EventLoop详解
由下图所示,NioEventLop是EventLoop的一个具体实现,EventLoop是EventLoopGroup的一个属性,NioEventLoopGroup是EventLoopGroup的具体实现,都是基于ExecutorService进行的线程池管理,因此EventLoop、EventLoopGroup组件的核心作用就是进行Selector的维护以及线程池的维护。一个EventLoop绑定一个Selector,同时管理多个SelectKeys,
由上图可知,一个NioEventLop可以理解成一个线程,他维护的一个Selector,同时会去管理多个Channel,因此当连接建立后,Channel对应的EventLoop已经确定!
ChannelHandler详解
从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,让用户自定义添加ChannelHandler去处理读写事件,整个设计是基于责任链模式去实现。
ChannelHandler主要分为两大类,ChannelInboundHandler(入)和ChannelOutboundHandler(出),netty中使用ChannelPipeline去保存ChannelHandler双向链表的维护,链表在初始化的时候会生成一个Tail跟Header节点,当一个channel触发写事件时候是从Tail->Head触发,匹配当前handler是ChannelOutboundHandler类型,相反写事件则是Head->Tail匹配当前handler类型为ChannelInboundHandler。
ByteBuf设计
网络数据的基本单位总是字节,java NIO提供ByteBuffer作为字节的容器,但是ByteBuffer使用起来过于复杂和繁琐。
ByteBuf是netty的Server与Client之间通信的数据传输载体(Netty的数据容器),它提供了一个byte数组(byte[])的抽象视图,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的API。
ByteBuffer缺点
ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常;
ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;
ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。
ByteBuf优点
- 容量可以按需增长
- 读写模式切换不需要调用flip()
- 读写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
- 可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现透明的零拷贝
使用用例
使用Unpooled工具类来创建非池化的ByteBuf
@Test
public void testHeapByteBuf() {
ByteBuf heapBuf = Unpooled.buffer(10);
if (heapBuf.hasArray()) {
byte[] array = heapBuf.array();
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
int length = heapBuf.readableBytes();
//0,0
logger.info("offset:{},length:{}", offset, length);
}
}
@Test
public void testGetSet(){
ByteBuf byteBuf = Unpooled.copiedBuffer("jannal", Charset.forName("utf-8"));
System.out.println((char)byteBuf.getByte(0));
int readIndex = byteBuf.readerIndex();
int writerIndex= byteBuf.writerIndex();
byteBuf.setByte(0,(byte)'J');
System.out.println((char)byteBuf.getByte(0));
assert readIndex== byteBuf.readerIndex();
assert writerIndex== byteBuf.writerIndex();
}
@Test
public void testReadWrite(){
ByteBuf byteBuf = Unpooled.copiedBuffer("jannal", Charset.forName("utf-8"));
System.out.println((char)byteBuf.readByte());
int readIndex = byteBuf.readerIndex();
int writerIndex= byteBuf.writerIndex();
byteBuf.writeByte((byte)'J');
assert readIndex== byteBuf.readerIndex();
assert writerIndex!= byteBuf.writerIndex();
}
Channel详解
Channel是由netty抽象出来的网络I/O操作的接口,作为Netty传输的核心,负责处理所有的I/O操作。Channel提供了一组用于传输的API,主要包括网络的读/写,客户端主动发起连接、关闭连接,服务端绑定端口,获取通讯双方的网络地址等;同时,还提供了与netty框架相关的操作,如获取channel相关联的EventLoop、pipeline等。
一个Channel可以拥有父Channel,服务端Channel的parent为空,对客户端Channel来说,它的parent就是创建它的ServerSocketChannel。当一个Channel相关的网络操作完成后,请务必调用ChannelOutboundInvoker.close()或ChannelOutboundInvoker.close(ChannelPromise)来释放所有资源,如文件句柄。
每个Channel都会被分配一个ChannelPipeline和ChannelConfig。ChannelConfig主要负责Channel的所有配置,并且支持热更新。此外,每个Channel都会绑定一个EventLoop,该通道整个生命周期内的事件都将由这个特定EventLoop负责处理。
Channel是独一无二的,所以为了保证顺序将Channel声明为Comparable的一个子接口。如果两个Channel实例返回了相同的hashcode,那么AbstractChannel中compareTo()方法的实现将会抛出一个Error。
在netty中,所有的I/O操作都是异步的,这些操作被调用将立即返回一个ChannelFuture实例,用户通过ChannelFuture实例获取操作的结果。下面主要介绍下Channel中的传输API及其作用。
Channel的Api
请求将当前的msg通过ChannelPipeline写入到目标Channel中。注意,write操作只是将消息存入到消息发送环形数组中,并没有被真正发送,只有调用flush操作才会将消息写入到Channel,发送给对方。区别在于方法2提供了ChannelPromise实例,用于设置写入操作的结果。该操作会触发outbound事件,该事件会级联触发ChannelPipeline中ChannelHandler.write(ChannelHandlerContext,msg,ChannelPromise)方法被调用。
ChannelFuture write(Object msg);
ChannelFuture write(Object msg, ChannelPromise promise);
将write操作写入环形数组中的消息全部写入到Channel中,发送给通信对方。该操作会触发outbound事件,该事件会级联触发ChannelPipeline中ChannelHandler.flush(ChannelHandlerContext)方法被调用。
Channel flush();
write操作和flush操作的组合。
ChannelFuture writeAndFlush(Object msg);
ChannelFuture writeAndFlush(Object msg, ChannelPromise promise);
主动关闭当前连接,close操作会触发链路关闭事件,该事件会级联触发ChannelPipeline中ChannelOutboundHandler.close(ChannelHandlerContext,ChannelPromise)方法被调用;区别在于方法2提供了ChannelPromise实例,用于设置close操作的结果,无论成功与否。
ChannelFuture close();
ChannelFuture close(ChannelPromise promise);
用于绑定指定的本地Socket地址localAddress,触发outbound事件,该事件会级联触发ChannelPipeline中ChannelHandler.bind(ChannelHandlerContext,SocketAddress,ChannelPromise)方法被调用。
ChannelFuture connect(SocketAddress remoteAddress)
ChannelFuture connect(SocketAddress remoteAddress,ChannelPromise promise);
其他api请参考 https://netty.io/4.1/api/io/netty/channel/Channel.html
结束
识别下方二维码!回复:
入群
,扫码加入我们交流群!