45、Go语言基础 - 网络编程 - TCP 服务端与客户端
hi,我是温新
TCP 协议
TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。
TCP 服务端
一个 TCP 服务端可以同时连接很多个客户端。创建 TCP 服务端流程如下:
-
创建监听器:
服务端首先创建一个监听器(listener),用于侦听来自客户端的连接请求。这通常使用
net.Listen
函数来完成。监听器指定了要侦听的网络协议(TCP、UDP)、IP地址和端口号。例如,net.Listen("tcp", "localhost:8080")
会创建一个TCP监听器,用于侦听本地地址的8080端口。 -
等待客户端连接:
服务端通过监听器等待客户端的连接请求。它会进入一个无限循环,使用
listener.Accept()
来接受客户端的连接请求。一旦有客户端连接请求到达,Accept()
会返回一个表示与客户端连接的net.Conn
对象。 -
处理客户端连接:
对于每个客户端连接,服务端通常会创建一个新的goroutine来处理它。这是因为服务端需要同时处理多个客户端,每个客户端连接都需要独立的处理。处理客户端连接的代码通常包含在这个goroutine中。
-
与客户端通信:
在处理客户端连接的goroutine中,服务端可以与客户端进行双向通信。它可以使用
net.Conn
对象的Read
方法来从客户端接收数据,以及Write
方法来向客户端发送数据。这允许服务端与客户端交换信息,执行所需的业务逻辑。 -
关闭连接:
一旦与客户端的通信完成,服务端应该关闭与客户端的连接,以释放资源。通常,这是通过在处理客户端连接的goroutine中使用
defer conn.Close()
来完成的。客户端也可以选择关闭连接,这将终止与服务端的通信。 -
处理更多的客户端:
服务端会继续循环接受其他客户端的连接请求,重复步骤3到步骤5。
-
关闭监听器:
当服务端不再需要接受新的连接请求时,它可以关闭监听器,使用
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粘包问题的方法
- 固定长度消息:发送方在每个消息之前添加一个固定长度的消息头,用于指示消息的长度。接收方通过读取消息头来划分消息。这种方法适用于消息长度固定的情况。
- 特殊字符分隔:发送方在消息之间插入特殊字符,接收方根据这些特殊字符来划分消息。这种方法适用于文本协议,并且特殊字符不会出现在消息内容中。
- 消息头中包含消息长度:发送方在消息头中包含消息的实际长度信息,接收方根据消息长度来划分消息。
- 使用消息结束符:发送方在每个消息的末尾添加一个特殊的结束符,接收方根据结束符来划分消息。
- 使用分隔符协议:定义一种协议,其中消息之间使用特定的分隔符(如换行符或空格)来分隔。接收方根据分隔符来划分消息。
- 使用应用层协议:在应用层设计协议,明确定义消息的格式和边界,以便发送方和接收方都能正确解析消息。
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客户端,它连接到服务器并发送一条消息,然后关闭连接。在实际应用中,服务器通常会在接收到消息后进行处理并返回响应。