拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Go 网络编程四范式详解:Socket、WebSocket、REST 与 RPC(《build-web-application-with-golang》第 8 章总结)

Go 网络编程四范式详解:Socket、WebSocket、REST 与 RPC(《build-web-application-with-golang》第 8 章总结)
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

本篇技术指南围绕 《Go Web 编程》西班牙语版第 8 章总结(es/08.5.md) 展开,系统梳理 Go 语言构建 Web 服务所依托的四种核心网络编程范式:Socket、WebSocket、REST 与 RPC。文中既完整还原原章节的结论与要点,又逐节回溯 es/08.1.md、es/08.2.md、es/08.3.md、es/08.4.md 的源码级示例,帮助读者理解每种范式的适用场景、实现方式与底层原理,并掌握在真实项目中选择与组合这些技术的能力。

一、第 8 章知识全景:四类网络编程范式

第 8 章以"服务型 Web 应用(Servicios web)"为总纲(见 es/08.0.md),指出 Web 服务本质上是借助 HTTP 之上的 XML、JSON 等格式交换信息,并强调其平台无关性——用 Go 编写的服务可以部署在 Linux 上,与 Windows 上的 ASP.NET、JSP 或 FreeBSD 上的应用正常交互。

章节把 Go 网络编程拆解为四个递进的层面,总结章(es/08.5.md)对此给出了清晰的定位:

范式章节核心定位适用场景
Socket8.1一切网络通信的底层基石,源自 Unix"一切皆文件"哲学实时服务、性能敏感的低层通信
WebSocket8.2HTML5 双向通信,浏览器与服务器全双工交互聊天、在线游戏、实时推送
REST8.3资源导向的架构风格,基于 HTTP 方法操作 URI 资源Web API、移动端后端服务
RPC8.4跨进程"像调用本地函数一样调用远程服务"分布式系统、内部服务间调用

结论章特别强调:Go 对这四种开发方式都提供了出色的支持,而承载这些能力的核心正是net包及其子包——它是 Go 放置全部网络工具的地方。如果想深入实现细节,应当直接阅读这些包的源码。

二、Socket:网络编程的地基(8.1)

2.1 Socket 的基本概念

Socket 起源于 Unix。基于"一切皆文件"的基本哲学,一切操作都可以用"打开 → 读写 → 关闭"来描述,Socket 也实现了这一哲学:通过打开 socket 的函数返回一个描述符,后续用它建立连接、传输信息。

常用 Socket 分两类:

  • 流式 Socket(SOCK_STREAM):面向连接,如 TCP;
  • 数据报 Socket(SOCK_DGRAM):不建立连接,如 UDP。

Socket 的唯一性由 TCP/IP 体系保证:网络层的 IP 地址在主机网络内唯一,"协议 + 端口"在应用网络内唯一,两者组合即可唯一标识一个 socket。

图 1:网络协议分层示意图(TCP/IP 下 socket 的定位)

2.2 IP 地址与 Go 的net包

  • IPv4:32 位地址,理论最多可连接 2^32 台设备,格式如127.0.0.1、172.122.121.111。随着互联网快速发展,地址早已逼近上限。
  • IPv6:128 位地址,彻底解决地址唯一性顾虑(按书中估算,地球上每平方米可分配超过 1000 个地址),并改进了点对点连接、QoS、安全性、多播等问题,格式如2002:c0e8:82e7:0:0:0:c0e8:82e7。

Go 的net包提供了大量网络编程的类型、函数与方法。IP 的定义非常朴素——就是一个字节切片:

type IP []byte

配套的解析函数ParseIP(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,程序会输出对应 IP 的规范化格式;传入非法地址则输出Invalid address。

2.3 TCP Socket:客户端与服务端

net包提供TCPConn类型承载客户端/服务端的读写交互,两个核心方法为:

func (c *TCPConn) Write(b []byte) (n int, err error) func (c *TCPConn) Read(b []byte) (n int, err error)

地址信息由TCPAddr表示(包含IP与Port两个字段),通过ResolveTCPAddr(net, addr string) (*TCPAddr, error)解析得到。其中:

  • net参数可取"tcp4"(仅 IPv4)、"tcp6"(仅 IPv6)或"tcp"(IPv4/IPv6 均可);
  • addr可以是域名或 IP 加端口,如"www.google.com:80"、"127.0.0.1:22"。

客户端使用DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)建立连接:laddr表示本地地址,通常传nil;raddr是远端地址。书中用一个向 HTTP 服务器发送HEAD / HTTP/1.0\r\n\r\n请求的例子演示了完整流程——解析地址、拨号建连、写入请求头、用ioutil.ReadAll读取整段响应。

服务端则使用ListenTCP与Accept:

func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error) func (l *TCPListener) Accept() (c Conn, err error)

最简单的服务端实现是把监听绑定到端口(如:7777),在for循环中Accept客户端连接,写入当前时间字符串后关闭连接。值得注意:循环内的错误不能导致服务退出,Accept出错时应continue继续运行,而不是终止进程。

2.4 用 goroutine 实现并发与连接控制

单线程串行Accept无法处理并发请求。书中紧接着展示了 Go 的并发威力——把业务处理拆到独立的handleClient(conn net.Conn)函数中,用go关键字启动即可:

for { conn, err := listener.Accept() if err != nil { continue } go handleClient(conn) // 每个连接一个 goroutine }

更进一步,如果需要在同一连接上处理多条请求,就不能在读完一次后立刻Close。示例改用循环读取,并引入两个关键防护手段:

conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // 两分钟读超时 request := make([]byte, 128) // 限制请求长度,防溢出

SetReadDeadline保证连接在超时后自动断开,避免客户端挂死占用资源;请求缓冲区固定大小则是防止缓冲区溢出攻击的基本手段。连接控制相关 API 还有:

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

书中建议认真权衡连接的超时长短:长连接可以减少反复建连的开销,适合需要频繁交换信息的应用。

2.5 UDP Socket:无连接的数据报通信

UDP 与 TCP 的唯一本质区别在服务端没有Accept——它不维护连接状态,收到数据报即处理。其余函数只需把TCP换成UDP:

func ResolveUDPAddr(net, addr string) (*UDPAddr, error) func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error) func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, 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)

服务端示例在for循环中通过ReadFromUDP读取数据报(记录来源地址addr),再用WriteToUDP把当前时间写回该地址——无需任何连接管理,天然适合"发完即走"的场景。

三、WebSocket:HTML5 双向通信(8.2)

3.1 为什么需要 WebSocket

在 WebSocket 诞生之前,即时消息类服务普遍使用"轮询"(roll polling):客户端周期性发送 HTTP 请求,服务端返回最新数据。这种方案的致命缺点是客户端必须持续发起大量连接,严重消耗带宽。

WebSocket 作为 HTML5 的重要特性,实现了浏览器与服务器的双向通信。它通过特殊的握手头把建连所需的握手次数压缩到一次,此后连接在整个生命周期内保持活跃,JavaScript 可以像操作普通 TCP socket 一样收发数据。相比传统 HTTP,它带来三点优势:

  • 每个 Web 客户端只需一条 TCP 连接;
  • 服务端可以主动向客户端推送信息;
  • 更轻量的头部,降低网络传输负载。

WebSocket URL 以ws://或wss://开头。

3.2 握手协议:从 Key 到 Accept

WebSocket 协议本身非常简单:完成初始握手后即建立连接。握手过程如下——

客户端请求中携带随机生成并经过 Base64 编码的Sec-WebSocket-Key(书中示例为f7cb4ezEAl6C3wRaU6JORA==)。服务端收到后将其与固定魔数拼接:

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

得到:

f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

随后对该字符串做 SHA-1 运算,再 Base64 编码,最终得到:

rE91AJhfC+6JdVcVXOGJEADEJdQ=

这个值作为响应头Sec-WebSocket-Accept返回,握手即完成。服务端在响应中返回该值后,连接建立成功。

3.3 Go 实现 Echo 服务

Go 标准库本身不直接提供 WebSocket,但golang.org/x/net/websocket这个官方维护的子包提供了完整支持,安装命令为:

go get golang.org/x/net/websocket

**客户端(HTML/JavaScript)**只需创建WebSocket对象并挂接四个事件:

  • onopen:连接建立成功后触发;
  • onmessage:收到消息后触发;
  • onerror:发生错误时触发;
  • onclose:连接关闭时触发。
<script type="text/javascript"> var sock = null; var wsuri = "ws://127.0.0.1:1234"; window.onload = function() { console.log("onload"); sock = new WebSocket(wsuri); sock.onopen = function() { console.log("connected to " + wsuri); } sock.onclose = function(e) { console.log("connection closed (" + e.code + ")"); } sock.onmessage = function(e) { console.log("message received: " + e.data); } }; function send() { var msg = document.getElementById('message').value; sock.send(msg); }; </script>

**服务端(Go)**定义Echo处理器,在循环中Receive客户端消息并Send回显结果,最后通过websocket.Handler挂到 HTTP 路由上:

package main import ( "golang.org/x/net/websocket" "fmt" "log" "net/http" ) func Echo(ws *websocket.Conn) { var err error for { var reply string if err = websocket.Message.Receive(ws, &reply); err != nil { fmt.Println("Can't receive") break } fmt.Println("Received back from client: " + reply) msg := "Received: " + reply fmt.Println("Sending to client: " + msg) if err = websocket.Message.Send(ws, msg); err != nil { fmt.Println("Can't send") break } } } func main() { http.Handle("/", websocket.Handler(Echo)) if err := http.ListenAndServe(":1234", nil); err != nil { log.Fatal("ListenAndServe:", err) } }

从源码结构可以推断,websocket.Handler是net/http的Handler适配层,它把 HTTP 升级请求转换为*websocket.Conn后交给业务函数,因此 Echo 服务可以直接复用http.ListenAndServe的标准 HTTP 服务生命周期。

四、REST:资源导向的架构风格(8.3)

4.1 REST 的三要素

REST(Representational State Transfer,表述性状态转移)概念由 HTTP 协议联合创始人 Roy Thomas Fielding 于 2000 年的博士论文中首次提出。理解 REST 需要抓住三个关键概念:

  • 资源(Recursos):图片、文档、视频等一切可通过 URI 定位的信息实体;
  • 表述(Representación):同一资源可以有多种呈现形式——TXT 文档可以表示为 HTML、JSON、XML,图片可以是 jpg 或 png。具体采用哪种表述由 HTTP 头Accept与Content-Type决定;
  • 状态转移(Estado de transferencia):HTTP 是无状态协议,客户端对资源的修改必须通过明确的动作告知服务端,即用 HTTP 方法表达状态变化:
    • GET获取资源;
    • POST创建或更新资源;
    • PUT更新资源;
    • DELETE删除资源。

简言之:每一条 URI 代表一个资源;资源在客户端与服务端之间存在表述层;客户端用四种 HTTP 方法实现对远程资源的操作。

REST 系统还有两个重要原则:无状态——每个请求必须封装全部所需信息,服务端可随时重启而不影响客户端,请求可被同服务的任意节点响应(这使其天然适配云计算),且客户端可缓存数据提升性能;分层(delaminación)——各层不能直接与跨层组件交互,从而限制系统复杂度、促进组件间独立。

图 2:REST 架构示意(无状态、资源导向、统一接口)

4.2 用 httprouter 实现 RESTful 服务

Go 本身没有对 REST 的直接支持,但 RESTful 应用基于 HTTP,因此完全可以用net/http自行实现。书中引入第三方路由包github.com/julienschmidt/httprouter,其路由映射简洁直观,非常适合承载 REST 架构:

package main import ( "fmt" "github.com/julienschmidt/httprouter" "log" "net/http" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome!\n") } func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) } func getuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { uid := ps.ByName("uid") fmt.Fprintf(w, "you are get user %s", uid) } func modifyuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { uid := ps.ByName("uid") fmt.Fprintf(w, "you are modify user %s", uid) } func deleteuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { uid := ps.ByName("uid") fmt.Fprintf(w, "you are delete user %s", uid) } func adduser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { uid := ps.ByName("uid") fmt.Fprintf(w, "you are add user %s", uid) } func main() { router := httprouter.New() router.GET("/", Index) router.GET("/hello/:name", Hello) router.GET("/user/:uid", getuser) router.POST("/adduser/:uid", adduser) router.DELETE("/deluser/:uid", deleteuser) router.PUT("/moduser/:uid", modifyuser) log.Fatal(http.ListenAndServe(":8080", router)) }

从代码可见,REST 的核心实现手法是"同一资源、按 HTTP 方法分派不同处理逻辑":/user/:uid的 GET 对应查询,/adduser的 POST 对应新增,/deluser的 DELETE 对应删除,/moduser的 PUT 对应修改。:uid路径参数通过ps.ByName("uid")取出。

书中同时提醒了工程现实:HTML 标准只允许链接和表单发送GET与POST,没有 AJAX 就无法发出PUT/DELETE;部分防火墙还会拦截这两种方法。常见的妥协方案是在POST表单中附加隐藏字段_method模拟PUT/DELETE,由服务端在路由处理前将其转换回真实方法。

五、Go RPC:跨进程函数调用(8.4)

5.1 RPC 的思想与十个步骤

前面介绍的 Socket 与 HTTP 都属于"信息交换"模型:客户端发请求、服务端回响应。而 RPC(Remote Procedure Call)走的是另一条路——让客户端像调用本地函数一样调用远程服务:客户端把函数名与参数打包,通过网络发送给服务端;服务端解包、执行、把结果返回客户端。

书中给出了一个典型 RPC 调用的十个步骤(对应 es/images/8.4.rpc.png 的流程图):

  1. 调用客户端的 stub(桩)执行参数传递;
  2. 调用系统内核发送网络消息;
  3. 消息发送到远程主机;
  4. 服务端接收并处理参数;
  5. 执行远程过程;
  6. 将执行结果返回给 stub;
  7. 服务端调用远程内核;
  8. 消息被发送回本地内核;
  9. 客户端处理内核消息;
  10. 客户端从对应 stub 获取结果。

图 3:RPC 工作原理(客户端 stub 与内核、服务端之间十步交互)

5.2 Go RPC 的注册规则

Go 标准库原生支持 RPC,覆盖TCP、HTTP、JSON三个层级。需要特别注意的是:Go RPC 与传统的跨语言 RPC 不同,它要求客户端和服务端都是 Go 程序,因为默认使用 Gob 编码。

远程可调用的函数必须严格遵循以下规则,否则调用会被忽略:

  • 函数必须是导出的(首字母大写);
  • 函数必须有两个参数,且类型都是导出的;
  • 第一个参数用于接收客户端传入数据,第二个必须是指针,用于向客户端回传结果;
  • 函数必须返回error类型(作为上报值)。

签名范式为:

func (t *T) MethodName(argType T1, replyType *T2) error

其中T、T1、T2都必须能被package/gob编码。

5.3 HTTP RPC 完整示例

服务端:定义Args(入参)与Quotient(商与余数)两个结构体,在Arith类型上实现Multiply与Divide方法,然后注册并暴露 HTTP 服务:

package main import ( "errors" "fmt" "net/http" "net/rpc" ) type Args struct { A, B int } type Quotient struct { Quo, Rem int } type Arith int func (t *Arith) Multiply(args *Args, reply *int) error { *reply = args.A * args.B return nil } func (t *Arith) Divide(args *Args, quo *Quotient) error { if args.B == 0 { return errors.New("divide by zero") } quo.Quo = args.A / args.B quo.Rem = args.A % args.B return nil } func main() { arith := new(Arith) rpc.Register(arith) rpc.HandleHTTP() err := http.ListenAndServe(":1234", nil) if err != nil { fmt.Println(err.Error()) } }

流程是:rpc.Register注册服务 →rpc.HandleHTTP把 RPC 服务挂到 HTTP 上 → 启动标准 HTTP 服务。

客户端:用rpc.DialHTTP("tcp", serverAddress+":1234")拨号,随后通过client.Call发起同步调用:

client, err := rpc.DialHTTP("tcp", serverAddress+":1234") if err != nil { log.Fatal("dialing:", err) } // Sincronizamos la llamada args := Args{17, 8} var reply int err = client.Call("Arith.Multiply", args, &reply) if err != nil { log.Fatal("arith error:", err) } fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) var quot Quotient err = client.Call("Arith.Divide", args, &quot) if err != nil { log.Fatal("arith error:", err) } fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

Call的三个参数分别是:要调用的函数名("Arith.Multiply")、入参、指向返回值的指针。编译并分别启动服务端与客户端后,控制台输出:

$ ./http_c localhost Arith: 17*8=136 Arith: 17/8=2 remainder 1

5.4 TCP RPC 与 JSON RPC

TCP RPC与 HTTP RPC 的唯一区别是连接管理方式:TCP 版需要自己net.ListenTCP+Accept维护连接,再交给rpc.ServeConn(conn)处理;客户端对应改用rpc.Dial("tcp", service)。书中指出这种模式本质上是阻塞式的,读者可以自行用 goroutine 做进阶实验。

JSON RPC则把编码方式从 Gob 换成 JSON,服务端调用jsonrpc.ServeConn(conn),客户端使用jsonrpc.Dial("tcp", service)。值得注意的是,JSON RPC 基于 TCP,当时尚不支持 HTTP。这一特性让它具备了与异种语言客户端互通的可能(JSON 是通用文本格式),代价是失去了 Gob 的高效紧凑编码。

章节总结指出,Go 对 HTTP、TCP、JSON 三种 RPC 实现都有良好支持,可轻松开发分布式应用;遗憾之处在于标准库未内置 SOAP RPC,不过已有若干第三方开源包可以补充。

六、net包:Go 网络能力的汇聚点

回到 es/08.5.md 的核心结论:四种范式虽然形态各异,但它们的共同根基是 Go 的net包及其子包。从本章代码可以清楚地看到这种"一个包、多个层次"的格局:

  • 地址解析层:net.ParseIP、ResolveTCPAddr、ResolveUDPAddr——统一处理 IPv4/IPv6 与主机名;
  • 连接传输层:TCPConn、UDPConn、DialTCP、ListenTCP、ReadFromUDP、WriteToUDP——直接承载字节流;
  • 协议适配层:net/http(支撑 REST 与 HTTP RPC)、net/rpc(含jsonrpc子包)、golang.org/x/net/websocket(官方子包提供 WebSocket 支持)。

也就是说,无论选择哪种范式,最终都收敛到net包这同一套地址与连接抽象之上。这也正是总结章建议读者阅读net包源码的原因:理解了底层实现,才能在实际项目中做出正确的技术选型与性能取舍。

七、实践路线与延伸阅读

如果要把本章内容落到工程实践,可以遵循以下递进路线:

  1. 先用 Socket 理解字节流:跑通 es/08.1.md 中的 TCP 客户端/服务端示例,重点体会Accept循环 +go handleClient的并发模型,以及SetReadDeadline、SetKeepAlive等连接控制手段;
  2. 用 WebSocket 处理实时交互:按 es/08.2.md 实现 Echo 服务,用浏览器控制台验证握手与双向收发;
  3. 用 REST 组织资源接口:参考 es/08.3.md 的 httprouter 路由分派模式,为同一资源按方法拆出独立处理器;
  4. 用 RPC 打通分布式调用:从 es/08.4.md 的 HTTP RPC 入手,再对比 TCP 与 JSON 变体,理解三种传输层级的差异。

后续章节将进入安全与加密(第 9 章),届时这些网络范式会作为安全防护的对象再次出现。本章涉及的所有 Go 源码示例,也都可以对照英文版仓库的 en/code 目录下的src/apps/ch.*目录进一步运行验证。

本章相关文档

  • 章节总览:es/08.0.md(Web 服务与四种范式概览)
  • 各节正文:es/08.1.md(Socket)|es/08.2.md(WebSocket)|es/08.3.md(REST)|es/08.4.md(RPC)
  • 本章总结:es/08.5.md
  • 目录索引:es/preface.md|es/SUMMARY.md
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表