前言
本文讨论的主题是Netty框架,本着3W原则 (What 是什么?->Why 为什么?->How 如何做?)来一步步探究Netty原理和本质以及运用场景。
了解基本名词
1.BIO、NIO和AIO是什么?
- BIO:同步阻塞,一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理,面向流的,各种流是阻塞的,流是单向的。
- NIO:同步非阻塞,一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理,面向缓冲区的,非阻塞,流是双向的,事件驱动模型,基于block的传输比基于流的传输更高效、更高级的IO函数zero-copy、IO多路复用大大提高了Java网络应用的可伸缩性和实用性,基于Reactor线程模型。
- AIO:异步非阻塞,一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
2.什么是Netty零拷贝?
Netty 的零拷贝主要包含三个方面:
- Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
- Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。
- Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。
3.select、poll、epoll的机制及其区别?
**
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
- select:单个进程可监视的fd数量被限制,消息传递方式通过内核需要将消息传递到用户空间,都需要内核拷贝动作。
- poll:同select,主要区别是它没有最大连接数的限制,原因是它是基于链表来存储的。
- epoll:没有最大并发连接的限制,效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;消息传递方式通过内核和用户空间共享一块内存来实现的。
4.TCP的粘包/拆包原因及其解决方法是什么?
原因:
要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包
解决方案:
- 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
- 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
- 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
何为Netty?
Netty是一个异步事件驱动(NIO)的网络应用程序框架,用于快速开发可维护的搞性能协议服务器和客户端。极大的简化了TCP和UDP套接字服务器等网络编程。Netty支持多种协议,如FTP,SMTP,HTTP以及各种二进制和基于文本的传输协议。
为什么用Netty?
特点
- 一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持
- 使用更高效的socket底层,对epoll空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO的陷阱,简化了NIO的处理方式。
- 采用多种decoder/encoder 支持,对TCP粘包/分包进行自动化处理
- 可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持
- 可配置IO线程数、TCP参数, TCP接收和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf
- 通过引用计数器及时申请释放不再引用的对象,降低了GC频率
- 使用单线程串行化的方式,高效的Reactor线程模型
- 大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用
设计
- 适用于各种传输类型的统一API - 阻塞和非阻塞套接字
- 基于灵活且可扩展的事件模型,可以清晰地分离关注点
- 高度可定制的线程模型 - 单线程,一个或多个线程池,如SEDA
- 真正的无连接数据报套接字支持(自3.1起)
性能
- 更高的吞吐量,更低的延迟
- 减少资源消耗
- 最小化不必要的内存复制
安全
首先引入Maven包
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
编写服务端,代码如下:
package netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
/**
* @author LWX-PC
* @version 1.0
* @class DiscardServerHandler
* @date 2019/8/18 18:39
* @description
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
try {
while (in.isReadable()) { // (1)
System.out.print((char) in.readByte());
System.out.flush();
}
} finally {
ReferenceCountUtil.release(msg); // (2)
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
package netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* Discards any incoming data.
*/
public class DiscardServer {
private int port;
public DiscardServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // (3)
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128) // (5)
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(port).sync(); // (7)
// Wait until the server socket is closed.
// In this example, this does not happen, but you can do that to gracefully
// shut down your server.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 9000;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
new DiscardServer(port).run();
}
}
测试:
回车后,输入内容:
控制台可打印出输入信息,如下:
这样整个通信就建立了。