一、编解码器使用概述
期初使用Netty的时候感觉Netty很复杂需要做很多东西,随着学习的深入到编解码器这一部分主键的感觉实际上Netty在网络编程的场景中确实是为我们简化了很多的操作。一个是各种网络通信类型之间的交换升级还有一个的话就是我们不再需要关注资源的释放以及在NIO场景下的资源详细监听。我们业务开发人员主要关注的是编写Handler即可。同样在Netty的编解码器模块中他的使用也可以看成是对特定类型Handler的使用,有关编解码器可以大概分为两种——编码器、解码器。这两种类型的他们顶层对应的就是InBoundHandler以及OutBoundHandler。
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter
public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter
为了后面代码讲解的方便这里想将代码服务端、客户端的主体结构写出来:
服务端客户端引导类
public class NettyClientCodec {
private static final String SERVER_ADDRESS = "127.0.0.1";
private static final int SERVER_PORT = 8877;
private static void createClient(){
NioEventLoopGroup clientGroup = new NioEventLoopGroup(1);
Bootstrap clientBootstrap = new Bootstrap()
.group(clientGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline channelPipeline = ch.pipeline();
//是一个出站的操作
channelPipeline.addLast(new ClientHandler1());
// channelPipeline.addLast(new ClientLongToByteEncode());
}
});
try {
ChannelFuture bindFuture = clientBootstrap.connect(SERVER_ADDRESS, SERVER_PORT).sync();
bindFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()){
System.out.println("***服务端绑定成功***");
}else {
System.out.println("***服务端绑定失败***");
}
}
});
ChannelFuture closeFuture = bindFuture.channel().closeFuture();
closeFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()){
System.out.println("***通道关闭成功***");
}else{
System.out.println("***通道关闭失败***");
}
}
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
createClient();
}
}
public class NettyServerCodec {
private static int PORT= 8877;
/**
*@description: 创建一个常规的服务器
*/
private static void createServer(){
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap()
.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.option(ChannelOption.SO_KEEPALIVE,true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline serverPipeline = ch.pipeline();
//handler的调用顺序不能够被打乱;handler链条就相当于是一个数据清洗管道
//serverPipeline.addLast(new ServerByteToLongDecode());
//serverPipeline.addLast(new ServerLongToUserDecode());
serverPipeline.addLast(new ServerHandler1());
}
});
// .childHandler(new DelimiterBaseHandlerInitializer());
try {
ChannelFuture bindFuture = serverBootstrap.bind(PORT).sync();
bindFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()){
System.out.println("***服务端"+PORT+"端口绑定成功***");
}else{
System.out.println("***服务端"+PORT+"端口绑定失败***");
}
}
});
ChannelFuture closeFuture = bindFuture.channel().closeFuture().sync();
closeFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()) {
System.out.println("***服务端连接关闭成功***");
}else{
System.out.println("***服务端连接关闭失败***");
}
}
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
createServer();
}
}
服务端与客户端的业务逻辑处理器
public class ClientHandler1 extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("&&&调用了ClientHandler1.channelActive");
//直接写入信息,会经过client的编码器
//ctx.channel().write(1L);
//ctx.channel().write(2L);
//ctx.channel().write(3L);
//ctx.channel().flush();
//直接向通道中写入Byte,此种传递方式没有走client端自定义的编码器
//ByteBuf msgBuf = ByteBufAllocator.DEFAULT.buffer();
//msgBuf.writeLong(1L);
//msgBuf.writeLong(2L);
//msgBuf.writeLong(3L);
//***SeverByteToLongDecode.decode***
//&&&ServerHandler1.channelRead,msg = 8388070249163485984
//***SeverByteToLongDecode.decode***
//&&&ServerHandler1.channelRead,msg = 7453298384152322425
//***SeverByteToLongDecode.decode***
//msgBuf.writeBytes("this is good day !".getBytes());
//ctx.channel().writeAndFlush(msgBuf);
//测试分隔符的写法
//ByteBuf msgBuf = ByteBufAllocator.DEFAULT.buffer();
//msgBuf.writeBytes("ABC abc a bc ba\r\nDEF abc\r\n".getBytes());
//自定义协议
// ByteBuf msgBuf = ByteBufAllocator.DEFAULT.buffer();
// msgBuf.writeBytes("ABC&&&DEF".getBytes());
// ctx.channel().writeAndFlush(msgBuf);
// ctx.channel().eventLoop().schedule(new Runnable() {
// @Override
// public void run() {
// ByteBuf msgBuf2 = ByteBufAllocator.DEFAULT.buffer();
// msgBuf2.writeBytes("&&&".getBytes());
// ctx.channel().writeAndFlush(msgBuf2);
// }
// },10, TimeUnit.SECONDS);
//大文件传输
File file = new File("E:\\NCC开发相关文档\\恒力化纤NCC升级项目开发方案(部门虚拟仓功能)v1.6.pdf");
FileInputStream inputStream = new FileInputStream(file);
FileRegion region = new DefaultFileRegion(inputStream.getChannel(),0,file.length());
ctx.channel().writeAndFlush(region).addListener(
new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()){
System.out.println("***大文件发送失败***");
}else{
System.out.println("***大文件发送成功***");
}
}
}
);
}
}
public class ServerHandler1 extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("&&&调用到了ServerHandler1.channelActive");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// ServerUser serverUser = (ServerUser) msg;
// System.out.println("&&&ServerHandler1.channelRead,msg = "+serverUser.toString());
System.out.println("&&&ServerHandler1.channelRead,msg = ");
ByteBuf fileBuffer = ByteBufAllocator.DEFAULT.buffer();
fileBuffer.writeBytes((byte[]) msg);
FileOutputStream fileOutputStream = new FileOutputStream("E:\\NCC开发相关文档\\1.pdf");
FileChannel fileChannel = fileOutputStream.getChannel();
// fileChannel.write(fileBuffer.getBytes());
}
}
二、自定义编解码器
服务端通过连续使用两个解码器将Byte字节流转换为了我们期望的数据实体,我们可以将这个的handler链条看做是一个处理数据的通道,而所谓的编解码器是这个通达中的一部分,他们就像流式编程一样对流经的数据进行处理。
public class ClientLongToByteEncode extends MessageToByteEncoder<Long> {
@Override
protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
System.out.println("&&&——调用了ClientLongToByteEncode的encode(),msg="+msg);
out.writeLong(msg);
}
}
public class ServerByteToLongDecode extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("***SeverByteToLongDecode.decode***");
if (in.readableBytes()>=8){
out.add(in.readLong());
}
}
}
public class ServerLongToUserDecode extends MessageToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, Object msg, List out) throws Exception {
System.out.println("&&&ServerLongToUserDecode.decode,msg: "+msg);
out.add(new ServerUser("test+", (Long) msg));
}
}
public class ServerUser {
private String name ;
private long userId;
public ServerUser(String name, long userId) {
this.name = name;
this.userId = userId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
@Override
public String toString() {
return "ServerUser{" +
"name='" + name + '\'' +
", userId=" + userId +
'}';
}
}
三、HTTP(S)的使用
不管是HTTP还是HTTPS都是一种网络通信协议,Netty中对于通信协议的支持也是通过添加相应的Handler实现的——出站入站的两端都是通过网络传输的字节,至于书如何处理这些字节(传送的数据结构应该是什么样的以及通过什么样的模式进行对数据进行编解码、是否要对传送的数据进行加密)。
为了支持 SSL/TLS,Java 提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 类使得实现解密和加密相当简单直接。Netty 通过一个名为 SslHandler 的 ChannelHandler 实现利用了这个 API,其中 SslHandler 在内部使用 SSLEngine 来完成实际的工作。
使用Handler的时候需要注意的一点是添加Handler的先后顺序,就像对协议支持的Handler在入栈的顺序上应该是在解码器之后的。加密的又应该是在最前面的。一个完整的HTTP请求应该包含三种类型——HTTP相应头的HttpRequest、一个值多个用来包括数据的HttpContent、以及表示HTTP结束的LastHttpContent,响应的与请求相对应也是三部分:
添加SSL支持是很简单的不过《Netty in Action》中的一种写法是值得借鉴的就是将SSL相关的参数放在ChannelInitializer的构造函数中:
public class SslChannelInitializer extends ChannelInitializer<Channel> {
private final SslContext sslContext;
private final boolean startTls;
public SslChannelInitializer(SslContext sslContext, boolean startTls) {
this.sslContext = sslContext;
this.startTls = startTls;
}
@Override
protected void initChannel(Channel ch) throws Exception {
//ch.alloc是返回了一个与Channel相关联的ByteBuf池(工厂)
SSLEngine engine = sslContext.newEngine(ch.alloc());
//正常情况下SSLHandler要作为第一个Handler添加
ch.pipeline().addLast("ssl",new SslHandler(engine,startTls));
}
}
添加对HTTP的支持实际上是添加相应的编解码器,这一个应该是在加密协议之后:
那么我们的一个HTTP请求肯能是很长的怎么才能够讲一个完整消息传递给后面的业务Handler进行处理呢,这就需要一个聚合的Handler来处理这个任务:
//将最大消息为512KB的HTTP聚合处理器加入到pipeline
pipeline.addLast("aggregator",new HttpObjectAggregator(512 * 1024));
其余有关HTTP相关的压缩也都是通过这个的形式来添加,他是在编解码器之前的(❓入栈的时候先经过解码器在经过解压,出站的时候应该是先经过解压缩再进行编码):
//向客户端添加
pipeline.addLast("decompressor",new HttpContentDecompressor());
//向服务端添加
pipeline.addLast("compressor",new HttpContentCompressor());
四、大文件传输
由于写 操作是非阻塞的,所以即使没有写出所有的数据,写操作也会在完成时返回并通知 ChannelFuture。当这种情况发生时,如果仍然不停地写入,就有内存耗尽的风险。所以在写大型数据 时,需要准备好处理到远程节点的连接是慢速连接的情况,这种情况会导致内存释放的延迟。
有关大文件的写入在ClientHandler1的代码中也已经写了主要是借助的FileRegion来记性,背后使用的也是零拷贝的相关理念。不过FileRegion的方式针对的只是不对文件作任何改动的情况,如果在传输之前还想要对文件进行一定程度的改动则需要使用ChunkedWriteHandler,他可以支持异步写大型数据同时又不会消耗大量的内存。
public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {
private final File file;
private final SslContext sslContext;
public ChunkedWriteHandlerInitializer(File file, SslContext sslContext) {
this.file = file;
this.sslContext = sslContext;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline channelPipeline = ch.pipeline();
//想要创建SSLHandler就要使用引擎,而引擎则需要一个ByteBuf的池,这个池又要与当前的通道相关联
channelPipeline.addLast(new SslHandler(sslContext.newEngine(ch.alloc())));
//添加ChunkedWriteHandler以处理作为ChunkedInput传入的数据,支持异步写大型数据
//流,而又不会导致大量的内存消耗。
channelPipeline.addLast(new ChunkedWriteHandler());
channelPipeline.addLast(new WriteStreamHandler());
}
public final class WriteStreamHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//这里有又调用一下父类的方法倒是真没看懂
super.channelActive(ctx);
// 从 InputStream 中逐块传输内容
ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file)));
}
}
}
五、自定义协议
1、自定义分隔符
虽然前面已经对通道管道的概念做过很多次的阐述但是在使用自定义分割符的时候仍旧体现的是通道管道的概念。当客户端在想服务端发送数据的时候,数据就像通过套接字的管道流过来,流经的数据就会通过我们自定义的分隔符来进行分割,这个动作都是连贯的。
/**
* @description: 自定义的一个分割协议,使用&&&来进行分割
* @function:
* @author: Liu Menglei
* @Date: 2022/5/3 0003 11:46
*/
public class DelimiterBaseHandlerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline channelPipeline = ch.pipeline();
ByteBuf SPACE_FRAME = ByteBufAllocator.DEFAULT.buffer();
SPACE_FRAME.writeBytes("&&&".getBytes());
channelPipeline.addLast(new DelimiterBasedFrameDecoder(1024,SPACE_FRAME));
channelPipeline.addLast(new DelimiterHandler1());
}
public static class DelimiterHandler1 extends SimpleChannelInboundHandler<ByteBuf>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("&&&DelimiterBaseHandlerInitializer.DelimiterHandler1.channelRead0");
System.out.println(msg.toString(CharsetUtil.UTF_8));
}
}
}
在客户端我们发送数据的时候使用了一个延时发送,发送的是分隔符,如果定时任务中的分隔符没有发送出去服务端是接受不到DEF的
//自定义协议
ByteBuf msgBuf = ByteBufAllocator.DEFAULT.buffer();
msgBuf.writeBytes("ABC&&&DEF".getBytes());
ctx.channel().writeAndFlush(msgBuf);
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
ByteBuf msgBuf2 = ByteBufAllocator.DEFAULT.buffer();
msgBuf2.writeBytes("&&&".getBytes());
ctx.channel().writeAndFlush(msgBuf2);
}
},10, TimeUnit.SECONDS);
2、复合协议
线面代码使用的场景是我们使用系统中集成的基于\r\n的协议,同时在这个协议基础上我们又定义了一个数据包含的信息是命令的名称以及命令参数,命令名称以及不同的参数都通过空格隔开,第一个空格一定是命令的名称。里面实现的总体思路就是通过继承LineBasedFrameDecoder在重写decode方法的时候通过使用父类decode方法的能力对数据按照\r\n的规则进行第一层的切分。之后再重写的方法中在进行自己业务逻辑上的切分。这样是一步到位直接将将两层数据放在了一个解码器中分割,实际上也可以通过不嵌套协议而是多添加一个Handler将第二层的切割放在后面的Handler中进行处理。
public class CmdHandlerInitializer extends ChannelInitializer<Channel> {
final static byte SPACE = ' ';
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline channelPipeline = ch.pipeline();
channelPipeline.addLast(new CmdDecoder(1024));
channelPipeline.addLast(new CmdHandler());
}
//命令的格式
public static final class Cmd{
private final ByteBuf name;
private final ByteBuf args;
public Cmd(ByteBuf name, ByteBuf args) {
this.name = name;
this.args = args;
}
public ByteBuf name(){
return name;
}
public ByteBuf args(){
return args;
}
@Override
public String toString() {
return "Cmd{" +
"name=" + name +
", args=" + args +
'}';
}
}
public static final class CmdDecoder extends LineBasedFrameDecoder{
public CmdDecoder(int maxLength) {
super(maxLength);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
//这个相当于是借用父类的一个能力实现对数据的第一层切割
ByteBuf frame = (ByteBuf) super.decode(ctx, buffer);
if (frame == null){
return null;
}
//下面是根据自己的业务逻辑对数据进行的第二层切割
int index = frame.indexOf(frame.readerIndex(),frame.writerIndex(),SPACE);
return new Cmd(frame.slice(frame.readerIndex(),index),frame.slice(index+1,frame.writerIndex()));
}
}
public static final class CmdHandler extends SimpleChannelInboundHandler<Cmd>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, Cmd msg) throws Exception {
//处理传经ChannelPipeline的Cmd对象
System.out.println("&&&CmdHandlerInitializer.CmdHandler.channelRead0,msg = "+msg.toString());
}
}
}
w Cmd(frame.slice(frame.readerIndex(),index),frame.slice(index+1,frame.writerIndex()));
}
}
public static final class CmdHandler extends SimpleChannelInboundHandler<Cmd>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, Cmd msg) throws Exception {
//处理传经ChannelPipeline的Cmd对象
System.out.println("&&&CmdHandlerInitializer.CmdHandler.channelRead0,msg = "+msg.toString());
}
}
}