TCP以流的方式进行数据传输,上层的应用协议为了对消息进行区分,往往采用如下4种方式。

(1)消息长度固定,累计读取到长度总和为定长LEN的报文后,就认为读取到了一个完整的消息;将计数器置位,重新开始读取下一个数据报;

(2)将回车换行符作为消息结束符,例如FTP协议,这种方式在文本协议中应用比较广泛;

(3)将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符;

(4)通过在消息头中定义长度字段来标识消息的总长度。

Netty对上面四种应用做了统一的抽象,提供了4种解码器来解决对应的问题,使用起来非常方便。有了这些解码器,用户不需要自己对读取的报文进行人工解码,也不需要考虑TCP的粘包和拆包。

两种实用的解码器——DelimiterBasedFrameDecoder和FixedLengthFrameDecoder,前者可以自动完成以分隔符做结束标志的消息的解码,后者可以自动完成对定长消息的解码,它们都能解决TCP粘包/拆包导致的读半包问题。

DelimiterBasedFrameDecoder应用开发

演示程序以经典的Echo服务为例。EchoServer接收到EchoClient的请求消息后,将其打印出来,然后将原始消息返回给客户端,消息以“$_”作为分隔符。

服务端示例:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler; public class EchoServer { public void bind(int port) throws Exception {
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChildChannelHandler());
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync(); // 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} private class ChildChannelHandler extends ChannelInitializer {
@Override
protected void initChannel(Channel arg0) throws Exception {
//首先创建分隔符缓冲对象ByteBuf,本例程中使用“$_”作为分隔符。
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
//创建DelimiterBasedFrameDecoder对象,将其加入到ChannelPipeline中。
//DelimiterBasedFrameDecoder有多个构造方法,这里我们传递两个参数,
//第一个1024表示单条消息的最大长度,当达到该长度后仍然没有查找到分隔符,
//就抛出TooLongFrame Exception异常,防止由于异常码流缺失分隔符导致的内存溢出,
//这是Netty解码器的可靠性保护;第二个参数就是分隔符缓冲对象。
arg0.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
arg0.pipeline().addLast(new StringDecoder());
arg0.pipeline().addLast(new EchoServerHandler());
}
} public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new EchoServer().bind(port);
}
} import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext; @ChannelHandler.Sharable
public class EchoServerHandler extends ChannelHandlerAdapter { int counter = 0; @Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
//直接将接收的消息打印出来,由于DelimiterBasedFrameDecoder自动对请求消息进行了解码
//后续的ChannelHandler接收到的msg对象就是个完整的消息包;
//第二个ChannelHandler是StringDecoder,它将ByteBuf解码成字符串对象
//第三个EchoServerHandler接收到的msg消息就是解码后的字符串对象。
String body = (String) msg;
System.out.println("This is " + ++counter + " times receive client : ["+ body + "]");
body += "$_";
//由于我们设置DelimiterBasedFrameDecoder过滤掉了分隔符,
//所以,返回给客户端时需要在请求消息尾部拼接分隔符“$_”,
//最后创建ByteBuf,将原始消息重新返回给客户端。
ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
ctx.writeAndFlush(echo);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();// 发生异常,关闭链路
}
}

客户端示例:

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder; public class EchoClient { public void connect(int port, String host) throws Exception {
// 配置客户端NIO线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer() {
@Override
public void initChannel(Channel ch)
throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoClientHandler());
}
}); // 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync(); // 等待客户端链路关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
} public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new EchoClient().connect(port, "127.0.0.1");
}
} import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext; public class EchoClientHandler extends ChannelHandlerAdapter { private int counter; static final String ECHO_REQ = "Hi, Netty. Welcome to Netty.$_"; public EchoClientHandler() {
} @Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 10; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
}
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("This is " + ++counter + " times receive server : ["
+ msg + "]");
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

运行结果:

服务端

This is 1 times receive client : [Hi, Netty. Welcome to Netty.]
...............................
This is 10 times receive client : [Hi, Netty. Welcome to Netty.]

客户端

This is 1 times receive client : [Hi, Netty. Welcome to Netty.]
...............................
This is 10 times receive client : [Hi, Netty. Welcome to Netty.]

FixedLengthFrameDecoder应用开发

FixedLengthFrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包问题,非常实用。

利用FixedLengthFrameDecoder解码器,无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。

服务端示例:

在服务端的ChannelPipeline中新增FixedLengthFrameDecoder,长度设置为20,然后再依次增加字符串解码器和EchoServerHandler

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler; public class EchoServer {
public void bind(int port) throws Exception {
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(Channel ch)throws Exception {
ch.pipeline().addLast( new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoServerHandler());
}
}); // 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync(); // 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new EchoServer().bind(port);
}
} import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext; @ChannelHandler.Sharable
public class EchoServerHandler extends ChannelHandlerAdapter { @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("Receive client : [" + msg + "]");
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();// 发生异常,关闭链路
}
}

客户端示例:

  1. telnet localhost 8080
  2. 通过set localecho命令打开本地回显功能
➜  zcy-fixed git:(feature/transaction) ✗ telnet localhost 8080
Trying ::1...
Connected to localhost.
Escape character is '^]'.
12
123456789012345678

//连接日志
10:10:59.755 [nioEventLoopGroup-2-1] INFO i.n.handler.logging.LoggingHandler - [id: 0x979fc7a5, /0:0:0:0:0:0:0:0:8080] RECEIVED: [id: 0x79dc7a78, /0:0:0:0:0:0:0:1:50749 => /0:0:0:0:0:0:0:1:8080]
Receive client : []
Receive client : [12
1234567890123456]

TCP拆包粘包之分隔符解码器的更多相关文章

  1. tomcat Http11NioProtocol如何解析http请求及如何解决TCP拆包粘包

    前言 tomcat是常用的Web 应用服务器,目前国内有很多文章讲解了tomcat架构,请求流程等,但是没有如何解析http请求及如何解决TCP粘包拆包,所以这篇文章的目的就是介绍这块内容,一下内容完 ...

  2. 架构师养成记--20.netty的tcp拆包粘包问题

    问题描述 比如要发ABC DEFG HIJK 这一串数据,其中ABC是一个包,DEFG是一个包,HIJK是一个包.由于TCP是基于流发送的,所以有可能出现ABCD EFGH 这种情况,那么ABC和D就 ...

  3. Netty—TCP的粘包和拆包问题

    一.前言 虽然TCP协议是可靠性传输协议,但是对于TCP长连接而言,对于消息发送仍然可能会发生粘贴的情形.主要是因为TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分.有可能将一个大数据包 ...

  4. 关于TCP的粘包和拆包

    问题产生 一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题. 下面可以看一张图,是客户端向服务端发送包: 1. 第一种情况 ...

  5. Netty 拆包粘包和服务启动流程分析

    Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你能掌握EventLoopGroup的工作流程,ServerBootstrap的启动流程,ChannelPipeline是如何操作管理Ch ...

  6. Netty_TCP拆包粘包解决方案

    一.问题 熟悉tcp编程的可能都知道,无论是服务器端还是客户端,当我们读取或者发送数据的时候,都需要考虑TCP底层的粘包/拆包机制. TCP是一个“流”协议,所谓流就是没有界限的遗传数据,大家可以想象 ...

  7. 【转】Netty 拆包粘包和服务启动流程分析

    原文:https://www.cnblogs.com/itdragon/archive/2018/01/29/8365694.html Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你 ...

  8. 使用Netty如何解决拆包粘包的问题

    首先,我们通过一个DEMO来模拟TCP的拆包粘包的情况:客户端连续向服务端发送100个相同消息.服务端的代码如下: AtomicLong count = new AtomicLong(0); NioE ...

  9. python 全栈开发,Day35(TCP协议 粘包现象 和解决方案)

    一.TCP协议 粘包现象 和解决方案 黏包现象让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)执行远程命令的模块 需要用到模块subprocess sub ...

随机推荐

  1. java转换json需要导入的jar包,org/apache/commons/lang/exception/NestableRuntimeException

    缺少相应jar包都会有异常,根据异常找jar包导入......     这里我说下lang包,因为这个包我找了好半天:   我用的是: commons-lang3-3.1.jar  出现异常: jav ...

  2. windows服务

    .net windows 服务创建.安装.卸载和调试   原文:http://www.cnblogs.com/hfliyi/archive/2012/08/12/2635290.html 我对例子做了 ...

  3. 【opencv】图片标注文字

    IplImage* pstImg; HI_CHAR as8Title[25];CvFont stTimeFont;//字体信息cvInitFont(&stTimeFont,CV_FONT_HE ...

  4. 【Git】标签管理

    来源:廖雪峰 为什么要标签: 发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本.将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来. ...

  5. php原型模式的研究

    <?php class Sea{} class EarthSea extends Sea{} class MarsSea extends Sea{} class Plains{} class E ...

  6. CSS命名法

    一.Css命名法: 1.驼峰命名法:除第一个单词的首字母小写之外,其余的单词首字母均大写.如:#headBlock(2). 2.帕斯卡命名法:所有单词的首字母均大写.如:#HeadBlock(3). ...

  7. oracle两时间相减得到相差的时间

    1.months_between(date1,date2);date1和date2相减得到相差的月份. select months_between(to_date('2015-05-11','yyyy ...

  8. ALT+数字直接取得字符的原理

    字符 字节码 utf-8编码 机器二进制码 之间的联系 byte数组与二进制码关系参考文献 例如:字符“我” byte字节码是 [-26, -120, -111] 绝对值源码 [00011010, 0 ...

  9. Java观察者模式(Observer模式)

    Java深入到一定程度,就不可避免的碰到设计模式(design pattern)这一概念,了解设计模式,将使自己对java中的接口或抽象类应用有更深的理解.设计模式在java的中型系统中应用广泛,遵循 ...

  10. Swift - 语言指南,来自github学习

    @SwiftLanguage 更新于 2016-6-6,更新内容详见 Issue 55.往期更新回顾详见<收录周报> 这份指南汇集了 Swift 语言主流学习资源,并以开发者的视角整理编排 ...