精华内容
下载资源
问答
  • 主要给大家介绍了Golang TCP粘包拆包问题的解决方法,文中通过示例代码介绍的非常详细,对大家学习或者使用Golang具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
  • TCP拆包和黏包的过程和解决 粘包、拆包解决办法 通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:   1、...

    TCP拆包和黏包的过程和解决

    粘包、拆包解决办法

    通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

     

    1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

     

    2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

     

    3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

    展开全文
  • go语言处理TCP拆包/粘包

    千次阅读 2019-02-17 09:46:59
    part 1 最近在学习go自带的rpc,看完了一遍想着自己实现一个codec,...而这个过程中就需要处理TCP拆包粘包了。 TCP拆包/粘包也算是网络编程中一个比较基础的问题了,具体的问题含义和解决方式也不再详细描述了。...

    part 1

    最近在学习go自带的rpc,看完了一遍想着自己实现一个codec,也就是自定义消息的序列化和反序列化。消息的序列化和反序列化涉及到两步:1、从网络中读取数据和将数据写到网络中;2、根据拿到的二进制数据反序列化以及把现有的对象序列化成二进制数据。而这个过程中就需要处理TCP的拆包粘包了。

    TCP的拆包/粘包也算是网络编程中一个比较基础的问题了,具体的问题含义和解决方式也不再详细描述了。虽然作为实现应用层逻辑的程序员可能根本不需要关心这个,作为中间件的开发,同时也出于学习go语言的目的,还是稍微进行一下练习。

    part 2

    TCP拆包粘包的解决思路:在读取数据时,将读取到的二进制数据在正确的位置进行分割即可。这里直接使用head+body的方式,也就是在发送数据时先将整个数据的大小附加在数据前,就像这样:

    +++++++++++++++++++++++++++++++++++++
    size (2 bytes)  | body (size bytes)
    +++++++++++++++++++++++++++++++++++++
    复制代码

    这里将数据包的size作为head 注:这里举的例子里,size占2个byte;size具体占用的byte数可以根据实际情况决定

    part 3

    server具体实现:

    func doConn(conn net.Conn) {
        var (
            buffer = bytes.NewBuffer(make([]byte, 0, BUF_SIZE)) //buffer用来缓存读取到的数据
            readBytes = make([]byte, BUF_SIZE) //readBytes用来接收每次读取的数据,每次读取完成之后将readBytes添加到buffer中
            isHead = true //用来标识当前的状态:正在处理size部分还是body部分
            bodyLen = 0 //表示body的长度
        )
    
        for {
            //首先读取数据
            readByteNum, err := conn.Read(readBytes)
            if err != nil {
                log.Fatal(err)
                return
            }
            buffer.Write(readBytes[0:readByteNum])//将读取到的数据放到buffer中
            
            // 然后处理数据
            for {
                if isHead {
                    if buffer.Len() >= HEAD_SIZE {
                        isHead = false
                        head := make([]byte, HEAD_SIZE)
                        _, err = buffer.Read(head)
                        if err != nil {
                            log.Fatal(err)
                            return
                        }
                        bodyLen = int(binary.BigEndian.Uint16(head))
                    } else {
                        break;
                    }
                }
    
                if !isHead {
                    if buffer.Len() >= bodyLen {
                        body := make([]byte, bodyLen)
                        _, err = buffer.Read(body[:bodyLen])
                        if err != nil {
                            log.Fatal(err)
                            return
                        }
                        fmt.Println("received body: " + string(body[:bodyLen]))
                        isHead = true
                    } else {
                        break;
                    }
                }
            }
        }
        
        func HandleTcp() {
    	listener, err := net.Listen("tcp", ":1234")
    	if err != nil {
    		log.Fatal(err)
    		return
    	}
    	log.Println("start listening on 1234")
    	for {
    		conn, err := listener.Accept()
    		if err != nil {
    			log.Fatal(err)
    			return
    		}
    		go doConn(conn)
    	}
    }
    复制代码

    client具体实现:

    func SendStringwithTcp(arg string) error {
    	conn, err := net.Dial("tcp", ":1234")
    	if err != nil {
    		log.Fatal(err)
    		return err
    	}
    
    	head := make([]byte, server.HEAD_SIZE)
    	content := []byte(arg)
    	headSize := len(content)
    	binary.BigEndian.PutUint16(head, uint16(headSize))
    
        //先写入head部分,再写入body部分
    	_, err = conn.Write(head)
    	if err != nil {
    		log.Fatal(err)
    		return err
    	}
    	_, err = conn.Write(content)
    	if err != nil {
    		log.Fatal(err)
    		return err
    	}
    	return nil
    }
    复制代码

    转载于:https://juejin.im/post/5c69270451882562851b3bd3

    展开全文
  • Socket通信之TCP拆包和封包

    千次阅读 2021-08-16 15:11:17
    最近我在做一个工业物联网的项目,其中就用到了TCP,只要是通过TCP远程操控和实时获取工业设备信息(比如控制电灯,空调,智慧屏…),由于设备众多,为了保证服务端实时数据的完整性,我在数据链路层采用了异步传输,...

    前言

    我们在使用Socket传递信息时候,经常会遇到这样的问题,输入流数据读取不完整,或者输入流读取数据错乱的问题。下面就针对这俩个问题谈谈我的想法,以及解决思路。

    举例说明

    最近我在做一个工业物联网的项目,其中就用到了TCP,只要是通过TCP远程操控和实时获取工业设备信息(比如控制电灯,空调,智慧屏…),由于设备众多,为了保证服务端实时数据的完整性,我在数据链路层采用了异步传输,半双工传输。服务端读取数据后,通过CRC校验的方式来确定数据的完整性。但是后发现还是不能百分百保证数据的完整性。

    public class SocketOkioManager {
    
    	
    	private List<Socket> mList = new ArrayList<Socket>();
    	private ServerSocket server = null;
    	private ExecutorService mExecutorService = null;
    	private final String TAG = SocketOkioManager.class.getName();
    
    
    	public SocketOkioManager(int PORT) {
    		try {
    			server = new ServerSocket(PORT);
    			mExecutorService = Executors.newCachedThreadPool();
    			Log.i(TAG,"服务器已启动...");
    			Socket client = null;
    			while (true) {
    				client = server.accept();
    				mList.add(client);
    				mExecutorService.execute(new Service(client));
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    
    	class Service implements Runnable {
    		private Socket socket;
    		private BufferedSink mSink;
    		private BufferedSource mSource;
    
    		public Service(Socket socket) {
    			this.socket = socket;
    			try {
    				mSink = Okio.buffer(Okio.sink(socket));
    				mSource = Okio.buffer(Okio.source(socket));
    				mSink.flush();
    				Log.i(TAG,"成功连接服务器");
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		}
    
    		@Override
    		public void run() {
    			try {
    				byte[] bytes = new byte[1024];
    				while (true) {
    					if (mSource.read(bytes)!=-1){
    						Log.i(TAG,"服务器收到数据is:"+HexUtils.encode(bytes));
    						//CRC-16/XMODEM x16+x12+x5+1
    						char content_char = HexUtils.getCRC1021(bytes, bytes.length);
    						//生成校验码
    						String content_code = Integer.toHexString(content_char).toLowerCase();
    						String cecekCode=HexUtils.encode(bytes);
    						cecekCode=cecekCode.substring(cecekCode.length()-4,cecekCode.length()).toLowerCase();
    						if (content_code.equals(cecekCode)){
    							Log.i(TAG,"数据检验完整:");
    						}
    					}
    				}
    
    			} catch (Exception e) {
    				Log.i(TAG,"Exception:"+e.toString());
    				e.printStackTrace();
    			}
    		}
    	}
    }
    
    

    下面是CRC校验的代码(注意:我这里校验采用的是CRC-16/XMODEM x16+x12+x5+1,不同的CRC校验方式结果是不同的

    /**
     * CRC1021余式表
     */
    static final char TABLE1021[] = {
                    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
                    0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
                    0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
                    0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
                    0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
                    0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
                    0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
                    0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
                    0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
                    0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
                    0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
                    0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
                    0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
                    0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
                    0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
                    0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
                    0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
                    0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
                    0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
                    0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
                    0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
                    0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
                    0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
                    0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
                    0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
                    0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
                    0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
                    0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
                    0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
                    0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
                    0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
                    0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
            };
    
        //CRC-16/XMODEM x16+x12+x5+1
        public static char getCRC1021(byte b[], int len) {
            char crc = 0;
            byte hb = 0;
            int j = 0;
            int index;
            while (len-- != 0) {
                hb = (byte) (crc / 256); //以8位二进制数的形式暂存CRC的高8位
                index = ((hb ^ b[j]) & 0xff); //求得所查索引下标
                crc <<= 8; // 左移8位,相当于CRC的低8位乘以
                crc ^= (TABLE1021[index]); // 高8位和当前字节相加后再查表求CRC ,再加上以前的CRC
                j++;
            }
            return (crc);
        }
    

    然后,我们运行看一下结果:

    2021-08-16 15:34:00.699 2540-2683/com.test.andserver.sample I/com.test.andserver.sample.util.SocketOkioManager : 服务器收到数据is:5aa50000ff3201e10201ff00016400000000000094b42a3c0000000000001f6a6425b30509ffff0295ff2a430006000001000000001fba55a20fff00ffff0496862a3c0002000000000000001fe20714f21717ffff0494222a3c00020065e3001ffecf67c40b24ffff0d948b2a3c0004003be5001feed8eb601827ffff1191c12a41000100000002830112000000001f6d4694e5121effff1491162a410000000000027a010f000000001f8cc5265e0209ffff1597c106060606b23fd0000000c65275cb0103ffff1794b52a3c0000000000001f052144c50509ffff2295022a430006000001000000001fc4b88623ff00ffff2396872a3c0002000000000000001f374ff1551713ffff2594232a3c00020065e3001f918a06b20c23ffff2895032a430006000001000000001fb982c9b5ff00ffff28948c2a3c0004003be5001ff22297861827ffff2c91172a410000000000027a010f000000001f9c27e29c0209ffff2e91c22a41000100000002840113000000001fded8e570121effff3195052a430006000001000000001f72bec9d8ff00ffff3c96882a3c0002000000000000001f4454ba801715ffff3d94b62a3c0000000000001fb4eee75f0609ffff4297c206060606b23fd0000000bea853a30003ffff4394242a3c00020065e3001f8d707a540c21ffff47ff8e5aa50000ff3200
    I/com.test.andserver.sample.util.SocketOkioManager:检测不通过
    
    I/com.test.andserver.sample.util.SocketOkioManager : 服务器收到数据is:5aa50000ff3201e10201ff00016400000000000094b42a3c0000000000001f6a6425b30509ffff0295ff2a430006000001000000001fba55a20fff00ffff0496862a3c0002000000000000001fe20714f21717ffff0494222a3c00020065e3001ffecf67c40b24ffff0d948b2a3c0004003be5001feed8eb601827ffff1191c12a41000100000002830112000000001f6d4694e5121effff1491162a410000000000027a010f000000001f8cc5265e0209ffff1597c106060606b23fd0000000c65275cb0103ffff1794b52a3c0000000000001f052144c50509ffff2295022a430006000001000000001fc4b88623ff00ffff2396872a3c0002000000000000001f374ff1551713ffff2594232a3c00020065e3001f918a06b20c23ffff2895032a430006000001000000001fb982c9b5ff00ffff28948c2a3c0004003be5001ff22297861827ffff2c91172a410000000000027a010f000000001f9c27e29c0209ffff2e91c22a41000100000002840113000000001fded8e570121effff3195052a430006000001000000001f72bec9d8ff00ffff3c96882a3c0002000000000000001f4454ba801715ffff3d94b62a3c0000000000001fb4eee75f0609ffff4297c206060606b23fd0000000bea853a30003ffff4394242a3c00020065e3001f8d707a540c21ffff47ff8e5aa50000ff3200a7020291182a410000000000027a010e000000001f2c390c840209ffff47948d2a3c0004003be5001f9d67f6f01827ffff4991c32a41000100000002830112000000001f4c82d471121effff5195062a430006000001000000001f778d177dff00ffff5a96892a3c0002000000000000001fb2a16a401713ffff5a94b72a3c0000000000001fdbab8629070effff5e91192a410000000000027a010f000000001f797ba35a030dffff635324
    I/com.test.andserver.sample.util.SocketOkioManager:检测不通过
    

    我们认证分析16进制字符串数据,你会发现俩次检测都不通过,第一次crc校验出的结果是:1B5F,如下所示,但是实际设备的校验码是:3200(读取到的数据的最后4个字符)
    在这里插入图片描述
    第二次crc校验出的结果是:CD07,如下所示,但是实际设备的校验码是:5324(读取到的数据的最后4个字符)
    在这里插入图片描述

    那么为什么会出现数据不完整,校验不通过的问题,其实主要还是因为TCP协议本身导致的,所以,在使用TCP时,如果要保证数据的完整性,我们必须的对数据进行封包和拆包。

    为什么基于TCP的通讯程序需要进行封包和拆包?

    TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况:

    假设我们连续调用两次send方法,分别发送两段数据A和B,在接收端有可能存在有以下几种接收情况(这里只列出了有代表性的情况):

    1. 先接收到A,然后接收到B
    2. .先接收到A的部分数据,然后接收到A余下的部分以及B的全部.
    3. 先接收到了A的全部数据和B的部分数据,然后接收到了B的余下的数据.
    4. 一次性接收到了A和B的全部数据.

    对于1这种情况正是我们需要的,不再做讨论.对于2,3,4的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行“拆包”,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.另外补充一点,对于UDP来说,就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.

    下面我们就分析一下如何会出现2,3,4这三种情况。

    为什么会发生 “粘包”

    以上后三者就是我们俗称的"粘包"。"粘包"可发生在发送端也可发生在接收端。所以,我们要分端来分析:

    1. 粘包发生在发送端
      Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据拼接在一起发送出去.如果等待期间没有,那么收到什么就发送什么。

    2. 粘包发生在接受端
      接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据.当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据,那么,最终可能导致应用层取到的数据有可能是好多次何在一起的结果,但是,对于应用层此时所获取的数据的完整性,就需要更复杂的通信或者数据数据链路层的规范来确定数据的合法性和完整性。

    好了,既然知道了原因,我们回过头来,看看上面例子的的数据,我们来分析一下,看看数据是不是被分割发送了,也就是发送端出现了"粘包"。我们把俩次接受到的数据合并到一起看看:

    I/com.test.andserver.sample.util.SocketOkioManager : 服务器收到数据is:5aa50000ff3201e10201ff00016400000000000094b42a3c0000000000001f6a6425b30509ffff0295ff2a430006000001000000001fba55a20fff00ffff0496862a3c0002000000000000001fe20714f21717ffff0494222a3c00020065e3001ffecf67c40b24ffff0d948b2a3c0004003be5001feed8eb601827ffff1191c12a41000100000002830112000000001f6d4694e5121effff1491162a410000000000027a010f000000001f8cc5265e0209ffff1597c106060606b23fd0000000c65275cb0103ffff1794b52a3c0000000000001f052144c50509ffff2295022a430006000001000000001fc4b88623ff00ffff2396872a3c0002000000000000001f374ff1551713ffff2594232a3c00020065e3001f918a06b20c23ffff2895032a430006000001000000001fb982c9b5ff00ffff28948c2a3c0004003be5001ff22297861827ffff2c91172a410000000000027a010f000000001f9c27e29c0209ffff2e91c22a41000100000002840113000000001fded8e570121effff3195052a430006000001000000001f72bec9d8ff00ffff3c96882a3c0002000000000000001f4454ba801715ffff3d94b62a3c0000000000001fb4eee75f0609ffff4297c206060606b23fd0000000bea853a30003ffff4394242a3c00020065e3001f8d707a540c21ffff47ff8e5aa50000ff32005aa50000ff3201e10201ff00016400000000000094b42a3c0000000000001f6a6425b30509ffff0295ff2a430006000001000000001fba55a20fff00ffff0496862a3c0002000000000000001fe20714f21717ffff0494222a3c00020065e3001ffecf67c40b24ffff0d948b2a3c0004003be5001feed8eb601827ffff1191c12a41000100000002830112000000001f6d4694e5121effff1491162a410000000000027a010f000000001f8cc5265e0209ffff1597c106060606b23fd0000000c65275cb0103ffff1794b52a3c0000000000001f052144c50509ffff2295022a430006000001000000001fc4b88623ff00ffff2396872a3c0002000000000000001f374ff1551713ffff2594232a3c00020065e3001f918a06b20c23ffff2895032a430006000001000000001fb982c9b5ff00ffff28948c2a3c0004003be5001ff22297861827ffff2c91172a410000000000027a010f000000001f9c27e29c0209ffff2e91c22a41000100000002840113000000001fded8e570121effff3195052a430006000001000000001f72bec9d8ff00ffff3c96882a3c0002000000000000001f4454ba801715ffff3d94b62a3c0000000000001fb4eee75f0609ffff4297c206060606b23fd0000000bea853a30003ffff4394242a3c00020065e3001f8d707a540c21ffff47ff8e5aa50000ff3200a7020291182a410000000000027a010e000000001f2c390c840209ffff47948d2a3c0004003be5001f9d67f6f01827ffff4991c32a41000100000002830112000000001f4c82d471121effff5195062a430006000001000000001f778d177dff00ffff5a96892a3c0002000000000000001fb2a16a401713ffff5a94b72a3c0000000000001fdbab8629070effff5e91192a410000000000027a010f000000001f797ba35a030dffff635324
    

    通过CRC校验,你会发现,果然实在发送端发生了"粘包",我们把俩次读取的数据合拼拼接在一起,CRC校验后果然是5324,说明CRC校验通过,接受到的数据是一帧完整数据。

    怎样封包和拆包

    最初遇到"粘包"的问题时,我是通过在两次send之间调用sleep来休眠一小段时间来解决。这个解决方法的缺点是显而易见的,使传输效率大大降低,而且也并不可靠。后来就是通过应答的方式来解决,尽管在大多数时候是可行的,但是不能解决像2的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷(但是像FTP等协议采用的就是应答方式)。再后来就是对数据包进行封包和拆包的操作

    1. 封包
      封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了。包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.

    2. 拆包
      对于拆包目前我最常用的是以下两种方式.

      1.动态缓冲区暂存方式:之所以说缓冲区是动态的,是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.大概过程描述如下:

      • 为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.

      • 当接收到数据时首先把此段数据存放在缓冲区中.

      • 判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.

      • 根据包头数据解析出里面代表包体长度的变量.

      • 判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.

      • 取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

        这种方法有两个缺点:

           1. 为每个连接动态分配一个缓冲区增大了内存的使用
         
           2. 有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除,这种拆包的改进方法会解决和完善部分缺点
        

      先看包头结构定义

      #pragma pack(push,1) //开始定义数据包, 采用字节对齐方式
      /*----------------------包头---------------------*/
      typedef struct tagPACKAGEHEAD
      {
      BYTE Version;
      WORD Command;
      WORD nDataLen;//包体的长度
      }PACKAGE_HEAD;
      #pragma pack(pop) //结束定义数据包, 恢复原来对齐方式
      

      然后看存放数据和"取"数据函数:

      // Description:添加数据到缓存
      // Input:pBuff[in]-待添加的数据;nLen[in]-待添加数据长度
      // Return: 如果当前缓冲区没有足够的空间存放pBuff则返回FALSE;否则返回TRUE。
      BOOL CDataBufferPool::AddBuff( char *pBuff, int nLen )
      {
          m_cs.Lock();///临界区锁
      	if ( nLen < 0 )
      	{
      	    m_cs.Unlock();
      	    return FALSE;
      	}
      
      	if ( nLen <= GetFreeSize()) //判断剩余空间是否足够存放nLen长的数据
      	{
      	memcpy(m_pBuff + m_nOffset, pBuff, nLen);
      	   m_nOffset += nLen;
      	}
      	else{
      	  //若不够则扩充原有的空间
      	{ 
      	char *p = m_pBuff;
      	m_nSize += nLen*2;//每次增长2*nLen
      	m_pBuff = new char[m_nSize];
      	memcpy(m_pBuff,p,m_nOffset);
      	delete []p;
      	memcpy(m_pBuff + m_nOffset, pBuff, nLen);
      	m_nOffset += nLen;
      	m_cs.Unlock();
      	return FALSE;
      	}
          m_cs.Unlock();
      return TRUE;
      }
      

      校验包的合法性:

      // Description:获取一个完整的包
      // Input:Buf[out]-获取到的数据;nLen[out]-获取到的数据长度
      // Return: 1、当前缓冲区不够一个包头的数据 2、当前缓冲区不够一个包体的数据
      
      int CDataBufferPool::GetFullPacket( char *Buf, int& nLen )
      {
         	m_cs.Lock();
      	if ( m_nOffset < m_PacketHeadLen )//当前缓冲区不够一个包头的数据
      	{
      	     m_cs.Unlock();
           	return 1;
      	}
      	PACKAGE_HEAD *p = (PACKAGE_HEAD *)m_pBuff;
      	if( (m_nOffset-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据
      	{
      	   m_cs.Unlock();
      	   return 2;
      	}
      	//判断包的合法性
      	/* int IsIntegrallity = ValidatePackIntegrality(p);
      	if( IsIntegrallity != 0 )
      	{
      	m_cs.Unlock();
      	return IsIntegrallity;
      	}
      	*/
      	nLen = m_PacketHeadLen+p->nDataLen;
      	memcpy( Buf, m_pBuff, nLen );
      	m_nOffset -= nLen;
      	memcpy( m_pBuff, m_pBuff+nLen, m_nOffset );
      	
      	m_cs.Unlock();
      	return 0;
      }
      
      

      前面提到过这种方法的缺点。下面给出一个改进办法, 即采用环形缓冲。但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方)。

      环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾。在存放数据和删除数据时只是进行头尾指针的移动。下面的代码是采用一个开源的游戏服务器的代码,我对此代码有所修改。

      int CCircularBufferPool::PutData(TCHAR *pData, int len)
      {
      	if( len <= 0 ) 
      	return 1;
      
      	EnterCriticalSection(&m_cs);
      	while (IsOverFlowCondition(len))///判断缓冲区剩余空间是否够存放len长的数据
      	{
      	BufferResize(len);///若不够,则扩充缓冲区.
      	}
      
      	if (IsIndexOverFlow(len))///判断"尾"指针的位置.
      	{
      		int FirstCopyLen = m_iBufSize-m_iTailPos;
      		int SecondCopyLen = len - FirstCopyLen;
      		CopyMemory(m_pBuffer+m_iTailPos, pData, FirstCopyLen);
      		if (SecondCopyLen)
      		{
      		   CopyMemory(m_pBuffer, pData+FirstCopyLen, SecondCopyLen);
      		   m_iTailPos = SecondCopyLen;
      		}else{
      		   m_iTailPos = 0;
      		}
      	}else{
      	    CopyMemory(m_pBuffer+m_iTailPos, pData, len);
      	    m_iTailPos += len;
      	}
      	LeaveCriticalSection(&m_cs);
      	return 0;	
       }
      
      
      void CCircularBufferPool::GetData(TCHAR *pData, int len, bool Delete)
      {
      	if (len < m_iBufSize-m_iHeadPos)
      	{
      	  CopyMemory(pData, m_pBuffer+m_iHeadPos, len);
      		if(Delete==true)
      		   m_iHeadPos += len;
      		}else{
      		int fc, sc;
      		fc = m_iBufSize-m_iHeadPos;
      		sc = len - fc;
      		 CopyMemory(pData, m_pBuffer+m_iHeadPos, fc);
      		 if (sc) CopyMemory(pData+fc, m_pBuffer, sc);
      		 
      		 if(Delete==true)
      		    m_iHeadPos = sc;
      		    
      		 if(m_iHeadPos >= m_iBufSize)
      		   m_iHeadPos = 0;
      		}
      	}
      

      进行自定义包的解析

      int CCircularBufferPool::GetFullPacket( TCHAR *Buf, int &nLen )
      {
          EnterCriticalSection(&m_cs);
      	if( GetValidCount() < m_PacketHeadLen )//当前缓冲区不够一个包头的数据
      	{
      	LeaveCriticalSection(&m_cs);
      	return 1;
      	}
          GetData(Buf,m_PacketHeadLen,false);
          PACKAGE_HEAD *p = (PACKAGE_HEAD *)Buf;
      	if( (GetValidCount()-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据
      	{
      	LeaveCriticalSection(&m_cs);
      	 return 2;
      	}
      

      判断包的合法性:

      int IsIntegrallity = ValidatePackIntegrality(p);
      	if( IsIntegrallity != 0 )
      	{
      	LeaveCriticalSection(&m_cs);
      	return IsIntegrallity;
      	}
      
         GetData(Buf,m_PacketHeadLen+p->nDataLen,true);
         nLen = m_PacketHeadLen+p->nDataLen;
         LeaveCriticalSection(&m_cs);
          return 0;
      }
      

    2.利用底层的缓冲区来进行拆包:
    由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了。另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据。利用这两个条件我们就可以对第一种方法进行优化了。

    对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据。相关代码如下:

    char PackageHead[1024];
    char PackageContext[1024*20];
    
    int len;
    PACKAGE_HEAD *pPackageHead;
    while( m_bClose == false )
    {
        memset(PackageHead,0,sizeof(PACKAGE_HEAD));
        len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));
    	if( len == SOCKET_ERROR )
    	{
    	      break;
    	}
    	if(len == 0)
    	{
    	      break;
    	}
    	
        pPackageHead = (PACKAGE_HEAD *)PackageHead;
        memset(PackageContext,0,sizeof(PackageContext));
    	if(pPackageHead->nDataLen>0)
    	{
    	   len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);
    	}       
    }
    

    m_TcpSock是一个封装了SOCKET的类的变量,其中的ReceiveSize用于接收一定长度的数据,直到接收了一定长度的数据或者网络出错才返回

    int winSocket::ReceiveSize( char* strData, int iLen )
    {
    	if( strData == NULL )
    	return ERR_BADPARAM;
    	char *p = strData;
    	int len = iLen;
    	int ret = 0;
    	int returnlen = 0;
    	while( len > 0)
    	{
    	   ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, 0 );
    	   if ( ret == SOCKET_ERROR || ret == 0 ){
    	   return ret;
    	  }
           len -= ret;
    	   returnlen += ret;
    	}
       return returnlen;
    }
    

    对于非阻塞的SOCKET。比如完成端口,我们可以提交接收包头长度的数据的请求,当GetQueuedCompletionStatus返回时,我们判断接收的数据长度是否等于包头长度,若等于,则提交接收包体长度的数据的请求,若不等于则提交接收剩余数据的请求。当接收包体时,采用类似的方法。

    enum IOType 
    {
    	IOInitialize,
    	IORead,
    	IOWrite,
    	IOIdle
    };
    
    class OVERLAPPEDPLUS 
    {
    public:
    	OVERLAPPED   m_ol;
    	IOType    m_ioType;
    	bool         m_bIsPackageHead;//当前接收的数据是否是包头数据。
    	
    	int          m_count;
    	WSABUF       m_wsaBuffer;
    	int          m_RecvPos;
    	char         m_Buffer[1024*8];//此缓冲要尽可能大
    	
    	OVERLAPPEDPLUS(IOType ioType) {
    	ZeroMemory(this, sizeof(OVERLAPPEDPLUS));
    	m_ioType = ioType;
     }
    };
    
    	//接收连接后发出的第一个请求,请求接收包头大小的数据.
    	OVERLAPPEDPLUS *pOverlappedPlus = new OVERLAPPEDPLUS;
    	pOverlappedPlus->m_wsaBuffer.buf = pOverlappedPlus->m_Buffer;
    	pOverlappedPlus->m_wsaBuffer.len = PACKAGE_HEAD_LEN;///包头的长度
    	pOverlappedPlus->m_bIsPackageHead = true;
    	pOverlappedPlus->m_RecvPos = 0;
    	pOverlappedPlus->m_ioType = IORead;
    
    	DWORD RecvBytes;
    	     DWORD Flags;
    	Flags = 0;
    	if (WSARecv(clientSocket, &(pOverlappedPlus->m_wsaBuffer), 1, &RecvBytes, &Flags,
    	&pOverlappedPlus->m_ol, NULL) == SOCKET_ERROR)
    	{
    		if (WSAGetLastError() != ERROR_IO_PENDING)
    		{
    		   delete pOverlappedPlus;
    		}
    		else
    		{
    		   //相关的错误处理	
    		}
    	}
    	else
    	{
    	//相关的错误处理
    	}
    
    
    //在GetQueuedCompletionStatus所在的函数中.
    	if( pOverlapPlus->m_ioType== IORead)
    	{ 
    	if( pOverlapPlus->m_wsaBuffer.len == dwIoSize )
    	{
    	   if( pOverlapPlus->m_bIsPackageHead == true )//接收到的是包头。
    	   {
    	    PACKAGE_HEAD *pPackageHead = (PACKAGE_HEAD *)(pOverlapPlus->m_Buffer);
    	
    	    if(pThis->IsLegalityPackageHead(pPackageHead)==false)//判断是否是合法的包
    	    {
    	     closesocket(lpClientContext->m_Socket);
    	     continue;
    	    }	
    	    pOverlapPlus->m_bIsPackageHead = false;
    	    pOverlapPlus->m_wsaBuffer.len = pPackageHead->nDataLen;
    	    pOverlapPlus->m_RecvPos += dwIoSize;
    	    pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;	
    	   }
    	   else///接收到的是包体
    	   {
    	   pOverlapPlus->m_RecvPos += dwIoSize;
    	   //这时pOverlapPlus->m_Buffer里就存放了一个完整的数据包,长度为pOverlapPlus->m_RecvPos	
    	   //继续请求 请求下一个数据包的包头
    	   pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer;
    	   memset(pOverlapPlus->m_Buffer,0,sizeof(pOverlapPlus->m_Buffer));
    	   pOverlapPlus->m_wsaBuffer.len = PACKAGE_HEAD_LEN;
    	   pOverlapPlus->m_bIsPackageHead = true;
    	   pOverlapPlus->m_RecvPos = 0;
    	     
    	   }
    	}
    	else///接收的数据还不完整
    	{
    	   pOverlapPlus->m_wsaBuffer.len -= dwIoSize;
    	   pOverlapPlus->m_RecvPos += dwIoSize;
    	   pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;
    	}
    	pOverlapPlus->m_ioType = IORead;
    	state = WSARecv(lpClientContext->m_Socket, &(pOverlapPlus->m_wsaBuffer), 1, &RecvBytes, &Flags,
    	    &pOverlapPlus->m_ol, NULL);
    	if ( state == SOCKET_ERROR)
    	{
    	   if(WSAGetLastError() != ERROR_IO_PENDING)
    	   {
    	       
    	       //关闭套接字 释放相应资源
    	     continue;
    	   }
    	}
    	   
    	}
    

    解决办法

    通过上面的分析,我们可以进一步把代码完善如下,从而确保数据完整接受,解决方式如下:

    1. 更具包头长度读取
      在传递需要的数据之前,先传递数据的长度,然后在输入流中的读取数据长度做比较,如果读取的长度小于完整数据的长度就一直读取:
    int count = 0;
    byte[] data = new byte[dataLen];
    //封装输入流(接收客户端的流)
    BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
    DataInputStream dis = new DataInputStream(bis);
    while(count<dataLen){
        int available = dis.available();
        if(available>data.length-count){
            available=data.length-count
        }else{
           //CRC-16/XMODEM x16+x12+x5+1
    	   char content_char = HexUtils.getCRC1021(content_byte, content_byte.length);
    	   //生成校验码
    	   String content_code = Integer.toHexString(content_char).toLowerCase();
        }
        count += dis.read(data,count,available)
    }
    
    1. 一字节一字节读取,循环校验
    
                    //封装输入流(接收客户端的流)
                    BufferedInputStream bis = new BufferedInputStream(
                            socket.getInputStream());
                    DataInputStream dis = new DataInputStream(bis);
                    List<String> newData = new ArrayList<>();
                    byte[] bytes = new byte[1]; // 一次读取一个byte
                    StringBuilder msg = new StringBuilder();
                    long time=System.currentTimeMillis();
                    while (dis.read(bytes) != -1) {
                        msg.append(HexUtils.encode(bytes));
                        if (dis.available() == 0) {
                            Log.i(TAG, "服务器收到数据is:" + msg);
                            String datas=msg.toString();
                            //取出字符串后4位作为校验码,用来校验数据是否合格
                            String checkCode=datas.substring(datas.length()-4,datas.length());
                            Log.i(TAG, "本轮 checkCode is:" +checkCode);
                            //先crc检验是否合格
                            byte[] content_byte = HexUtils.decode(datas.substring(0,datas.length()-4));
                            //CRC-16/XMODEM x16+x12+x5+1
                            char content_char = HexUtils.getCRC1021(content_byte, content_byte.length);
                            //生成校验码
                            String content_code = Integer.toHexString(content_char).toLowerCase();
                            Log.i(TAG, "this content_code start is:" +content_code);
                            //自动拼接让content_code为2字节
                            if (content_code.length() == 1) {
                                content_code = "000" + content_code;
                            }
                            if (content_code.length() == 2) {
                                content_code = "00" + content_code;
                            }
                            if (content_code.length() == 3) {
                                content_code = "0" + content_code;
                            }
                            Log.i(TAG, "this  content_code end is:" +content_code);
                            //在判断是否是完整的针数据
                            if (content_code.equals(checkCode)){
                                //处理完整数据,并清空接受对象
                                if (datas.contains(target)){
                                    String[] split = datas.split(target);
                                    if (split.length >= 2) {
                                        for (int i = 0; i < split.length; i++) {
                                            String data = split[i];
                                            if (!data.equals("") && data.length() > 0) {
                                                newData.add(target + data);
                                            }
                                        }
                                        //回调
                                        //todo ....
                                    }
                                }
                                Log.i(TAG, "this is complete data!");
                                //置空
                                msg.delete(0,msg.length());
                                newData.clear();
                            }else {
                                //不完整跳过不处理
                                Log.i(TAG, "this is not complete data!");
                            }
                        }
                    }
    

    总结

    我们已经知道TCP是个"流"协议,所谓流,就是没有界限的一串数据.真是因为如此,会导致数据的不完整性,为了解决这种问题,在使用TCP时,一定要先拆包和粘包,拆包粘包之后再判断包的合法性,通过校验(比如CRC校验)从而确保包数据的完整性。

    判断包的合法性可以结合下面两种方式来判断

    1. 通过包头的结构来判断包的合法性.

      最初的时候我是根据包头来判断包的合法性,比如判断Command是否超出命令范围,nDataLen是否大于最大包的长度。但是这种方法无法过滤掉非法包,当出现非法包时我们唯一能做的就是断开连接,或许这也是最好的处理办法。

      我们可以给一个完整的包加上开始和结束标志,标志可以是个整数,也可以是一串字符串。以第一种拆包方式为例来说明。当要拆一个完整包时我们先从缓冲区有效数据头指针地址搜索包的开始标志,搜索到后并且当前数据够一个包头数据,则判断开始标志和包头是否合法,若合法则根据代表数据长度的变量的值定位到包尾,判断包尾标志是否与我们定义的一致,若一致则这个包是合法的包。若有一项不一致则继续寻找下个包的开始标志,并把下个合法包的前面的数据全部舍弃。

    2. 通过逻辑层来判断包的合法性
      当取出一个合法的包时,我们还要根据当前数据处理的逻辑来判断包的合法性.比如说在登陆成功后的某段时间服务器又收到了同一个客户端的登陆包,那我们就可以判断这个包是非法的,简单处理就是断开连接。

    展开全文
  • TCP协议是面向流的协议,是流式的,没有业务上的分段,只会根据当前套接字缓冲区的情况进行拆包或者粘包: 发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取。 2、TCP粘包和...


    1、TCP协议传输过程

    TCP协议是面向流的协议,是流式的,没有业务上的分段,只会根据当前套接字缓冲区的情况进行拆包或者粘包: TCP协议传输过程.jpg

    发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取。

    2、TCP粘包和拆包概念

    因为TCP会根据缓冲区的实际情况进行包的划分,在业务上认为,有的包被拆分成多个包进行发送,也可能多个晓小的包封装成一个大的包发送,这就是TCP的粘包或者拆包。

    3、TCP粘包和拆包图解

    粘包拆包图解.png

    假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下几种情况: 1. 服务端分两次读取到两个独立的数据包,分别是D1和D2,没有粘包和拆包。 2. 服务端一次接收到了两个数据包,D1和D2粘在一起,发生粘包。 3. 服务端分两次读取到数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,发生拆包。 4. 服务端分两次读取到数据包,第一次读取到部分D1包,第二次读取到剩余的D1包和全部的D2包。

    当TCP缓存再小一点的话,会把D1和D2分别拆成多个包发送。

    4、TCP粘包和拆包解决策略

    因为TCP只负责数据发送,并不处理业务上的数据,所以只能在上层应用协议栈解决,目前的解决方案归纳: 1. 消息定长,每个报文的大小固定,如果数据不够,空位补空格。 2. 在包的尾部加回车换行符标识。 3. 将消息分为消息头与消息体,消息头中包含消息总长度。 4. 设计更复杂的协议。

    5、Netty中的解决办法

    Netty提供了多种默认的编码器解决粘包和拆包: Netty解决方案.png

    5.1、LineBasedFrameDecoder

    基于回车换行符的解码器,当遇到”\n”或者 “\r\n”结束符时,分为一组。支持携带结束符或者不带结束符两种编码方式,也支持配置单行的最大长度。 LineBasedFrameDecoder与StringDecoder搭配时,相当于按行切换的文本解析器,用来支持TCP的粘包和拆包。 使用例子:

    private void start() throws Exception {
            //创建 EventLoopGroup
            NioEventLoopGroup group = new NioEventLoopGroup();
            NioEventLoopGroup work = new NioEventLoopGroup();
            try {
                //创建 ServerBootstrap
                ServerBootstrap b = new ServerBootstrap();
                b.group(group, work)
                        //指定使用 NIO 的传输 Channel
                        .channel(NioServerSocketChannel.class)
                        //设置 socket 地址使用所选的端口
                        .localAddress(new InetSocketAddress(port))
                        //添加 EchoServerHandler 到 Channel 的 ChannelPipeline
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            public void initChannel(SocketChannel ch) {
                                ChannelPipeline p = ch.pipeline();
                                p.addLast(new LineBasedFrameDecoder(1024));
                                p.addLast(new StringDecoder());
                                p.addLast(new StringEncoder());
                                p.addLast(new EchoServerHandler());
                            }
                        });
                //绑定的服务器;sync 等待服务器关闭
                ChannelFuture f = b.bind().sync();
                System.out.println(EchoServer.class.getName()   " started and listen on "   f.channel().localAddress());
                //关闭 channel 和 块,直到它被关闭
                f.channel().closeFuture().sync();
            } finally {
                //关机的 EventLoopGroup,释放所有资源。
                group.shutdownGracefully().sync();
            }
        }
    

    注意ChannelPipeline 中ChannelHandler的顺序,

    5.2、DelimiterBasedFrameDecoder

    分隔符解码器,可以指定消息结束的分隔符,它可以自动完成以分隔符作为码流结束标识的消息的解码。回车换行解码器实际上是一种特殊的DelimiterBasedFrameDecoder解码器。 使用例子(后面的代码只贴ChannelPipeline部分):

    ChannelPipeline p = ch.pipeline();
    p.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("制定的分隔符".getBytes())));
    p.addLast(new StringDecoder());
    p.addLast(new StringEncoder());
    p.addLast(new EchoServerHandler());
    

    5.3、FixedLengthFrameDecoder

    固定长度解码器,它能够按照指定的长度对消息进行自动解码,当制定的长度过大,消息过短时会有资源浪费,但是使用起来简单。

     ChannelPipeline p = ch.pipeline();
    p.addLast(new FixedLengthFrameDecoder(1 << 5));
    p.addLast(new StringDecoder());
    p.addLast(new StringEncoder());
    p.addLast(new EchoServerHandler());
    

    5.4、LengthFieldBasedFrameDecoder

    通用解码器,一般协议头中带有长度字段,通过使用LengthFieldBasedFrameDecoder传入特定的参数,来解决拆包粘包。 io.netty.handler.codec.LengthFieldBasedFrameDecoder的实例化:

        /**
         * Creates a new instance.
         *
         * @param maxFrameLength      最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。
         * @param lengthFieldOffset   长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。
         * @param lengthFieldLength   长度域字节数。用几个字节来表示数据长度。
         * @param lengthAdjustment    数据长度修正。因为长度域指定的长度可以是header body的整个长度,也可以只是body的长度。如果表示header body的整个长度,那么我们需要修正数据长度。
         * @param initialBytesToStrip 跳过的字节数。如果你需要接收header body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。
         * @param failFast            如果为true,则在解码器注意到帧的长度将超过maxFrameLength时立即抛出TooLongFrameException,而不管是否已读取整个帧。
         *                            如果为false,则在读取了超过maxFrameLength的整个帧之后引发TooLongFrameException。
         */
        public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
                                            int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
            //略
        }
    
    • maxFrameLength 最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。
    • lengthFieldOffset 长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。
    • lengthFieldLength 长度域字节数。用几个字节来表示数据长度。
    • lengthAdjustment 数据长度修正。因为长度域指定的长度可以是header body的整个长度,也可以只是body的长度。如果表示header body的整个长度,那么我们需要修正数据长度。
    • initialBytesToStrip 跳过的字节数。如果你需要接收header body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。
    • failFast 如果为true,则在解码器注意到帧的长度将超过maxFrameLength时立即抛出TooLongFrameException,而不管是否已读取整个帧。 如果为false,则在读取了超过maxFrameLength的整个帧之后引发TooLongFrameException。

    下面通过Netty源码中LengthFieldBasedFrameDecoder的注释几个例子看一下参数的使用:

    5.4.1、2 bytes length field at offset 0, do not strip header

    本例中的length字段的值是12 (0x0C),它表示“HELLO, WORLD”的长度。默认情况下,解码器假定长度字段表示长度字段后面的字节数。

    • lengthFieldOffset = 0: 开始的2个字节就是长度域,所以不需要长度域偏移。
    • lengthFieldLength = 2: 长度域2个字节。
    • lengthAdjustment = 0: 数据长度修正为0,因为长度域只包含数据的长度,所以不需要修正。
    • initialBytesToStrip = 0: 发送和接收的数据完全一致,所以不需要跳过任何字节。

    LengthFieldBasedFrameDecoder-1.png

    5.4.2、2 bytes length field at offset 0, strip header

    因为我们可以通过调用readableBytes()来获得内容的长度,所以可能希望通过指定initialbystrip来删除长度字段。在本例中,我们指定2(与length字段的长度相同)来去掉前两个字节。

    • lengthFieldOffset = 0: 开始的2个字节就是长度域,所以不需要长度域偏移。
    • lengthFieldLength = 2 :长度域2个字节。
    • lengthAdjustment = 0: 数据长度修正为0,因为长度域只包含数据的长度,所以不需要修正。
    • initialBytesToStrip = 2 :我们发现接收的数据没有长度域的数据,所以要跳过长度域的2个字节。

    LengthFieldBasedFrameDecoder-2.png

    5.4.3、2 bytes length field at offset 0, do not strip header, the length field represents the length of the whole message

    在大多数情况下,length字段仅表示消息体的长度,如前面的示例所示。但是,在一些协议中,长度字段表示整个消息的长度,包括消息头。在这种情况下,我们指定一个非零长度调整。因为这个示例消息中的长度值总是比主体长度大2,所以我们指定-2作为补偿的长度调整。

    • lengthFieldOffset = 0: 开始的2个字节就是长度域,所以不需要长度域偏移。
    • lengthFieldLength = 2: 长度域2个字节。
    • lengthAdjustment = -2 :因为长度域为总长度,所以我们需要修正数据长度,也就是减去2。
    • initialBytesToStrip = 0 :发送和接收的数据完全一致,所以不需要跳过任何字节。

    LengthFieldBasedFrameDecoder-3.png

    5.4.4、3 bytes length field at the end of 5 bytes header, do not strip header

    下面的消息是第一个示例的简单变体。一个额外的头值被预先写入消息中。长度调整再次为零,因为译码器在计算帧长时总是考虑到预写数据的长度。

    • lengthFieldOffset = 2 :(= the length of Header 1)跳过2字节之后才是长度域
    • lengthFieldLength = 3:长度域3个字节。
    • lengthAdjustment = 0:数据长度修正为0,因为长度域只包含数据的长度,所以不需要修正。
    • initialBytesToStrip = 0:发送和接收的数据完全一致,所以不需要跳过任何字节。

    LengthFieldBasedFrameDecoder-4.png

    5.4.5、3 bytes length field at the beginning of 5 bytes header, do not strip header

    这是一个高级示例,展示了在长度字段和消息正文之间有一个额外头的情况。您必须指定一个正的长度调整,以便解码器将额外的标头计数到帧长度计算中。

    • lengthFieldOffset = 0:开始的就是长度域,所以不需要长度域偏移。
    • lengthFieldLength = 3:长度域3个字节。
    • lengthAdjustment = 2 :(= the length of Header 1) 长度修正2个字节,加2
    • initialBytesToStrip = 0:发送和接收的数据完全一致,所以不需要跳过任何字节。

    LengthFieldBasedFrameDecoder-5.png

    5.4.6、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field

    这是上述所有示例的组合。在长度字段之前有预写的header,在长度字段之后有额外的header。预先设置的header会影响lengthFieldOffset,而额外的leader会影响lengthAdjustment。我们还指定了一个非零initialBytesToStrip来从帧中去除长度字段和预定的header。如果不想去掉预写的header,可以为initialBytesToSkip指定0。

    • lengthFieldOffset = 1 :(= the length of HDR1) ,跳过1个字节之后才是长度域
    • lengthFieldLength = 2:长度域2个字节
    • lengthAdjustment = 1: (= the length of HDR2)
    • initialBytesToStrip = 3 :(= the length of HDR1 LEN)

    LengthFieldBasedFrameDecoder-6.png

    5.4.7、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field, the length field represents the length of the whole message

    让我们对前面的示例进行另一个修改。与前一个示例的惟一区别是,length字段表示整个消息的长度,而不是消息正文的长度,就像第三个示例一样。我们必须把HDR1的长度和长度计算进长度调整里。请注意,我们不需要考虑HDR2的长度,因为length字段已经包含了整个头的长度。

    • lengthFieldOffset = 1:长度域偏移1个字节,之后才是长度域。
    • lengthFieldLength = 2:长度域2个字节。
    • lengthAdjustment = -3: (= the length of HDR1 LEN, negative)数据长度修正-3个字节。
    • initialBytesToStrip = 3:因为接受的数据比发送的数据少3个字节,所以跳过3个字节。

    LengthFieldBasedFrameDecoder-7.png

    tencent.jpg

    展开全文
  • 一、TCP协议 TCP是一个面向字节流的协议,它的性质是流式的,所以它并没有分段。就像水流一样,你没法知道什么时候开始,什么时候结束。 所以他会根据当前的套接字缓冲区的情况进行拆包或是粘包。 发送端的字节...
  • tcp 粘包 拆包解决思路以代码,提供DEMO,采用 包长+内容缓冲区 组织方法,未采用分隔符以及定长包,因为我觉得包长+内容缓冲区比较灵活
  • #include "StdAfx.h"#include "LibEvent.h"#include #include #include #ifdef WIN32#include #include #else#include#include#include#include #include #include #include #endifusing namespace std;...
  • C/C++笔记三:tcp拆包与封包

    千次阅读 2018-09-02 21:45:31
    Tcp封包是 长度(2字节) + 数据体,其中长度包含了这2个字节,所以再解包的时候需要把这2字节长度信息去掉 unsigned char* tcp_protocol::package(const unsigned char* raw_data, int len, int* pkg_len){ int ...
  • TCP拆包方案(C/C++)

    2021-07-27 10:43:57
    当然这里仅提供一种思路、方案,具体的代码优化,比如像GetBodyBuff函数中存在来回拷贝的情况,可以根据实际情况进行优化,拆包函数如下: #define MAX_BUFF_SIZE 4096 ////接口说明:从缓存区中提取消息体 ////...
  • TCP 拆包分包

    千次阅读 2017-03-01 15:17:47
    对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包.下面就针对这个问题谈谈我的想法,抛砖引玉.若有不对,不妥之处,恳求大家指正.在此先谢过大家了.   一.为什么基于TCP的通讯程序需要进行封包和...
  • 我是个小白,刚开始接触Socket,哪位能提供一下C++ TCP拆包、组包代码一份,我想学习一下,多谢
  • 这就是今天要讲的TCP拆包粘包现象。 拆包粘包产生的原因 我们可以通过以下图进行说明 1.图一是正常的情况下包的发送和接受,客户端发送p1,p2包,服务端先后接受到p1,p2包,没有发生粘包和拆包。 2.图...
  • tcp拆包和粘包

    2020-12-11 13:18:06
    tcp拆包和粘包 简介 拆包和粘包是在socket编程中经常出现的情况,在socket通讯过程中,如果通讯的一端一次性连续发送多条数据包,tcp协议会将多个数据包打包成一个tcp报文发送出去,这就是所谓的粘包。而如果通讯...
  • 本demo模拟了TCP通信中发送端和接收端的行为,并利用序列化和反序列化的思想,自定义协议来解决TCP的粘包和拆包问题。
  • TCP拆包和粘包问题

    2020-07-08 10:50:33
    我们日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。 1. 什么是粘包、拆包? 假设客户端向服务端连续发送了两个数据包,用packet1和packet...
  • 发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充, 1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。 2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。 3、...
  • socket通信的拆包和装包源码 TCP、UDP的区别 TCP面向连接,UDP是无连接的,即发送数据之前不需要建立连接 TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力...
  • 转:https://blog.csdn.net/zhaowen25/article/details/41122501简单地说,网络...以便于我们能知道报文的意思,这个提取报文的过程就是TCP拆包与粘包,在我们自己做底层的通信设计时,这是必须要考虑的。结合最近...
  • 使用方式 1. 依赖im_lib库,implementation project(':im_lib') 2. 自定义IMSEventListener,实现OnEventListener,重写对应的方法配置参数 3. 自定义IMSConnectStatusListener,实现IMSConnectStatusCallback,重现...
  • TCP是个“流”协议,所谓流,就是没有界限的一串数据。大家可以想象河里的流水,他们是连成一片的,其间并没有分界线。TCP底层并不了解上层业务数据的具体含义,他...一、TCP粘包/拆包问题说明我们可以通过图解对TCP...
  • https://blog.csdn.net/xufengyongyou/article/details/45305947 void SockBox::AddBox(const char *data, const size_t size) { if (data) { m_data.SetDataSize(m_sizeBuf...
  • 通过socket通讯实现服务器与客户端的连接。首先服务器利用udp广播发送自己的ip地址,客户端在收到广播后通过此ip以tcp连接的方式连接服务器来通讯。
  • 3.1 TCP粘包拆包的问题TCP是一个“流”协议,所谓流就是没有界限的遗传数据。TCP底层并不知道上层的业务数据的具体含义,他会根据TCP缓冲区的实际情况进行包的划分,也就是在业务上,我们一个完整的包可能会被TCP...
  • 在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题。 我们都知道TCP属于传输层的协议,...
  • TCP粘包及拆包详解

    2020-02-05 22:53:10
    一、 TCP粘包 TCP粘包 就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。 正常情况...
  • TCP粘包和拆包

    千次阅读 2019-07-15 11:04:27
    TCP粘包和拆包 TCP是个“流”协议,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也...
  • 1.TCP拆包、粘包问题 tcp编程,无论是服务器端还是客户端,当我们读取或者发送数据的时候,都需要考虑TCP底层的粘包/拆包机制。TCP是一个“流”协议,所谓流就是没有界限的遗传数据。可以想象下河里的水就好比数据...
  • 写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下!...这就是今天要讲的TCP拆包粘包现象。 拆包粘包产生的原因 我们可
  • 协议定义 报文长度(4字节) 报文内容[]byte 服务端代码 package main import ( "encoding/binary" ... listener, err := net.Listen("tcp", "localhost:50000") if err != nil { fmt.Println("E

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 12,999
精华内容 5,199
关键字:

tcp拆包