开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
TCP粘包
由于TCP用于传输流式数据,在同一个连接中,发送者连续发送多次数据时,接收者有可能一次接收多条数据,且数据见没有分隔,就像东西粘连在一起,因此叫粘包。下面用代码进行演示:
// tcp沾包演示
func TestServer2(t *testing.T) {
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process2(conn)
}
}
func TestClient2(t *testing.T) {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
conn.Write([]byte(msg))
}
}
=== RUN TestServer2
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
end
可以看到连续发了20条数据,接收者读取到只有两条,而且都粘在了一起。
粘包的原因
发送方
发送方为了在大数据量提高传输效率,防止网络冲斥着很多细小的数据包,使用Nagle 算法:等待服务器应答包到达后,再发送下一个数据包,所以在短时间内快速地连续发送我们就看到多个数据捆绑到了一起发送。
接收方
接收方由于速度限制,在没有读取完读缓冲区时,又有数据发送了过来,这时接收方再读,会把新老数据都取出来,就粘在了一起。
解决办法
出现粘包时,我们很难确定到底是哪个原因,因为原因1、2是相辅相成的,其实也没必要知道。我们主要的解决方法分为两种:
减缓发送方的速度
发送方不要太频繁地发送,可以睡一会,这样子可以同时抑制两种原因,但是对性能有极大损失。
func TestClient2(t *testing.T) {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
conn.Write([]byte(msg))
// 睡一下
time.Sleep(time.Nanosecond)
}
}
=== RUN TestServer2
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
...
收到client发来的数据: Hello, Hello. How are you?
end
禁用Nagle 算法
相比睡一会,还不如直接摆烂不干,相比下性能会有所提升。
自定协议:添加数据长度头
利用tcp的有序性,我们每次发送时先发送此次数据的长度。接收方在接收到数据时,根据数据长度大小,提取出对应长度的数据即可。
package tcp
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"net"
"testing"
)
const BUF_SIZE = 1024
const HEAD_SIZE = 16
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 Test_tcp_serve1(t *testing.T) {
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)
}
}
func SendStringwithTcp(arg string) error {
conn, err := net.Dial("tcp", ":1234")
if err != nil {
log.Fatal(err)
return err
}
head := make([]byte, 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
}
func Test_tcp_cli1(t *testing.T) {
conn, _ := net.Dial("tcp", ":1234")
msg := "YFR718"
for i := 0; i < 200; i++ {
head := make([]byte, HEAD_SIZE)
content := []byte(msg)
headSize := len(content)
binary.BigEndian.PutUint16(head, uint16(headSize))
//先写入head部分,再写入body部分
conn.Write(head)
conn.Write(content)
}
}
输出结果:
=== RUN Test_tcp_serve1
2023/02/04 21:52:47 start listening on 1234
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
received body: YFR718
2023/02/04 21:52:53 read tcp 127.0.0.1:1234->127.0.0.1:50714: wsarecv: An existing connection was forcibly closed by the remote host.