Golang解决TCP粘包拆包问题

什么是粘包问题

最近在使用Golang编写Socket层,发现有时候接收端会一次读到多个数据包的问题。因而经过查阅资料,发现这个就是传说中的TCP粘包问题。下面经过编写代码来重现这个问题:golang

服务端代码 server/main.go

func main() {
	l, err := net.Listen("tcp", ":4044")
	if err != nil {
		panic(err)
	}
	fmt.Println("listen to 4044")
	for {
        // 监听到新的链接,建立新的 goroutine 交给 handleConn函数 处理
		conn, err := l.Accept()
		if err != nil {
			fmt.Println("conn err:", err)
		} else {
			go handleConn(conn)
		}
	}
}

func handleConn(conn net.Conn) {
	defer conn.Close()
	defer fmt.Println("关闭")
	fmt.Println("新链接:", conn.RemoteAddr())

	result := bytes.NewBuffer(nil)
	var buf [1024]byte
	for {
		n, err := conn.Read(buf[0:])
		result.Write(buf[0:n])
		if err != nil {
			if err == io.EOF {
				continue
			} else {
				fmt.Println("read err:", err)
				break
			}
		} else {
			fmt.Println("recv:", result.String())
		}
		result.Reset()
	}
}
复制代码

客户端代码 client/main.go

func main() {
	data := []byte("[这里才是一个完整的数据包]")
	conn, err := net.DialTimeout("tcp", "localhost:4044", time.Second*30)
	if err != nil {
		fmt.Printf("connect failed, err : %v\n", err.Error())
        return
	}
	for i := 0; i <1000; i++ {
		_, err = conn.Write(data)
		if err != nil {
			fmt.Printf("write failed , err : %v\n", err)
			break
		}
	}
}
复制代码

运行结果

listen to 4044
新链接: [::1]:53079
recv: [这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据�
recv: �][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包][这里才是一个完整的数据包][这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
...省略其它的...
复制代码

从服务端的控制台输出能够看出,存在三种类型的输出:bash

  1. 一种是正常的一个数据包输出。
  2. 一种是多个数据包“粘”在了一块儿,咱们定义这种读到的包为粘包。
  3. 一种是一个数据包被“拆”开,造成一个破碎的包,咱们定义这种包为半包。

为何会出现半包和粘包?

  1. 客户端一段时间内发送包的速度太多,服务端没有所有处理完。因而数据就会积压起来,产生粘包。
  2. 定义的读的buffer不够大,而数据包太大或者因为粘包产生,服务端不能一次所有读完,产生半包。

何时须要考虑处理半包和粘包?

  1. TCP链接是长链接,即一次链接屡次发送数据。
  2. 每次发送的数据是结构的,好比 JSON格式的数据 或者 数据包的协议是由咱们本身定义的(包头部包含实际数据长度、协议魔数等)。

解决思路

  1. 定长分隔(每一个数据包最大为该长度,不足时使用特殊字符填充) ,可是数据不足时会浪费传输资源
  2. 使用特定字符来分割数据包,可是若数据中含有分割字符则会出现Bug
  3. 在数据包中添加长度字段,弥补了以上两种思路的不足,推荐使用

拆包演示

经过上述分析,咱们最好经过第三种思路来解决拆包粘包问题。app

Golang的bufio库中有为咱们提供了Scanner,来解决这类分割数据的问题。tcp

type Scanner

Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text. Successive calls to the Scan method will step through the 'tokens' of a file, skipping the bytes between the tokens. The specification of a token is defined by a split function of type SplitFunc; the default split function breaks the input into lines with line termination stripped. Split functions are defined in this package for scanning a file into lines, bytes, UTF-8-encoded runes, and space-delimited words. The client may instead provide a custom split function.ide

简单来说便是:函数

Scanner为 读取数据 提供了方便的 接口。连续调用Scan方法会逐个获得文件的“tokens”,跳过 tokens 之间的字节。token 的规范由 SplitFunc 类型的函数定义。咱们能够改成提供自定义拆分功能。ui

接下来看看 SplitFunc 类型的函数是什么样子的:this

type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error) 复制代码

Golang官网文档上提供的使用例子🌰:spa

func main() {
	// An artificial input source.
	const input = "1234 5678 1234567901234567890"
	scanner := bufio.NewScanner(strings.NewReader(input))
	// Create a custom split function by wrapping the existing ScanWords function.
	split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
		advance, token, err = bufio.ScanWords(data, atEOF)
		if err == nil && token != nil {
			_, err = strconv.ParseInt(string(token), 10, 32)
		}
		return
	}
	// Set the split function for the scanning operation.
	scanner.Split(split)
	// Validate the input
	for scanner.Scan() {
		fmt.Printf("%s\n", scanner.Text())
	}

	if err := scanner.Err(); err != nil {
		fmt.Printf("Invalid input: %s", err)
	}
}
复制代码

因而,咱们能够这样改写咱们的程序:code

服务端代码 server/main.go

func main() {
	l, err := net.Listen("tcp", ":4044")
	if err != nil {
		panic(err)
	}
	fmt.Println("listen to 4044")
	for {
		conn, err := l.Accept()
		if err != nil {
			fmt.Println("conn err:", err)
		} else {
			go handleConn2(conn)
		}
	}
}

func packetSlitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
        // 检查 atEOF 参数 和 数据包头部的四个字节是否 为 0x123456(咱们定义的协议的魔数)
	if !atEOF && len(data) > 6 && binary.BigEndian.Uint32(data[:4]) == 0x123456 {
		var l int16
                // 读出 数据包中 实际数据 的长度(大小为 0 ~ 2^16)
		binary.Read(bytes.NewReader(data[4:6]), binary.BigEndian, &l)
		pl := int(l) + 6
		if pl <= len(data) {
			return pl, data[:pl], nil
		}
	}
	return
}

func handleConn2(conn net.Conn) {
	defer conn.Close()
	defer fmt.Println("关闭")
	fmt.Println("新链接:", conn.RemoteAddr())
	result := bytes.NewBuffer(nil)
        var buf [65542]byte // 因为 标识数据包长度 的只有两个字节 故数据包最大为 2^16+4(魔数)+2(长度标识)
	for {
		n, err := conn.Read(buf[0:])
		result.Write(buf[0:n])
		if err != nil {
			if err == io.EOF {
				continue
			} else {
				fmt.Println("read err:", err)
				break
			}
		} else {
			scanner := bufio.NewScanner(result)
			scanner.Split(packetSlitFunc)
			for scanner.Scan() {
				fmt.Println("recv:", string(scanner.Bytes()[6:]))
			}
		}
		result.Reset()
	}
}
复制代码

客户端代码 client/main.go

func main() {
	data := []byte("[这里才是一个完整的数据包]")
	l := len(data)
	fmt.Println(l)
	magicNum := make([]byte, 4)
	binary.BigEndian.PutUint32(magicNum, 0x123456)
	lenNum := make([]byte, 2)
	binary.BigEndian.PutUint16(lenNum, uint16(l))
	packetBuf := bytes.NewBuffer(magicNum)
	packetBuf.Write(lenNum)
	packetBuf.Write(data)
	conn, err := net.DialTimeout("tcp", "localhost:4044", time.Second*30)
	if err != nil {
		fmt.Printf("connect failed, err : %v\n", err.Error())
                return
	}
	for i := 0; i <1000; i++ {
		_, err = conn.Write(packetBuf.Bytes())
		if err != nil {
			fmt.Printf("write failed , err : %v\n", err)
			break
		}
	}
}
复制代码

运行结果

listen to 4044
新链接: [::1]:55738
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
recv: [这里才是一个完整的数据包]
...省略其它的...
复制代码
相关文章
相关标签/搜索