文章目录
Netty 入门学习
@author lisiwen
本文参考 https://bugstack.cn/itstack-demo-netty/itstack-demo-netty-1.html、https://www.jianshu.com/p/b9f3f6a16911
1. 认识Netty
1.1. 什么是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 等,都利用其强大的对于网络抽象的核心代码。
1.2. Netty特点
1) 高并发
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架。
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。
2)传输快
Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。
零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽。
3)封装好
Netty封装了NIO操作的很多细节,提供易于使用的API。
1.3.Netty和Tomcat有什么区别?
Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。
2. Netty核心组件
2.1.Bootstrap 和 ServerBootstrap
BootStarp 和 ServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。
BootStrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和 connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、没有父 Channel 的 Channel 来实现所有的网络交换。
ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind() 方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。
2.2. Channel
Channel是Java NIO的一个基本构造。可以看作是传入或传出数据的载体。因此,它可以被打开或关闭,连接或者断开连接。以下是常用的Channel:
– EmbeddedChannel
– LocalServerChannel
– NioDatagramChannel
– NioSctpChannel
– NioSocketChannel
2.3. 回调
当一个回调被触发时,相应的事件可以被一个interface-ChannelHandler的实现处理。
2.4. Future
Netty中所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种在之后的某个时间点确定其结果的方法。
Future 和 回调 是相互补充的机制,提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
Netty 提供了ChannelFuture,用于在执行异步操作的时候使用。每个Netty的出站I/O操作都会返回一个ChannelFuture。ChannelFuture能够注册一个或者多个ChannelFutureListener 实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用。
2.5.ChannelHandler
Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态,每个事件都可以被分发给ChannelHandler类中某个用户实现的方法。Netty提供了大量预定义的可以开箱即用的ChannelHandler实现,包括用于各种协议的ChannelHandler。
现在,事件可以被分发给ChannelHandler类中某个用户实现的方法。那么,如果 ChannelHandler 处理完成后不直接返回给客户端,而是传递给下一个ChannelHandler 继续处理呢?那么就要说到 ChannelPipeline !
ChannelPipeline 提供了 ChannelHandler链 的容器,并定义了用于在该链上传播入站和出站事件流的API。使得事件流经 ChannelPipeline 是 ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行他们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler:
1、一个ChannelInitializer的实现被注册到了ServerBootstrap中。
2、当 ChannelInitializer.initChannel()方法被调用时, ChannelInitializer将在 ChannelPipeline 中安装一组自定义的 ChannelHandler。
3、ChannelInitializer 将它自己从 ChannelPipeline 中移除。
2.6. EventLoop
EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,在内部,将会为每个Channel分配一个EventLoop。
EventLoop本身只由一个线程驱动,其处理了一个Channel的所有I/O事件,并且在该EventLoop的整个生命周期内都不会改变。这个简单而强大的设计消除了你可能有的在ChannelHandler实现中需要进行同步的任何顾虑。
这里需要说到,EventLoop的管理是通过EventLoopGroup来实现的。还要一点要注意的是,客户端引导类是 Bootstrap,只需要一个EventLoopGroup。服务端引导类是 ServerBootstrap,通常需要两个 EventLoopGroup,一个用来接收客户端连接,一个用来处理 I/O 事件(也可以只使用一个 EventLoopGroup,此时其将在两个场景下共用同一个 EventLoopGroup)。
1、一个 EventLoopGroup 包含一个或者多个 EventLoop;
2、一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
3、所有由 EventLoop 处理的 I/O 事件都将在它专有的Thread 上被处理;
4、一个 Channel 在它的生命周期内只注册于一个EventLoop;
5、NIO中,一个 EventLoop 分配给多个 Channel(面对多个Channel,一个 EventLoop 按照事件触发,顺序执行); OIO中,一个 EventLoop 分配给一个 Channel。
tips:Netty 应用程序的一个一般准则:尽可能的重用 EventLoop,以减少线程创建所带来的开销。
3.项目代码示例
3.1.服务端部分
NettyServer.java
/**
* netty服务端
*
* @author: lisiwen
* @date: 2020/9/12 10:33
**/
public class NettyServer {
public static void main(String[] args) {
new NettyServer().bing(8080);
}
private void bing(int port) {
//配置服务端NIO线程组
EventLoopGroup parentGroup = new NioEventLoopGroup();
EventLoopGroup childGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(parentGroup, childGroup)
//非阻塞模式
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childHandler(new MyChannelInitializer());
ChannelFuture f = b.bind(port).sync();
System.out.println("服务端启动成功");
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
childGroup.shutdownGracefully();
parentGroup.shutdownGracefully();
}
}
}
MyChannelInitializer.java
/**
* 管道初始化
*
* @author: lisiwen
* @date: 2020/9/12 10:44
**/
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//对象传输处理
socketChannel.pipeline().addLast(new ObjDecoder(SendMessage.class));
socketChannel.pipeline().addLast(new ObjEncoder(FeedBackMessage.class));
// 在管道中添加我们自己的接收数据实现方法
socketChannel.pipeline().addLast(new MyServerHandler());
}
}
MyServerHandler.java
/**
* 服务端处理
*
* @author: lisiwen
* @date: 2020/9/12 10:44
**/
public class MyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SocketChannel channel = (SocketChannel) ctx.channel();
System.out.println("链接报告开始");
System.out.println("链接报告信息:有一客户端链接到本服务端。channelId:" + channel.id());
System.out.println("链接报告IP:" + channel.localAddress().getHostString());
System.out.println("链接报告Port:" + channel.localAddress().getPort());
System.out.println("链接报告完毕");
//通知客户端链接建立成功
String str = "通知客户端链接建立成功" + " " + new Date() + " " + channel.localAddress().getHostString() + "\r\n";
ctx.writeAndFlush(FeedBackMessage.SuccessMessage(str, channel.id().toString()));
}
/**
* 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("客户端断开链接" + ctx.channel().localAddress().toString());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//接收msg消息{与上一章节相比,此处已经不需要自己进行解码}
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息类型:" + msg.getClass());
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息内容:" + JSONUtil.toJsonStr(msg));
}
/**
* 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
System.out.println("异常信息:\r\n" + cause.getMessage());
}
}
3.2.客户端部分
NettyClient.java
/**
* netty客户端
*
* @author: lisiwen
* @date: 2020/9/12 10:32
**/
public class NettyClient {
public static void main(String[] args) {
new NettyClient().connect("127.0.0.1", 8080);
}
private void connect(String inetHost, int inetPort) {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.AUTO_READ, true);
b.handler(new MyChannelInitializer());
ChannelFuture f = b.connect(inetHost, inetPort).sync();
System.out.println("客户端启动成功");
f.channel().writeAndFlush(SendMessage.build("你好,使用protobuf通信格式的服务端,我是客户端。", f.channel().id().toString()));
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
}
}
}
MyChannelInitializer.java
/**
* 管道初始化
*
* @author: lisiwen
* @date: 2020/9/12 10:44
**/
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) {
//对象传输处理
socketChannel.pipeline().addLast(new ObjDecoder(FeedBackMessage.class));
socketChannel.pipeline().addLast(new ObjEncoder(SendMessage.class));
// 在管道中添加我们自己的接收数据实现方法
socketChannel.pipeline().addLast(new MyClientHandler());
}
}
MyClientHandler.java
/**
* 客户端处理
*
* @author: lisiwen
* @date: 2020/9/12 10:44
**/
public class MyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SocketChannel channel = (SocketChannel) ctx.channel();
System.out.println("链接报告开始");
System.out.println("链接报告信息:本客户端链接到服务端。channelId:" + channel.id());
System.out.println("链接报告IP:" + channel.localAddress().getHostString());
System.out.println("链接报告Port:" + channel.localAddress().getPort());
System.out.println("链接报告完毕");
//通知客户端链接建立成功
String str = "通知服务端链接建立成功" + " " + new Date() + " " + channel.localAddress().getHostString();
ctx.writeAndFlush(FeedBackMessage.SuccessMessage(channel.id().toString(), str));
}
/**
* 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("断开链接" + ctx.channel().localAddress().toString());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//接收msg消息{与上一章节相比,此处已经不需要自己进行解码}
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息类型:" + msg.getClass());
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息内容:" + JSONUtil.toJsonStr(msg));
}
/**
* 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
System.out.println("异常信息:\r\n" + cause.getMessage());
}
}
3.3.编码解码器部分
ObjDecoder.java
/**
* 对象解码器
*
* @author: lisiwen
* @date: 2020/9/12 11:09
**/
public class ObjDecoder extends ByteToMessageDecoder {
private Class<?> genericClass;
public ObjDecoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
if (byteBuf.readableBytes() < 4) {
return;
}
byteBuf.markReaderIndex();
int dataLength = byteBuf.readInt();
if (byteBuf.readableBytes() < dataLength) {
byteBuf.resetReaderIndex();
return;
}
byte[] data = new byte[dataLength];
byteBuf.readBytes(data);
list.add(ObjectUtil.unserialize(data));
}
}
ObjEncoder.java
/**
* 对象编码器
*
* @author: lisiwen
* @date: 2020/9/12 11:09
**/
public class ObjEncoder extends MessageToByteEncoder {
private Class<?> genericClass;
public ObjEncoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
if (genericClass.isInstance(o)) {
byte[] data = ObjectUtil.serialize(o);
byteBuf.writeInt(data.length);
byteBuf.writeBytes(data);
}
}
}
3.4.传输对象部分
SendMessage.java
/**
* netty发送对象
*
* @author: lisiwen
* @date: 2020/9/12 11:39
**/
@Data
public class SendMessage implements Serializable {
private String message;
private String channelId;
public SendMessage() {}
public SendMessage(String message, String channelId) {
this.message=message;
this.channelId=channelId;
}
public static SendMessage build(String message, String channelId) {
return new SendMessage(message, channelId);
}
}
FeedBackMessage.java
/**
* netty反馈消息对象
*
* @author: lisiwen
* @date: 2020/9/12 11:41
**/
@Data
public class FeedBackMessage<T> implements Serializable {
private String code;
private String message;
private String channelId;
private T data;
public FeedBackMessage() {
}
public FeedBackMessage(String code, String message) {
this.code = code;
this.message = message;
}
public static <T> FeedBackMessage SuccessMessage() {
return new FeedBackMessage("200", "请求成功");
}
public static <T> FeedBackMessage SuccessMessage(T data) {
FeedBackMessage<T> feedBackMessage = SuccessMessage();
feedBackMessage.setData(data);
return feedBackMessage;
}
public static <T> FeedBackMessage SuccessMessage(T data, String channelId) {
FeedBackMessage<T> feedBackMessage = SuccessMessage(data);
feedBackMessage.setChannelId(channelId);
return feedBackMessage;
}
}
3.5.项目启动结果
服务端日志:
服务端启动成功
链接报告开始
链接报告信息:有一客户端链接到本服务端。channelId:d9273a36
链接报告IP:127.0.0.1
链接报告Port:8080
链接报告完毕
2020-09-14 13:49:45 接收到消息类型:class nettytest.object.SendMessage
2020-09-14 13:49:45 接收到消息内容:{"message":"你好,服务端,我是客户端。","channelId":"b6d87929"}
客户端日志:
客户端启动成功
链接报告开始
链接报告信息:本客户端链接到服务端。channelId:b6d87929
链接报告IP:127.0.0.1
链接报告Port:52002
链接报告完毕
2020-09-14 13:49:45 接收到消息类型:class nettytest.object.FeedBackMessage
2020-09-14 13:49:45 接收到消息内容:{"code":"200","data":"通知客户端链接建立成功 Mon Sep 14 13:49:45 CST 2020 127.0.0.1\r\n","message":"请求成功","channelId":"d9273a36"}