《Build Web Application with Golang》精读:Go Socket 网络编程实战(TCP/UDP 客户端与服务器)
发布时间:2026/10/1 8:48:21来源:尧图网络
文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载本文基于开源 Go 电子书 Build Web Application with Golang仓库英文版第 8.1 节《Sockets》展开系统讲解 Socket 的起源、通信原理与寻址模型并围绕 Go 标准库net包给出完整的 TCP 客户端/服务器、并发处理、长连接控制与 UDP 通信的可运行代码。读完本文你将掌握ResolveTCPAddr/DialTCP/ListenTCP与ResolveUDPAddr/DialUDP/ListenUDP的完整调用链能够独立用 Go 实现一个基于 Socket 的时间同步服务、HTTP 探测客户端等实战程序。认识 Socket一切皆文件的网络抽象Socket套接字起源于 Unix。基于 Unix 一切皆文件everything is a file的基本哲学一切对象都可以用「打开 - 写入/读取 - 关闭」的方式操作Socket 正是这一哲学在网络领域的实现。就像打开文件一样Socket 也有一系列打开操作最终返回一个int类型的描述符descriptor后续创建连接、传输数据等都围绕该描述符进行。日常使用中最常见的两种 Socket 类型是流式 SocketSOCK_STREAM面向连接等价于 TCP先建立连接再按序传输字节流数据报 SocketSOCK_DGRAM不建立连接等价于 UDP直接以报文为单位收发数据。许多现代网络应用正是依赖这两类 Socket 完成数据传输浏览器与 Web 服务器之间的通信、即时聊天工具中消息的实时转发底层都离不开 Socket。这也解释了为何部分网络开发者认为上层应用几乎都是 Socket 编程。Socket 通信与唯一性原理要让两个 Socket 建立可靠的通信通道首先必须保证每个 Socket 是唯一的。在本机范围内可以给每个进程分配唯一的 PID 来区分但 PID 无法跨网络生效。TCP/IP 协议族恰好解决了这一难题网络层的IP 地址在主机网络中唯一传输层的协议 端口在主机内应用间唯一。两者组合即可得到全局唯一的 Socket 端点。下图展示了 Socket 在整个网络协议栈中的位置——它位于应用层与传输层TCP/UDP之间是所有基于 TCP/IP 的应用与内核网络协议交互的通用接口。从上图可以看出用户应用如 FTP、HTTP 服务器通过 Socket Application Interface 接入传输层向下依次经过 TCP/UDP、IP 网络层最终由 PPP串行线路或以太网Ethernet/IEEE 802.3等链路层承载。换言之基于 TCP/IP 的应用无论采用何种语言其代码中都以这样或那样的方式调用了 Socket API——这也是一切都是 Socket说法的由来。Socket 基础知识IP 寻址模型Socket 通信需要协议 IP 地址 端口号三要素。IP 地址是网络层TCP/IP 的核心部分的寻址标识目前存在 IPv4 与 IPv6 两代。IPv4全球互联网以 TCP/IP 为协议基础IPv4 即第 4 版 IP。IPv4 地址长度为32 位理论上最多可让2^32台设备同时接入互联网。随着互联网的高速发展IPv4 地址近年已接近枯竭。其地址形如127.0.0.1 172.122.121.111IPv6IPv6 是下一代互联网协议旨在解决 IPv4 的诸多固有问题。IPv6 地址长度为128 位地址空间极其充裕——按文档中的估算地球上每平方米可分配到超过 1000 个 IPv6 地址几乎无需再担心地址短缺。同时IPv6 在端到端直连peer to peer、服务质量QoS、安全性、组播multicast等方面也做了改进。其地址形如2002:c0e8:82e7:0:0:0:c0e8:82e7Go 中的 IP 类型Go 标准库net包提供了丰富的网络编程类型、函数与方法。其中 IP 的定义是一个字节切片type IP []byteParseIP(s string) IP函数负责把 IPv4 或 IPv6 格式的字符串转换为IP对象。原文给出如下示例——通过命令行参数传入一个 IP 地址字符串解析后打印其规范化形式package main import ( fmt net os ) func main() { if len(os.Args) ! 2 { fmt.Fprintf(os.Stderr, Usage: %s ip-addr\n, os.Args[0]) os.Exit(1) } name : os.Args[1] addr : net.ParseIP(name) if addr nil { fmt.Println(Invalid address) } else { fmt.Println(The address is , addr.String()) } os.Exit(0) }运行方式go run main.go 127.0.0.1 # The address is 127.0.0.1若传入非法地址ParseIP返回nil程序输出Invalid address。addr.String()会把解析后的字节序列还原为标准的点分十进制或冒号分隔的 IPv6 文本形式。TCP Socket 编程知道了如何通过网络端口访问 Web 服务后剩下的事情就很清晰了作为客户端向指定网络端口发送请求并读取响应作为服务器将服务绑定到指定网络端口等待客户端请求并给出响应。Go 的net包通过TCPConn类型承载这种客户端/服务器交互其核心是两个方法func (c *TCPConn) Write(b []byte) (n int, err error) func (c *TCPConn) Read(b []byte) (n int, err error)无论客户端还是服务器都通过同一个TCPConn对象读写数据。注意原文档写作时使用的os.Error类型在现代 Go 中已统一为error下文示例均按现行 API 编写。TCPAddr则用于表示 TCP 地址信息type TCPAddr struct { IP IP Port int }通过ResolveTCPAddr函数可以把地址字符串解析为TCPAddrfunc ResolveTCPAddr(net, addr string) (*TCPAddr, error)参数说明net可选tcp4、tcp6或tcp分别表示仅 IPv4、仅 IPv6、IPv4/IPv6 皆可addr可以是域名或 IP 地址加端口例如www.google.com:80、127.0.0.1:22。编写 TCP 客户端Go 客户端使用net包的DialTCP函数创建 TCP 连接返回TCPConn对象连接建立后服务器端也持有同一个连接的TCPConn双方即可开始交换数据。通常客户端通过TCPConn发送请求并读取服务器响应服务器则读取并解析客户端请求后返回反馈。连接会一直保持直到某一方主动关闭。创建连接的原型为func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error)参数说明net同样为tcp4、tcp6或tcpladdr本地地址大多数场景直接设为nilraddr远程目标地址。下面用一个模拟 HTTP 请求的客户端示例客户端向服务器发送一行简单的 HTTP 请求头然后读取并打印服务器响应。请求头内容为HEAD / HTTP/1.0\r\n\r\n服务器响应信息的格式可能如下HTTP/1.0 200 OK ETag: -9985996 Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT Content-Length: 18074 Connection: close Date: Sat, 28 Aug 2010 00:43:48 GMT Server: lighttpd/1.4.23客户端完整代码package main import ( fmt io/ioutil net os ) func main() { if len(os.Args) ! 2 { fmt.Fprintf(os.Stderr, Usage: %s host:port , os.Args[0]) os.Exit(1) } service : os.Args[1] tcpAddr, err : net.ResolveTCPAddr(tcp4, service) checkError(err) conn, err : net.DialTCP(tcp, nil, tcpAddr) checkError(err) _, err conn.Write([]byte(HEAD / HTTP/1.0\r\n\r\n)) checkError(err) result, err : ioutil.ReadAll(conn) checkError(err) fmt.Println(string(result)) os.Exit(0) } func checkError(err error) { if err ! nil { fmt.Fprintf(os.Stderr, Fatal error: %s, err.Error()) os.Exit(1) } }执行流程梳理将命令行参数host:port如localhost:80作为service传入net.ResolveTCPAddr(tcp4, service)得到tcpAddr把tcpAddr传给DialTCP创建 TCP 连接conn通过conn.Write发送HEAD / HTTP/1.0\r\n\r\n请求用ioutil.ReadAll(conn)读取全部响应内容并打印。编写 TCP 服务器服务器端需要把服务绑定到指定端口并监听客户端连接核心函数为func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err error) func (l *TCPListener) Accept() (c Conn, err error)参数含义与DialTCP一致。下面实现一个监听7777端口的时间同步服务package main import ( fmt net os time ) func main() { service : :7777 tcpAddr, err : net.ResolveTCPAddr(tcp4, service) checkError(err) listener, err : net.ListenTCP(tcp, tcpAddr) checkError(err) for { conn, err : listener.Accept() if err ! nil { continue } daytime : time.Now().String() conn.Write([]byte(daytime)) // dont care about return value conn.Close() // were finished with this client } } func checkError(err error) { if err ! nil { fmt.Fprintf(os.Stderr, Fatal error: %s, err.Error()) os.Exit(1) } }服务启动后即进入循环等待客户端请求每收到一个连接就Accept它把当前时间字符串写回客户端后关闭连接。值得注意的设计点是for循环中若Accept出错程序并不退出而是continue继续监听——服务器不会因为单个连接的异常而崩溃而是将错误记录下来继续服务。用 goroutine 实现并发处理上面的代码还不够好它没有使用 goroutine无法同时处理多个请求。将业务处理逻辑抽到独立的handleClient函数中并用go关键字为每个连接启动一个协程服务即具备并发能力package main import ( fmt net os time ) func main() { service : :1200 tcpAddr, err : net.ResolveTCPAddr(tcp4, service) checkError(err) listener, err : net.ListenTCP(tcp, tcpAddr) checkError(err) for { conn, err : listener.Accept() if err ! nil { continue } go handleClient(conn) } } func handleClient(conn net.Conn) { defer conn.Close() daytime : time.Now().String() conn.Write([]byte(daytime)) // dont care about return value // were finished with this client } func checkError(err error) { if err ! nil { fmt.Fprintf(os.Stderr, Fatal error: %s, err.Error()) os.Exit(1) } }通过把业务过程拆分进handleClient并使用go关键字原本串行的服务立刻变成了并发服务。这正是 goroutine 简洁而强大的体现——服务器主循环只管Accept每个连接的处理都在独立协程中完成互不阻塞。在单条连接上处理多个请求上面的服务器每次连接只应答一次。如果需要在同一条连接上按请求内容返回不同格式的时间例如客户端发送timestamp时返回 Unix 时间戳否则返回完整时间字符串则需要持续读取连接直到超时或客户端关闭。示例代码如下package main import ( fmt net os strconv time ) func main() { service : :1200 tcpAddr, err : net.ResolveTCPAddr(tcp4, service) checkError(err) listener, err : net.ListenTCP(tcp, tcpAddr) checkError(err) for { conn, err : listener.Accept() if err ! nil { continue } go handleClient(conn) } } func handleClient(conn net.Conn) { conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout request : make([]byte, 128) // set maximum request length to 128B to prevent flood based attacks defer conn.Close() // close connection before exit for { read_len, err : conn.Read(request) if err ! nil { fmt.Println(err) break } if read_len 0 { break // connection already closed by client } else if string(request[:read_len]) timestamp { daytime : strconv.FormatInt(time.Now().Unix(), 10) conn.Write([]byte(daytime)) } else { daytime : time.Now().String() conn.Write([]byte(daytime)) } } } func checkError(err error) { if err ! nil { fmt.Fprintf(os.Stderr, Fatal error: %s, err.Error()) os.Exit(1) } }这个版本的关键设计持续读取使用conn.Read()循环读取客户端请求。因为客户端可能连续发出多次请求所以不能在第一次读取后立即关闭连接读取超时通过conn.SetReadDeadline(time.Now().Add(2 * time.Minute))设置 2 分钟超时超时后Read返回错误程序break退出循环连接随之自动关闭配合defer conn.Close()防洪水攻击request : make([]byte, 128)将单次请求上限限制为 128 字节避免客户端一次写入海量数据拖垮服务器请求分派当读取内容为timestamp时返回strconv.FormatInt(time.Now().Unix(), 10)Unix 秒级时间戳否则返回time.Now().String()的完整时间格式。控制 TCP 连接net包还提供了一套连接控制函数可用于客户端和服务器两端func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)用于设置连接建立的超时时间。针对单条连接可以分别设置读写超时func (c *TCPConn) SetReadDeadline(t time.Time) error func (c *TCPConn) SetWriteDeadline(t time.Time) error此外还有func (c *TCPConn) SetKeepAlive(keepalive bool) error用于启用/关闭连接的 keep-alive 保活机制。设计连接超时时需要权衡长连接可以减少频繁创建连接的开销特别适合需要高频交换数据的应用但过长的空闲连接也会占用服务器资源因此要根据业务特征为读写、拨号分别设定合理的超时时间。更多细节可查阅 Go 标准库net包的官方文档。UDP Socket 编程UDP Socket 与 TCP Socket 的唯一区别在于服务器端对多请求的处理方式——UDP 没有类似Accept的函数其余函数只需把TCP替换为UDP即可。核心 API 如下func ResolveUDPAddr(net, addr string) (*UDPAddr, error) func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err error) func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err error) func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error) func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err error)其中ReadFromUDP/WriteToUDP之所以需要携带addr参数正是因为 UDP 无连接——服务器必须从报文中得知该响应发给谁。UDP 客户端示例package main import ( fmt net os ) func main() { if len(os.Args) ! 2 { fmt.Fprintf(os.Stderr, Usage: %s host:port, os.Args[0]) os.Exit(1) } service : os.Args[1] udpAddr, err : net.ResolveUDPAddr(udp4, service) checkError(err) conn, err : net.DialUDP(udp, nil, udpAddr) checkError(err) _, err conn.Write([]byte(anything)) checkError(err) var buf [512]byte n, err : conn.Read(buf[0:]) checkError(err) fmt.Println(string(buf[0:n])) os.Exit(0) } func checkError(err error) { if err ! nil { fmt.Fprintf(os.Stderr, Fatal error: %s, err.Error()) os.Exit(1) } }流程说明先用ResolveUDPAddr(udp4, service)解析目标地址再用DialUDP建立 UDP 连接对象connUDP 的连接仅是逻辑上的收发端点写入任意数据后从[512]byte缓冲区读取服务器回包并打印。需要说明的是原文档 UDP 示例的错误处理函数缺少格式占位符Fatal error 此处已按 TCP 示例统一修正为Fatal error: %s以保证代码可编译运行。UDP 服务器示例package main import ( fmt net os time ) func main() { service : :1200 udpAddr, err : net.ResolveUDPAddr(udp4, service) checkError(err) conn, err : net.ListenUDP(udp, udpAddr) checkError(err) for { handleClient(conn) } } func handleClient(conn *net.UDPConn) { var buf [512]byte _, addr, err : conn.ReadFromUDP(buf[0:]) if err ! nil { return } daytime : time.Now().String() conn.WriteToUDP([]byte(daytime), addr) } func checkError(err error) { if err ! nil { fmt.Fprintf(os.Stderr, Fatal error: %s, err.Error()) os.Exit(1) } }UDP 服务器把handleClient放在for循环中反复调用每次用ReadFromUDP从同一个UDPConn上读到一个数据报及其来源地址addr然后用WriteToUDP把当前时间发回该地址。与 TCP 不同UDP 服务器无需为每个客户端维护独立连接所有客户端共享同一个监听 Socket天然免去了连接管理开销——代价则是无连接、不保证送达顺序与可靠性。小结通过编写 TCP 与 UDP 的客户端、服务器、并发版与长连接多请求版等一系列小程序可以看到 Go 对 Socket 编程提供了出色的支持net包以Resolve*Addr→Dial*/Listen*→Read/Write的统一调用链封装了底层网络细节配合 goroutine 可以极简地实现高并发服务配合SetReadDeadline/SetWriteDeadline/DialTimeout等函数又能精细控制连接行为。可以说Socket 是 Go 构建高性能网络应用的基石。延伸阅读章节目录上一节Web 服务总览下一节WebSocket基于 Socket 的 HTML5 全双工实时通信后续章节RPC在 TCP/HTTP 之上实现远程过程调用更多章节仓库 README赞分享文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载相关推荐Go Web 服务编程全景Socket、WebSocket、REST 与 RPC 四大网络模型实战导览build-web-application-with-golang 第 8 章Go Web 服务编程全景Socket、WebSocket、REST 与 RPC 四大网络模型实战导览build web application with文档教程build-web-application-with-golang服务网格Istio与Go微服务治理build web application with golang服务网格Istio与Go微服务治理 随着Go语言在微服务开发中的广泛应用构建可靠、可扩展的文档教程Go Web 编程Build Web Application with Golang14.1Beego 静态文件服务与 Bootstrap 前端集成实战Go Web 编程Build Web Application with Golang14.1Beego 静态文件服务与 Bootstrap 前端集成实战文档教程上一篇Kindle电子书封面损坏的终极解决方案Fix-Kindle-Ebook-Cover完全指南下一篇解放双手macOS自动点击器完整指南让重复操作一键自动化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网