在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. 每天学点node系列-zlib

    永不放弃,永不放弃又有两个原则,第一个原则是永不放弃,第二个原则就是:当你想放弃时回头看第一个原则. 概览 做过web性能优化的同学,对性能优化大杀器gzip应该不陌生.浏览器向服务器发起资源请求,比 ...

  2. python之pip install

    安装方式1 wget  http://python-distribute.org/distribute_setup.py sudo python distribute_setup.py wget  h ...

  3. Netty-新连接接入源码解读

    本片博文来看Netty的服务端是如何处理新连接接入问题的 什么是新连接接入?以及新连接接入前,Netty处于什么状态 netty的服务端NioServerSocketChannel初始化,注册在Bos ...

  4. 手机web app开发笔记

    各位朋友好,最近自学开发了一个手机Web APP,“编程之路”,主要功能包括文章的展示,留言,注册登录,音乐播放等.为了记录学习心得,提高自己的编程水平,也许对其他朋友有点启发,特整理开发笔记如下. ...

  5. word 2010 页眉 http://jingyan.baidu.com/article/a65957f4b0208624e77f9b55.html

    word 2010 页眉 http://jingyan.baidu.com/article/a65957f4b0208624e77f9b55.html

  6. error: 'commit' is not possible because you have unmerged files.

    解决方案: 1.把修改的文件add下,如:git add bidder_mod/src/common/dragon_bidder_data.cc2.git commit

  7. 最大公约数GCD学习笔记

    引理 已知:k|a,k|b 求证:k|(m*a+n*b) 证明:∵ k|a ∴ 有p*k=a 同理可得q*k=b ∴ p*k*m=m*a,q*k*n=n*b ∴ k(p*m+q*n)=m*a+n*b ...

  8. 洛谷P2880 [USACO07JAN]平衡的阵容Balanced Lineup 题解

    题目链接: https://www.luogu.org/problemnew/show/P2880 分析: ST表实现即可,一个最大值数组和最小值数组同时维护 代码: #include<cstd ...

  9. SpringBoot解决cors跨域问题

    1.使用@CrossOrigin注解实现 (1).对单个接口配置CORS @CrossOrigin(origins = {"*"}) @PostMapping("/hel ...

  10. [剑指offer] 40. 数组中只出现一次的数字

    题目描述 一个整型数组里除了两个数字之外,其他的数字都出现了偶数次.请写程序找出这两个只出现一次的数字. 思路: 解法一: 哈希表 class Solution { public: void Find ...