ConnectRPC 是 Buf 团队推出的 RPC 框架,兼容 gRPC 协议的同时支持 HTTP/1.1 和 JSON,可以直接用 curl 调试。本文介绍如何用 Go + ConnectRPC 从零构建一个 API 服务,并与前端 connect-web 对接。
为什么不直接用 gRPC
gRPC 基于 HTTP/2,在以下场景会碰到摩擦:
- 浏览器直调不行 — 浏览器不暴露 HTTP/2 帧控制,必须走 gRPC-Web 代理(Envoy 等)
- curl 调试困难 — 二进制 protobuf + HTTP/2 trailers,命令行调试需要
grpcurl - 负载均衡 — 很多 L7 负载均衡器对 HTTP/2 长连接支持不完善
ConnectRPC 同时支持三种协议:
| 协议 | 传输 | 编码 | 适用场景 |
|---|---|---|---|
| Connect | HTTP/1.1 或 HTTP/2 | JSON 或 Protobuf | 通用,curl 友好 |
| gRPC | HTTP/2 | Protobuf | 与现有 gRPC 服务互通 |
| gRPC-Web | HTTP/1.1 | Protobuf | 浏览器(无需代理) |
一个 ConnectRPC 服务端同时服务这三种协议,客户端自动协商。
Protobuf 定义
// proto/greet/v1/greet.proto
syntax = "proto3";
package greet.v1;
option go_package = "example.com/myapp/gen/greet/v1;greetv1";
message GreetRequest {
string name = 1;
}
message GreetResponse {
string greeting = 1;
}
service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse) {}
}
使用 buf CLI 生成代码:
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/protocolbuffers/go
out: gen
opt: paths=source_relative
- remote: buf.build/connectrpc/go
out: gen
opt: paths=source_relative
# 生成
buf generate
这会生成 gen/greet/v1/greet.pb.go(消息)和 gen/greet/v1/greetv1connect/greet.connect.go(服务接口)。
Go 服务端实现
package main
import (
"context"
"fmt"
"log"
"net/http"
"connectrpc.com/connect"
greetv1 "example.com/myapp/gen/greet/v1"
"example.com/myapp/gen/greet/v1/greetv1connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
type GreetServer struct{}
func (s *GreetServer) Greet(
ctx context.Context,
req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
log.Printf("Headers: %v", req.Header())
if req.Msg.Name == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("name is required"))
}
return connect.NewResponse(&greetv1.GreetResponse{
Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
}), nil
}
func main() {
greeter := &GreetServer{}
mux := http.NewServeMux()
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
mux.Handle(path, handler)
// h2c 支持无 TLS 的 HTTP/2(开发环境)
addr := ":8080"
log.Printf("Listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, h2c.NewHandler(mux, &http2.Server{})))
}
启动后可以直接用 curl 调用:
curl http://localhost:8080/greet.v1.GreetService/Greet \
-H "Content-Type: application/json" \
-d '{"name": "World"}'
# {"greeting": "Hello, World!"}
不需要任何代理,不需要 grpcurl,就是普通的 HTTP + JSON。
拦截器(Interceptor)
ConnectRPC 的拦截器类似 gRPC 的 UnaryInterceptor,但 API 更简洁:
func loggingInterceptor() connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
log.Printf("[%s] %s", req.Spec().Procedure, req.Header().Get("User-Agent"))
resp, err := next(ctx, req)
if err != nil {
log.Printf(" error: %v", err)
}
return resp, err
}
}
}
// 注册
path, handler := greetv1connect.NewGreetServiceHandler(
greeter,
connect.WithInterceptors(loggingInterceptor()),
)
前端:connect-web
import { createConnectTransport } from "@connectrpc/connect-web";
import { createClient } from "@connectrpc/connect";
import { GreetService } from "./gen/greet/v1/greet_pb";
const transport = createConnectTransport({
baseUrl: "http://localhost:8080",
});
const client = createClient(GreetService, transport);
const res = await client.greet({ name: "Frontend" });
console.log(res.greeting); // "Hello, Frontend!"
无需 Envoy 代理,直接从浏览器调用后端。
与 gRPC-Gateway 对比
| 维度 | ConnectRPC | gRPC-Gateway |
|---|---|---|
| 额外组件 | 无 | 需要反向代理进程 |
| 协议 | Connect + gRPC + gRPC-Web 三合一 | gRPC -> REST 转译 |
| 浏览器调用 | 原生支持 | 需要代理 |
| 性能开销 | 直接服务 | 多一层转译 |
| 代码生成 | buf 插件 | protoc-gen-grpc-gateway |
| 兼容性 | 可与任何 gRPC 客户端/服务端互通 | 仅提供 REST 入口 |
ConnectRPC 的核心优势是零额外基础设施:同一个 handler 同时服务 gRPC 客户端和 HTTP+JSON 客户端。如果你正在开始一个新项目,ConnectRPC 是比 gRPC + gRPC-Gateway 更简洁的选择。