go语言处理TCP粘包

 阅读大约需要3分钟

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)
	}
}

服务器端先读取消息的长度,之后在读取消息体中的内容

分别运行服务端和客户端,可以看到服务器端接收数据正常。