加载中,别急别急...

【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

追问:
此时在nc客户端ctrl+c关闭进程,会触发四次挥手?
那此时nc客户端突然断网呢?

  1. ctrl+c 断开连接:
    会触发
    客户端发送FIN包,服务端回复ACK包,服务端再发送FIN包,客户端回复ACK包,完成四次挥手
  2. 客户端突然断网:
    不会触发
    • 需要依赖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

posted @ 2025-10-29 20:57  端口扫描  阅读(9)  评论(0)    收藏  举报