tcp服务端和客户端建立连接后会长时间维持这个连接,用于互相传递数据,tcp是以流的方式传输数据的,就像一个水管里的水一样,从一头不断的流向另一头。

理想情况下,发送的数据包都是独立的,

现实要复杂一些,发送方和接收方都有各自的缓冲区。

发送缓冲区:应用不断的把数据发送到缓冲区,系统不断的从缓冲区取数据发送到接收端。

接收缓冲区:系统把接收到的数据放入缓冲区,应用不断的从缓冲区获取数据。

当发送方快速的发送多个数据包时,每个数据包都小于缓冲区,tcp会将多次写入的数据放入缓冲区,一次发送出去,服务器在接收到数据流无法区分哪部分数据包独立的,这样产生了粘包。

或者接收方因为各种原因没有从缓冲区里读取数据,缓冲区的数据会积压,等再取出数据时,也是无法区分哪部分数据包独立的,一样会产生粘包。

发送方的数据包大于缓存区了,其中有一部分数据会在下一次发送,接收端一次接收到时的数据不是完整的数据,就会出现半包的情况。



我们可以还原一下粘包和半包,写一个测试代码

服务端

func main() {
l, err := net.Listen("tcp", ":8899")
if err != nil {
panic(err)
}
fmt.Println("listen to 8899")
for {
conn, err := l.Accept()
if err != nil {
panic(err)
} else {
go handleConn(conn)
}
}
} func handleConn(conn net.Conn) {
defer conn.Close()
var buf [1024]byte
for {
n, err := conn.Read(buf[:])
if err != nil {
break
} else {
fmt.Printf("recv: %s \n", string(buf[0:n]))
}
}
}

客户端

func main() {
data := []byte("~测试数据:一二三四五~")
conn, err := net.Dial("tcp", ":8899")
if err != nil {
panic(err)
}
for i := 0; i < 2000; i++ {
if _, err = conn.Write(data); err != nil {
fmt.Printf("write failed , err : %v\n", err)
break
}
}
}

查看一下输出

recv: ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~ ~测试数据:一二三四五~
recv: ~测试数据:一�
recv: ��三四五~ ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~ ~测试数据:一二三四五~ ~测试数据:一二三四五~ ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~

正常情况下输出是recv: ~测试数据:一二三四五~,发生粘包的时候会输出多个数据包,当有半包的情况下输出的是乱码数据,再下一次会把剩下的半包数据也输出。

要解决也简单的就想办法确定数据的边界,常见的处理方式:

  • 固定长度: 比如规定所有的数据包长度为100byte,如果不够则补充至100长度。优点就是实现很简单,缺点就是空间有极大的浪费,如果传递的消息中大部分都比较短,这样就会有很多空间是浪费的,同样浪费的还有流量。
  • 分隔符:用分隔符来确定数据的边界,这样做比较简单也不浪费空间,但数据包内就不能包含相应的分隔符,如果有会造成错误的解析。
  • 数据头:通过数据头部来解析数据包长度,比如用4个字节来当数据头,保存每个实数据包的长度。

个人更推荐数据头方式来确定数据边界,在发送和接收数据时做好规定,每个数据包是不定长的,比如4字节的包头+真实的数据可以根据自己的业务进行扩展,比如上更多的包头或者包尾,加上数据校验等。

我修改一下上面的代码:

客户端

	data := []byte("~测试数据:一二三四五~")
conn, err := net.Dial("tcp", ":8899")
if err != nil {
panic(err)
}
for i := 0; i < 2000; i++ {
var total int64 = -1
var buf [4]byte
bufs := buf[:]
binary.BigEndian.PutUint32(bufs, uint32(len(data)))
n, err := conn.Write(bufs)
total += int64(n)
n, err = conn.Write(data)
total += int64(n)
if err != nil {
fmt.Printf("write failed , err : %v\n", err)
break
}
}

服务端

func main() {
l, err := net.Listen("tcp", ":8899")
if err != nil {
panic(err)
}
fmt.Println("listen to 8899")
for {
conn, err := l.Accept()
if err != nil {
panic(err)
} else {
go handleConn(conn)
}
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
for {
var msgSize int32
err := binary.Read(conn, binary.BigEndian, &msgSize)
if err != nil {
break
}
buf := make([]byte, msgSize)
_, err = io.ReadFull(conn, buf)
if err != nil {
break
}
fmt.Printf("recv: %s \n", string(buf))
}
}

执行再看一下输出,没有粘包或者半包的情况

recv: ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~
recv: ~测试数据:一二三四五~

也可以像第一个例子一样用一个指定大小的buf var buf [1024]byte,每次从conn里取出指定大小的数据,然后进行数据解析,如果发现有半包的情况,就再读取一次,加上上次未解析的数据,再次重新解析。

详说tcp粘包和半包的更多相关文章

  1. 关于TCP封包、粘包、半包

    关于Tcp封包 很多朋友已经对此作了不少研究,也花费不少心血编写了实现代码和blog文档.当然也充斥着一些各式的评论,自己看了一下,总结一些心得. 首先我们学习一下这些朋友的心得,他们是: http: ...

  2. socket编程 TCP 粘包和半包 的问题及解决办法

    一般在socket处理大数据量传输的时候会产生粘包和半包问题,有的时候tcp为了提高效率会缓冲N个包后再一起发出去,这个与缓存和网络有关系. 粘包 为x.5个包 半包 为0.5个包 由于网络原因 一次 ...

  3. TCP的粘包、半包和Netty的处理

    参考文献:极客时间傅健老师的<Netty源码剖析与实战>Talk is cheap.show me the code! 什么是粘包和半包 在客户端发送数据时,实际是把数据写入到了TCP发送 ...

  4. Netty - 粘包和半包(上)

    在网络传输中,粘包和半包应该是最常出现的问题,作为 Java 中最常使用的 NIO 网络框架 Netty,它又是如何解决的呢?今天就让我们来看看. 定义 TCP 传输中,客户端发送数据,实际是把数据写 ...

  5. C#下利用封包、拆包原理解决Socket粘包、半包问题(新手篇)

    介于网络上充斥着大量的含糊其辞的Socket初级教程,扰乱着新手的学习方向,我来扼要的教一下新手应该怎么合理的处理Socket这个玩意儿. 一般来说,教你C#下Socket编程的老师,很少会教你如何解 ...

  6. Netty - 粘包和半包(下)

    上一篇介绍了粘包和半包及其通用的解决方案,今天重点来看一下 Netty 是如何实现封装成帧(Framing)方案的. 解码核心流程 之前介绍过三种解码器FixedLengthFrameDecoder. ...

  7. TCP的组包、半包、粘包与分包

    一.概念 1)组包.简单的说就是tcp协议把过大的数据包分成了几个小的包传输,接收方要把同一组的数据包重新组合成一个完整的数据包. 2)半包.指接受方没有接受到一个完整的包,只接受了部分,这种情况主要 ...

  8. c# socket 解决粘包,半包

    处理原理: 半包:即一条消息底层分几次发送,先有个头包读取整条消息的长度,当不满足长度时,将消息临时缓存起来,直到满足长度再解码 粘包:两条完整/不完整消息粘在一起,一般是解码完上一条消息,然后再判断 ...

  9. TCP粘包和半包的处理方法

    先把处理的方法的代码放这里: 解析数据帧的代码: bool CSocket::findData(byte* buff, int& len) { for (int i = 0; i <= ...

随机推荐

  1. php日志监控

    <?php date_default_timezone_set('Asia/Shanghai'); $time = date('Y-m-d H:i:s',time()); //访问时间 $ip ...

  2. [工具-001]C++更换EXE的ICON图标

    我们都知道每个可执行文件EXE都会有自己的图标,它可以在项目生成的时候进行指认,但是有时候我们会遇到两种情况:1.没有源代码,2.我们的项目很多,一个个进行更换很耗时.本人就是因为接到这么一个需求,要 ...

  3. Java IO(十二) 字符流 Writer 和 Reader

    Java IO(十二) 字符流 Reader和 Writer 一.介绍 涉及到文件(如果是纯文本文件形式)操作时,Java除了提供 FIle(文件和目录路径名的抽象表示形式) 和 FileDescri ...

  4. python中的数据存储认识

    声明:本人是一个初学者,博客内容基本也是一些基础的东西,如果说的有什么问题欢迎纠正. 前言 许多人初学python之前应该也学习过其他的语言,比如博大精深的c语言,笔者在学习python之前就学习过c ...

  5. & 异步使用场景

    异步的使用场景: 1.不涉及共享资源,或对共享资源只读,即非互斥操作 2.没有时序上的严格关系 3.不需要原子操作,或可以通过其他方式控制原子性 4.常用于IO操作等耗时操作,因为比较影响客户体验和使 ...

  6. PYTHON 黑帽子第二章总结

    基于python3编写 import sys, socket, getopt, threading, argparse, subprocess # globals options listen = F ...

  7. uwsgi+nginx 502 bad get away 错误

    用uwsgi和nginx部署网站时有时候访问网站会出现502错误 配置,启动文件等完全没有问题. 目前解决方法是重启uwsgi就可以了(虽然说502错误应该有很多产生原因啦) 所用命令: $ ps - ...

  8. [Python3]星号*的打开方式

    python中,* 号除了用来做数量乘法,还有其他的用处. 结论 概括的来说,就是对修饰的变量进行拆分, 对修饰的形式参数进行参数聚集. 单*号,将被修饰的变量按元素方式拆分, 对修饰的形式参数进行参 ...

  9. Flutter,webview里面实现上传和下载的功能

    前提:Flutter 与 webview(vue) 一起开发的项目 开始的时候并没有想到什么移动端的,所以上传就用input,下载就用iframe来实现,然而真机实测的时候,input那个方法IOS支 ...

  10. Python机器学习笔记:SVM(1)——SVM概述

    前言 整理SVM(support vector machine)的笔记是一个非常麻烦的事情,一方面这个东西本来就不好理解,要深入学习需要花费大量的时间和精力,另一方面我本身也是个初学者,整理起来难免思 ...