【Golang实战学八股】-TCP连接与粘包问题复现与解决
TCP连接与粘包问题复现与解决
首先学习三个Linux命令
- telnet 可以进行TCP连接,目前常用于端口测试,例如测试80端口是否开放
- nc 命令用于创建 TCP 或 UDP 连接,以及传输数据
- lsof 命令用于查看当前系统打开的文件和网络连接,例如 lsof -i :8080 可以查看本地8080端口是否有进程监听
# 测试80端口是否开放
telnet localhost 80
# 连接8080端口运行的TCP服务
nc localhost 8080
# 查看本地8080端口是否有进程监听
lsof -i :8080
用Go实现一个最简单的多客户端TCP服务
package main
import (
"fmt"
"net"
)
func main() {
listener, _ := net.Listen("tcp", ":8080")
defer listener.Close()
fmt.Println("服务器启动,监听 8080...")
for {
conn, _ := listener.Accept()
go handle(conn)
}
}
func handle(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
fmt.Println("客户端断开:", err)
return
}
fmt.Printf("收到原始数据: %q\n", buf[:n])
}
}
运行服务端,并通过nc连接
go run main.go
# 连接8080端口运行的TCP服务
nc localhost 8080
此时可以在服务端看到客户端的连接信息,并且可以在客户端看到服务端的响应信息:

追问:
此时在nc客户端ctrl+c关闭进程,会触发四次挥手?
那此时nc客户端突然断网呢?
- ctrl+c 断开连接:
会触发
客户端发送FIN包,服务端回复ACK包,服务端再发送FIN包,客户端回复ACK包,完成四次挥手 - 客户端突然断网:
不会触发- 需要依赖TCP的保活机制:即服务端会发送保活探测包,询问客户端是否还活着。如果客户端没有回复,服务端会继续发送保活探测包,直到超过最大次数,才会关闭连接;
- 或者客户端又恢复了网络,这时客户端会回送RST包,服务端会立即关闭连接(客户端没网络是不会发RST的)
粘包问题
就是因为TCP是面向数据流的协议,可能将太大的包拆分成多个小包发送,也可能将多个小包合并成一个大包发送;此时分不清一个个包的边界,就会导致粘包问题。
触发:
- 客户端发送的包大小超过了TCP的MSS(最大报文大小),导致包被拆分发送
- 多个小数据发送间隔短,被TCP合并为一个包发送--Nagle算法优化
演示:
// 粘包客户端
package main
import (
"net"
)
func main() {
conn, _ := net.Dial("tcp", "127.0.0.1:8080")
defer conn.Close()
// 连续写入多条数据(没有停顿)
for i := 0; i < 10; i++ {
conn.Write([]byte(fmt.Sprintf("msg%d", i)))
}
}
实际收到如下图:

解决粘包问题常用方案:
- 固定长度协议
- 特殊分隔符
- 长度前缀协议--最常用,例如HTTP协议的头部字段有
Content-Length,表示后面跟着的数据长度
实现代码:
服务端
package main
import (
"encoding/binary"
"fmt"
"io"
"net"
)
// 读取一条完整的消息
func readPacket(conn net.Conn) (string, error) {
// 先读 4 个字节,表示消息长度
lenBuf := make([]byte, 4)
_, err := io.ReadFull(conn, lenBuf)
if err != nil {
return "", err
}
length := binary.BigEndian.Uint32(lenBuf)
if length == 0 {
return "", fmt.Errorf("invalid length: 0")
}
// 再读 length 个字节的数据
data := make([]byte, length)
_, err = io.ReadFull(conn, data)
if err != nil {
return "", err
}
return string(data), nil
}
func handle(conn net.Conn) {
defer conn.Close()
addr := conn.RemoteAddr().String()
fmt.Println("客户端连接:", addr)
for {
msg, err := readPacket(conn)
if err != nil {
if err == io.EOF {
fmt.Println("客户端断开连接:", addr)
} else {
fmt.Printf("读取消息出错:%v\n", err)
}
return
}
fmt.Printf("收到消息:%s\n", msg)
}
}
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("服务器启动,监听端口 8080...")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("接受连接出错:", err)
continue
}
go handle(conn)
}
}
客户端:
package main
import (
"encoding/binary"
"fmt"
"net"
"time"
)
// sendPacket 发送一条带长度前缀的消息
func sendPacket(conn net.Conn, msg string) error {
data := []byte(msg)
length := uint32(len(data))
// 长度前缀 + 消息体
buf := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(buf[:4], length)
copy(buf[4:], data)
_, err := conn.Write(buf)
return err
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()
fmt.Println("客户端已连接服务器")
// 模拟连续快速发包,造成“粘包”环境
for i := 1; i <= 10; i++ {
msg := fmt.Sprintf("msg%d", i)
sendPacket(conn, msg)
// 你可以注释掉这行 Sleep,看“粘包”是否仍能正确解析
time.Sleep(50 * time.Millisecond)
}
}
此时运行服务端,会看到客户端发送的10条消息,都被正确解析了:

另外,处于好奇想验证TIME_WAIT状态
于是主动关闭服务器后,立刻重新运行并连接。是正常的!
这时因为服务器默认开启了端口复用功能,允许新进程绑定处于TIME_WAIT状态的端口;
可以在连接时主动关闭这个功能,再重试发现服务器不能启动,报错:
bind: address already in use
此时用netstat -antp | grep 8080 查看端口状态,发现确实是:TIME_WAIT

浙公网安备 33010602011771号