44·网络编程进阶

TCP/UDP 网络编程

tcpudpnetconn

第 44 课:TCP/UDP 网络编程

学习目标

完成本课学习后,你将能够:

  1. 理解 TCP 和 UDP 协议的基本特点与适用场景
  2. 掌握 Go 语言 net 包中 TCP 连接的核心操作
  3. 实现一个简单的 TCP 服务器与客户端程序
  4. 了解 UDP 无连接通信的编程模式
  5. 学会处理网络编程中的基本错误和资源清理

核心概念

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:回显服务器增强版(简单)

要求

  1. 修改 TCP 服务器代码,使其能回显客户端消息,并添加时间戳
  2. 当客户端发送 "time" 命令时,返回当前服务器时间
  3. 当客户端发送 "quit" 时,主动关闭该连接

预期输出示例

客户端发送: Hello Server
服务器响应: [14:30:25] 回显: Hello Server

客户端发送: time
服务器响应: [14:30:28] 当前时间: 2024-01-15 14:30:28

客户端发送: quit
客户端断开连接

练习 2:简单的文件传输(中等)

要求

  1. 实现一个 TCP 文件传输服务器,客户端可以请求特定文件
  2. 服务器检查文件是否存在,存在则发送文件内容,否则返回错误
  3. 使用固定长度的头部表示文件大小,然后发送文件内容

提示

  • 文件大小可以用 4 字节的二进制整数表示
  • 先发送头部(文件大小),再发送文件内容
  • 客户端根据头部信息接收完整文件

练习 3:UDP 广播聊天室(困难)

要求

  1. 实现一个基于 UDP 的简易聊天室
  2. 服务器维护客户端地址列表
  3. 当收到某个客户端的消息时,将消息转发给所有其他客户端
  4. 客户端可以发送 "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 缓冲区

小结

关键要点回顾

  1. TCP vs UDP

    • TCP:可靠、有序、流式传输,适合需要数据完整性的应用
    • UDP:快速、无连接、数据报传输,适合实时性要求高的应用
  2. Go 网络编程核心

    • net.Dial():创建客户端连接
    • net.Listen():创建服务器监听
    • net.Conn 接口:统一的网络连接抽象
    • bufio 包:提高网络 I/O 效率
  3. 并发处理模式

    • 每个客户端连接使用独立的 goroutine 处理
    • 使用 defer 确保连接和资源被正确释放
    • 在并发环境中注意连接的安全访问
  4. 最佳实践

    • 始终检查错误并适当处理
    • 使用带缓冲的读写器优化性能
    • 合理设计协议和消息格式
    • 注意资源清理,避免泄露
  5. 下一步学习

    • 了解 TLS/SSL 安全连接
    • 学习 HTTP/2 等高级协议
    • 探索 WebSocket 等实时通信技术
    • 研究 gRPC 等 RPC 框架

性能提示

  • 对于高并发服务器,考虑使用 net.Listener 的非阻塞模式
  • 使用连接池复用 TCP 连接,减少握手开销
  • 对于 UDP 应用,实现自己的可靠性和重传机制(如 QUIC 协议)

通过本课学习,你已经掌握了 Go 语言网络编程的基础知识。在接下来的课程中,我们将学习 WebSocket 实时通信,这建立在本课的 TCP 基础之上。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「WebSocket 实时通信」 以巩固所学知识。