使用Go语言实现Socket服务端和客户端并解决粘包问题

下面是一个使用Go语言实现的Socket服务端和客户端的示例代码,并通过定义消息长度协议来解决粘包问题。

消息长度协议

我们在消息的头部添加一个固定长度的字段(例如4字节),用于表示消息的实际长度。这样接收方在读取消息时,可以先读取前4字节获取消息长度,然后根据该长度读取完整的消息内容。

服务端代码

package main

import (
	"bufio"
	"encoding/binary"
	"fmt"
	"io"
	"net"
)

const headerLength = 4 // 消息长度字段的字节数

func main() {
	// 监听端口
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("Error listening:", err)
		return
	}
	defer listener.Close()
	fmt.Println("Server listening on :8080")

	for {
		// 接受客户端连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Error accepting connection:", err)
			continue
		}
		fmt.Println("Client connected:", conn.RemoteAddr())

		// 处理客户端连接
		go handleClient(conn)
	}
}

func handleClient(conn net.Conn) {
	defer conn.Close()

	reader := bufio.NewReader(conn)
	for {
		// 读取消息长度(前4字节)
		header := make([]byte, headerLength)
		_, err := io.ReadFull(reader, header)
		if err != nil {
			if err == io.EOF {
				fmt.Println("Client disconnected:", conn.RemoteAddr())
			} else {
				fmt.Println("Error reading header:", err)
			}
			return
		}

		// 解析消息长度
		msgLength := int(binary.BigEndian.Uint32(header))
		fmt.Printf("Message length: %d\n", msgLength)

		// 读取消息内容
		msg := make([]byte, msgLength)
		_, err = io.ReadFull(reader, msg)
		if err != nil {
			fmt.Println("Error reading message:", err)
			return
		}

		// 处理消息
		message := string(msg)
		fmt.Printf("Received message from %s: %s\n", conn.RemoteAddr(), message)

		// 回显消息
		response := "Server received: " + message
		sendMessage(conn, []byte(response))
	}
}

func sendMessage(conn net.Conn, data []byte) {
	// 构造消息头(4字节长度)
	length := uint32(len(data))
	header := make([]byte, headerLength)
	binary.BigEndian.PutUint32(header, length)

	// 发送消息头和消息内容
	_, err := conn.Write(append(header, data...))
	if err != nil {
		fmt.Println("Error sending message:", err)
	}
}

客户端代码

package main

import (
	"bufio"
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"os"
)

const headerLength = 4 // 消息长度字段的字节数

func main() {
	// 连接到服务端
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("Error connecting to server:", err)
		return
	}
	defer conn.Close()
	fmt.Println("Connected to server")

	// 启动一个goroutine接收服务端消息
	go receiveMessages(conn)

	// 从控制台读取用户输入并发送到服务端
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		message := scanner.Text()
		sendMessage(conn, []byte(message))
	}

	if err := scanner.Err(); err != nil {
		fmt.Println("Error reading input:", err)
	}
}

func receiveMessages(conn net.Conn) {
	reader := bufio.NewReader(conn)
	for {
		// 读取消息长度(前4字节)
		header := make([]byte, headerLength)
		_, err := io.ReadFull(reader, header)
		if err != nil {
			if err == io.EOF {
				fmt.Println("Disconnected from server")
			} else {
				fmt.Println("Error reading header:", err)
			}
			os.Exit(0)
		}

		// 解析消息长度
		msgLength := int(binary.BigEndian.Uint32(header))
		fmt.Printf("Message length: %d\n", msgLength)

		// 读取消息内容
		msg := make([]byte, msgLength)
		_, err = io.ReadFull(reader, msg)
		if err != nil {
			fmt.Println("Error reading message:", err)
			os.Exit(0)
		}

		// 显示消息
		message := string(msg)
		fmt.Printf("Received from server: %s\n", message)
	}
}

func sendMessage(conn net.Conn, data []byte) {
	// 构造消息头(4字节长度)
	length := uint32(len(data))
	header := make([]byte, headerLength)
	binary.BigEndian.PutUint32(header, length)

	// 发送消息头和消息内容
	_, err := conn.Write(append(header, data...))
	if err != nil {
		fmt.Println("Error sending message:", err)
	}
}


测试方法

  1. 先运行服务端代码。
  2. 再运行客户端代码。
  3. 在客户端输入消息并发送,服务端会接收消息并回显。
  4. 客户端会接收并显示服务端回显的消息。

关键点说明

  1. 消息长度协议:通过在消息前添加4字节的长度字段,接收方可以明确知道消息的长度,从而避免粘包问题。
  2. 并发处理:服务端使用goroutine处理每个客户端连接,支持多个客户端同时连接。
  3. 数据读取:使用io.ReadFull确保完整读取固定长度的头部和消息内容。
通过这种方式,可以有效地解决TCP通信中的粘包问题。
正文到此结束