ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

使用 Go 进行 Socket 网络编程:TCP/UDP 客户端与服务端的完整实战

使用 Go 进行 Socket 网络编程:TCP/UDP 客户端与服务端的完整实战 文档教程【免费下载链接】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点击查看免费下载Socket套接字是 Go 网络编程的地基。本篇文章基于《build-web-application-with-golang》第 8 章第 1 节es/08.1.md展开系统讲解 socket 的由来与通信原理、IPv4/IPv6 基础、Gonet包中 IP 解析、TCP 客户端与服务端的完整写法、并发处理与连接控制以及 UDP 无连接编程范式。读完本文你将能独立用 Go 编写可运行的 TCP/UDP 客户端与服务端程序并理解长连接、读写超时、防溢出等关键工程细节。什么是 SocketSocket 起源于 Unix。在 Unix「一切皆文件」的哲学下任何资源都可以用「打开 → 写入/读取 → 关闭」的方式操作socket 正是这一哲学的产物。打开一个 socket 会返回一个整数描述符descriptor后续创建连接、传输数据等操作都通过它进行。常用 socket 分为两类流式 socketSOCK_STREAM面向连接典型代表是 TCP传输可靠、按序到达数据报 socketSOCK_DGRAM无连接典型代表是 UDP不保证可靠与顺序但开销更小。Socket 通信的标识原理要让两个 socket 可靠通信前提是每个 socket 必须唯一。本地环境下可以用进程 PID 区分但 PID 无法跨网络使用。TCP/IP 协议族给出了解决方案网络层IP 地址在主机网络中是唯一的「协议 端口号」在主机应用之间是唯一的。两者组合即可得到全局唯一的 socket 标识。下图展示了 socket 在 TCP/IP 协议栈中的位置它处于用户应用FTP、http-server、SNMP 等与传输层TCP、UDP之间向上屏蔽下层细节向下复用 IP、以太网等底层设施。所有基于 TCP/IP 的应用本质上都在以某种方式使用 socket API。这也是「一切皆 socket」说法的来源。Socket 基础知识IPv4 与 IPv6socket 唯一性依赖 IP 地址和端口因此先认识两种 IP 版本IPv4地址为 32 位理论可容纳 2^32 个地址例如127.0.0.1、172.122.121.111。由于互联网高速发展地址已接近枯竭IPv6下一代互联网协议地址为 128 位地址空间大到无需担心稀缺按地球面积估算每平方米可拥有超过 1000 个地址同时改善了端到端连接P2P、服务质量QoS、安全性、组播等能力。地址格式示例2002:c0e8:82e7:0:0:0:c0e8:82e7。Go 中的 IP 类型与解析Go 标准库net包提供了丰富的网络编程类型、函数与方法。IP 在 Go 中被定义为字节切片type IP []byteParseIP(s string) IP函数用于把 IPv4 或 IPv6 地址字符串解析为IP类型。下面的示例接收命令行参数输出解析后的规范化地址package main import ( net os fmt ) 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传入非法地址则会提示Invalid address。IP.String()负责把内部字节表示还原为可读的点分十进制或十六进制冒号格式。需要说明书中示例基于早期 Go 版本os.Error、os.Exit风格在现代 Go 中错误类型统一为error但net.ParseIP、IP.String()的语义与用法保持一致。TCP Socket 编程知道如何通过端口访问网络服务后就可以扮演两种角色客户端向指定端口发送请求并读取响应服务端把服务绑定到指定端口等待客户端请求并回送响应。Go 的net包提供了TCPConn类型来支撑这种客户端/服务端交互它有两个核心方法func (c *TCPConn) Write(b []byte) (n int, err error) func (c *TCPConn) Read(b []byte) (n int, err error)客户端和服务端共用同一套读写接口。表示 TCP 地址信息的是TCPAddrtype 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 客户端客户端用net包的DialTCP创建 TCP 连接返回*TCPConn。连接建立后服务端对当前连接持有同样类型的连接对象双方即可开始交换数据。该连接在任一方关闭前一直有效。func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error)参数说明net同ResolveTCPAddr可取tcp4、tcp6、tcpladdr本地地址多数场景传nil由系统自动分配raddr远程地址。下面用一个 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作为service参数传给ResolveTCPAddr得到tcpAddr再把它交给DialTCP建立 TCP 连接conn通过conn.Write发送请求头最后用ioutil.ReadAll一次性读取连接中的全部内容即服务端响应并打印。在更现代的 Go 中io/ioutil.ReadAll已迁移为io.ReadAll用法等价。TCP 服务端服务端同样基于net包实现把服务绑定到指定端口等待客户端连接。核心函数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)) // 不关心返回值 conn.Close() // 关闭与客户端的连接 } } func checkError(err error) { if err ! nil { fmt.Fprintf(os.Stderr, Fatal error: %s, err.Error()) os.Exit(1) } }注意service : :7777只写端口、省略 IP表示监听所有网卡。服务启动后进入for循环等待请求Accept接受客户端连接后立即回写当前时间字符串并关闭连接。for循环内的错误处理值得学习出错时continue服务继续运行而不是退出生产环境中应把错误记入服务端日志。用 goroutine 实现并发服务端上面的实现一次只能处理一个连接无法并发。把业务逻辑抽到handleClient函数用go关键字启动 goroutine就得到了并发版本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)) // 不关心返回值 // 结束与当前客户端的交互 } func checkError(err error) { if err ! nil { fmt.Fprintf(os.Stderr, Fatal error: %s, err.Error()) os.Exit(1) } }每接受一个连接就启动一个 goroutine 处理互不阻塞——这正是 Go 并发模型在 socket 编程上的简洁体现。单连接多请求读写超时与防溢出前面的服务端「一次请求即关闭」。如果客户端要在同一条连接里连续发送多个请求例如请求不同格式的时间就需要循环读取并保持连接package main import ( fmt net os time strconv ) 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)) // 设置两分钟读超时 request : make([]byte, 128) // 限制请求最大长度防止溢出攻击 defer conn.Close() // 退出前关闭连接 for { read_len, err : conn.Read(request) if err ! nil { fmt.Println(err) break } if read_len 0 { break // 客户端关闭了连接 } 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))设置读超时超时后Read返回错误程序跳出for循环并关闭连接request缓冲区固定为 128 字节限制单次请求长度防止恶意超长数据造成缓冲区溢出收到字符串timestamp时返回 Unix 时间戳秒否则返回完整时间字符串——演示了同一连接内的多分支响应。控制 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超时时间需要结合业务谨慎设计长连接可以省去频繁建连的开销对需要频繁交换信息的应用是好选择但也要配合保活与超时防止僵尸连接。更完整的 API 细节可查阅 Go 官方net包文档对应仓库内各语言版本的 08.1 章节如 en/08.1.md 与 zh/08.1.md。UDP Socket 编程UDP 与 TCP 的唯一区别在服务端的多连接处理方式UDP 没有Accept之类的函数因为 UDP 本身无连接。其余函数只需把TCP换成UDP即可得到对应版本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)与 TCP 不同UDP 收发数据时必须携带*UDPAddr对端地址ReadFromUDP返回来源地址WriteToUDP需显式指定目标地址。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 , err.Error()) os.Exit(1) } }DialUDP同样返回一个连接对象写入任意字节后读取服务端回包。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 , err.Error()) os.Exit(1) } }服务端监听 1200 端口循环调用handleClientReadFromUDP读取数据报并拿到来源地址addr随后WriteToUDP把当前时间写回该地址。因为每次读写都带完整地址UDP 服务端天然可以服务多个客户端无需Accept与连接管理。小结通过 TCP 与 UDP 两类 socket 的讲解和完整示例可以看到Go 对 socket 编程提供了出色的支持net包屏蔽了底层细节DialTCP/ListenTCP/Accept构成简洁的客户端/服务端模型go关键字一行即可实现并发SetReadDeadline、SetKeepAlive等控制函数让连接管理不再棘手。在此基础上可以继续阅读本仓库第 8 章的后续内容WebSocket、REST 与 RPC构建更完整的网络服务能力。延伸阅读第 8 章导读Web 服务下一节WebSocket全书记目录英文原版对应章节en/08.1.md简体中文版本zh/08.1.md赞分享文档教程【免费下载链接】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点击查看免费下载相关推荐《Build Web Application with Golang》精读Go Socket 网络编程实战TCP/UDP 客户端与服务器《Build Web Application with Golang》精读Go Socket 网络编程实战TCP/UDP 客户端与服务器 本文基于开源 G文档教程MicroPython ESP8266 网络编程实战基于 TCP Socket 的客户端与服务器开发指南MicroPython ESP8266 网络编程实战基于 TCP Socket 的客户端与服务器开发指南 导读 TCP Socket 是互联网应用最基础的通信嵌入式语言运行时编程语言解释器编译器物联网系统编程如何用C语言从零实现TCP/UDP客户端服务器通信完整指南如何用C语言从零实现TCP/UDP客户端服务器通信完整指南 GitHub加速计划的c/C项目是一个面向教育目的的算法集合包含数学、机器学习、计算机科学和物理示例工程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表