什么是Netty
Netty是一个开源的异步事件驱动的网络应用程序框架(基于Java
NIO
技术封装的一套框架),用于快速开发可维护的高性能协议服务器和客户端。
JDK NIO 与 Netty
JDK 本身提供了一套 NIO 的 API,但是这一套原生的 API 存在一系列的问题。
-
Java NIO 的 API 非常复杂。 要写出成熟可用的 Java NIO 代码,需要熟练掌握 JDK 中的 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等组件,还要理解其中一些反人类的设计以及底层原理,这对新手来说是非常不友好的。
-
如果直接使用 Java NIO 进行开发,难度和开发量会非常大。我们需要自己补齐很多可靠性方面的实现,例如,网络波动导致的连接重连、半包读写等。这就会导致一些本末倒置的情况出现:核心业务逻辑比较简单,但补齐其他公共能力的代码非常多,开发耗时比较长。这时就需要一个统一的 NIO 框架来封装这些公共能力了。
-
JDK 自身的 Bug。其中比较出名的就要属 Epoll Bug 了,这个 Bug 会导致 Selector 空轮询,CPU 使用率达到 100%,这样就会导致业务逻辑无法执行,降低服务性能。
Netty
在 JDK 自带的 NIO API 基础之上进行了封装,解决了 JDK 自身的一些问题,具备如下优点:
-
1、入门简单,使用方便,文档齐全,无其他依赖,只依赖 JDK 就够了。
-
2、高性能,高吞吐,低延迟,资源消耗少。
-
3、灵活的线程模型,支持阻塞和非阻塞的I/O 模型。
-
4、代码质量高,目前主流版本基本没有 Bug。
Netty能做什么
HTTP服务器
先说传统的HTTP服务器的原理
1、创建一个ServerSocket,监听并绑定一个端口
2、一系列客户端来请求这个端口
3、服务器使用Accept,获得一个来自客户端的Socket连接对象
4、启动一个新线程处理连接
4.1、读Socket,得到字节流
4.2、解码协议,得到Http请求对象
4.3、处理Http请求,得到一个结果,封装成一个HttpResponse对象
4.4、编码协议,将结果序列化字节流 写Socket,将字节流发给客户端
5、继续循环步骤3
HTTP服务器之所以称为HTTP服务器,是因为编码解码协议是HTTP协议,如果协议是Redis协议,那它就成了Redis服务器,如果协议是WebSocket,那它就成了WebSocket服务器,等等。 使用Netty你就可以定制编解码协议,实现自己的特定协议的服务器。
总结
Netty可以作为基础通信组件、能够轻松解决之前有较高门槛的通信系统开发,不用再为如何解析各类简单、或复杂的通讯协议而烦恼
可以基于Netty来实现自己得HTTP服务器,FTP服务器,UDP服务器,RPC服务器,WebSocket服务器,Redis的Proxy服务器,MySQL的Proxy服务器等等
Netty的特点
- 高并发:Netty 是一款基于
NIO
(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于BIO
(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。 - 传输快:Netty 的传输依赖于
零拷贝
特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。 - 封装好:Netty 封装了
NIO
操作的很多细节,提供了易于使用调用接口。
Netty的优势
- 使用简单:封装了
NIO
的很多细节,使用更简单。 - 功能强大:预置了多种编解码功能,支持多种主流协议。
- 定制能力强:可以通过
ChannelHandler
对通信框架进行灵活地扩展。 - 性能高:通过与其他业界主流的
NIO
框架对比,Netty 的综合性能最优。 - 稳定:Netty 修复了已经发现的所有
NIO
的 bug,让开发人员可以专注于业务本身。 - 社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。
Netty高性能表现在哪些方面?
- IO 线程模型:同步非阻塞,用最少的资源做更多的事。
- 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
- 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
- 串形化处理读写:避免使用锁带来的性能开销。
- 高性能序列化协议:支持
protobuf
等高性能序列化协议。
Netty开发的Hello World
创建Maven工程加入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
创建客户端Handler
首先创建Handler类,该类用于接收服务器端发送的数据,这是一个简化的类,只重写了消息读取方法channelRead0
、捕捉异常方法exceptionCaught
。
客户端的Handler一般继承的是SimpleChannelInboundHandler
,该类有丰富的方法,心跳、超时检测、连接状态等等。
代码如下:
/**
* @description: 通用handler,处理IO事件
* @author: DAIHAO
* @time: 2020/7/8 10:10
*/
@ChannelHandler.Sharable//@ChannelHandler.Sharable 标识这类的实例之间可以在 channel 里面共享,为了线程安全,不在乎是否线程安全,不加也可以
public class HandlerClient extends SimpleChannelInboundHandler<ByteBuf> {//类型可以是ByteBuf,也可以是String,还可以是对象,根据实际情况来。
//服务器的连接被建立后调用
@Override
public void channelActive(ChannelHandlerContext ctx) {
//ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
}
/**
* channelRead0 数据后从服务器接收到调用 消息读取方法,注意名称中有个0
*
* @param channelHandlerContext 通道上下文,代指Channel
* @param byteBuf 字节序列,通过ByteBuf操作基础的字节数组和缓冲区。
* 因为JDK原生操作字节麻烦,效率低。所以Netty对字节的操作进行了封装,实现了指数级的性能提升,同时使用更加便利
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
/**
* 处理接受到的消息 CharsetUtil.UTF_8:这个是JDK原生的方法,用于指定字节数组转换为字符串时的编码格式
*/
System.out.println("接收到的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 捕获一个异常时调用 处理I/O事件的异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
创建客户端启动类
/**
* @description: 客户端启动类
* @author: DAIHAO
* @time: 2020/7/8 10:27
*/
public class AppClient {
private final String host;
private final int port;
public AppClient(String host, int port) {
this.host = host;
this.port = port;
}
/**
* 配置相应的参数,提供连接到远端的方法
*/
public void run() throws Exception {
//I/O线程池
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
try {
//客户端辅助启动类
Bootstrap bs = new Bootstrap();
bs.group(eventExecutors)
.channel(NioSocketChannel.class)//实例化一个Channel
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {//进行通道初始化配置 如加入多个handler,都在这里进行
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
/**
* 添加我们指定自定义Handler
*
* pipeline(),连接建立后,都会自动创建一个管道pipeline,
* 这个管道也被称为责任链,保证顺序执行,同时又可以灵活的配置各类Handler,
* 这是一个很精妙的设计,既减少了线程切换带来的资源开销、避免好多麻烦事,同时性能又得到了极大增强。
*/
socketChannel.pipeline().addLast(new HandlerClient());
}
});
//连接到远程节点;等待连接完成 这里的sync()表示采用的同步方法,这样连接建立成功后,才继续往下执行。
ChannelFuture future = bs.connect().sync();
//发送消息到服务器端,编码格式是utf-8
future.channel().writeAndFlush(Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));
//阻塞操作,closeFuture()开启了一个channel的监听器(这期间channel在进行各项工作),直到链路断开
future.channel().closeFuture().sync();
} finally {
eventExecutors.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new AppClient("127.0.0.1", 18080).run();
}
}
创建服务端Handler
package com.demo.netty.server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @description: 服务器端I/O处理类 定义处理入站事件的方法
* @author: DAIHAO
* @time: 2020/7/8 10:10
*/
@ChannelHandler.Sharable
public class HandlerServer extends ChannelInboundHandlerAdapter {
/**
* channelRead 每个信息入站都会调用
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//处理收到的数据,并反馈消息到到客户端
ByteBuf in = (ByteBuf) msg;
System.out.println("收到客户端发过来的消息: " + in.toString(CharsetUtil.UTF_8));
//写入并发送信息到远端(客户端)
ctx.writeAndFlush(Unpooled.copiedBuffer("你好,我是服务端,我已经收到你发送的消息", CharsetUtil.UTF_8));
}
/**
* channelReadComplete 通知处理器最后的 channelread() 是当前批处理中的最后一条消息时调用
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
/**
* 读操作时捕获到异常时调用
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//出现异常的时候执行的动作(打印并关闭通道)
cause.printStackTrace();
ctx.close();
}
/**
//如果异常没有被捕获,会发生什么?
*
* 每个 Channel 都有一个关联的 ChannelPipeline,它代表了 ChannelHandler 实例的链。
* 适配器处理的实现只是将一个处理方法调用转发到链中的下一个处理器。
* 因此,如果一个 Netty 应用程序不覆盖exceptionCaught ,那么这些错误将最终到达 ChannelPipeline,并且结束警告将被记录。
* 出于这个原因,你应该提供至少一个 实现 exceptionCaught 的 ChannelHandler。
*
//关键点要牢记:
*
* ChannelHandler 是给不同类型的事件调用
* 应用程序实现或扩展 ChannelHandler 挂接到事件生命周期和 提供自定义应用逻辑。
*/
}
创建服务端启动类
/**
* @description: 服务器端启动类 最少需要设置服务器绑定的端口,用来监听连接请求。
* @author: DAIHAO
* @time: 2020/7/8 10:27
*/
public class AppServer {
private int port;
public AppServer(int port) {
this.port = port;
}
public void run() throws Exception {
//EventLoopGroup,实际项目中,这里创建两个EventLoopGroup的实例,一个负责接收客户端的连接,另一个负责处理消息I/O,这里为了简单展示流程,让一个实例把这两方面的活都干了
//Netty的Reactor线程池,初始化了一个NioEventLoop数组,用来处理I/O操作,如接受新的连接和读/写数据
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();//用于启动NIO服务
b.group(eventExecutors)
.channel(NioServerSocketChannel.class) //通过工厂方法设计模式实例化一个channel
.localAddress(new InetSocketAddress(port))//设置监听端口
.childHandler(new ChannelInitializer<SocketChannel>() {
//ChannelInitializer是一个特殊的处理类,他的目的是帮助使用者配置一个新的Channel,用于把许多自定义的处理类增加到pipline上来
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HandlerServer());//配置childHandler来通知一个关于消息处理的InfoServerHandler实例
}
});
//绑定服务器,该实例将提供有关IO操作的结果或状态的信息
ChannelFuture channelFuture = b.bind().sync();
System.out.println("在" + channelFuture.channel().localAddress() + "上开启监听");
//阻塞操作,closeFuture()开启了一个channel的监听器(这期间channel在进行各项工作),直到链路断开
channelFuture.channel().closeFuture().sync();
} finally {
eventExecutors.shutdownGracefully().sync();//关闭EventLoopGroup并释放所有资源,包括所有创建的线程
}
}
public static void main(String[] args) throws Exception {
new AppServer(18080).run();
}
}
代码说明:
- EventLoopGroup:实际项目中,这里创建两个EventLoopGroup的实例,一个负责接收客户端的连接,另一个负责处理消息I/O,这里为了简单展示流程,让一个实例把这两方面的活都干了。
- NioServerSocketChannel:通过工厂通过工厂方法设计模式实例化一个channel,这个在大家还没有能够熟练使用Netty进行项目开发的情况下,不用去深究。
最后先启动服务器端,后启动客户端启动类。
服务器端先开启监听,后客户端启动后收到消息:
在/0:0:0:0:0:0:0:0:18080上开启监听
收到客户端发过来的消息: Hello World
客户端:
接收到的消息:你好,我是服务端,我已经收到你发送的消息
- Group:实际项目中,这里创建两个EventLoopGroup的实例,一个负责接收客户端的连接,另一个负责处理消息I/O,这里为了简单展示流程,让一个实例把这两方面的活都干了。
- NioServerSocketChannel:通过工厂通过工厂方法设计模式实例化一个channel,这个在大家还没有能够熟练使用Netty进行项目开发的情况下,不用去深究。
最后先启动服务器端,后启动客户端启动类。
服务器端先开启监听,后客户端启动后收到消息:
在/0:0:0:0:0:0:0:0:18080上开启监听
收到客户端发过来的消息: Hello World
客户端:
接收到的消息:你好,我是服务端,我已经收到你发送的消息