ARTICLE DETAIL

资讯详情

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

Go Web 服务开发全景指南:Socket、WebSocket、REST 与 RPC 四技术路线深度解读(build-web-application-with-golang 第 8 章)

Go Web 服务开发全景指南:Socket、WebSocket、REST 与 RPC 四技术路线深度解读(build-web-application-with-golang 第 8 章) 文档教程【免费下载链接】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 电子书第 8 章对应 pt-br/08.0.md 章节导读为核心骨架系统讲解在 Go 中构建 Web 服务的四大技术路线底层 Socket 网络编程、HTML5 WebSocket 全双工通信、REST 架构风格以及 Go 标准库自带的 RPC 远程调用。读完本文你将理解 Web 服务跨平台交换信息的本质掌握 Gonet包中 TCP/UDP 的核心 API 与并发模型并能够依据场景在 REST 与 RPC 之间做出合理选型。1. Web services 是什么跨平台的信息交换范式Web servicesWeb 服务允许我们通过 HTTP 使用 XML 或 JSON 等格式来交换信息。例如查询上海明天的天气、获取 Apple 当前的股价、拉取 Amazon 上的商品信息都可以通过编写一段代码从开放平台获取。在 Go 中这一过程可以类比为调用一个本地函数并取得其返回值——只不过底层实际上走的是网络请求。Web 服务的关键特性是平台无关性platform independent。这意味着你可以把应用部署在 Linux 上却毫无障碍地与运行在 Windows 上的 ASP.NET 应用交互与 FreeBSD 上的 JSP 应用交互也是一样。平台无关性让异构系统之间的集成变得简单这也是 Web 服务在分布式系统与开放平台中占据核心地位的原因。当前实现 Web 服务的两大主流风格是REST 架构与SOAP 协议本章导读对此做了明确的取舍REST请求非常直白因为它本身就基于 HTTP。每一个 REST 请求本质上就是一个 HTTP 请求服务器通过不同的 HTTP 方法来处理请求。由于大多数开发者已经熟悉 HTTPREST 上手门槛很低详见 pt-br/08.3.md。SOAP由 W3C 发起是跨网络信息传输与远程计算机函数调用的标准。但 SOAP 的规范非常冗长复杂而且还在不断变长。Go 信奉简单因此本书不讨论 SOAP好在 Go 提供了性能良好、易于开发的RPC远程过程调用作为替代详见 pt-br/08.4.md。Go 被誉为21 世纪的 C 语言追求简洁与高性能并存。基于这一理念本章还安排了另外两条路线实时服务器常用 Socket 来弥补 HTTP 性能不足的问题pt-br/08.1.md以及伴随 HTML5 快速普及、被众多网页游戏公司采用的 WebSocketpt-br/08.2.md。2. 本章技术路线总览对照 pt-br/SUMMARY.md 中第 8 章的目录结构本章完整的技术地图如下章节技术主题核心要点08.1 Sockets底层网络编程TCP/UDP Socket、net包、TCPConn、goroutine 并发模型08.2 WebSocketHTML5 全双工通信握手协议、Sec-WebSocket-Key、x/net/websocket包08.3 REST架构风格资源、表现层、状态转移、httprouter路由映射08.4 RPC远程调用HTTP RPC / TCP RPC / JSON RPCGob 编码08.5 Summary章节总结net包及其子包是 Go 网络编程工具集所在Go 对这四种开发方式都提供了良好的支持。值得强调的一点是net包及其子包是 Go 网络编程工具的真正所在地想深入理解实现细节最好的方式是直接阅读这些包的源码见 pt-br/08.5.md。本章的可运行示例代码均内嵌在各小节文档中pt-br/code目录则收录了其他章节的配套示例例如 pt-br/code/readme.md 中说明了示例的组织方式。3. Socket 网络编程一切皆 Socket第 8.1 节3.1 Socket 的由来与类型Socket 起源于 Unix。秉承 Unix一切皆文件的哲学一切都可以用打开 → 读写 → 关闭来操作Socket 正是这一哲学的一种实现打开 Socket 会返回一个整型描述符descriptor后续用它建立连接、传输数据。两种最常用的 Socket 类型是流式 SocketSOCK_STREAM面向连接对应 TCP数据报 SocketSOCK_DGRAM不建立连接对应 UDP。要让 Socket 在网络上唯一需要综合三方面信息网络层的IP 地址在主机网络内唯一协议 端口在主机应用间唯一二者组合即可得到全局唯一的 Socket。下图展示了 TCP/IP 网络协议的分层模型基于 TCP/IP 的应用无一例外都会在代码中以某种方式使用 Socket API这也解释了为什么有人会说一切都归结为 Socket。3.2 IPv4 与 IPv6IPv4地址 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负责将 IP 字符串解析为 IP 类型下面的程序演示了它的用法兼容 IPv4/IPv6 格式输入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) }3.3 TCP Socket客户端与服务端Gonet包中的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)解析得到func 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 客户端使用DialTCP建立连接func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error)laddr为本地地址大多数情况下设为nilraddr为远端地址。下面是一个模拟 HTTP 请求的 TCP 客户端它向服务器发送HEAD / HTTP/1.0请求头并读取完整响应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) } }TCP 服务端需要把服务绑定到空闲端口并监听客户端请求核心 API 为func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err error) func (l *TCPListener) Accept() (c Conn, err error)下面是一个绑定 7777 端口的时间同步服务它不断 Accept 连接并返回当前时间字符串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) } }注意for循环中即使 Accept 出错服务也会继续运行而不是退出错误被记录到服务器错误日志中。这个版本还不够好——它没有利用 goroutine 来并发处理请求。改造方案是把业务逻辑抽到handleClient函数中用go关键字启动并发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)) }通过把业务处理与连接管理分离、并配合go关键字我们一行代码就为服务实现了并发能力——这正是 goroutine 强大与简洁之处的绝佳演示。进一步地如果需要在一条连接上连续处理多个请求例如客户端发送不同时间格式的请求可以结合SetReadDeadline超时与循环读取实现监听 1200 端口设置 2 分钟读超时、限制单次请求最大 128 字节以防洪水攻击、收到timestamp时返回 Unix 时间戳否则返回完整时间字符串、每次处理完后重置request缓冲区因为conn.Read()是追加写入而非覆盖。3.4 连接控制 APIfunc 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前三个分别控制连接建立超时、单次读超时与单次写超时客户端与服务端均适用SetKeepAlive用于设置长连接的保活。长连接能减少频繁建连的开销适合需要频繁交换数据的应用——超时时长值得认真设计。3.5 UDP SocketUDP 与 TCP 的唯一区别在于服务端对多请求的处理方式UDP 没有Accept其余 API 只是把TCP换成UDPfunc 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)UDP 客户端示例先ResolveUDPAddr(udp4, service)解析再DialUDP建立连接、写入任意数据最后从conn读取响应。UDP 服务端示例监听 1200 端口则用ReadFromUDP读取客户端数据及其地址再用WriteToUDP把时间字符串回写给对应地址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) }4. HTML5 WebSocket全双工实时通信第 8.2 节4.1 为什么需要 WebSocketWebSocket 是 HTML5 的重要特性它实现了基于浏览器的远程 Socket让浏览器与服务器之间可以进行全双工通信。Firefox、Google Chrome、Safari 等主流浏览器都提供支持。在 WebSocket 诞生之前即时消息服务普遍采用轮询polling客户端周期性发送 HTTP 请求服务器返回最新数据。这种方式的弊端是客户端需要持续向服务器发送大量请求消耗大量带宽。WebSocket 使用特殊头部把浏览器与服务器之间的握手次数减少到一次连接建立后在整个生命周期内保持活跃可以用 JavaScript 像操作传统 TCP Socket 一样读写数据。相比传统 HTTP 的优势单个 Web 客户端只需要一条 TCP 连接WebSocket 服务器可以主动向客户端推送数据轻量级头部降低了数据传输开销。WebSocket URL 以ws://或wss://SSL开头。4.2 握手原理WebSocket 协议本身相当简单完成初始握手后连接建立后续数据通信都以\x00开头、以\xFF结尾WebSocket 会自动剥离首尾标记把原始数据交给客户端。握手由浏览器发起请求、服务器响应完成。Sec-WebSocket-Key 由客户端随机生成并做 base64 编码服务器收到请求后需要把这个 key 拼接到一个固定字符串后面258EAFA5-E914-47DA-95CA-C5AB0DC85B11例如 key 为f7cb4ezEAl6C3wRaU6JORA则拼接为f7cb4ezEAl6C3wRaU6JORA258EAFA5-E914-47DA-95CA-C5AB0DC85B11对该串做 SHA-1 得到二进制值再做 base64 编码得到rE91AJhfC6JdVcVXOGJEADEJdQ这个值就是响应头Sec-WebSocket-Accept的内容。4.3 Go 中的 WebSocket 实战Go 标准库本身不支持 WebSocket但go.net的子包websocket提供官方维护的支持安装命令为go get golang.org/x/net/websocket客户端HTML JavaScript非常简洁new WebSocket(wsuri)建立连接客户端通常要绑定四个事件onopen连接建立成功后触发onmessage收到消息后触发onerror发生错误后触发onclose连接关闭后触发。script typetext/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 h1WebSocket Echo Test/h1 form pMessage: input idmessage typetext valueHello, world!/p /form button onclicksend();Send Message/button服务端Go使用websocket.Handler包装处理函数并注册到http路由实现一个 Echo 服务package main import ( fmt log net/http golang.org/x/net/websocket ) func Echo(ws *websocket.Conn) { var err error for { var reply string if err websocket.Message.Receive(ws, reply); err ! nil { fmt.Println(Cant 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(Cant send) break } } } func main() { http.Handle(/, websocket.Handler(Echo)) if err : http.ListenAndServe(:1234, nil); err ! nil { log.Fatal(ListenAndServe:, err) } }客户端Send用户输入后服务器Receive到内容再Send回去作为响应。从示例可见WebSocket 的客户端与服务端实现都非常便捷Go 甚至可以直接复用net包的能力。5. REST资源导向的架构风格第 8.3 节5.1 REST 的核心概念RESTREpresentational State Transfer表现层状态转移概念于 2000 年由 HTTP 协议联合创始人 Roy Thomas Fielding 的博士论文首次提出它规定了架构的约束与原则凡是符合该架构实现的系统都可以称为 RESTful 系统。理解 REST 需要先厘清三个概念资源Resources图片、文档、视频等都是资源可通过 URI 定位。REST 中的表现层实际上是资源的表现层。表现Representation资源是具体的信息实体在表现层可以多种方式呈现——TXT 文档可表现为 HTML、JSON、XML 等图片可表现为 jpg、png 等。URI 用来标识资源而具体表现形态由 HTTP 请求头中的Accept与Content-Type两个字段描述。状态转移State Transfer每次访问网站页面客户端与服务端之间都会发起交互过程中与当前页面状态相关的数据需要保存但 HTTP 是无状态协议因此客户端状态需要由服务端保存客户端修改数据后想持久化就必须有办法把新状态告知服务端。多数情况下客户端通过 HTTP 的四种操作来完成状态转移GET获取资源POST创建或更新资源PUT更新资源DELETE删除资源。归纳起来1每个 URI 代表一种资源2客户端与服务端之间存在传输资源的表现层3客户端通过四种 HTTP 方法实现表现层状态转移从而操作远端资源。REST 最重要的一条原则是客户端与服务器之间的交互是无状态的每个请求都应封装全部所需信息服务器可以随时重启而无需通知客户端同一服务的任意服务器都能响应请求——这一点对云计算非常友好同时因为无状态客户端可以缓存数据提升性能。另一条重要原则是系统分层delamination某一层的组件不能直接与其它层的组件交互从而限制系统复杂度、鼓励底层组件的独立性当 RESTful 约束得到合理遵守时Web 应用可以扩展以容纳海量客户端还能减少客户端与服务器之间的延迟、简化系统架构、提高子系统的可见性。5.2 Go 中的 RESTful 实现Go 没有对 REST 的直接支持但 RESTful Web 应用都是基于 HTTP 的因此可以用net/http包自行实现。REST 依据与资源的交互方式使用不同 HTTP 方法实践中常见三个实现层级从只支持 GET/POST 到完整支持 PUT/DELETE。很多自称 RESTful 的应用其实并未真正实现 REST。现实约束有两个HTML 标准只允许客户端通过链接和表单发送GET与POST没有 AJAX 支持就无法发送PUT、DELETE某些防火墙会拦截PUT和DELETE请求客户端只能改用 POST 实现完全 RESTful 的服务负责还原原始 HTTP 方法。可以通过在 POST 请求中加入隐藏字段_method来模拟PUT/DELETE但服务器处理前必须先转换。标准 RESTful 接口在 Go 中很容易实现下面是引入第三方路由库github.com/julienschmidt/httprouter的完整示例——资源是 user对同一资源的不同方法挂载不同处理函数package main import ( fmt log net/http github.com/julienschmidt/httprouter ) 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)) }httprouter提供了非常便利的路由映射规则前几章已介绍过如何实现自定义路由REST 架构要求我们对同一资源的不同 HTTP 方法实现不同逻辑——这就是示例的核心模式。6. RPC像调用本地函数一样调用远程服务第 8.4 节6.1 RPC 的工作原理前文介绍的 Socket 与 HTTP 应用都使用信息交换模型客户端发请求、服务器给响应数据基于特定格式以便双方通信。但很多独立应用并不采用这种模型而是像调用普通函数一样调用服务。RPC远程过程调用正是为网络化系统设计的函数调用模式客户端像调用本地函数一样执行 RPC只是把函数参数打包通过网络发送给服务器服务器解包参数、处理请求并把结果执行回客户端。从计算机科学角度RPC 是一种进程间通信方式允许程序在另一个地址空间通常是共享网络上的另一台计算机执行子程序或过程而程序员无需显式编写远程交互细节——本地调用与远程调用代码几乎一致。若软件采用面向对象原则RPC 又被称为远程调用或远程方法调用。一次从客户端到服务器的典型 RPC 调用包含十个步骤调用客户端句柄执行传递参数调用本地系统内核发送网络消息消息发送到远程主机服务器接收句柄与参数执行远程过程把执行结果返回给相应句柄服务器句柄调用远程系统内核消息发回本地系统内核客户端句柄从系统内核接收消息客户端从相应句柄取得结果。6.2 Go RPC 的规则与三种形态Go 标准库在三个层面官方支持 RPCTCP、HTTP 与 JSON RPC。与传统 RPC 系统不同Go RPC 使用Gob编码内容因此要求客户端与服务端都使用 Go 应用。可被远程访问的 Go RPC 函数必须遵守以下规则否则相应调用会被忽略函数必须被导出首字母大写函数必须有两个导出类型的参数第一个参数用于接收客户端数据第二个参数必须是指针类型用于回复客户端函数返回值必须是error类型。例如func (t *T) MethodName(argType T1, replyType *T2) error其中 T、T1、T2 必须能被gob包编码。任何 RPC 都要通过网络传输数据Go RPC 可以选择 HTTP 或 TCP使用 HTTP 的好处是可以复用net/http包的部分函数。HTTP RPC服务端注册Arith服务并通过rpc.HandleHTTP()挂到 HTTP 上然后http.ListenAndServe(:1234, nil)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.DialHTTP(tcp, serverAddress:1234)建立连接再通过client.Call(Arith.Multiply, args, reply)做同步调用。Call有三个参数第一个是待调用的函数名第二个是要传递的参数第三个是指针类型的返回值client, err : rpc.DialHTTP(tcp, serverAddress:1234) if err ! nil { log.Fatal(dialing:, err) } 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)编译运行服务端与客户端后输出形如$ ./http_c localhost Arith: 17*8136 Arith: 17/82 remainder 1TCP RPC与 HTTP RPC 的差别在于必须自己控制连接。服务端rpc.Register(arith)后用net.ListenTCP监听循环Accept并把每个连接交给rpc.ServeConn(conn)处理这种模式是阻塞式的可以自行用 goroutine 扩展为更高级的实验。客户端只把DialHTTP换成rpc.Dial(tcp, service)即可。JSON RPC编码格式从 gob 换成 JSON。服务端同样基于 TCPJSON RPC 尚不支持 HTTP差异仅在把rpc.ServeConn(conn)换成jsonrpc.ServeConn(conn)客户端使用jsonrpc.Dial(tcp, service)建立连接其余调用方式与 HTTP/TCP RPC 完全一致。Go 对 HTTP、TCP 与 JSON 三种 RPC 的良好支持让我们可以轻松开发分布式 Web 应用稍有遗憾的是 Go 没有内建 SOAP RPC 支持不过部分开源第三方包提供了该能力见 pt-br/08.4.md 章末总结。7. 章节小结与后续学习路径pt-br/08.5.md 对本章做了收束Socket 是网络技术演进的基石必须掌握其概念才能成为合格的 Web 开发者WebSocket 支持客户端与服务器全双工通信消除了 AJAX 轮询REST 架构特别适合网络 API 开发随着移动应用的快速崛起RESTful API 将是持续的趋势RPC 则提供像本地调用一样简单的分布式函数调用。再次强调Go 对上述四种开发方式支持良好而net包及其子包正是 Go 网络编程工具的所在地想深入了解实现细节应当尝试阅读这些包的源码。下一章将进入 安全与加密第 9 章涵盖 CSRF、输入过滤、XSS、SQL 注入、密码存储与数据加解密在掌握 Web 服务的构建能力后为你的应用补上安全防护这一环。赞分享文档教程【免费下载链接】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 的 Go 实现build-web-application-with-golang 第 8 章Go Web 服务开发指南Socket、WebSocket、REST 与 RPC 的 Go 实现build web application with gol文档教程Go Web 服务编程全景Socket、WebSocket、REST 与 RPC 四大网络模型实战导览build-web-application-with-golang 第 8 章Go Web 服务编程全景Socket、WebSocket、REST 与 RPC 四大网络模型实战导览build web application with文档教程Go 网络编程核心范式总结Socket、WebSocket、REST 与 RPCbuild-web-application-with-golang 第八章导读Go 网络编程核心范式总结Socket、WebSocket、REST 与 RPCbuild web application with golang 第八章导文档教程上一篇实战指南如何高效生成车牌数据解决智能交通系统训练难题下一篇开源车牌生成工具实战指南5分钟创建车牌识别训练数据创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表