熟悉TCP编程的可能都知道,无论是服务端还是客户端,当进行数据读取或者发送数据的时候,都需要考虑TCP底层的粘包和拆包机制的问题。
TCP是一个流协议,流是没有界限的数据,是连成一片的,没有分界线。TCP底层并不理解上层的业务数据具体的含义,它会根据TCP缓冲区的实际情况进行包的划分,也就是说,在业务上,一个完整的包可能会被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去,这就是所谓的TCP粘包、拆包问题。
1、应用程序Write写入的字节大小大于套接字发送的缓冲区的大小。
2、进行MSS大小的TCP分段
3、以太网帧的payload大于MTU进行IP分片。
1、消息定长:例如每个包的大小固定为200个字节,不够用空格补。
2、特殊字符:在包尾部增加特殊字符进行分割,例如加回车等,在实际中经常使用的是这种。
3、自定义:将消息分为消息头和消息体,消息头中包含消息总长度的字段,然后进行业务逻辑的处理,但是要考虑安全等问题。
/** * Tcp拆包、粘包客户端 */ public class N02TcpUnpacketAndNotUnServer { public static void main(String[] args) throws InterruptedException {
//用于客户端链接 EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); //用于传输数据 EventLoopGroup dataEventLoopGroup = new NioEventLoopGroup();
//数据启动 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(clientEventLoopGroup, dataEventLoopGroup) //设置链接和数据处理 .channel(NioServerSocketChannel.class) //设置模式 .option(ChannelOption.SO_BACKLOG, 1024) //TCP缓冲区 .option(ChannelOption.SO_SNDBUF, 32 * 1024) //发送数据缓冲区 .option(ChannelOption.SO_RCVBUF, 32 * 1024) //接受数据缓冲区 .childHandler(new ChannelInitializer @Override protected void initChannel(SocketChannel ch) throws Exception { //3、自定义分隔符的方式解决粘包拆包问题 //注意:设置顺序 // ByteBuf byteBuf = Unpooled.copiedBuffer("¥_".getBytes()); // ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, byteBuf));
//5、定长数据包 //注意:设置顺序 ch.pipeline().addLast(new FixedLengthFrameDecoder(3));
ch.pipeline().addLast(new StringDecoder()); //设置字符串解码类 ch.pipeline().addLast(new TcpUServerHandler()); //设置处理类 } });
//绑定链接 ChannelFuture channelFuture = serverBootstrap.bind(1111).sync(); channelFuture.channel().closeFuture().sync();
//关闭 clientEventLoopGroup.shutdownGracefully(); dataEventLoopGroup.shutdownGracefully(); } } class TcpUServerHandler extends ChannelHandlerAdapter {
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("服务端:活跃..."); }
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //在上面时,已经设置了编解码器就是String类型了 String data = (String) msg; System.out.println("服务端:" + data); //响应数据 ctx.writeAndFlush(Unpooled.copiedBuffer("你好,客户端".getBytes())); }
@Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { System.out.println("服务端:读取完成"); }
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); } } |
/** * Tcp拆包、粘包客户端 */ public class N02TcpUnpacketAndNotUnClient { public static void main(String[] args) throws InterruptedException { //创建链接 EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
//启动类 Bootstrap bootstrap = new Bootstrap(); bootstrap.group(eventLoopGroup) //启动类 .channel(NioSocketChannel.class) //设置模式 .handler(new ChannelInitializer @Override protected void initChannel(SocketChannel ch) throws Exception { //3、自定义分隔符的方式解决粘包拆包问题 // ByteBuf byteBuf = Unpooled.copiedBuffer("¥_".getBytes()); // ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,byteBuf));
//5、定长数据包 ch.pipeline().addLast(new FixedLengthFrameDecoder(3));
ch.pipeline().addLast(new StringDecoder()); //编解码器 ch.pipeline().addLast(new TcpUnClientHandler()); //数据处理类 } });
//绑定地址和端口 ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 1111).sync();
/* * 1、模拟粘包、拆包问题 * 会发现服务端收到的数据是链接在一起的:111111222222333333 * * 2、添加一个Thread.sleep(100);模拟延迟发送 * 会发现服务端接收的数据是一行一行的,似乎每个数据包都是相隔开的。 * 问题在实际的应用中,不可能这样去停止等待的,效率太低了。 * * Netty提供了2个类用于解决这个粘包、拆包问题: * 自定义分隔符:DelimiterBasedFrameDecoder * 定长数据包:FixedLengthFrameDecoder */ // channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer("111111".getBytes())); // Thread.sleep(100); // channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer("222222".getBytes())); // Thread.sleep(100); // channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer("333333".getBytes()));
//4、模拟自定义分隔符,解决粘包、拆包问题 // channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer("aaa¥_".getBytes())); // channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer("bbb¥_".getBytes()));
//6、模拟定长数据包,解决粘包、拆包问题 channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer("aaaaaaabbbbbbbccccccc".getBytes()));
//阻塞 channelFuture.channel().closeFuture().sync();
//关闭 eventLoopGroup.shutdownGracefully(); } } /** * 数据处理 */ class TcpUnClientHandler extends ChannelHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("客户端:活跃状态"); }
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { //上面设置了编解码器 String data = (String) msg; System.out.println("客户端:" + data); } finally { ReferenceCountUtil.release(msg); } }
@Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { System.out.println("客户端:数据读取完成"); }
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); } } |
其实就是Java序列化技术,序列化的目的就有两个,一是网络传输、而是对象持久化。
虽然可以使用Java进行对象序列化,Netty去传输,但是Java序列化的硬伤太多了,且明显,比如没法跨语音,序列化后码流太大,序列化性能低等等。
JBOSS的Marshalling:对JDK默认的序列化框架进行了优化,但又保持了跟java.io.Serializable接口的兼容,同时增加了一些可选的参数和附加特性。
Google的Protobuf:基于Protobuf协议,序列号后码流小,性能高,依赖于工具生成代码,支持语音也比较少,但是在性能要求高的RPC调用,而且具有很好的跨防火墙的访问属性,适合应用程对象持久化。
基于Protobuf的Kyro(Spark):只支持java语言。
MessagePack框架:高效的二进制序列化格式。
Avro:Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题,对于习惯于静态类型语音的用户不直观,适用于Hadoop中的Hive这些做持久化数据格式。
/** * 压缩工具类 */ public class N03GzipUtils { public static void main(String[] args) throws Exception { String inPath = "C:\\Users\\EDZ\\Desktop\\1.gif";
//读取文件 FileInputStream fileInputStream = new FileInputStream(new File(inPath)); byte [] data = new byte[fileInputStream.available()]; fileInputStream.read(data); fileInputStream.close(); System.out.println("文件原始大小:" + data.length);
byte[] gzipData = N03GzipUtils.gzip(data); System.out.println("压缩大小:" + gzipData.length);
byte[] ungzipData = N03GzipUtils.ungzip(gzipData); System.out.println("解压缩大小:" + ungzipData.length);
//写出文件 String outPath = "C:\\Users\\EDZ\\Desktop\\2.gif"; FileOutputStream fileOutputStream = new FileOutputStream(outPath); fileOutputStream.write(ungzipData); fileOutputStream.close(); }
/** * 压缩 */ public static byte[] gzip(byte[] data) throws Exception { //创建二进制流写出 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); gzipOutputStream.write(data); gzipOutputStream.finish(); gzipOutputStream.close();
//读取到的数据 byte[] byteArray = byteArrayOutputStream.toByteArray(); byteArrayOutputStream.close(); return byteArray; }
/** * 解压 */ public static byte[] ungzip(byte[] data) throws Exception { //创建二进制流写出 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data); GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream); byte [] buf = new byte[1024]; int num = -1;
//写出 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); while ((num = gzipInputStream.read(buf, 0, buf.length)) != -1) { byteArrayOutputStream.write(buf,0,num); } gzipInputStream.close(); byteArrayInputStream.close();
byte [] rData = byteArrayOutputStream.toByteArray(); byteArrayOutputStream.flush(); byteArrayOutputStream.close(); return rData; } } |
import org.jboss.marshalling.MarshallerFactory; import org.jboss.marshalling.Marshalling; import org.jboss.marshalling.MarshallingConfiguration;
import io.netty.handler.codec.marshalling.DefaultMarshallerProvider; import io.netty.handler.codec.marshalling.DefaultUnmarshallerProvider; import io.netty.handler.codec.marshalling.MarshallerProvider; import io.netty.handler.codec.marshalling.MarshallingDecoder; import io.netty.handler.codec.marshalling.MarshallingEncoder;
/** * 序列化编解码器: * JBoss:Marshalling * Google:Protobuf * 基于Protobuf:Kyro * MessagePack框架 */ public class N03MarshallingDecoderEncoderUtils {
public final static String TAG = "serial";
/** * 解码器:将二进制序列化为java对象 */ public static MarshallingDecoder buildDecoder() { //通过Marshalling工具类获取到实例对象,serial标识创建的是java的序列化工厂对象 MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory(TAG);
//创建MarshallingConfiguration对象,并配置版本号 MarshallingConfiguration marshallingConfiguration = new MarshallingConfiguration(); marshallingConfiguration.setVersion(5);
//创建Provider DefaultUnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, marshallingConfiguration);
//构建Netty的解码对象,并配置provider和单个消息序列化的最大长度 return new MarshallingDecoder(provider, 1024 * 1024); }
/** * 编码器:将对象序列化为二进制数组 */ public static MarshallingEncoder buildEncoder() { //序列化java工厂类 MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory(TAG);
//创建配置类 MarshallingConfiguration marshallingConfiguration = new MarshallingConfiguration(); marshallingConfiguration.setVersion(5);
//创建供应者 MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, marshallingConfiguration);
//构建Netty的MarshallingEncoder对象 MarshallingEncoder marshallingEncoder = new MarshallingEncoder(provider); return marshallingEncoder; } } |
import java.io.Serializable;
/** * 请求数据 */ public class N03Request implements Serializable {
private static final long serialVersionUID = -7633763905125419996L;
private String data;
private byte[] file;
public String getData() { return data; }
public void setData(String data) { this.data = data; }
public byte[] getFile() { return file; }
public void setFile(byte[] file) { this.file = file; } }
|
import java.io.Serializable;
/** * 响应数据 */ public class N03Response implements Serializable {
private static final long serialVersionUID = -6704864849770619898L;
private String data;
public String getData() { return data; }
public void setData(String data) { this.data = data; } }
|
import java.io.FileOutputStream; import java.util.Random;
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; 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.logging.LogLevel; import io.netty.handler.logging.LoggingHandler;
/** * 序列化的编解码Api,服务端 */ public class N03SerialServer { public static void main(String[] args) throws InterruptedException { //用于客户端链接 EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); //用于数据读写 EventLoopGroup dataEventLoopGroup = new NioEventLoopGroup();
//创建启动类 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(clientEventLoopGroup, dataEventLoopGroup) //配置链接类 .channel(NioServerSocketChannel.class) //设置模式 .option(ChannelOption.SO_BACKLOG, 1024) //TCP缓冲区 .handler(new LoggingHandler(LogLevel.INFO)) //日志级别 .childHandler(new ChannelInitializer @Override protected void initChannel(SocketChannel ch) throws Exception { //编解码器 ch.pipeline().addLast(N03MarshallingDecoderEncoderUtils.buildDecoder()); ch.pipeline().addLast(N03MarshallingDecoderEncoderUtils.buildEncoder()); //处理类 ch.pipeline().addLast(new SServerHandler()); } });
//绑定端口 ChannelFuture channelFuture = serverBootstrap.bind(1111).sync(); channelFuture.channel().closeFuture().sync();
//关闭 clientEventLoopGroup.shutdownGracefully(); dataEventLoopGroup.shutdownGracefully(); } } class SServerHandler extends ChannelHandlerAdapter {
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("服务端:活跃"); }
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("---------------------"); //已经设置了对象的序列化编解码器 N03Request request = (N03Request) msg; System.out.println("服务端:" + request.getData());
byte[] ungzipData = N03GzipUtils.ungzip(request.getFile());
//写出文件 String outPath = "C:\\Users\\EDZ\\Desktop\\" + new Random().nextInt() + ".gif"; FileOutputStream fileOutputStream = new FileOutputStream(outPath); fileOutputStream.write(ungzipData); fileOutputStream.close();
//响应客户端 N03Response response = new N03Response(); response.setData("已收到文件,长度是:" + ungzipData.length); ctx.writeAndFlush(response); }
@Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { System.out.println("服务端:读取完毕"); }
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); } } |
import java.io.FileInputStream;
import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; 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.util.ReferenceCountUtil;
/** * 序列化的编解码Api,客户端 */ public class N03SerialClient { public static void main(String[] args) throws Exception { //链接服务端 EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
//创建启动类 Bootstrap bootstrap = new Bootstrap(); bootstrap.group(eventLoopGroup) //绑定 .channel(NioSocketChannel.class) //设置模式 .handler(new ChannelInitializer @Override protected void initChannel(SocketChannel ch) throws Exception { //编解码器 ch.pipeline().addLast(N03MarshallingDecoderEncoderUtils.buildDecoder()); ch.pipeline().addLast(N03MarshallingDecoderEncoderUtils.buildEncoder()); //处理类 ch.pipeline().addLast(new SClientHandler()); } });
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",1111).sync();
//读取文件数据 String inPath = "C:\\Users\\EDZ\\Desktop\\1.gif"; FileInputStream fileInputStream = new FileInputStream(inPath); byte [] fileData = new byte[fileInputStream.available()]; fileInputStream.read(fileData); fileInputStream.close(); N03Request request = new N03Request(); request.setData("你好,服务端!"); request.setFile(N03GzipUtils.gzip(fileData)); //写出数据 channelFuture.channel().writeAndFlush(request);
System.out.println("==================");
//阻塞,关闭 channelFuture.channel().closeFuture().sync(); eventLoopGroup.shutdownGracefully();
} } class SClientHandler extends ChannelHandlerAdapter {
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("客户端:活跃"); }
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { N03Response response = (N03Response) msg; System.out.println("客户端:" + response.getData()); } finally { ReferenceCountUtil.release(msg); } }
@Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { System.out.println("客户端:读取完成"); }
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); } } |