本文最后更新于:2023年7月29日 下午

近期开发了一个支持高并发的syslog日志发送工具,手写了一个简单tcp server作为接收端,在打日志的过程中偶然发现了终端打印的日志数据有些不完整,tcp server代码如下:

package main

import (
	"bufio"
	"fmt"
	"net"
)

// TCP Server端(本地调试用)
// 处理函数
func process(conn net.Conn) {
	defer conn.Close() // 关闭连接
        reader := bufio.NewReader(conn)
	var buf [128]byte
	n, err := reader.Read(buf[:]) // 读取数据
	if err != nil {
		fmt.Println("read from client failed, err: ", err)
		break
	}
	recvStr := string(buf[:n])
	fmt.Println("收到Client端发来的数据:", recvStr)
	//conn.Write([]byte(recvStr)) // 发送数据
}

func main() {
	listen, err := net.Listen("tcp", "0.0.0.0:3333")
	if err != nil {
		fmt.Println("Listen() failed, err: ", err)
		return
	}
	for {
		conn, err := listen.Accept() // 监听客户端的连接请求
		if err != nil {
			fmt.Println("Accept() failed, err: ", err)
			continue
		}
		go process(conn) // 启动一个goroutine来处理客户端的连接请求
	}
}

按逻辑是每行日志之间会换行,但实际过程中,有几条日志会和上一条粘在一起,而且被截断,现象如下:

image

排查后发现,“粘包“是由于,tcp是流传输,会有缓存区,不一定能够及时把消息发出去,像Nagle优化算法会将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包是,而buf只设置每次读取了128字节。

google了一下解决办法:

  • TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:
  • 1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
  • 2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  • 3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

工具发送的日志都是以 \n结尾,且长度不固定,因此选择用分隔符切分,tcp server process函数修改如下:

func process(conn net.Conn) {
	defer conn.Close() // 关闭连接
	for {
		reader := bufio.NewReader(conn)
		for {
			data, err := reader.ReadSlice('\n')
			if err != nil {
				if err != io.EOF {
					log.Println(err)
				} else {
					fmt.Println("read from client failed, err: ", err)
					break
				}
			}
			fmt.Println("收到Client端发来的数据:", string(data))
		}
	}
}

再次接受日志,就没有截断和数据粘黏的问题啦~
image

ps:使用抓包工具就可以发现,像http是用\r\n去做数据的边界区分的

最后,“粘包”的时候都是粘了日志的前半部分,而单条日志的后半部分在之后的传输中就神奇的丢失了,这个目前还没找到原因

编辑做下补充,tcp是流传输,”粘包”两个字刻意加了引号,本意是想指代业务数据的边界处理问题,用词不当,希望不误导各位


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

记一次go base64编码排查 上一篇
Python项目打包为whl包 下一篇