我们都知道TCP是基于字节流的传输协议。那么数据在通信层传播其实就像河水一样并没有明显的分界线,而数据具体表示什么意思什么地方有句号什么地方有分号这个对于TCP底层来说并不清楚。应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段,之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。所以对于这个数据拆分成大包小包的问题就是我们今天要讲的粘包和拆包的问题。

1 TCP粘包拆包问题说明

粘包和拆包这两个概念估计大家还不清楚,通过下面这张图我们来分析一下:

假设客户端分别发送两个数据包D1,D2个服务端,但是发送过程中数据是何种形式进行传播这个并不清楚,分别有下列4种情况:

  1. 服务端一次接受到了D1和D2两个数据包,两个包粘在一起,称为粘包;
  2. 服务端分两次读取到数据包D1和D2,没有发生粘包和拆包;
  3. 服务端分两次读到了数据包,第一次读到了D1和D2的部分内容,第二次读到了D2的剩下部分,这个称为拆包;
  4. 服务器分三次读到了数据部分,第一次读到了D1包,第二次读到了D2包的部分内容,第三次读到了D2包的剩下内容。

2. TCP粘包产生原因

我们知道在TCP协议中,应用数据分割成TCP认为最适合发送的数据块,这部分是通过“MSS”(最大数据包长度)选项来控制的,通常这种机制也被称为一种协商机制,MSS规定了TCP传往另一端的最大数据块的长度。这个值TCP协议在实现的时候往往用MTU值代替(需要减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes)所以往往MSS为1460。通讯双方会根据双方提供的MSS值得最小值确定为这次连接的最大MSS值。

tcp为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。

发生粘包拆包的原因主要有以下这些:

  1. 应用程序写入数据的字节大小大于套接字发送缓冲区的大小将发生拆包;

  2. 进行MSS大小的TCP分段。MSS是TCP报文段中的数据字段的最大长度,当TCP报文长度-TCP头部长度>mss的时候将发生拆包;

  3. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,将发生粘包;

  4. 数据包大于MTU的时候将会进行切片。MTU即(Maxitum Transmission Unit) 最大传输单元,由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes,刨去以太网帧的帧头14Bytes和帧尾CRC校验部分4Bytes,那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。这个就是网络层协议非常关心的地方,因为网络层协议比如IP协议会根据这个值来决定是否把上层传下来的数据进行分片。

3. 如何解决TCP粘包拆包

我们知道tcp是无界的数据流,且协议本身无法避免粘包,拆包的发生,那我们只能在应用层数据协议上,加以控制。通常在制定传输数据时,可以使用如下方法:

  1. 设置定长消息,服务端每次读取既定长度的内容作为一条完整消息;

  2. 使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容;

  3. 设置消息边界,服务端从网络流中按消息边界分离出消息内容。比如在消息末尾加上换行符用以区分消息结束。

当然应用层还有更多复杂的方式可以解决这个问题,这个就属于网络层的问题了,我们还是用java提供的方式来解决这个问题。我们先看一个例子看看粘包是如何发生的。

服务端:

public class HelloWordServer {
private int port; public HelloWordServer(int port) {
this.port = port;
} public void start(){
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup(); ServerBootstrap server = new ServerBootstrap().group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer()); try {
ChannelFuture future = server.bind(port).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
} public static void main(String[] args) {
HelloWordServer server = new HelloWordServer(7788);
server.start();
}
}

服务端Initializer:

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline(); // 字符串解码 和 编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder()); // 自己的逻辑Handler
pipeline.addLast("handler", new HelloWordServerHandler());
}
}

服务端handler:

public class HelloWordServerHandler extends ChannelInboundHandlerAdapter {
private int counter; @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String)msg;
System.out.println("server receive order : " + body + ";the counter is: " + ++counter);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}

客户端:

public class HelloWorldClient {
private int port;
private String address; public HelloWorldClient(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)
.handler(new ClientChannelInitializer()); 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) {
HelloWorldClient client = new HelloWorldClient(7788,"127.0.0.1");
client.start();
}
}

客户端Initializer:

public class ClientChannelInitializer extends  ChannelInitializer<SocketChannel> {

    protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder()); // 客户端的逻辑
pipeline.addLast("handler", new HelloWorldClientHandler());
}
}

客户端handler:

public class HelloWorldClientHandler extends ChannelInboundHandlerAdapter {
private byte[] req;
private int counter; public BaseClientHandler() {
req = ("Unless required by applicable law or agreed to in writing, software\n" +
" distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
" See the License for the specific language governing permissions and\n" +
" limitations under the License.This connector uses the BIO implementation that requires the JSSE\n" +
" style configuration. When using the APR/native implementation, the\n" +
" penSSL style configuration is required as described in the APR/native\n" +
" documentation.An Engine represents the entry point (within Catalina) that processes\n" +
" every request. The Engine implementation for Tomcat stand alone\n" +
" analyzes the HTTP headers included with the request, and passes them\n" +
" on to the appropriate Host (virtual host)# Unless required by applicable law or agreed to in writing, software\n" +
"# distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
"# See the License for the specific language governing permissions and\n" +
"# limitations under the License.# For example, set the org.apache.catalina.util.LifecycleBase logger to log\n" +
"# each component that extends LifecycleBase changing state:\n" +
"#org.apache.catalina.util.LifecycleBase.level = FINE"
).getBytes();
} @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message; //将上面的所有字符串作为一个消息体发送出去
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String buf = (String)msg;
System.out.println("Now is : " + buf + " ; the counter is : "+ (++counter));
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

运行客户端和服务端我们能看到:

我们看到这个长长的字符串被截成了2段发送,这就是发生了拆包的现象。同样粘包我们也很容易去模拟,我们把BaseClientHandler中的channelActive方法里面的:

message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);

这几行代码是把我们上面的一长串字符转成的byte数组写进流里发送出去,那么我们可以在这里把上面发送消息的这几行循环几遍这样发送的内容增多了就有可能在拆包的时候把上一条消息的一部分分配到下一条消息里面了,修改如下:

for (int i = 0; i < 3; i++) {
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}

改完之后我们再运行一下,输出太长不好截图,我们在输出结果中能看到循环3次之后的消息服务端收到的就不是之前的完整的一条了,而是被拆分了4次发送。

对于上面出现的粘包和拆包的问题,Netty已有考虑,并且有实施的方案:LineBasedFrameDecoder。

我们重新改写一下ServerChannelInitializer:

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(new LineBasedFrameDecoder(2048));
// 字符串解码 和 编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder()); // 自己的逻辑Handler
pipeline.addLast("handler", new BaseServerHandler());
}
}

新增:pipeline.addLast(new LineBasedFrameDecoder(2048))。同时,我们还得对上面发送的消息进行改造BaseClientHandler:

public class BaseClientHandler extends ChannelInboundHandlerAdapter {
private byte[] req;
private int counter; req = ("Unless required by applicable dfslaw or agreed to in writing, software" +
" distributed under the License is distributed on an \"AS IS\" BASIS," +
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." +
" See the License for the specific language governing permissions and" +
" limitations under the License.This connector uses the BIO implementation that requires the JSSE" +
" style configuration. When using the APR/native implementation, the" +
" penSSL style configuration is required as described in the APR/native" +
" documentation.An Engine represents the entry point (within Catalina) that processes" +
" every request. The Engine implementation for Tomcat stand alone" +
" analyzes the HTTP headers included with the request, and passes them" +
" on to the appropriate Host (virtual host)# Unless required by applicable law or agreed to in writing, software" +
"# distributed under the License is distributed on an \"AS IS\" BASIS," +
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." +
"# See the License for the specific language governing permissions and" +
"# limitations under the License.# For example, set the org.apache.catalina.util.LifecycleBase logger to log" +
"# each component that extends LifecycleBase changing state:" +
"#org.apache.catalina.util.LifecycleBase.level = FINE\n"
).getBytes(); @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message; message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String buf = (String)msg;
System.out.println("Now is : " + buf + " ; the counter is : "+ (++counter));
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

去掉所有的”\n”,只保留字符串末尾的这一个。原因稍后再说。channelActive方法中我们不必再用循环多次发送消息了,只发送一次就好(第一个例子中发送一次的时候是发生了拆包的),然后我们再次运行,大家会看到这么长一串字符只发送了一串就发送完毕。程序输出我就不截图了。下面来解释一下LineBasedFrameDecoder。

LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf 中的可读字节,判断看是否有”\n” 或者” \r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器。支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。这个对于我们确定消息最大长度的应用场景还是很有帮助。

对于上面的判断看是否有”\n” 或者” \r\n”以此作为结束的标志我们可能回想,要是没有”\n” 或者” \r\n”那还有什么别的方式可以判断消息是否结束呢。别担心,Netty对于此已经有考虑,还有别的解码器可以帮助我们解决问题,下节我们继续学习。

Netty学习(四)-TCP粘包和拆包的更多相关文章

  1. 【Netty】TCP粘包和拆包

    一.前言 前面已经基本上讲解完了Netty的主要内容,现在来学习Netty中的一些可能存在的问题,如TCP粘包和拆包. 二.粘包和拆包 对于TCP协议而言,当底层发送消息和接受消息时,都需要考虑TCP ...

  2. netty 解决TCP粘包与拆包问题(一)

    1.什么是TCP粘包与拆包 首先TCP是一个"流"协议,犹如河中水一样连成一片,没有严格的分界线.当我们在发送数据的时候就会出现多发送与少发送问题,也就是TCP粘包与拆包.得不到我 ...

  3. tcp粘包和拆包的处理方案

    随着智能硬件越来越流行,很多后端开发人员都有可能接触到socket编程.而很多情况下,服务器与端上需要保证数据的有序,稳定到达,自然而然就会选择基于tcp/ip协议的socekt开发.开发过程中,经常 ...

  4. TCP粘包,拆包及解决方法

    在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题.我们都知道TCP属于传输 ...

  5. TCP粘包、拆包

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

  6. netty的解码器与粘包和拆包

    tcp是一个“流”的协议,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 假设客户端分别发送数据包D1和D2给服务端,由于服务 ...

  7. 【游戏开发】网络编程之浅谈TCP粘包、拆包问题及其解决方案

    引子 现如今手游开发中网络编程是必不可少的重要一环,如果使用的是TCP协议的话,那么不可避免的就会遇见TCP粘包和拆包的问题,马三觉得haifeiWu博主的 TCP 粘包问题浅析及其解决方案 这篇博客 ...

  8. TCP粘包和拆包问题

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

  9. 关于TCP粘包和拆包的终极解答

    关于TCP粘包和拆包的终极解答 程序员行业有一些奇怪的错误的观点(误解),这些误解非常之流行,而且持有这些错误观点的人经常言之凿凿,打死也不相信自己有错,实在让人啼笑皆非.究其原因,还是因为这些错误观 ...

随机推荐

  1. 机器学习读书笔记(五)AdaBoost

    一.Boosting算法 .Boosting算法是一种把若干个分类器整合为一个分类器的方法,在boosting算法产生之前,还出现过两种比较重要的将多个分类器整合为一个分类器的方法,即boostrap ...

  2. vue.js打包部署线上

    你完成了工程开发,需要部署到外网环境,要进行下面的步骤: 一.首先你要购买一个服务器或者有自己的服务器.我介绍给大家的一个免费的服务器:http://free.3v.do/index.html可以免费 ...

  3. MyBatis 核心配置综述之Executor

    目录 MyBatis四大组件之 Executor执行器 Executor的继承结构 Executor创建过程以及源码分析 Executor接口的主要方法 Executor 的现实抽象 上一篇我们对Sq ...

  4. .NET Core 3.0之深入源码理解Kestrel的集成与应用(二)

      前言 前一篇文章主要介绍了.NET Core继承Kestrel的目的.运行方式以及相关的使用,接下来将进一步从源码角度探讨.NET Core 3.0中关于Kestrel的其他内容,该部分内容,我们 ...

  5. 网络设置管理 NetSetMan Pro v4.7.1 Lite 绿色便携版

    下载地址:点我 基本介绍 Netsetman是一个小巧好用的工具,你可以设置六组不同的网络参数值,针对不同的网络环境,而调用不同的参数,当你在家中.学校.工作单位等不同环境切换网络配置文件时,只需要通 ...

  6. Linux系统下word转pdf,xls转pdf,ppt转pdf

    word转换pdf的技术方案,供参考.[doc/docx/ppt/pptx/xls/xlsx均支持转换]           本方案是Java结合shell命令完成,不同于以往的仅依赖java组件转换 ...

  7. 修改mysql错误日志级别

    show variables like '%log_warnings%'; 1代表开启warning信息,0代表关闭warning信息 set session log_warnings=0; set ...

  8. css3系列之弹性盒子 flex

    弹性盒子(伸缩盒) 注意,本篇会很长,非常长, 因为弹性盒子的知识点比较多 搜索 弹性盒子的属性  ctrl + F   如果觉得图太小, ctrl + +键 设置弹性盒子的属性: display:f ...

  9. 将web工程署到Linux简单实现

    1,将数据库文件导出并导入到Linux下的数据库中 2,将数据库连接池的连接IP改为Linux所在服务器的 3,将工程文件以war包形式导出 4,利用secureCRT,Xshell等工具远程连接Li ...

  10. C#3.0新增功能09 LINQ 基础05 使用 LINQ 进行数据转换

    连载目录    [已更新最新开发文章,点击查看详细] 语言集成查询 (LINQ) 不只是检索数据. 它也是用于转换数据的强大工具. 通过使用 LINQ查询,可以使用源序列作为输入,并通过多种方式对其进 ...