文档章节

Netty5入门学习笔记002-TCP粘包/拆包问题的解决之道(上)

山东-小木
 山东-小木
发布于 2014/12/17 17:19
字数 1490
阅读 15184
收藏 131

TCP网络通信时候会发生粘包/拆包的问题,接下来探讨其解决之道。

什么是粘包/拆包

一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。

情况分析

TCP粘包通常在流传输中出现,UDP则不会出现粘包,因为UDP有消息边界,发送数据段需要等待缓冲区满了才将数据发送出去,当满的时候有可能不是一条消息而是几条消息合并在换中去内,在成粘包;另外接收数据端没能及时接收缓冲区的包,造成了缓冲区多包合并接收,也是粘包。

解决办法

1、消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。

2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。

3、将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段

4、更复杂的自定义应用层协议

代码例子

1、Netty中提供了FixedLengthFrameDecoder定长解码器可以帮助我们轻松实现第一种解决方案,定长解码报文。

服务器端:

package im;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
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;
/**
 * 定长解码  服务器端
 * @author xwalker
 */
public class Server {
	
	public void bind(int port) throws Exception{
		//接收客户端连接用
		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<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel ch) throws Exception {
				ch.pipeline().addLast(new FixedLengthFrameDecoder(30));//设置定长解码器 长度设置为30
				ch.pipeline().addLast(new StringDecoder());//设置字符串解码器 自动将报文转为字符串
				ch.pipeline().addLast(new Serverhandler());//处理网络IO 处理器
			}
		});
		//绑定端口 等待绑定成功
		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=8000;
		new Server().bind(port);
	}

}

package im;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
 * 服务器handler
 * @author xwalker
 */
public class Serverhandler extends ChannelHandlerAdapter {
	int counter=0;
	private static final String MESSAGE="It greatly simplifies and streamlines network programming such as TCP and UDP socket server.";
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg)
			throws Exception {
		System.out.println("接收客户端msg:["+msg+"]");
		ByteBuf echo=Unpooled.copiedBuffer(MESSAGE.getBytes());
		ctx.writeAndFlush(echo);
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
			throws Exception {
		cause.printStackTrace();
		ctx.close();
	}

}

客户端:

package im;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
/**
 * 客户端 
 * @author xwalker
 *
 */
public class Client {
	/**
	 * 链接服务器
	 * @param port
	 * @param host
	 * @throws Exception
	 */
	public void connect(int port,String host)throws Exception{
		//网络事件处理线程组
		EventLoopGroup group=new NioEventLoopGroup();
		try{
		//配置客户端启动类
		Bootstrap b=new Bootstrap();
		b.group(group).channel(NioSocketChannel.class)
		.option(ChannelOption.TCP_NODELAY, true)//设置封包 使用一次大数据的写操作,而不是多次小数据的写操作
		.handler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel ch) throws Exception {
				ch.pipeline().addLast(new FixedLengthFrameDecoder(30));//设置定长解码器
				ch.pipeline().addLast(new StringDecoder());//设置字符串解码器
				ch.pipeline().addLast(new ClientHandler());//设置客户端网络IO处理器
			}
		});
		//连接服务器 同步等待成功
		ChannelFuture f=b.connect(host,port).sync();
		//同步等待客户端通道关闭
		f.channel().closeFuture().sync();
		}finally{
			//释放线程组资源
			group.shutdownGracefully();
		}
	}
	public static void main(String[] args) throws Exception {
		int port=8000;
		new Client().connect(port, "127.0.0.1");

	}

}

package im;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
 * 客户端处理器
 * @author xwalker
 *
 */
public class ClientHandler extends ChannelHandlerAdapter {
	private static final String MESSAGE="Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients.";
	public ClientHandler(){}
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
			ctx.writeAndFlush(Unpooled.copiedBuffer(MESSAGE.getBytes()));
	}
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg)
			throws Exception {
		System.out.println("接收服务器响应msg:["+msg+"]");
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		ctx.flush();
	}
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
			throws Exception {
		cause.printStackTrace();
		ctx.close();
	}
}

服务器和客户端分别设置了定长解码器 长度为30字节,也就是规定发送和接收一次报文定长为30字节。

运行结果:

客户端接收到服务器的响应报文 一段文字被定长分成若干段接收。

服务器端接收客户端发送的报文 一段话也是分成了等长的若干段。

上述是一个简单长字符串传输例子,将一个长字符串分割成若干段。我们也可以自定义一系列定长的指令发送出去

例如指令长度都是30个字节,批量发出N条指令,这样客户端粘包后发出一个比较大的数据指令集,服务器接收到的数据在缓冲区内,只需要按照定长一个个指令取出来执行即可。


JFinal经典入门到精通课程

© 著作权归作者所有

山东-小木

山东-小木

粉丝 244
博文 49
码字总数 29603
作品 2
东营
CEO
私信 提问
加载中

评论(11)

山东-小木
山东-小木 博主

引用来自“贴膜boy”的评论

好文,除了定长的,应该还有其他Decoder吧,毕竟定长场景不叫少见,对Netty不熟
有 这篇文章是002 还设有003 004
PacoXie
PacoXie
好文,除了定长的,应该还有其他Decoder吧,毕竟定长场景不叫少见,对Netty不熟
山东-小木
山东-小木 博主

引用来自“teeceepee”的评论

粘包拆包是伪问题吧,TCP是传输协议,不能直接当作应用协议来用。这两个词我查过好多资料,找到的全都是中文的,相关的英文资料一点也没有,估计可能是中国人自己创造出的“问题”吧。
你可以实际模拟一下客户端同时发送1000条指令到服务器,看看服务器端接收的是1000条还是少于一千条
teeceepee
teeceepee
粘包拆包是伪问题吧,TCP是传输协议,不能直接当作应用协议来用。这两个词我查过好多资料,找到的全都是中文的,相关的英文资料一点也没有,估计可能是中国人自己创造出的“问题”吧。
山东-小木
山东-小木 博主

引用来自“JavaGG”的评论

这个例子是不是太理想化,那有所有包的标准长度都是30个字节的
使用定长解码器需要根据自己实际的客户端和服务端的约定 传输固定长度的指令 并不是随意设置
JavaGG
JavaGG
这个例子是不是太理想化,那有所有包的标准长度都是30个字节的
丁富贵

引用来自“平安北京”的评论

包头+包体,包头=包长+序列号+命令号

引用来自“xwalker”的评论

mysql通信协议
山东-小木
山东-小木 博主

引用来自“景愿”的评论

为什么用5.x版本线呢?这个版本自13年更新后就没动过了,反而4.0.x线更新的很快

这个版本并没有太大的变化
景愿
景愿
为什么用5.x版本线呢?这个版本自13年更新后就没动过了,反而4.0.x线更新的很快
山东-小木
山东-小木 博主

引用来自“平安北京”的评论

包头+包体,包头=包长+序列号+命令号
Netty5入门学习笔记001

Netty官网:http://netty.io/ 本例程使用最新的netty5.x版本编写 服务器端: TimeServer 时间服务器 服务端接收客户端的连接请求和查询当前时间的指令,判断指令正确后响应返回当前服务器的校...

山东-小木
2014/12/17
15.4K
10
OSChina 技术周刊第十四期 —— 每周技术精粹

每周技术抢先看,总有你想要的! 移动开发 【软件】医疗和生物医学移动应用框架 mHealhDroid 【博客】Android Studio 使用NDK开发 【博客】Android 4.4(KK)中利用APP打开关闭数据流量 前端...

OSC编辑部
2014/12/21
2.6K
1
Netty5入门学习笔记003-TCP粘包/拆包问题的解决之道(下)

TCP网络通信时候会发生粘包/拆包的问题,上节使用定长解码器解码,本次使用Netty提供的特殊分隔符解码器 还是用上节中的代码例子,但是只需要修改一下发送的消息和配置一下解码器就可以了 客...

山东-小木
2014/12/18
3.6K
4
netty入门笔记

尝试一下helloword demo -->官方HelloWord -->简书的入门级netty聊天demo 地址:https://www.jianshu.com/p/216881b0573d 2.netty入门级群聊demo,发现有3个问题 问题1:没有显示用户的名字 (现...

谜男amu
2018/01/23
138
0
【Netty】Netty实例开源项目

版权声明:本文为谙忆原创文章,转载请附上本文链接,谢谢。 https://blog.csdn.net/qq_26525215/article/details/81989644 Netty netty-not-sticky-pack-demo 项目地址 Netty 本篇博客讲解:...

谙忆
2018/08/23
0
0

没有更多内容

加载失败,请刷新页面

加载更多

数据库表与表之间的一对一、一对多、多对多关系

表1 foreign key 表2 多对一:表 1 的多条记录对应表 2 的一条记录 利用foreign key的原理我们可以制作两张表的多对多,一对一关系 多对多: 表1的多条记录可以对应表2的一条记录 表2的多条记...

Garphy
42分钟前
6
0
MySQL 表崩溃修复

MySQL日志报错 2019-10-19 13:41:51 19916 [ERROR] /usr/local/mysql/bin/mysqld: Table './initread_hss/user_info' is marked as crashed and should be repaired2019-10-19 13:41:51 1......

雁南飞丶
51分钟前
6
0
Error和Exception

1.Error类和Exception类都是继承Throwable类 2.Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问...

大瑞清_liurq
今天
4
0
8086汇编基础 start 程序入口标签的示例

    IDE : Masm for Windows 集成实验环境 2015     OS : Windows 10 x64 typesetting : Markdown    blog : my.oschina.net/zhichengjiu    gitee : gitee.com/zhichengjiu   ......

志成就
今天
4
0
uni app 零基础小白到项目实战2

<template> <scroll-view v-for="(card, index) in list" :key="index"> <view v-for =(item, itemIndex) in card"> {{item.value}}</view> </scroll-view></template> GraceUi va......

达达前端小酒馆
今天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部