Go语言网络编程

📅 2026/7/19 3:31:39
Go语言网络编程
Go语言网络编程完全指南从协议基础到TCP/UDP实战前言现在我们几乎每天都在使用互联网但你是否想过程序之间是如何通过网络互相通信的Go语言作为一门以高并发为核心优势的语言其标准库net包提供了非常简洁而强大的网络编程接口。本文将从最基础的互联网协议讲起一步步带你掌握Go语言中的TCP和UDP网络编程并深入探讨TCP粘包这一经典问题及其解决方案。 网络编程是一个非常庞大的领域本文聚焦于使用net包进行TCP和UDP通信的核心实践。互联网协议基础互联网分层模型互联网的核心是一系列协议总称为互联网协议Internet Protocol Suite。为了理解这些协议我们需要先了解分层模型。就像建筑物一样每一层都有自己的功能每一层都靠下一层支持。用户接触到的只是最上面的那一层根本不会感觉到下面的几层。OSI七层模型 vs TCP/IP五层模型┌─────────────────────────────────────────────────────────────┐ │ OSI七层模型 │ ├─────────────┬─────────────────┬─────────────────────────────┤ │ 应用层 │ 应用层 │ 应用层 │ │ 表示层 │ │ 表示层 │ │ 会话层 │ │ 会话层 │ ├─────────────┼─────────────────┼─────────────────────────────┤ │ 传输层 │ 传输层 │ 传输层 │ ├─────────────┼─────────────────┼─────────────────────────────┤ │ 网络层 │ 网络层 │ 网络层 │ ├─────────────┼─────────────────┼─────────────────────────────┤ │ 数据链路层 │ 网络接口层 │ 数据链路层 │ │ 物理层 │ │ 物理层 │ └─────────────┴─────────────────┴─────────────────────────────┘ ↑ ↑ ↑ 五层模型(教学用) TCP/IP四层模型 OSI七层模型越往上的层越靠近用户越往下的层越靠近硬件。在软件开发中我们使用最多的是五层模型。各层核心功能速览层级核心功能关键概念物理层负责传送0和1的电信号双绞线、光纤、无线电波数据链路层规定解读电信号的方式以太网(Ethernet)、MAC地址、帧(Frame)网络层区分不同子网络实现跨网络通信IP地址、IP数据包、路由器传输层区分同一主机上的不同程序端口(Port)、TCP/UDP协议应用层规定应用程序使用的数据格式HTTP、FTP、SMTP、Email 关键概念解析MAC地址每块网卡出厂时都有一个全球唯一的MAC地址48位二进制通常用12个十六进制数表示。前6位是厂商编号后6位是网卡流水号。IP地址网络层引入的新地址用于区分不同的子网络。IPv4由32位二进制组成通常表示为四段十进制数如192.168.1.1。端口(Port)0~65535之间的整数用于标识同一台主机上的不同程序。0~1023被系统占用用户只能使用大于1023的端口。数据封装与解封装发送方的HTTP数据经过互联网传输时会依次添加各层协议的标头信息接收方收到数据包后再依次根据协议解包得到原始数据。发送方从上到下封装 接收方从下到上解封装 ​ HTTP数据 HTTP数据 ↓ ↑ TCP首部 ───────→ 传输层 ───────→ - TCP首部 ↓ ↑ IP首部 ───────→ 网络层 ───────→ - IP首部 ↓ ↑ 以太网首部 ──────→ 数据链路层 ──────→ - 以太网首部 ↓ ↑ 双绞线/光纤/无线电波 ←──物理层──→ 双绞线/光纤/无线电波Socket编程原理什么是SocketSocket套接字是BSD UNIX的进程通信机制用于描述IP地址和端口是一个通信链的句柄。Socket可以理解为TCP/IP网络的API。它定义了许多函数或例程程序员可以用它们来开发TCP/IP网络上的应用程序。Socket的本质门面模式Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中Socket其实就是一个门面模式Facade Pattern┌─────────────────────────────────────────┐ │ 应用层用户进程 │ │ [QQ] [浏览器] [邮件客户端] [游戏] │ └──────────────┬────────────────────────────┘ ↓ ┌──────────────┐ │ Socket抽象层 │ ← 门面模式隐藏底层复杂性 └──────┬───────┘ ↓ ┌──────────────┼────────────────────────────┐ │ 传输层 ↓ │ │ [TCP] ←────┘──→ [UDP] │ ├──────────────┼────────────────────────────┤ │ 网络层 ↓ │ │ [IP] ←──[ICMP]──→ [IGMP] │ ├──────────────┼────────────────────────────┤ │ 数据链路层 [ARP] ←──→ [硬件接口] ←──→ [RARP] │ ├──────────────┼────────────────────────────┤ │ 物理层 ↓ │ │ [传输介质] │ └──────────────┴────────────────────────────┘对用户来说只需要调用Socket规定的相关函数让Socket去组织符合指定协议的数据然后进行通信。Go实现TCP通信TCP协议简介TCP/IPTransmission Control Protocol/Internet Protocol即传输控制协议/网间协议是一种✅面向连接连接导向✅可靠的✅基于字节流的传输层通信协议⚠️ 因为是面向连接的协议数据像水流一样传输会存在黏包问题。TCP服务端一个TCP服务端可以同时连接很多个客户端。Go语言中创建多个goroutine实现并发非常方便和高效所以我们可以每建立一次连接就创建一个goroutine去处理。服务端处理流程1. 监听端口 (net.Listen) ↓ 2. 接收客户端请求建立链接 (listen.Accept) ↓ 3. 创建goroutine处理链接 (go process(conn))TCP服务端代码// tcp/server/main.go package main ​ import ( bufio fmt net ) ​ // process 处理函数 - 每个连接一个goroutine func process(conn net.Conn) { defer conn.Close() // 函数结束时关闭连接 for { reader : bufio.NewReader(conn) var buf [128]byte n, err : reader.Read(buf[:]) // 读取数据 if err ! nil { fmt.Println(read from client failed, err:, err) break } recvStr : string(buf[:n]) fmt.Println(收到client端发来的数据: , recvStr) conn.Write([]byte(recvStr)) // 发送数据回显 } } ​ func main() { // 1. 监听端口 listen, err : net.Listen(tcp, 127.0.0.1:20000) if err ! nil { fmt.Println(listen failed, err:, err) return } defer listen.Close() ​ // 2. 循环接收连接 for { conn, err : listen.Accept() // 建立连接 if err ! nil { fmt.Println(accept failed, err:, err) continue } // 3. 启动一个goroutine处理连接 go process(conn) } }核心API解析net.Listen(tcp, 127.0.0.1:20000)创建TCP监听器listen.Accept()阻塞等待客户端连接go process(conn)并发处理每个连接这是Go的杀手锏TCP客户端客户端处理流程1. 建立与服务端的链接 (net.Dial) ↓ 2. 进行数据收发 (conn.Write / conn.Read) ↓ 3. 关闭链接 (conn.Close)TCP客户端代码// tcp/client/main.go package main ​ import ( bufio fmt net os strings ) ​ func main() { // 1. 连接到服务端 conn, err : net.Dial(tcp, 127.0.0.1:20000) if err ! nil { fmt.Println(err :, err) return } defer conn.Close() // 关闭连接 ​ inputReader : bufio.NewReader(os.Stdin) for { // 读取用户输入 input, _ : inputReader.ReadString(\n) inputInfo : strings.Trim(input, \r\n) if strings.ToUpper(inputInfo) Q { // 输入q退出 return } // 2. 发送数据 _, err conn.Write([]byte(inputInfo)) if err ! nil { return } // 接收服务端响应 buf : [512]byte{} n, err : conn.Read(buf[:]) if err ! nil { fmt.Println(recv failed, err:, err) return } fmt.Println(string(buf[:n])) } }运行方式先编译运行服务端go run server/main.go再编译运行客户端go run client/main.go在客户端输入内容回车服务端会回显数据TCP粘包问题与解决方案什么是粘包粘包是指发送方发送的多个小数据包在接收方被粘在了一起当作一个大数据包接收。粘包示例客户端代码连续发送20次消息// socket_stick/client/main.go func main() { conn, err : net.Dial(tcp, 127.0.0.1:30000) if err ! nil { fmt.Println(dial failed, err, err) return } defer conn.Close() for i : 0; i 20; i { msg : Hello, Hello. How are you? conn.Write([]byte(msg)) } }服务端输出数据被粘在一起收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?... 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?... ...客户端分20次发送的数据在服务端并没有成功输出20次而是多条数据粘到了一起。为什么会出现粘包主要原因就是TCP数据传递模式是流模式在保持长连接的时候可以进行多次的收和发。粘包可发生在发送端也可发生在接收端位置原因说明发送端Nagle算法TCP不立刻发送小段数据而是等待一小段时间若有更多数据则合并发送接收端接收不及时TCP把接收到的数据存在缓冲区应用层未及时取出导致多段数据累积解决方案自定义协议封包/拆包出现粘包的关键在于接收方不确定将要传输的数据包的大小。因此我们可以对数据包进行封包和拆包的操作封包给一段数据加上包头数据包分为包头和包体两部分。包头长度固定存储了包体的长度。自定义协议实现// socket_stick/proto/proto.go package proto ​ import ( bufio bytes encoding/binary ) ​ // Encode 将消息编码封包 func Encode(message string) ([]byte, error) { // 读取消息的长度转换成int32类型占4个字节 var length int32(len(message)) var pkg new(bytes.Buffer) // 写入消息头4字节长度 err : binary.Write(pkg, binary.LittleEndian, length) if err ! nil { return nil, err } // 写入消息实体 err binary.Write(pkg, binary.LittleEndian, []byte(message)) if err ! nil { return nil, err } return pkg.Bytes(), nil } ​ // Decode 解码消息拆包 func Decode(reader *bufio.Reader) (string, error) { // 1. 读取消息的长度前4个字节 lengthByte, _ : reader.Peek(4) lengthBuff : bytes.NewBuffer(lengthByte) var length int32 err : binary.Read(lengthBuff, binary.LittleEndian, length) if err ! nil { return , err } // 2. 检查缓冲区中是否有足够的数据 if int32(reader.Buffered()) length4 { return , err } // 3. 读取真正的消息数据 pack : make([]byte, int(4length)) _, err reader.Read(pack) if err ! nil { return , err } return string(pack[4:]), nil }使用自定义协议的服务端// socket_stick/server2/main.go func process(conn net.Conn) { defer conn.Close() reader : bufio.NewReader(conn) for { // 使用proto.Decode拆包 msg, err : proto.Decode(reader) if err io.EOF { return } if err ! nil { fmt.Println(decode msg failed, err:, err) return } fmt.Println(收到client发来的数据: , msg) } } ​ func main() { listen, err : net.Listen(tcp, 127.0.0.1:30000) // ... 省略错误处理和accept循环 }使用自定义协议的客户端// socket_stick/client2/main.go func main() { conn, err : net.Dial(tcp, 127.0.0.1:30000) // ... for i : 0; i 20; i { msg : Hello, Hello. How are you? // 使用proto.Encode封包 data, err : proto.Encode(msg) if err ! nil { fmt.Println(encode msg failed, err:, err) return } conn.Write(data) } }协议设计要点前4个字节为包头存储消息长度int32接收方先读取4字节获取长度再根据长度读取完整消息完美解决粘包问题Go实现UDP通信UDP协议简介UDPUser Datagram Protocol中文名称是用户数据报协议是OSI参考模型中一种无连接的传输层协议。特性说明连接方式不需要建立连接就能直接发送和接收可靠性不可靠不保证数据到达时序性没有时序保证实时性✅ 实时性比较好典型应用视频直播、实时通信、DNSUDP服务端// UDP/server/main.go package main ​ import ( fmt net ) ​ func main() { // 监听UDP端口 listen, err : net.ListenUDP(udp, net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), // 0.0.0.0 监听所有网卡 Port: 30000, }) if err ! nil { fmt.Println(listen failed, err:, err) return } defer listen.Close() ​ for { var data [1024]byte // 接收数据同时获取发送方地址 n, addr, err : listen.ReadFromUDP(data[:]) if err ! nil { fmt.Println(read udp failed, err:, err) continue } fmt.Printf(data:%v addr:%v count:%v\n, string(data[:n]), addr, n) // 发送数据回显给发送方 _, err listen.WriteToUDP(data[:n], addr) if err ! nil { fmt.Println(write to udp failed, err:, err) continue } } }核心API解析net.ListenUDP(udp, net.UDPAddr{...})创建UDP监听器ReadFromUDP(data)接收数据返回数据长度、发送方地址、错误WriteToUDP(data, addr)向指定地址发送数据UDP客户端// UDP/client/main.go package main ​ import ( fmt net ) ​ func main() { // 创建UDP连接指定服务端地址 socket, err : net.DialUDP(udp, nil, net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err ! nil { fmt.Println(连接服务端失败, err:, err) return } defer socket.Close() ​ // 发送数据 sendData : []byte(Hello server) _, err socket.Write(sendData) if err ! nil { fmt.Println(发送数据失败, err:, err) return } ​ // 接收数据 data : make([]byte, 4096) n, remoteAddr, err : socket.ReadFromUDP(data) if err ! nil { fmt.Println(接收数据失败, err:, err) return } fmt.Printf(recv:%v addr:%v count:%v\n, string(data[:n]), remoteAddr, n) }TCP vs UDP 对比总结特性TCPUDP连接方式面向连接三次握手无连接可靠性✅ 可靠保证数据不丢失、不重复、按序到达❌ 不可靠可能丢失、乱序传输效率较低需要确认、重传、流量控制较高直接发送无需确认实时性较低✅ 较高数据边界字节流无边界需处理粘包数据报有边界一次发送一次接收资源消耗较高较低典型应用HTTP、FTP、邮件、文件传输视频直播、DNS、实时游戏、VoIPGo实现net.Listennet.Dialnet.ListenUDPnet.DialUDP总结本文从互联网协议的分层模型出发逐步深入到Go语言的Socket编程实践理解分层模型→ 物理层到应用层各司其职掌握Socket抽象→ 它是应用层与TCP/IP协议族的门面TCP编程→ 面向连接、可靠传输但要注意粘包问题粘包解决→ 自定义协议长度前缀消息体是最通用的方案UDP编程→ 无连接、高效率适合实时场景Go语言的net包设计得非常优雅配合goroutine的并发模型让网络编程变得异常简单。无论是构建高并发的TCP服务器还是实现低延迟的UDP通信Go都能轻松胜任。