我们都知道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. JAVA写入大文件DEMO

    /**     * 读取filePath的文件     * @param filePath    文件的路径     * @return     List集合       文件中一行一行的数据     ...

  2. 【POJ - 2386】Lake Counting (dfs+染色)

    -->Lake Counting 直接上中文了 Descriptions: 由于近日阴雨连天,约翰的农场中中积水汇聚成一个个不同的池塘,农场可以用 N x M (1 <= N <= ...

  3. 我的那些年(12)~公司技术转行,我也跟着转到java了

    回到目录 我的那些年(12)~公司技术转行,我也跟着转到java了 CTO换人了 微软技术栈不被认可经常被喷 技术统一向java转 换了mac book后,docker还是很占内存 学习springb ...

  4. MySQL数据库设计与开发规范

    目录 1. 规范背景与目的 2. 设计规范 2.1. 数据库设计 2.1.1. 库名 2.1.2. 表结构 2.1.3. 列数据类型优化 2.1.4. 索引设计 2.1.5. 分库分表.分区表 2.1 ...

  5. TCP/IP协议、三次握手、四次挥手

    1.什么是TCP/IP协议 TCP/IP 是一类协议系统,它是用于网络通信的一套协议集合. 传统上来说 TCP/IP 被认为是一个四层协议 1) 网络接口层: 主要是指物理层次的一些接口,比如电缆等. ...

  6. Linux命令学习-cp命令

    Linux中,cp命令的全称是copy,主要作用是复制文件或文件夹,类似于Windows下的复制功能. 假设当前处于wintest用户的主目录,路径为 /home/wintest ,存在文件夹test ...

  7. 基于缓存或zookeeper的分布式锁实现

    缓存锁  我们常常将缓存作为分布式锁的解决方案,但是却不能单纯的判断某个 key 是否存在 来作为锁的获得依据,因为无论是 exists 和 get 命名都不是线程安全的,都无法保证只有一个线程可以获 ...

  8. 基于 MySQL Binlog 的 Elasticsearch 数据同步实践 原

    一.背景 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品.订单等数据的多维度检索. 使用 Elasticsearch 存储业务数据可以 ...

  9. webpack-dev-server 小记 原理介绍 概念解读

    使用 DevServer 提供 HTTP 服务而不是使用本地文件预览 监听文件的变化并自动刷新网页,做到实时预览 支持 Source Map,以方便调试 对于这些,Webpack 都为我们考虑好了.W ...

  10. linux下运行python3出现TypeError: a bytes-like object is required, not 'str'

    目标:用python将中文存入csv,且中文正常显示. 环境:linux,python3 百度N久,方法都不行或是比较复杂. 以上代码用python3运行后,出现TypeError: a bytes- ...