tcp粘包 java_详说tcp粘包和半包

本文详细介绍了TCP粘包和半包现象,分析了其产生的原因,并提供了通过固定长度、分隔符和数据头三种方式来确定数据边界以解决这类问题。文章通过示例代码展示了数据头方式的实现,有效地防止了粘包和半包的发生。
摘要由CSDN通过智能技术生成

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

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

9cc0c6f33922be1ddbba77e55d3be40e.png

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

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

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

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

66a4ac19ff62dcaa0d6df261b2b4285d.png

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

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

a38ab34101ca8e9f502687dabeff4756.png

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

服务端

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 buf [4]byte

bufs := buf[:]

binary.BigEndian.PutUint32(bufs, uint32(len(data)))

if _, err := conn.Write(bufs); err != nil {

fmt.Printf("write failed , err : %v\n", err)

break

}

if _, err = conn.Write(data); 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里取出指定大小的数据,然后进行数据解析,如果发现有半包的情况,就再读取一次,加上上次未解析的数据,再次重新解析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值