第 44 课:TCP/UDP 网络编程
学习目标
完成本课学习后,你将能够:
- 理解 TCP 和 UDP 协议的基本特点与适用场景
- 掌握 Go 语言
net包中 TCP 连接的核心操作 - 实现一个简单的 TCP 服务器与客户端程序
- 了解 UDP 无连接通信的编程模式
- 学会处理网络编程中的基本错误和资源清理
核心概念
TCP 与 UDP 的区别
想象一下寄送包裹:
- TCP(传输控制协议) 像可靠的快递服务,有签收确认、顺序保证、丢失重寄。适合需要数据完整性的场景(如网页、文件传输)。
- UDP(用户数据报协议) 像寄明信片,快速但不保证送达顺序,可能丢失。适合实时性要求高的场景(如视频会议、在线游戏)。
Go 语言的 net 包
Go 的 net 包提供了统一的网络编程接口:
net.Conn接口:代表网络连接,抽象了读写操作net.Dial:创建连接(客户端使用)net.Listen:开始监听端口(服务器使用)net.ListenPacket:用于 UDP 通信
代码示例
TCP 服务器示例
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
func main() {
// 1. 监听 TCP 端口
listener, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal("监听错误:", err)
}
defer listener.Close() // 确保程序退出时关闭监听器
fmt.Println("TCP 服务器已启动,监听 localhost:8080")
for {
// 2. 接受客户端连接
conn, err := listener.Accept()
if err != nil {
log.Println("接受连接错误:", err)
continue
}
// 3. 为每个连接启动独立的 goroutine 处理
go handleTCPConnection(conn)
}
}
func handleTCPConnection(conn net.Conn) {
// 4. 确保连接最终被关闭
defer conn.Close()
clientAddr := conn.RemoteAddr().String()
fmt.Printf("新客户端连接: %s\n", clientAddr)
// 5. 创建带缓冲的读取器,优化读取性能
reader := bufio.NewReader(conn)
for {
// 6. 读取客户端消息(以换行符 \n 结束)
message, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("客户端 %s 断开连接\n", clientAddr)
return
}
// 清理消息中的空白字符
message = strings.TrimSpace(message)
fmt.Printf("收到来自 %s 的消息: %s\n", clientAddr, message)
// 7. 处理客户端请求并返回响应
response := fmt.Sprintf("服务器已收到: %s\n", message)
_, err = conn.Write([]byte(response))
if err != nil {
log.Println("发送响应错误:", err)
return
}
}
}
TCP 客户端示例
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
)
func main() {
// 1. 连接到 TCP 服务器
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal("连接错误:", err)
}
defer conn.Close() // 确保连接被关闭
fmt.Println("已连接到服务器")
// 2. 创建带缓冲的读写器
writer := bufio.NewWriter(conn)
reader := bufio.NewReader(os.Stdin) // 从标准输入读取
for {
fmt.Print("请输入消息 (输入 'quit' 退出): ")
// 3. 从用户输入读取消息
message, _ := reader.ReadString('\n')
// 检查是否退出
if message == "quit\n" || message == "quit\r\n" {
fmt.Println("断开连接...")
return
}
// 4. 发送消息到服务器
_, err := writer.WriteString(message)
if err != nil {
log.Println("发送错误:", err)
return
}
writer.Flush() // 确保数据被发送
// 5. 读取服务器响应
response, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
log.Println("读取响应错误:", err)
return
}
fmt.Printf("服务器响应: %s", response)
}
}
UDP 通信示例
package main
import (
"fmt"
"log"
"net"
)
func main() {
// UDP 服务器部分
go func() {
// 1. 创建 UDP 地址
addr, err := net.ResolveUDPAddr("udp", "localhost:8081")
if err != nil {
log.Fatal("解析地址错误:", err)
}
// 2. 监听 UDP 端口
conn, err := net.ListenPacket("udp", addr.String())
if err != nil {
log.Fatal("监听错误:", err)
}
defer conn.Close()
fmt.Println("UDP 服务器已启动,监听 localhost:8081")
// 3. 循环接收数据
buffer := make([]byte, 1024)
for {
n, clientAddr, err := conn.ReadFrom(buffer)
if err != nil {
log.Println("读取错误:", err)
continue
}
message := string(buffer[:n])
fmt.Printf("收到来自 %s 的消息: %s\n", clientAddr, message)
// 4. 发送响应
response := "UDP 服务器已收到消息"
_, err = conn.WriteTo([]byte(response), clientAddr)
if err != nil {
log.Println("发送响应错误:", err)
}
}
}()
// UDP 客户端部分
func() {
// 等待服务器启动
// time.Sleep(time.Second)
// 1. 创建 UDP 连接
serverAddr, err := net.ResolveUDPAddr("udp", "localhost:8081")
if err != nil {
log.Fatal("解析地址错误:", err)
}
conn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
log.Fatal("拨号错误:", err)
}
defer conn.Close()
// 2. 发送数据
message := "Hello UDP Server!"
_, err = conn.Write([]byte(message))
if err != nil {
log.Fatal("发送错误:", err)
}
fmt.Printf("已发送: %s\n", message)
// 3. 接收响应
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
log.Fatal("读取错误:", err)
}
fmt.Printf("服务器响应: %s\n", string(buffer[:n]))
}()
// 阻塞主程序
select {}
}
实践练习
练习 1:回显服务器增强版(简单)
要求:
- 修改 TCP 服务器代码,使其能回显客户端消息,并添加时间戳
- 当客户端发送 "time" 命令时,返回当前服务器时间
- 当客户端发送 "quit" 时,主动关闭该连接
预期输出示例:
客户端发送: Hello Server
服务器响应: [14:30:25] 回显: Hello Server
客户端发送: time
服务器响应: [14:30:28] 当前时间: 2024-01-15 14:30:28
客户端发送: quit
客户端断开连接
练习 2:简单的文件传输(中等)
要求:
- 实现一个 TCP 文件传输服务器,客户端可以请求特定文件
- 服务器检查文件是否存在,存在则发送文件内容,否则返回错误
- 使用固定长度的头部表示文件大小,然后发送文件内容
提示:
- 文件大小可以用 4 字节的二进制整数表示
- 先发送头部(文件大小),再发送文件内容
- 客户端根据头部信息接收完整文件
练习 3:UDP 广播聊天室(困难)
要求:
- 实现一个基于 UDP 的简易聊天室
- 服务器维护客户端地址列表
- 当收到某个客户端的消息时,将消息转发给所有其他客户端
- 客户端可以发送 "join"、"leave"、"message" 命令
预期行为:
- 新客户端加入时,通知其他用户
- 用户离开时,从列表移除并通知
- 所有聊天消息被广播
常见错误
1. 忘记关闭连接
// 错误示例:连接没有关闭,会导致资源泄露
conn, _ := net.Dial("tcp", "localhost:8080")
conn.Write([]byte("Hello"))
// 缺少 conn.Close()
// 正确做法:使用 defer 确保关闭
conn, _ := net.Dial("tcp", "localhost:8080")
defer conn.Close()
2. 混淆 TCP 和 UDP 的使用场景
// 错误示例:在需要可靠传输的场景使用 UDP
// 发送重要数据库更新
udpConn.WriteTo([]byte("UPDATE users SET..."), serverAddr) // 可能丢失!
// 正确做法:重要数据使用 TCP
tcpConn.Write([]byte("UPDATE users SET..."))
3. 不处理网络错误
// 错误示例:忽略错误可能导致程序异常
conn, _ := net.Dial("tcp", "localhost:8080") // 不检查 err
n, _ := conn.Read(buffer) // 不检查是否读取完整
// 正确做法:始终检查错误
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal("连接失败:", err)
}
4. 并发访问连接不安全
// 错误示例:多个 goroutine 同时写入同一连接
go func() {
conn.Write([]byte("消息1")) // 可能与其他 goroutine 写入交错
}()
go func() {
conn.Write([]byte("消息2"))
}()
// 正确做法:使用互斥锁或通道同步
var mu sync.Mutex
mu.Lock()
conn.Write([]byte("消息"))
mu.Unlock()
5. 缓冲区大小不当
// 错误示例:缓冲区太小导致消息截断
buffer := make([]byte, 10) // 只能接收 10 字节
conn.Read(buffer) // 长消息会被截断
// 正确做法:根据协议设计合适的缓冲区
buffer := make([]byte, 4096) // 4KB 缓冲区
小结
关键要点回顾
-
TCP vs UDP:
- TCP:可靠、有序、流式传输,适合需要数据完整性的应用
- UDP:快速、无连接、数据报传输,适合实时性要求高的应用
-
Go 网络编程核心:
net.Dial():创建客户端连接net.Listen():创建服务器监听net.Conn接口:统一的网络连接抽象bufio包:提高网络 I/O 效率
-
并发处理模式:
- 每个客户端连接使用独立的 goroutine 处理
- 使用
defer确保连接和资源被正确释放 - 在并发环境中注意连接的安全访问
-
最佳实践:
- 始终检查错误并适当处理
- 使用带缓冲的读写器优化性能
- 合理设计协议和消息格式
- 注意资源清理,避免泄露
-
下一步学习:
- 了解 TLS/SSL 安全连接
- 学习 HTTP/2 等高级协议
- 探索 WebSocket 等实时通信技术
- 研究 gRPC 等 RPC 框架
性能提示
- 对于高并发服务器,考虑使用
net.Listener的非阻塞模式 - 使用连接池复用 TCP 连接,减少握手开销
- 对于 UDP 应用,实现自己的可靠性和重传机制(如 QUIC 协议)
通过本课学习,你已经掌握了 Go 语言网络编程的基础知识。在接下来的课程中,我们将学习 WebSocket 实时通信,这建立在本课的 TCP 基础之上。
练习编辑器
go
Loading...