在TCP协议中我们知道当我们在接收消息时候,我们如何判断我们一次读取到的包就是整包消息呢,特别是对于使用了长连接和使用了非阻塞I/O的程序。上节我们也说了上层应用协议为了对消息进行区分一般采用4种方式。前面三种我们都说了,第四种是:通过在消息头定义长度字段来标识消息总长度。这个我们还没讲。当然Netty也提供了相应的解码器:LengthFieldBasedFrameDecoder。

大多数的协议(私有或者公有),协议头中会携带长度字段,用于标识消息体或者整包消息的长度,例如SMPP、HTTP协议等。由于基于长度解码需求 的通用性,Netty提供了LengthFieldBasedFrameDecoder,自动屏蔽TCP底层的拆包和粘 包问题,只需要传入正确的参数,即可轻松解决“读半包“问题。

我们先来看一下他的构造函数:

   public LengthFieldBasedFrameDecoder(ByteOrder byteOrder,
int maxFrameLength,
int lengthFieldOffset,
int lengthFieldLength,
int lengthAdjustment,
int initialBytesToStrip,
boolean failFast) { }
  • byteOrder:表示字节流表示的数据是大端还是小端,用于长度域的读取;

  • maxFrameLength:表示的是包的最大长度,超出包的最大长度netty将会做一些特殊处理;

  • lengthFieldOffset:指的是长度域的偏移量,表示跳过指定长度个字节之后的才是长度域;

  • lengthFieldLength:记录该帧数据长度的字段本身的长度;

  • lengthAdjustment:该字段加长度字段等于数据帧的长度,包体长度调整的大小,长度域的数值表示的长度加上这个修正值表示的就是带header的包;

  • initialBytesToStrip:从数据帧中跳过的字节数,表示获取完一个完整的数据包之后,忽略前面的指定的位数个字节,应用解码器拿到的就是不带长度域的数据包;

  • failFast:如果为true,则表示读取到长度域,TA的值的超过maxFrameLength,就抛出一个 TooLongFrameException,而为false表示只有当真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。

LengthFieldBasedFrameDecoder定义了一个长度的字段来表示消息的长度,因此能够处理可变长度的消息。将消息分为消息头和消息体,消息头固定位置增加一个表示长度的字段,通过长度字段来获取整包的信息。LengthFieldBasedFrameDecoder继承了ByteToMessageDecoder,即转换字节这样的工作是由ByteToMessageDecoder来完成,而LengthFieldBasedFrameDecoder只用安心完成他的解码工作就好。Netty在解耦和方面确实做的不错。

既然我们知道了LengthFieldBasedFrameDecoder处理的是带有消息头和消息体的消息类型,那么我们完全可以来定义一个我们自己的消息,我们来写一个消息类:

public class Message {

    //消息类型
private byte type; //消息长度
private int length; //消息体
private String msgBody; public Message(byte type, int length, String msgBody) {
this.type = type;
this.length = length;
this.msgBody = msgBody;
} public byte getType() {
return type;
} public void setType(byte type) {
this.type = type;
} public int getLength() {
return length;
} public void setLength(int length) {
this.length = length;
} public String getMsgBody() {
return msgBody;
} public void setMsgBody(String msgBody) {
this.msgBody = msgBody;
}
}

我们先来写服务端:

public class NewServer {
private static final int MAX_FRAME_LENGTH = 1024 * 1024;
private static final int LENGTH_FIELD_LENGTH = 4;
private static final int LENGTH_FIELD_OFFSET = 1;
private static final int LENGTH_ADJUSTMENT = 0;
private static final int INITIAL_BYTES_TO_STRIP = 0; private int port; public NewServer(int port) {
this.port = port;
} public void start(){
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap sbs = new ServerBootstrap()
.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new NewServerChannelInitializer(MAX_FRAME_LENGTH,LENGTH_FIELD_LENGTH,LENGTH_FIELD_OFFSET,LENGTH_ADJUSTMENT,INITIAL_BYTES_TO_STRIP))
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture future = sbs.bind(port).sync(); System.out.println("Server start listen at " + port );
future.channel().closeFuture().sync();
} catch (Exception e) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} public static void main(String[] args) {
NewServer server = new NewServer(7788);
server.start();
} }

注意到服务端我们在上面定义了5个参数,这5个参数是为了传入LengthFieldBasedFrameDecoder里面用的,因为我们的LengthFieldBasedFrameDecoder写在了NewServerChannelInitializer类里面,所以这几个参数采用可配置的方式也更符合可扩展性,我们分别说一下这几个参数定值的含义:

  • MAX_FRAME_LENGTH = 1024 * 1024 :这个没什么说的,消息体的最大长度;
  • LENGTH_FIELD_LENGTH = 4 :指的就是我们的Message类中的length的长度,int占4位
  • LENGTH_FIELD_OFFSET = 1 :偏移多少位之后才是我们的消息体,因为我们消息头只有type一个参数,byte类型占1位,所以是1;
  • LENGTH_ADJUSTMENT = 0 :该字段加长度字段等于数据帧的长度,一般数据帧长度都是这样定义(即我们在设置Message中的length属性),加入你的消息体是20位,再加上
  • LENGTH_FIELD_LENGTH就是24位,所以在此处为了正确的解析出消息体,需要偏移4位才能解析出消息体的正确位置,我们在发送的消息里面设置的就是消息体本身的长度,所以无需偏移。
  • INITIAL_BYTES_TO_STRIP = 0 :这里我们也不需要跳过数据帧中的字节数,因为我们的消息体和长度是分别发送的,详情见下面EnCoder代码。

然后我们写ChannelInitializer:

public class NewServerChannelInitializer extends ChannelInitializer<SocketChannel> {
private final int MAX_FRAME_LENGTH;
private final int LENGTH_FIELD_LENGTH;
private final int LENGTH_FIELD_OFFSET;
private final int LENGTH_ADJUSTMENT;
private final int INITIAL_BYTES_TO_STRIP; public NewServerChannelInitializer(int MAX_FRAME_LENGTH, int LENGTH_FIELD_LENGTH, int LENGTH_FIELD_OFFSET, int LENGTH_ADJUSTMENT, int INITIAL_BYTES_TO_STRIP) {
this.MAX_FRAME_LENGTH = MAX_FRAME_LENGTH;
this.LENGTH_FIELD_LENGTH = LENGTH_FIELD_LENGTH;
this.LENGTH_FIELD_OFFSET = LENGTH_FIELD_OFFSET;
this.LENGTH_ADJUSTMENT = LENGTH_ADJUSTMENT;
this.INITIAL_BYTES_TO_STRIP = INITIAL_BYTES_TO_STRIP;
} @Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(new NewDecoder(MAX_FRAME_LENGTH,LENGTH_FIELD_LENGTH,LENGTH_FIELD_OFFSET,LENGTH_ADJUSTMENT,INITIAL_BYTES_TO_STRIP,false));
// 自己的逻辑Handler
pipeline.addLast("handler", new NewServerHandler());
} }

上面用到了我们自己写的Decoder,接下来定义一个Decoder,继承LengthFieldBasedFrameDecoder,以方便我们做一些改写:

public class NewDecoder extends LengthFieldBasedFrameDecoder {

    /**
* 我们在Message类中定义了type和length,这都放在消息头部
* type占1个字节,length占4个字节所以头部总长度是5个字节
*/
private static final int HEADER_SIZE = 5;
private byte type;
private int length;
private String msgBody; /**
*
* @param maxFrameLength 网络字节序,默认为大端字节序
* @param lengthFieldOffset 消息中长度字段偏移的字节数
* @param lengthFieldLength 数据帧的最大长度
* @param lengthAdjustment 该字段加长度字段等于数据帧的长度
* @param initialBytesToStrip 从数据帧中跳过的字节数
* @param failFast 如果为true,则表示读取到长度域,TA的值的超过maxFrameLength,就抛出一个 TooLongFrameException
*/
public NewDecoder(int maxFrameLength, int lengthFieldOffset,
int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip,
boolean failFast) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength,
lengthAdjustment, initialBytesToStrip, failFast);
} @Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if(in == null){
return null;
}
if(in.readableBytes() < HEADER_SIZE){
throw new Exception("错误的消息");
} /**
* 通过源码我们能看到在读的过程中
* 每读一次读过的字节即被抛弃
* 即指针会往前跳
*/
type = in.readByte(); length = in.readByte(); if(in.readableBytes() < length){
throw new Exception("消息不正确");
} ByteBuf buf = in.readBytes(length);
byte[] b = new byte[buf.readableBytes()];
buf.readBytes(b); msgBody = new String(b,"UTF-8");
Message msg = new Message(type,length,msgBody);
return msg;
}
}

在上面的NewDecoder中有一个HEADER_SIZE-消息头。上面也解释过了,我们在Message中定义的type和length分别占一个字节和4个字节(别问我为啥是4个哈)。所以我们的消息头就是5个字节啦。

接下来就是服务端的handler了:

public class NewServerHandler extends SimpleChannelInboundHandler<Object> {

    @Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
if(o instanceof Message) {
Message msg = (Message)o;
System.out.println("Client->Server:"+channelHandlerContext.channel().remoteAddress()+" send "+msg.getMsgBody());
}
}
}

在handler中我们用来接收已经被NewDecoder解码过后的客户端发送过来的消息。

下面是客户端:

public class NewClient {

    private  int port;
private String address; public NewClient(int port,String address) {
this.port = port;
this.address = address;
} public void start(){
EventLoopGroup group = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new NewClientChannelInitializer()); try {
ChannelFuture future = bootstrap.connect(address,port).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
} public static void main(String[] args) {
NewClient client = new NewClient(7788,"127.0.0.1");
client.start();
}
}

客户端Initializer:

public class NewClientChannelInitializer extends ChannelInitializer<SocketChannel> {

    protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(new NewEncoder());
pipeline.addLast(new NewClientHandler());
}
}

客户端中我们又定义了一个编码器NewEncoder,继承了MessageToByteEncoder,该类用于将文本信息转换为流:

public class NewEncoder extends MessageToByteEncoder<Message> {

    @Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
if(message == null){
throw new Exception("未获得消息内容");
} String msgBody = message.getMsgBody();
byte[] b = msgBody.getBytes(Charset.forName("utf-8"));
byteBuf.writeByte(message.getType());
byteBuf.writeByte(b.length);
byteBuf.writeBytes(b); }
}

接下来是我们的客户端handler:

public class NewClientHandler extends ChannelInboundHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String m = "你好啊,Netty。昂昂";
Message msg = new Message((byte)0xCA, m.length(), m);
ctx.writeAndFlush(msg);
}
}

注意到在handler中我们发送了一个Message对象。然后会由NewEncoder编码发送出去,服务端对消息解码获得消息头和消息体。分别启动服务端和客户端,打印结果为:

我们的消息就发送出去了。

Netty学习(六)-LengthFieldBasedFrameDecoder解码器的更多相关文章

  1. Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明

    Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明 作者: Grey 原文地址: 博客园:Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明 ...

  2. netty之LengthFieldBasedFrameDecoder解码器

    官方api:http://netty.io/4.1/api/io/netty/handler/codec/LengthFieldBasedFrameDecoder.html package com.e ...

  3. Netty学习笔记

    一些类与方法说明 1)ByteBuf ByteBuf的API说明: Creation of a buffer It is recommended to create a new buffer usin ...

  4. Netty学习笔记(二) 实现服务端和客户端

    在Netty学习笔记(一) 实现DISCARD服务中,我们使用Netty和Python实现了简单的丢弃DISCARD服务,这篇,我们使用Netty实现服务端和客户端交互的需求. 前置工作 开发环境 J ...

  5. Netty学习——Netty和Protobuf的整合(二)

    Netty学习——Netty和Protobuf的整合(二) 这程序是有瑕疵的,解码器那里不通用,耦合性太强,有两个很明显的问题,但是要怎么解决呢?如:再加一个内部类型 Person2,之前的代码就不能 ...

  6. Netty学习——Netty和Protobuf的整合(一)

    Netty学习——Netty和Protobuf的整合 Protobuf作为序列化的工具,将序列化后的数据,通过Netty来进行在网络上的传输 1.将proto文件里的java包的位置修改一下,然后再执 ...

  7. Netty学习——基于netty实现简单的客户端聊天小程序

    Netty学习——基于netty实现简单的客户端聊天小程序 效果图,聊天程序展示 (TCP编程实现) 后端代码: package com.dawa.netty.chatexample; import ...

  8. Netty学习篇③--整合springboot

    经过前面的netty学习,大概了解了netty各个组件的概念和作用,开始自己瞎鼓捣netty和我们常用的项目的整合(很简单的整合) 项目准备 工具:IDEA2017 jar包导入:maven 项目框架 ...

  9. netty学习资料

    netty学习资料推荐官方文档和<netty权威指南>和<netty in action>这两本书.下面收集下网上分享的资料 netty官方参考文档 Netty 4.x Use ...

随机推荐

  1. ZOJ 3962:Seven Segment Display(思维)

    https://vjudge.net/problem/ZOJ-3962 题意:有16种灯,每种灯的花费是灯管数目,代表0~F(十六进制),现在从x开始跳n-1秒,每一秒需要的花费是表示当前的数的花费之 ...

  2. C# 6 新语法

    1. using 声明的静态用法 2. 表达式体方法 3. 表达式体属性 4. 自动实现的属性初始化器 5. 只读的自动属性 6. nameof 运算符 7. 空值传播运算符 8. 字符串插值 9. ...

  3. Python 的8个关键要素

    1.数据类型 2.对象引用 3.组合数据类型 4.逻辑操作符 5.控制流语句 6.算术操作符 7.输入/输出 8.函数的创建与调用

  4. CentOS 6.5 下安装及使用中文输入法

    第一次在本机环境下搭载Linux环境,但搭载后发现在CentOS 6.5只能英文输入,于是试着安装并启用中文输入法,经过一翻折腾,实现了在终端和自带火狐浏览器下支持中文输入法. CentOS下安装中文 ...

  5. Windows下必备的开发神器之Cmder使用说明

    诚言,对于开发码字者,Mac和Linux果断要比Windows更贴心;但只要折腾下,Windows下也是有不少利器的.之前就有在Windows下效率必备软件一文中对此做了下记载:其虽没oh-my-zs ...

  6. cookie、sessionSttorage、localStory区别

    cookie.sessionSttorage.localStory都是在客户端以键值对存储的存储机制,并且只能将值存储为字符   cookie localStorage  sessionStorage ...

  7. Socket编程(C语言实现):socket()函数英文翻译

    最近开始研究使用Socket API来网络编程,想着把自己的感想.感悟写下来.我发现在编程之外还有不少概念性的东西要学习.我觉得应该有以下几点吧: 1.得了解下计算机网络的基本概念,如OSI的7层模型 ...

  8. 微信小程序之楼层效果

    今天做了一个小程序实现一个楼层效果  带大家分享下经验和api的使用吧 如图 将左边和右边各分了一个组件  目录如下 其中list页面是这个楼层效果的页面 components是组成这个页面的两个组件 ...

  9. Java用Xom生成XML文档

    这个总结源于Java编程思想第四版18.13节的案例: 完整代码地址: Java编程思想:XML 相关Api地址: Attribute Element Document Serializer 由于案例 ...

  10. Lucene04-Lucene的基本使用

    Lucene04-Lucene的基本使用 导入的包 import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.ap ...