go语言处理TCP粘包
TCP粘包是指发送方发送的若干数据包到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
粘包可能由发送方造成,也可能由接收方造成。
粘包的原因:
发送端原因: 由于TCP协议本身的机制(面向连接的可靠的协议-三次握手机制)客户端与服务器会维持一个连接,数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法对较小的数据包进行合并,然后再发送(超时或者包大小足够)。 那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包。
接收端原因: 服务器在接收到数据后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象。
TCP粘包一般有3种解决方案:
(1)发送固定长度的消息
(2)把消息的尺寸与消息一块发送
(3)使用特殊标记来区分消息间隔
这里采用第二种解决方案,下面来看下go语言实现的TCP粘包现象和封包、拆包。
tcpserver.go
package main
import (
"bufio"
"fmt"
"io"
"net"
)
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buf [2048]byte
for {
n, err := reader.Read(buf[:])
if err == io.EOF {
break
}
if err != nil {
fmt.Println("reader.Read error :", err)
break
}
recvStr := string(buf[:n])
fmt.Printf("received data:%s\n\n", recvStr)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("net.Listen error : ", err)
return
}
defer listen.Close()
fmt.Println("server start ... ")
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accept error :", err)
continue
}
go process(conn)
}
}
tcpclient.go
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("net.Dial error : ", err)
return
}
defer conn.Close()
fmt.Println("client start ... ")
for i := 0; i < 30; i++ {
msg := `hello hacker`
conn.Write([]byte(msg))
}
fmt.Println("send data over... ")
}
分别运行服务端和客户端,结果如下:
server start ...
received data:hello hackerhello hackerhello hackerhello hackerhello hacker
received data:hello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerello hackerhello hackerhello hackerhello hacker
received data:hello hackerhello hackerhello hackerhello hacker
received data:hello hacker
received data:hello hackerhello hackerhello hackerhello hackerhello hacker
可以看到发生了粘包现象,客户端发送了30次,服务器端接收到5次。
我们采用第二种方案把消息的长度和消息体一起发送。
tcpclient2.go
package main
import (
"bytes"
"encoding/binary"
"fmt"
"net"
)
func Encode(message string) ([]byte, error) {
var length = uint16(len(message))
var nb = new(bytes.Buffer)
// 写入消息头
err := binary.Write(nb, binary.BigEndian, length)
if err != nil {
return nil, err
}
// 写入消息体
nb.Write([]byte(message))
return nb.Bytes(), nil
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("net.Dial error : ", err)
return
}
defer conn.Close()
for i := 0; i < 30; i++ {
msg := `hello hacker`
data, err := Encode(msg)
if err != nil {
fmt.Println("Encode msg error : ", err)
return
}
conn.Write(data)
}
}
客户端封包先计算消息体的长度,作为消息的头部,之后写入消息体,一起发送。
tcpserver2.go
package main
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
)
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息的长度
lengthByte, _ := reader.Peek(2) // 读取前2个字节,看看包头
fmt.Println("get byte",lengthByte)
lengthBuff := bytes.NewBuffer(lengthByte)
var length int16
err := binary.Read(lengthBuff, binary.BigEndian, &length)
if err != nil {
return "", err
}
fmt.Println("get length ",length)
if int16(reader.Buffered()) < length+2 {
return "", err
}
realData := make([]byte, int(2+length))
_, err = reader.Read(realData)
if err != nil {
return "", err
}
fmt.Println("get data",realData)
return string(realData[2:]), nil
}
func process2(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
msg, err := Decode(reader)
if err == io.EOF {
return
}
if err != nil {
fmt.Println("Decode error : ", err)
return
}
fmt.Println("received data :", msg)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("net.Listen error :", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accept error :", err)
continue
}
go process2(conn)
}
}
服务器端先读取消息的长度,之后在读取消息体中的内容
分别运行服务端和客户端,可以看到服务器端接收数据正常。