自定义协议解决TCP粘包

1、什么是粘包/拆包

       一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。
1.1 http协议怎么解决TCP粘包:
(1)请求:
请求头以特定的符号("\r\n")结束,符合策略2,如果有请求体,则请求头中会有表示请求体的长度,符合策略3

http请求协议主要包括请求头,请求行、空行、请求体
请求头中若请求体不为空,需要指定请求体长度

(2)响应:
Content-Length用于描述HTTP消息实体的传输长度the transfer-length of the message-body。在HTTP协议中,消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度。
在具体的HTTP交互中,客户端是如何获取消息长度的呢,主要基于以下几个规则:
响应为1xx,204,304相应或者head请求,则直接忽视掉消息实体内容。
如果有Transfer-Encoding,则优先采用Transfer-Encoding里面的方法来找到对应的长度。比如说Chunked模式。
“如果head中有Content-Length,那么这个Content-Length既表示实体长度,又表示传输长度。如果实体长度和传输长度不相等(比如说设置了Transfer-Encoding),那么则不能设置Content-Length。如果设置了Transfer-Encoding,那么Content-Length将被忽视”。这句话翻译的优点饶,其实关键就一点:有了Transfer-Encoding,则不能有Content-Length。
Range传输。不关注,没详细看了:)
通过服务器关闭连接能确定消息的传输长度。(请求端不能通过关闭连接来指明请求消息体的结束,因为这样可以让服务器没有机会继续给予响应)。这种情况主要对应为短连接,即非keep-alive模式。
HTTP1.1必须支持chunk模式。因为当不确定消息长度的时候,可以通过chunk机制来处理这种情况。
在包含消息内容的header中,如果有content-length字段,那么该字段对应的值必须完全和消息主题里面的长度匹配。
“The entity-length of a message is the length of the message-body before any transfer-codings have been applied”
也就是有chunk就不能有content-length 。

其实后面几条几乎可以忽视,简单总结后如下:
a1、Content-Length如果存在并且有效的话,则必须和消息内容的传输长度完全一致。(经过测试,如果过短则会截断,过长则会导致超时。)
a2、如果存在Transfer-Encoding(重点是chunked),则在header中不能有Content-Length,有也会被忽视。
a3、如果采用短连接,则直接可以通过服务器关闭连接来确定消息的传输长度。(这个很容易懂)
结合HTTP协议其他的特点,比如说Http1.1之前的不支持keep alive。那么可以得出以下结论:
b1、在Http 1.0及之前版本中,content-length字段可有可无。
b2、在http1.1及之后版本。如果是keep alive,则content-length和chunk必然是二选一。若是非keep alive,则和http1.0一样。content-length可有可无。

2、解决办法

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

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

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

3、自定义协议,来实现TCP的粘包/拆包问题

      3.0  自定义协议,开始标记           

              自定义协议解决TCP粘包_第1张图片

      3.1  自定义协议的介绍

             自定义协议解决TCP粘包_第2张图片

      3.2  自定义协议的类的封装

             自定义协议解决TCP粘包_第3张图片

      3.3  自定义协议的编码器

             自定义协议解决TCP粘包_第4张图片

      3.4  自定义协议的解码器

         自定义协议解决TCP粘包_第5张图片 

4、协议相关的实现

      4.1  协议的封装

[java] view plain copy

 print?

  1. import java.util.Arrays;  
  2. /** 
  3.  * 
     
  4.  * 自己定义的协议 
  5.  *  数据包格式 
  6.  * +——----——+——-----——+——----——+ 
  7.  * |协议开始标志|  长度             |   数据       | 
  8.  * +——----——+——-----——+——----——+ 
  9.  * 1.协议开始标志head_data,为int类型的数据,16进制表示为0X76 
  10.  * 2.传输数据的长度contentLength,int类型 
  11.  * 3.要传输的数据 
  12.  *  
  13.  */  
  14. public class SmartCarProtocol {  
  15.     /** 
  16.      * 消息的开头的信息标志 
  17.      */  
  18.     private int head_data = ConstantValue.HEAD_DATA;  
  19.     /** 
  20.      * 消息的长度 
  21.      */  
  22.     private int contentLength;  
  23.     /** 
  24.      * 消息的内容 
  25.      */  
  26.     private byte[] content;  
  27.     /** 
  28.      * 用于初始化,SmartCarProtocol 
  29.      *  
  30.      * @param contentLength 
  31.      *            协议里面,消息数据的长度 
  32.      * @param content 
  33.      *            协议里面,消息的数据 
  34.      */  
  35.     public SmartCarProtocol(int contentLength, byte[] content) {  
  36.         this.contentLength = contentLength;  
  37.         this.content = content;  
  38.     }  
  39.     public int getHead_data() {  
  40.         return head_data;  
  41.     }  
  42.     public int getContentLength() {  
  43.         return contentLength;  
  44.     }  
  45.     public void setContentLength(int contentLength) {  
  46.         this.contentLength = contentLength;  
  47.     }  
  48.     public byte[] getContent() {  
  49.         return content;  
  50.     }  
  51.     public void setContent(byte[] content) {  
  52.         this.content = content;  
  53.     }  
  54.     @Override  
  55.     public String toString() {  
  56.         return "SmartCarProtocol [head_data=" + head_data + ", contentLength="  
  57.                 + contentLength + ", content=" + Arrays.toString(content) + "]";  
  58.     }  
  59. }  

      4.2  协议的编码器

[java] view plain copy

 print?

  1. /** 
  2.  * 
     
  3.  * 自己定义的协议 
  4.  *  数据包格式 
  5.  * +——----——+——-----——+——----——+ 
  6.  * |协议开始标志|  长度             |   数据       | 
  7.  * +——----——+——-----——+——----——+ 
  8.  * 1.协议开始标志head_data,为int类型的数据,16进制表示为0X76 
  9.  * 2.传输数据的长度contentLength,int类型 
  10.  * 3.要传输的数据 
  11.  *  
  12.  */  
  13. public class SmartCarEncoder extends MessageToByteEncoder {  
  14.     @Override  
  15.     protected void encode(ChannelHandlerContext tcx, SmartCarProtocol msg,  
  16.             ByteBuf out) throws Exception {  
  17.         // 写入消息SmartCar的具体内容  
  18.         // 1.写入消息的开头的信息标志(int类型)  
  19.         out.writeInt(msg.getHead_data());  
  20.         // 2.写入消息的长度(int 类型)  
  21.         out.writeInt(msg.getContentLength());  
  22.         // 3.写入消息的内容(byte[]类型)  
  23.         out.writeBytes(msg.getContent());  
  24.     }  
  25. }  

      4.3  协议的解码器

[java] view plain copy

 print?

  1. import java.util.List;  
  2. import io.netty.buffer.ByteBuf;  
  3. import io.netty.channel.ChannelHandlerContext;  
  4. import io.netty.handler.codec.ByteToMessageDecoder;  
  5. /** 
  6.  * 
     
  7.  * 自己定义的协议 
  8.  *  数据包格式 
  9.  * +——----——+——-----——+——----——+ 
  10.  * |协议开始标志|  长度             |   数据       | 
  11.  * +——----——+——-----——+——----——+ 
  12.  * 1.协议开始标志head_data,为int类型的数据,16进制表示为0X76 
  13.  * 2.传输数据的长度contentLength,int类型 
  14.  * 3.要传输的数据,长度不应该超过2048,防止socket流的攻击 
  15.  *  
  16.  */  
  17. public class SmartCarDecoder extends ByteToMessageDecoder {  
  18.     /** 
  19.      * 
     
  20.      * 协议开始的标准head_data,int类型,占据4个字节. 
  21.      * 表示数据的长度contentLength,int类型,占据4个字节. 
  22.      *  
  23.      */  
  24.     public final int BASE_LENGTH = 4 + 4;  
  25.     @Override  
  26.     protected void decode(ChannelHandlerContext ctx, ByteBuf buffer,  
  27.             List out) throws Exception {  
  28.         // 可读长度必须大于基本长度  
  29.         if (buffer.readableBytes() >= BASE_LENGTH) {  
  30.             // 防止socket字节流攻击  
  31.             // 防止,客户端传来的数据过大  
  32.             // 因为,太大的数据,是不合理的  
  33.             if (buffer.readableBytes() > 2048) {  
  34.                 buffer.skipBytes(buffer.readableBytes());  
  35.             }  
  36.             // 记录包头开始的index  
  37.             int beginReader;  
  38.             while (true) {  
  39.                 // 获取包头开始的index  
  40.                 beginReader = buffer.readerIndex();  
  41.                 // 标记包头开始的index  
  42.                 buffer.markReaderIndex();  
  43.                 // 读到了协议的开始标志,结束while循环  
  44.                 if (buffer.readInt() == ConstantValue.HEAD_DATA) {  
  45.                     break;  
  46.                 }  
  47.                 // 未读到包头,略过一个字节  
  48.                 // 每次略过,一个字节,去读取,包头信息的开始标记  
  49.                 buffer.resetReaderIndex();  
  50.                 buffer.readByte();  
  51.                 // 当略过,一个字节之后,  
  52.                 // 数据包的长度,又变得不满足  
  53.                 // 此时,应该结束。等待后面的数据到达  
  54.                 if (buffer.readableBytes() < BASE_LENGTH) {  
  55.                     return;  
  56.                 }  
  57.             }  
  58.             // 消息的长度  
  59.             int length = buffer.readInt();  
  60.             // 判断请求数据包数据是否到齐  
  61.             if (buffer.readableBytes() < length) {  
  62.                 // 还原读指针  
  63.                 buffer.readerIndex(beginReader);  
  64.                 return;  
  65.             }  
  66.             // 读取data数据  
  67.             byte[] data = new byte[length];  
  68.             buffer.readBytes(data);  
  69.             SmartCarProtocol protocol = new SmartCarProtocol(data.length, data);  
  70.             out.add(protocol);  
  71.         }  
  72.     }  
  73. }  
  74.       4.4  服务端加入协议的编/解码器

              自定义协议解决TCP粘包_第6张图片  

          4.5  客户端加入协议的编/解码器

              自定义协议解决TCP粘包_第7张图片

    5、服务端的实现

    [java] view plain copy

     print?

    1. import io.netty.bootstrap.ServerBootstrap;  
    2. import io.netty.channel.ChannelFuture;  
    3. import io.netty.channel.ChannelInitializer;  
    4. import io.netty.channel.ChannelOption;  
    5. import io.netty.channel.EventLoopGroup;  
    6. import io.netty.channel.nio.NioEventLoopGroup;  
    7. import io.netty.channel.socket.SocketChannel;  
    8. import io.netty.channel.socket.nio.NioServerSocketChannel;  
    9. import io.netty.handler.logging.LogLevel;  
    10. import io.netty.handler.logging.LoggingHandler;  
    11. public class Server {  
    12.     public Server() {  
    13.     }  
    14.     public void bind(int port) throws Exception {  
    15.         // 配置NIO线程组  
    16.         EventLoopGroup bossGroup = new NioEventLoopGroup();  
    17.         EventLoopGroup workerGroup = new NioEventLoopGroup();  
    18.         try {  
    19.             // 服务器辅助启动类配置  
    20.             ServerBootstrap b = new ServerBootstrap();  
    21.             b.group(bossGroup, workerGroup)  
    22.                     .channel(NioServerSocketChannel.class)  
    23.                     .handler(new LoggingHandler(LogLevel.INFO))  
    24.                     .childHandler(new ChildChannelHandler())//  
    25.                     .option(ChannelOption.SO_BACKLOG, 1024) // 设置tcp缓冲区 // (5)  
    26.                     .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)  
    27.             // 绑定端口 同步等待绑定成功  
    28.             ChannelFuture f = b.bind(port).sync(); // (7)  
    29.             // 等到服务端监听端口关闭  
    30.             f.channel().closeFuture().sync();  
    31.         } finally {  
    32.             // 优雅释放线程资源  
    33.             workerGroup.shutdownGracefully();  
    34.             bossGroup.shutdownGracefully();  
    35.         }  
    36.     }  
    37.     /** 
    38.      * 网络事件处理器 
    39.      */  
    40.     private class ChildChannelHandler extends ChannelInitializer {  
    41.         @Override  
    42.         protected void initChannel(SocketChannel ch) throws Exception {  
    43.             // 添加自定义协议的编解码工具  
    44.             ch.pipeline().addLast(new SmartCarEncoder());  
    45.             ch.pipeline().addLast(new SmartCarDecoder());  
    46.             // 处理网络IO  
    47.             ch.pipeline().addLast(new ServerHandler());  
    48.         }  
    49.     }  
    50.     public static void main(String[] args) throws Exception {  
    51.         new Server().bind(9999);  
    52.     }  
    53. }  

    6、服务端Handler的实现

    [java] view plain copy

     print?

    1. import io.netty.channel.ChannelHandlerAdapter;  
    2. import io.netty.channel.ChannelHandlerContext;  
    3. public class ServerHandler extends ChannelHandlerAdapter {  
    4.     // 用于获取客户端发送的信息  
    5.     @Override  
    6.     public void channelRead(ChannelHandlerContext ctx, Object msg)  
    7.             throws Exception {  
    8.         // 用于获取客户端发来的数据信息  
    9.         SmartCarProtocol body = (SmartCarProtocol) msg;  
    10.         System.out.println("Server接受的客户端的信息 :" + body.toString());  
    11.         // 会写数据给客户端  
    12.         String str = "Hi I am Server ...";  
    13.         SmartCarProtocol response = new SmartCarProtocol(str.getBytes().length,  
    14.                 str.getBytes());  
    15.         // 当服务端完成写操作后,关闭与客户端的连接  
    16.         ctx.writeAndFlush(response);  
    17.         // .addListener(ChannelFutureListener.CLOSE);  
    18.         // 当有写操作时,不需要手动释放msg的引用  
    19.         // 当只有读操作时,才需要手动释放msg的引用  
    20.     }  
    21.     @Override  
    22.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)  
    23.             throws Exception {  
    24.         // cause.printStackTrace();  
    25.         ctx.close();  
    26.     }  
    27. }  

    7、客户端的实现

    [java] view plain copy

     print?

    1. import io.netty.bootstrap.Bootstrap;  
    2. import io.netty.channel.ChannelFuture;  
    3. import io.netty.channel.ChannelInitializer;  
    4. import io.netty.channel.ChannelOption;  
    5. import io.netty.channel.EventLoopGroup;  
    6. import io.netty.channel.nio.NioEventLoopGroup;  
    7. import io.netty.channel.socket.SocketChannel;  
    8. import io.netty.channel.socket.nio.NioSocketChannel;  
    9. public class Client {  
    10.     /** 
    11.      * 连接服务器 
    12.      *  
    13.      * @param port 
    14.      * @param host 
    15.      * @throws Exception 
    16.      */  
    17.     public void connect(int port, String host) throws Exception {  
    18.         // 配置客户端NIO线程组  
    19.         EventLoopGroup group = new NioEventLoopGroup();  
    20.         try {  
    21.             // 客户端辅助启动类 对客户端配置  
    22.             Bootstrap b = new Bootstrap();  
    23.             b.group(group)//  
    24.                     .channel(NioSocketChannel.class)//  
    25.                     .option(ChannelOption.TCP_NODELAY, true)//  
    26.                     .handler(new MyChannelHandler());//  
    27.             // 异步链接服务器 同步等待链接成功  
    28.             ChannelFuture f = b.connect(host, port).sync();  
    29.             // 等待链接关闭  
    30.             f.channel().closeFuture().sync();  
    31.         } finally {  
    32.             group.shutdownGracefully();  
    33.             System.out.println("客户端优雅的释放了线程资源...");  
    34.         }  
    35.     }  
    36.     /** 
    37.      * 网络事件处理器 
    38.      */  
    39.     private class MyChannelHandler extends ChannelInitializer {  
    40.         @Override  
    41.         protected void initChannel(SocketChannel ch) throws Exception {  
    42.             // 添加自定义协议的编解码工具  
    43.             ch.pipeline().addLast(new SmartCarEncoder());  
    44.             ch.pipeline().addLast(new SmartCarDecoder());  
    45.             // 处理网络IO  
    46.             ch.pipeline().addLast(new ClientHandler());  
    47.         }  
    48.     }  
    49.     public static void main(String[] args) throws Exception {  
    50.         new Client().connect(9999, "127.0.0.1");  
    51.     }  
    52. }  

    8、客户端Handler的实现

    [java] view plain copy

     print?

    1. import io.netty.channel.ChannelHandlerAdapter;  
    2. import io.netty.channel.ChannelHandlerContext;  
    3. import io.netty.util.ReferenceCountUtil;  
    4. //用于读取客户端发来的信息  
    5. public class ClientHandler extends ChannelHandlerAdapter {  
    6.     // 客户端与服务端,连接成功的售后  
    7.     @Override  
    8.     public void channelActive(ChannelHandlerContext ctx) throws Exception {  
    9.         // 发送SmartCar协议的消息  
    10.         // 要发送的信息  
    11.         String data = "I am client ...";  
    12.         // 获得要发送信息的字节数组  
    13.         byte[] content = data.getBytes();  
    14.         // 要发送信息的长度  
    15.         int contentLength = content.length;  
    16.         SmartCarProtocol protocol = new SmartCarProtocol(contentLength, content);  
    17.         ctx.writeAndFlush(protocol);  
    18.     }  
    19.     // 只是读数据,没有写数据的话  
    20.     // 需要自己手动的释放的消息  
    21.     @Override  
    22.     public void channelRead(ChannelHandlerContext ctx, Object msg)  
    23.             throws Exception {  
    24.         try {  
    25.             // 用于获取客户端发来的数据信息  
    26.             SmartCarProtocol body = (SmartCarProtocol) msg;  
    27.             System.out.println("Client接受的客户端的信息 :" + body.toString());  
    28.         } finally {  
    29.             ReferenceCountUtil.release(msg);  
    30.         }  
    31.     }  
    32.     @Override  
    33.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)  
    34.             throws Exception {  
    35.         ctx.close();  
    36.     }  
    37. }

    你可能感兴趣的:(tcp)