45、Go语言基础 - 网络编程 - TCP 服务端与客户端

作者: 温新

分类: 【Go基础】

阅读: 285

时间: 2023-12-05 00:38:14

hi,我是温新

TCP 协议

TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。

TCP 服务端

一个 TCP 服务端可以同时连接很多个客户端。创建 TCP 服务端流程如下:

  1. 创建监听器

    服务端首先创建一个监听器(listener),用于侦听来自客户端的连接请求。这通常使用net.Listen函数来完成。监听器指定了要侦听的网络协议(TCP、UDP)、IP地址和端口号。例如,net.Listen("tcp", "localhost:8080")会创建一个TCP监听器,用于侦听本地地址的8080端口。

  2. 等待客户端连接

    服务端通过监听器等待客户端的连接请求。它会进入一个无限循环,使用listener.Accept()来接受客户端的连接请求。一旦有客户端连接请求到达,Accept()会返回一个表示与客户端连接的net.Conn对象。

  3. 处理客户端连接

    对于每个客户端连接,服务端通常会创建一个新的goroutine来处理它。这是因为服务端需要同时处理多个客户端,每个客户端连接都需要独立的处理。处理客户端连接的代码通常包含在这个goroutine中。

  4. 与客户端通信

    在处理客户端连接的goroutine中,服务端可以与客户端进行双向通信。它可以使用net.Conn对象的Read方法来从客户端接收数据,以及Write方法来向客户端发送数据。这允许服务端与客户端交换信息,执行所需的业务逻辑。

  5. 关闭连接

    一旦与客户端的通信完成,服务端应该关闭与客户端的连接,以释放资源。通常,这是通过在处理客户端连接的goroutine中使用defer conn.Close()来完成的。客户端也可以选择关闭连接,这将终止与服务端的通信。

  6. 处理更多的客户端

    服务端会继续循环接受其他客户端的连接请求,重复步骤3到步骤5。

  7. 关闭监听器

    当服务端不再需要接受新的连接请求时,它可以关闭监听器,使用listener.Close()。这将停止监听器,并防止新的连接请求。

tcp 服务端

package main

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

func main() {
    // 1、创建一个TCP监听器,侦听本地地址的端口20000
    listen, err := net.Listen("tcp", "127.0.0.1:20000")
    if err != nil {
        fmt.Println("监听失败,错误:", err)
        return
    }

    for {
        // 2、接受客户端连接请求
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("建立连接失败,错误:", err)
            continue
        }
        // 3、启动一个独立的goroutine来处理该连接
        go process(conn)
    }
}

func process(conn net.Conn) {
    // 5、关闭连接
    defer conn.Close()

    for {
        // 4、创建一个带缓冲的读取器,用于从连接中读取数据
        reader := bufio.NewReader(conn)
        var buf [128]byte
        // 从客户端读取数据到缓冲区
        n, err := reader.Read(buf[:])
        if err != nil {
            fmt.Println("来自客户端的数据读取失败", err)
            break
        }
        // 从缓冲区中提取接收到的数据并打印
        recvStr := string(buf[:n])
        fmt.Println("收到客户端数据:", recvStr)
        // 将接收到的数据发送回客户端
        conn.Write([]byte(recvStr))
    }
}

代码解释:

  • 该程序创建一个TCP服务器,监听在本地IP地址(127.0.0.1)的20000端口。
  • main函数中,程序使用net.Listen创建一个TCP监听器,然后进入无限循环,等待客户端的连接请求。
  • 当有客户端连接请求到达时,程序接受连接,并为每个客户端连接启动一个独立的goroutine,调用process函数来处理客户端通信。
  • process函数中,程序使用带缓冲的读取器bufio.NewReader来从连接中读取数据,然后将数据发送回客户端。

此示例是一个简单的回显服务器,它接收来自客户端的文本消息并将其原样发送回客户端。

tcp 客户端案例

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
)

func main() {
    // 建立与服务器的TCP连接
    conn, err := net.Dial("tcp", "127.0.0.1:20000")
    if err != nil {
        fmt.Println("连接服务端失败:", err)
        return
    }

    // 在函数结束时关闭连接
    defer conn.Close()
    
    // 从标准输入读取数据
    inputReader := bufio.NewReader(os.Stdin) 

    for {
        // 读取用户输入的文本
        input, _ := inputReader.ReadString('\n')
        inputInfo := strings.Trim(input, "\r\n")

        // 如果用户输入"Q",退出程序
        if strings.ToUpper(inputInfo) == "Q" {
            return 
        }

        // 将用户输入的文本发送到服务器
        _, err = conn.Write([]byte(inputInfo))
        if err != nil {
            // 发送失败时退出程序
            return 
        }

        // 创建一个 512 字节的数组
        buf := [512]byte{}
        // 从服务器接收响应数据
        n, err := conn.Read(buf[:])
        if err != nil {
            fmt.Println("读取服务端数据失败:", err)
            return // 读取失败时退出程序
        }

        fmt.Println("服务端返回数据:", string(buf[:n])) // 打印服务器的响应
    }
}

TCP 粘包问题及解决

为什么会粘包

主要原因是 TCP 数据传递模式是流模式,消息没有边界。

TCP 粘包问题是在使用 TCP 协议进行数据传输时可能会遇到的一种常见问题。它通常发生在发送方连续发送多个小数据包,并且接收方在一次接收中无法准确划分这些数据包的情况下。TCP是面向流的协议,它只负责将字节流传输给接收方,而不负责消息的边界。这导致了可能发生粘包和拆包问题。

粘包问题:多个小数据包被合并到一个数据包中发送,接收方需要额外的处理来区分这些消息。

拆包问题:一个大的数据包被拆分成多个小数据包发送,接收方需要组合这些小数据包以还原原始消息。

解决TCP粘包问题的方法

  1. 固定长度消息:发送方在每个消息之前添加一个固定长度的消息头,用于指示消息的长度。接收方通过读取消息头来划分消息。这种方法适用于消息长度固定的情况。
  2. 特殊字符分隔:发送方在消息之间插入特殊字符,接收方根据这些特殊字符来划分消息。这种方法适用于文本协议,并且特殊字符不会出现在消息内容中。
  3. 消息头中包含消息长度:发送方在消息头中包含消息的实际长度信息,接收方根据消息长度来划分消息。
  4. 使用消息结束符:发送方在每个消息的末尾添加一个特殊的结束符,接收方根据结束符来划分消息。
  5. 使用分隔符协议:定义一种协议,其中消息之间使用特定的分隔符(如换行符或空格)来分隔。接收方根据分隔符来划分消息。
  6. 使用应用层协议:在应用层设计协议,明确定义消息的格式和边界,以便发送方和接收方都能正确解析消息。

TCP 服务端

package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	// 创建TCP监听器,侦听在127.0.0.1的20001端口
	listen, err := net.Listen("tcp", "127.0.0.1:20001")
	if err != nil {
		fmt.Println("监听失败:", err)
		return
	}

	defer listen.Close()

	for {
		// 接受客户端连接请求
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("建立连接失败:", err)
			continue
		}
		go handleClient(conn) // 启动单独的goroutine来处理客户端连接
	}
}

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

	buffer := make([]byte, 1024)
	for {
		// 从客户端读取数据
		msg, err := conn.Read(buffer)
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端已关闭连接")
			} else {
				fmt.Println("读取客户端消息失败:", err)
			}

			return
		}
		receivedData := string(buffer[:msg])
		fmt.Println("来自客户端的消息:", receivedData)
	}
}

代码解释:

  • 服务器端创建TCP监听器,侦听在127.0.0.1的20001端口。
  • 服务器端接受客户端连接请求,为每个连接启动一个单独的goroutine。
  • handleClient函数用于处理客户端连接,它从客户端读取数据,当遇到io.EOF时知道客户端已关闭连接。
  • 服务器端会打印接收到的来自客户端的消息。

TCP 客户端

package main

import (
	"fmt"
	"net"
)

func main() {
	// 建立与服务器的TCP连接
	conn, err := net.Dial("tcp", "127.0.0.1:20001")
	if err != nil {
		fmt.Println("连接服务端失败:", err)
		return
	}
	defer conn.Close() // 在函数结束时关闭连接

	message := "hello, server"
	conn.Write([]byte(message)) // 向服务器发送消息
	conn.Close()               // 关闭连接

	fmt.Println("来自服务端的消息:", message)
}

代码解释:

  • 客户端通过net.Dial函数建立与服务器的TCP连接,连接到地址"127.0.0.1:20001"。
  • 如果连接失败,将打印错误信息并退出程序。
  • 使用defer语句确保在函数结束时关闭连接,以防止资源泄漏。
  • 客户端发送消息"hello, server"给服务器,通过conn.Write方法。
  • 接着,客户端关闭连接,此操作也会向服务器发送EOF标志,表示消息的结束。
  • 最后,客户端打印来自服务端的消息,但此时消息仍然是"hello, server",并没有来自服务端的响应。

这个示例演示了一个TCP客户端,它连接到服务器并发送一条消息,然后关闭连接。在实际应用中,服务器通常会在接收到消息后进行处理并返回响应。

请登录后再评论