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

发布时间:2026/6/26 2:05:44

多协议转换:用 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 为 1Wire 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[1n:]) int(length) { return , fmt.Errorf(数据长度不符) } return string(data[1n : 1nint(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[1n:]) int(length) { return , fmt.Errorf(数据长度不符) } return string(data[1n : 1nint(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(b0x7f) s s 7 } return 0, 0 } // 组装 gRPC 帧 func packGrpcFrame(payload []byte) []byte { frame : make([]byte, 5len(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。这说明理解协议格式后能绕过生成器直接通信。五、总结这次实践表明协议网关本质是数据包重组和转发的代理服务。其操作与手写帧头和二进制拼装无异。生产环境需用成熟框架保障效率但手动实现有助于深入理解技术。

相关新闻