多协议转换:用 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。这说明理解协议格式后,能绕过生成器直接通信。
五、总结
这次实践表明,协议网关本质是数据包重组和转发的代理服务。其操作与手写帧头和二进制拼装无异。生产环境需用成熟框架保障效率,但手动实现有助于深入理解技术。