1. 什么是 tcp 粘包与拆包
1.粘包(sticky packet)
粘包是指在发送多个小的数据包时,接收端会将这些数据包合并成一个数据包接收。由于 tcp 是面向流的协议,它并不会在每次数据发送时附加边界信息。所以当多个数据包按顺序发送时,接收端可能会一次性接收多个数据包的数据,造成数据被粘在一起。
粘包一般发生在发送端每次写入的数据 < 接收端套接字(socket)缓冲区的大小。
假设发送端发送了两个消息:消息1:“hello”,消息2:“world”;由于 tcp 是流协议,接收端可能会接收到如下数据:“helloworld”。这种情况就是粘包,接收端就无法准确区分这两个消息。
2.拆包(packet fragmentation)
拆包是指发送的数据包在传输过程中被分割成多个小包。尽管发送端可能发送了一个完整的消息,但由于 tcp 协议在网络传输时可能会对数据进行分段,接收端可能接收到的是多个小数据包。
拆包一般发生在发送端每次写入的数据 > 接收端套接字(socket)缓冲区的大小。
假设发送端发送了一个大的消息:“hello, this is a long message.”;但是在传输过程中,网络层可能会将该消息拆分成多个小包,接收端可能先收到一部分数据:“hello, this”,然后再收到另外一部分:“is a long message.”;这样接收端就会得到多个数据包,且它们并不代表单一的逻辑消息。
2. go 模拟tcp粘包
server.go(接收端)
package main import ( "bufio" "fmt" "io" "net" ) func handleconnection(conn net.conn) { defer conn.close() // 创建缓冲读取器,读取客户端数据 reader := bufio.newreader(conn) var buffer [1024]byte for { // 持续读取数据 n, err := reader.read(buffer[:]) if err == io.eof { break } if err != nil { fmt.println("error reading data:", err) break } recvstr := string(buffer[:n]) // 打印接收到的数据 fmt.println("received:", recvstr) } } func main() { // 启动服务器,监听 8080 端口 ln, err := net.listen("tcp", ":8080") if err != nil { fmt.println("error starting server:", err) return } defer ln.close() fmt.println("server started on port 8080...") for { // 等待客户端连接 conn, err := ln.accept() if err != nil { fmt.println("error accepting connection:", err) continue } // 处理连接 go handleconnection(conn) } }
client.go(发送端)
package main import ( "fmt" "net" "time" ) func main() { // 连接到服务器 conn, err := net.dial("tcp", "localhost:8080") if err != nil { fmt.println("error connecting to server:", err) return } defer conn.close() // 模拟粘包和拆包 for i := 0; i < 100; i++ { // 发送粘包情况:多个小消息一次发送 message := fmt.sprintf("message %d\n", i+1) conn.write([]byte(message)) } // 等待服务器输出接收到的消息 time.sleep(2 * time.second) }
执行结果分析
可以看到接收端收到的消息并非都是一条,说明发生了粘包
3. go模拟tcp拆包
server.go(接收端)
package main import ( "bufio" "fmt" "io" "net" ) func handleconnection(conn net.conn) { defer conn.close() // 创建缓冲读取器,读取客户端数据 reader := bufio.newreader(conn) var buffer [18]byte for { // 持续读取数据 n, err := reader.read(buffer[:]) if err == io.eof { break } if err != nil { fmt.println("error reading data:", err) break } recvstr := string(buffer[:n]) // 打印接收到的数据 fmt.println("received message :", recvstr) } } func main() { // 启动服务器,监听 8080 端口 ln, err := net.listen("tcp", ":8080") if err != nil { fmt.println("error starting server:", err) return } defer ln.close() fmt.println("server started on port 8080...") for { // 等待客户端连接 conn, err := ln.accept() if err != nil { fmt.println("error accepting connection:", err) continue } // 处理连接 go handleconnection(conn) } }
client.go(发送端)
package main import ( "fmt" "net" "strings" "time" ) func main() { // 连接到服务器 conn, err := net.dial("tcp", "localhost:8080") if err != nil { fmt.println("error connecting to server:", err) return } defer conn.close() // 构造一个超过默认 mtu 的大数据包(32 字节) message := strings.repeat("a", 32) // 模拟发送大量数据 for i := 0; i < 100; i++ { fmt.printf("sending message : %s\n", message) conn.write([]byte(message)) } // 等待服务器输出 time.sleep(2 * time.second) }
执行结果分析
可以看到接收端对接收到的数据进行了拆分,说明发生了拆包
4. 如何解决 tcp 粘包与拆包问题
4.1 自定义协议
发送端将请求的数据封装为两部分:消息头(发送数据大小)+消息体(发送具体数据);接收端根据消息头的值读取相应长度的消息体数据
server.go(接收端)
服务端接收到数据时,首先读取前4个字节来获取消息的长度,然后再根据该长度读取完整的消息体
package main import ( "encoding/binary" "fmt" "io" "log" "net" ) // readmessage 函数根据长度字段读取消息 func readmessage(conn net.conn) (string, error) { // 读取4个字节的长度字段 lenbytes := make([]byte, 4) _, err := io.readfull(conn, lenbytes) if err != nil { return "", fmt.errorf("failed to read length field: %v", err) } // 解析消息长度 msglength := binary.bigendian.uint32(lenbytes) // 读取消息体 msgbytes := make([]byte, msglength) _, err = io.readfull(conn, msgbytes) if err != nil { return "", fmt.errorf("failed to read message body: %v", err) } return string(msgbytes), nil } func handleconnection(conn net.conn) { defer conn.close() // 一直循环接收客户端发来的消息 for { msg, err := readmessage(conn) if err != nil { log.printf("error reading message: %v", err) break } fmt.println("received message:", msg) } } func main() { // 启动监听服务 listener, err := net.listen("tcp", ":8080") if err != nil { log.fatalf("error starting server: %v", err) } defer listener.close() fmt.println("server is listening on port 8080...") // 接受客户端连接并处理 for { conn, err := listener.accept() if err != nil { log.printf("error accepting connection: %v", err) continue } // 启动新的 goroutine 处理客户端请求 go handleconnection(conn) } }
client.go(发送端)
客户端将连接到服务端,并发送多个消息。每个消息的前4字节表示消息的长度,随后是消息体
package main import ( "bytes" "encoding/binary" "log" "net" ) // sendmessage 函数将消息和长度一起发送给服务端 func sendmessage(conn net.conn, msg string) { // 计算消息的长度 msglen := uint32(len(msg)) buf := new(bytes.buffer) // 将消息长度转换为4字节的二进制数据 binary.write(buf, binary.bigendian, msglen) // 将消息体内容添加到缓冲区 buf.write([]byte(msg)) // 发送缓冲区数据到服务端 conn.write(buf.bytes()) } func main() { // 连接到服务端 conn, err := net.dial("tcp", "127.0.0.1:8080") if err != nil { log.fatalf("error connecting to server: %v", err) } defer conn.close() // 发送多个消息 sendmessage(conn, "hello, server!") sendmessage(conn, "this is a second message.") sendmessage(conn, "goodbye!") }
4.2 固定长度数据包
每个消息的长度是固定的(例如 1024 字节)。如果客户端发送的数据长度不足指定长度,则会使用空格填充,确保每个数据包的大小一致
server.go(接收端)
服务端接收到的数据是固定长度的。每次接收 1024 字节的数据,并将其打印出来。如果数据不足 1024 字节,服务端会读取并处理这些数据。
package main import ( "fmt" "io" "log" "net" "strings" ) // handleconnection 函数处理每个客户端的连接 func handleconnection(conn net.conn) { defer conn.close() // 设定每个消息的固定长度 const messagelength = 1024 buf := make([]byte, messagelength) for { // 每次读取固定长度的消息 _, err := io.readfull(conn, buf) if err != nil { if err.error() == "eof" { // 客户端关闭连接 break } log.printf("error reading message: %v", err) break } // 将读取的字节转换为字符串并打印 msg := string(buf) // 去除空格填充 fmt.println("received message:", strings.trimspace(msg)) } } func main() { // 启动 tcp 监听 listener, err := net.listen("tcp", ":8080") if err != nil { log.fatalf("error starting server: %v", err) } defer listener.close() fmt.println("server is listening on port 8080...") // 等待客户端连接 for { conn, err := listener.accept() if err != nil { log.printf("error accepting connection: %v", err) continue } // 启动新的 goroutine 处理每个客户端的连接 go handleconnection(conn) } }
client.go(发送端)
客户端会向服务器发送固定长度的消息,如果消息长度不足 1024 字节,则会填充空格
package main import ( "log" "net" "strings" ) // sendfixedlengthmessage 函数向服务端发送固定长度的消息 func sendfixedlengthmessage(conn net.conn, msg string) { // 确保消息长度为 1024 字节,不足部分用空格填充 if len(msg) < 1024 { msg = msg + strings.repeat(" ", 1024-len(msg)) } // 发送消息到服务端 _, err := conn.write([]byte(msg)) if err != nil { log.fatalf("error sending message: %v", err) } } func main() { // 连接到服务端 conn, err := net.dial("tcp", "127.0.0.1:8080") if err != nil { log.fatalf("error connecting to server: %v", err) } defer conn.close() // 发送固定长度的消息 sendfixedlengthmessage(conn, "hello, server!") sendfixedlengthmessage(conn, "this is a second message.") sendfixedlengthmessage(conn, "goodbye!") }
4.3 特殊字符来标识消息边界
通过在发送端每条消息的末尾加上 \n,然后接收端使用 readline() 方法按行读取数据来区分每个数据包的边界
server.go(接收端)
服务端会监听端口,并按行读取客户端发送的消息。每个消息的末尾会有一个 \n 来标识消息的结束
package main import ( "bufio" "fmt" "log" "net" "strings" ) func handleconnection(conn net.conn) { defer conn.close() // 创建一个带缓冲的读取器 reader := bufio.newreader(conn) for { // 读取客户端发送的一行数据,直到遇到 '\n' 为止 line, err := reader.readstring('\n') if err != nil { log.printf("error reading from client: %v", err) break } // 去掉结尾的换行符 line = strings.trimspace(line) fmt.printf("received message: %s\n", line) } } func main() { // 启动 tcp 监听 listener, err := net.listen("tcp", ":8080") if err != nil { log.fatalf("error starting server: %v", err) } defer listener.close() fmt.println("server is listening on port 8080...") // 等待客户端连接 for { conn, err := listener.accept() if err != nil { log.printf("error accepting connection: %v", err) continue } // 启动新的 goroutine 处理每个客户端的连接 go handleconnection(conn) } }
client.go(发送端)
客户端向服务端发送消息,每条消息末尾都会加上一个 \n,然后发送到服务器
package main import ( "log" "net" ) func sendmessage(conn net.conn, message string) { // 将消息添加换行符并发送 message = message + "\n" _, err := conn.write([]byte(message)) if err != nil { log.fatalf("error sending message: %v", err) } } func main() { // 连接到服务端 conn, err := net.dial("tcp", "127.0.0.1:8080") if err != nil { log.fatalf("error connecting to server: %v", err) } defer conn.close() // 发送几条消息 sendmessage(conn, "hello, server!") sendmessage(conn, "how are you?") sendmessage(conn, "goodbye!") }
5. 三种方式的优缺点对比
特性 | 固定长度方式 | 特殊字符分隔方式 | 自定义协议方式 |
---|---|---|---|
实现简单 | 高 | 中 | 低 |
带宽效率 | 低(需要填充) | 高(仅传输有效数据) | 高(仅传输有效数据,且灵活处理) |
灵活性 | 低 | 中 | 高 |
易于调试 | 高(每包大小固定) | 中(需解析换行符等) | 低(需要解析协议头和体) |
性能开销 | 低 | 低 | 中等(需要额外解析消息头) |
适用场景 | 长度固定的消息 | 消息大小可变但有清晰的分隔符 | 复杂协议、支持多类型消息的场景 |
以上就是golang模拟tcp粘包和拆包的详细内容,更多关于go tcp粘包和拆包的资料请关注代码网其它相关文章!
发表评论