news 2026/6/26 2:05:34

多协议转换:用 Go 标准库手写 gRPC 翻译网关

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多协议转换:用 Go 标准库手写 gRPC 翻译网关

多协议转换:用 Go 标准库手写 gRPC 翻译网关

一、为什么需要协议网关

微服务架构流行后,内部服务常用 gRPC 提升通信效率。外部客户端和浏览器仍主要使用 HTTP/JSON。问题在于如何让外部客户端直接调用 gRPC 接口,而不改动后端服务。

常见做法是用 gRPC-Gateway 项目,通过 Protobuf 文件和代码生成器构建反向代理。但工具链复杂,生成的代码可能掩盖底层传输细节。

本文尝试用 Go 标准库手动组装 gRPC 帧,手写编解码,构建极简协议网关。这样能更直观理解 gRPC 和 HTTP/JSON 的数据转换过程。

graph TD Client[客户端 HTTP/JSON] -->|POST /translate| Gateway[翻译网关] Gateway -->|解析 JSON| Gateway Gateway -->|手动编码 Protobuf & 组装 gRPC 帧| Gateway Gateway -->|POST /TranslateService/Translate| Backend[后端服务] Backend -->|读取 gRPC 帧 & 解码 Protobuf| Backend Backend -->|业务处理| Backend Backend -->|编码响应 Protobuf & 组装 gRPC 帧| Backend Backend -->|返回响应| Gateway Gateway -->|解析 gRPC 帧 & 解码响应 Protobuf| Gateway Gateway -->|组装 JSON| Gateway Gateway -->|返回 JSON| Client

二、协议帧转换与二进制编解码

网关需要处理两种协议:HTTP/JSON 和 gRPC。客户端发来的 JSON 请求要转为 Protobuf 二进制流,而 gRPC 在 TCP 上增加了帧格式——长度前缀消息。

5 字节帧头中,首字节是压缩标志(0 表示未压缩),后 4 字节是大端序的 32 位整数,标明数据长度。网关负责处理帧头,并转换 JSON 和 Protobuf 数据。

关键点是 gRPC 的帧结构和 Protobuf 的二进制编码。Protobuf 用 Varint 编码整数,Wire Type 标识字段类型。理解这些后,即可用代码实现。

例如,String 字段的 Tag 为 1,Wire Type 为 2,组合后为 0x0a,后跟长度编码和 UTF-8 字节。手动编写编解码逻辑,能更深入理解协议设计。

三、代码实现

用 Go 标准库的net/http和二进制工具搭建网关。设计一个翻译接口:网关接收 JSON 请求,转为二进制帧发给后端;后端返回结果,网关再转回 JSON。

package main import ( "bytes" "encoding/binary" "encoding/json" "fmt" "io" "net/http" "time" ) // 编码请求:将单词转为二进制数据 func encodeRequest(word string) []byte { wordBytes := []byte(word) length := len(wordBytes) buf := new(bytes.Buffer) buf.WriteByte(0x0a) // Tag 1, Wire Type 2 writeVarint(buf, uint64(length)) buf.Write(wordBytes) return buf.Bytes() } // 解码请求:还原单词 func decodeRequest(data []byte) (string, error) { if len(data) < 2 || data[0] != 0x0a { return "", fmt.Errorf("数据格式错误") } length, n := readVarint(data[1:]) if n == 0 || len(data[1+n:]) < int(length) { return "", fmt.Errorf("数据长度不符") } return string(data[1+n : 1+n+int(length)]), nil } // 编码响应:将结果转为二进制 func encodeResponse(result string) []byte { resultBytes := []byte(result) length := len(resultBytes) buf := new(bytes.Buffer) buf.WriteByte(0x12) // Tag 2, Wire Type 2 writeVarint(buf, uint64(length)) buf.Write(resultBytes) return buf.Bytes() } // 解码响应:还原结果 func decodeResponse(data []byte) (string, error) { if len(data) < 2 || data[0] != 0x12 { return "", fmt.Errorf("数据格式错误") } length, n := readVarint(data[1:]) if n == 0 || len(data[1+n:]) < int(length) { return "", fmt.Errorf("数据长度不符") } return string(data[1+n : 1+n+int(length)]), nil } // Varint 编码辅助函数 func writeVarint(buf *bytes.Buffer, x uint64) { for x >= 0x80 { buf.WriteByte(byte(x|0x80)) x >>= 7 } buf.WriteByte(byte(x)) } func readVarint(data []byte) (uint64, int) { var x uint64 var s uint for i, b := range data { if b < 0x80 { if i > 9 || i == 9 && b > 1 { return 0, 0 } return x | uint64(b)<<s, i + 1 } x |= uint64(b&0x7f) << s s += 7 } return 0, 0 } // 组装 gRPC 帧 func packGrpcFrame(payload []byte) []byte { frame := make([]byte, 5+len(payload)) binary.BigEndian.PutUint32(frame[1:5], uint32(len(payload))) copy(frame[5:], payload) return frame } // 解析 gRPC 帧 func unpackGrpcFrame(r io.Reader) ([]byte, error) { header := make([]byte, 5) if _, err := io.ReadFull(r, header); err != nil { return nil, err } length := binary.BigEndian.Uint32(header[1:5]) payload := make([]byte, length) _, err := io.ReadFull(r, payload) return payload, err } // 模拟后端服务 func startGrpcBackend(addr string) { http.HandleFunc("/TranslateService/Translate", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.Header.Get("Content-Type") != "application/grpc" { w.WriteHeader(http.StatusUnsupportedMediaType) return } payload, _ := unpackGrpcFrame(r.Body) word, err := decodeRequest(payload) if err != nil { w.WriteHeader(http.StatusBadRequest) return } result := map[string]string{ "hello": "你好", "world": "世界", "gateway": "网关", }[word] respFrame := packGrpcFrame(encodeResponse(result)) w.Header().Set("Content-Type", "application/grpc") w.Write(respFrame) }) http.ListenAndServe(addr, nil) } // 网关服务 type TranslateGateway struct{ backendAddr string } func (g *TranslateGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "仅支持 POST", http.StatusMethodNotAllowed) return } var req struct{ Word string } json.NewDecoder(r.Body).Decode(&req) grpcFrame := packGrpcFrame(encodeRequest(req.Word)) resp, err := http.Post(fmt.Sprintf("http://%s/TranslateService/Translate", g.backendAddr), "application/grpc", bytes.NewReader(grpcFrame)) if err != nil { http.Error(w, "后端通信失败", http.StatusInternalServerError) return } defer resp.Body.Close() payload, _ := unpackGrpcFrame(resp.Body) result, _ := decodeResponse(payload) json.NewEncoder(w).Encode(map[string]string{"result": result}) } func main() { backendAddr := "127.0.0.1:50051" gatewayAddr := "127.0.0.1:8080" go startGrpcBackend(backendAddr) go http.ListenAndServe(gatewayAddr, &TranslateGateway{backendAddr}) time.Sleep(500 * time.Millisecond) // 测试请求 reqBody, _ := json.Marshal(map[string]string{"word": "hello"}) resp, _ := http.Post("http://"+gatewayAddr, "application/json", bytes.NewReader(reqBody)) defer resp.Body.Close() var result map[string]string json.NewDecoder(resp.Body).Decode(&result) fmt.Printf("输入: hello → 输出: %s\n", result["result"]) }

四、运行与验证

代码包含后端服务、网关和测试客户端。启动后,网关监听 8080 端口,后端监听 50051 端口。测试客户端发送 JSON 请求{"word": "hello"},网关转为 gRPC 帧发给后端,后端返回{"result": "你好"}

手写报文无需复杂工具链,有 Go 环境即可运行go run main.go。这说明理解协议格式后,能绕过生成器直接通信。

五、总结

这次实践表明,协议网关本质是数据包重组和转发的代理服务。其操作与手写帧头和二进制拼装无异。生产环境需用成熟框架保障效率,但手动实现有助于深入理解技术。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 2:05:29

AI 辅助 UI 生成:从提示词到可交付界面的工程化链路

AI 辅助 UI 生成&#xff1a;从提示词到可交付界面的工程化链路 一、当设计稿交付周期成为瓶颈——AI 介入 UI 生成的真实场景 在一个 SaaS 产品的迭代中&#xff0c;设计团队每周产出约 40 张设计稿&#xff0c;前端团队的平均还原周期为 3-5 天。瓶颈不在编码速度&#xff0c…

作者头像 李华
网站建设 2026/6/26 2:05:20

FreeRTOS 任务调度器:从就绪列表到 PendSV 上下文切换的寄存器级实现

FreeRTOS 任务调度器&#xff1a;从就绪列表到 PendSV 上下文切换的寄存器级实现一、实时调度的确定性危机与工程痛点 FreeRTOS 标称硬实时&#xff0c;但实际项目中任务抖动超标的情况并不少见。某电机控制项目&#xff0c;FOC 环路要求 10kHz&#xff08;100μs 周期&#xf…

作者头像 李华
网站建设 2026/6/26 2:02:37

RIS辅助ISAC系统物理层安全:波束成形与人工噪声的联合优化设计

1. 从“隔墙有耳”到“定向传音”&#xff1a;为什么我们需要RIS辅助的ISAC隐私保护&#xff1f;在无线通信的世界里&#xff0c;我们一直在追求两件事&#xff1a;一是信号要传得远、传得准&#xff0c;二是信息要传得安全、传得私密。传统的通信系统&#xff0c;比如你家里的…

作者头像 李华
网站建设 2026/6/26 2:02:33

AI 产品从 Demo 到生产:流式输出、幻觉抑制与成本控制的工程化实践

AI 产品从 Demo 到生产&#xff1a;流式输出、幻觉抑制与成本控制的工程化实践一、Demo 能跑&#xff0c;生产就崩——AI 产品的三道坎 AI 产品最危险的阶段往往不是想法验证&#xff0c;而是从 Demo 到生产的跨越。 在 Demo 阶段&#xff0c;你可能用 Jupyter Notebook 调通了…

作者头像 李华
网站建设 2026/6/26 1:58:48

AI 音乐生成实战:从提示词工程到多轨编曲的工程化生产路径

AI 音乐生成实战&#xff1a;从提示词工程到多轨编曲的工程化生产路径 一、AI 出来的音乐没法用——从 Demo 到生产的鸿沟 用 AI 音乐生成工具出一段背景音乐&#xff0c;听起来还行&#xff0c;但一放到视频里就暴露问题&#xff1a;结构不对&#xff08;没有明确的段落划分&a…

作者头像 李华
网站建设 2026/6/26 1:54:59

斐波那契数列拼接常数的数字分布:从均匀性到正态性的统计检验

1. 项目概述&#xff1a;从数列拼接到一个统计谜题最近在整理一些数值实验的旧项目时&#xff0c;我重新审视了一个挺有意思的问题&#xff1a;如果我们把斐波那契数列的项当作数字串拼接成一个超长的“常数”&#xff0c;这个数的各位数字&#xff0c;其分布看起来是随机的吗&…

作者头像 李华