gRPC 入门与实战:从零构建高效微服务通信

By | 2026年8月29日

gRPC 入门与实战:从零构建高效微服务通信

引言

在微服务架构盛行的今天,服务间通信的效率和稳定性至关重要。传统的 RESTful API 基于 HTTP/1.1,采用文本格式(如 JSON),虽然简单易用,但在高并发、低延迟的场景下逐渐暴露出性能瓶颈。gRPC 作为 Google 开源的高性能 RPC 框架,基于 HTTP/2 和 Protocol Buffers,提供了多语言支持、流式传输、强类型接口定义等特性,成为微服务通信的热门选择。

本文将通过一个完整的用户服务示例,带你从零开始掌握 gRPC 的核心概念、定义 proto 文件、实现服务端和客户端,并探讨最佳实践与常见坑。无论你是后端开发还是前端开发者,都能从中获得实用的技能。

1. gRPC 核心概念

1.1 什么是 gRPC?

gRPC 是一个高性能、开源的 RPC(远程过程调用)框架,由 Google 开发。它使用 HTTP/2 作为传输协议,Protocol Buffers(protobuf)作为接口定义语言(IDL)和消息序列化协议。

1.2 核心特性

  • HTTP/2 基础:支持多路复用、双向流、头部压缩,降低延迟。
  • Protocol Buffers:高效的二进制序列化,体积小、速度快。
  • 强类型接口:通过 .proto 文件定义服务接口和消息结构,自动生成客户端和服务端代码。
  • 多语言支持:官方支持 Go、Java、Python、C++ 等十余种语言。
  • 流式传输:支持四种调用模式:一元(Unary)、服务端流(Server streaming)、客户端流(Client streaming)、双向流(Bidirectional streaming)。

1.3 与 REST 对比

| 特性 | gRPC | REST | |——|——|——| | 传输协议 | HTTP/2 | HTTP/1.1/2 | | 数据格式 | 二进制(protobuf) | 文本(JSON/XML) | | 接口定义 | 强类型(.proto) | 无标准 | | 流式支持 | 原生支持 | 有限 | | 浏览器支持 | 需要 gRPC-Web | 原生支持 |

2. 环境准备

我们将使用 Go 语言进行实战演示。确保你已安装:

  • Go 1.16+(建议 1.20+)
  • protoc 编译器(Protocol Buffers 编译器)
  • protoc-gen-go 和 protoc-gen-go-grpc 插件

安装 protoc 和插件:


# 安装 protoc(macOS 使用 brew,Linux 使用 apt 或下载二进制)
brew install protobuf  # macOS

# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

确保 $PATH 中包含 $GOBIN(默认 $HOME/go/bin)。

3. 定义 Protocol Buffers 文件

首先,创建项目目录并初始化 Go module:


mkdir grpc-demo && cd grpc-demo
go mod init grpc-demo

创建 proto/user.proto 文件:


syntax = "proto3";

package user;

option go_package = "grpc-demo/pb/user";

// 用户服务定义
service UserService {
  // 一元调用:获取用户信息
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
  // 服务端流:获取用户列表(流式返回)
  rpc ListUsers (ListUsersRequest) returns (stream User);
  // 客户端流:批量创建用户(流式发送)
  rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
  // 双向流:聊天式交互
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

// 请求消息
message GetUserRequest {
  int32 id = 1;
}

message GetUserResponse {
  User user = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message CreateUsersResponse {
  repeated User users = 1;
}

message ChatMessage {
  string message = 1;
}

// 用户实体
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

4. 生成代码

在项目根目录执行:


mkdir -p pb/user
protoc --go_out=. --go-grpc_out=. proto/user.proto

这会生成 pb/user/user.pb.go(消息定义)和 pb/user/user_grpc.pb.go(服务接口和客户端)。

5. 实现 gRPC 服务端

创建 server/main.go,实现 UserService 接口:


package main

import (
    "context"
    "log"
    "net"
    "strconv"
    "sync"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    pb "grpc-demo/pb/user"
)

type userServer struct {
    pb.UnimplementedUserServiceServer
    mu    sync.Mutex
    users map[int32]*pb.User
    nextID int32
}

func newUserServer() *userServer {
    return &userServer{
        users: make(map[int32]*pb.User),
        nextID: 1,
    }
}

// 一元调用:获取用户
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    user, ok := s.users[req.Id]
    if !ok {
        return nil, status.Errorf(codes.NotFound, "user %d not found", req.Id)
    }
    return &pb.GetUserResponse{User: user}, nil
}

// 服务端流:列出所有用户
func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    for _, user := range s.users {
        if err := stream.Send(user); err != nil {
            return err
        }
    }
    return nil
}

// 客户端流:批量创建用户
func (s *userServer) CreateUsers(stream pb.UserService_CreateUsersServer) error {
    var created []*pb.User
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            return stream.SendAndClose(&pb.CreateUsersResponse{Users: created})
        }
        if err != nil {
            return err
        }
        s.mu.Lock()
        user := &pb.User{
            Id:    s.nextID,
            Name:  req.Name,
            Email: req.Email,
        }
        s.users[user.Id] = user
        s.nextID++
        created = append(created, user)
        s.mu.Unlock()
    }
}

// 双向流:简单聊天
func (s *userServer) Chat(stream pb.UserService_ChatServer) error {
    for {
        in, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        // 回显消息
        if err := stream.Send(&pb.ChatMessage{Message: "echo: " + in.Message}); err != nil {
            return err
        }
    }
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterUserServiceServer(s, newUserServer())
    log.Println("gRPC server listening on :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

注意:需要导入 io 包,并在 CreateUsersChat 中使用。

6. 实现 gRPC 客户端

创建 client/main.go


package main

import (
    "context"
    "io"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    pb "grpc-demo/pb/user"
)

func main() {
    // 连接服务端
    conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewUserServiceClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    defer cancel()

    // 1. 一元调用:获取用户(先创建再获取)
    // 创建用户
    createResp, err := client.CreateUsers(ctx)
    if err != nil {
        log.Fatalf("could not create stream: %v", err)
    }
    // 发送两个创建请求
    for _, name := range []string{"Alice", "Bob"} {
        if err := createResp.Send(&pb.CreateUserRequest{Name: name, Email: name + "@example.com"}); err != nil {
            log.Fatalf("could not send: %v", err)
        }
    }
    created, err := createResp.CloseAndRecv()
    if err != nil {
        log.Fatalf("could not receive response: %v", err)
    }
    log.Printf("Created users: %v\n", created.Users)

    // 获取第一个用户
    if len(created.Users) > 0 {
        getResp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: created.Users[0].Id})
        if err != nil {
            log.Fatalf("could not get user: %v", err)
        }
        log.Printf("GetUser: %v\n", getResp.User)
    }

    // 2. 服务端流:列出所有用户
    listStream, err := client.ListUsers(ctx, &pb.ListUsersRequest{Page: 1, PageSize: 10})
    if err != nil {
        log.Fatalf("could not list users: %v", err)
    }
    for {
        user, err := listStream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("list stream error: %v", err)
        }
        log.Printf("Listed user: %v\n", user)
    }

    // 3. 双向流:聊天
    chatStream, err := client.Chat(ctx)
    if err != nil {
        log.Fatalf("could not start chat: %v", err)
    }
    // 发送消息
    go func() {
        for i := 0; i < 3; i++ {
            if err := chatStream.Send(&pb.ChatMessage{Message: "hello " + strconv.Itoa(i)}); err != nil {
                log.Fatalf("chat send error: %v", err)
            }
            time.Sleep(time.Second)
        }
        chatStream.CloseSend()
    }()
    // 接收响应
    for {
        msg, err := chatStream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("chat recv error: %v", err)
        }
        log.Printf("Chat echo: %s\n", msg.Message)
    }
}

注意:grpc.NewClient 是较新版本的方法,旧版本使用 grpc.Dial。确保你的 grpc-go 版本较新。

7. 运行与测试

启动服务端:


go run server/main.go

另开终端运行客户端:


go run client/main.go

观察输出,应能看到创建的用户、列表和聊天回显。

8. 最佳实践与常见坑

8.1 最佳实践

  • 使用 context 控制超时:在客户端调用时总是传递 context,避免阻塞。
  • 错误处理:使用 status 包返回标准 gRPC 错误码,便于客户端处理。
  • 流式调用注意资源释放:确保流被关闭,避免资源泄漏。
  • 连接复用:客户端应复用连接,不要频繁创建。
  • 使用拦截器:实现日志、认证、监控等横切关注点。

8.2 常见坑

  • proto 文件路径错误:生成代码时确保 --goout--go-grpcout 路径正确。
  • 版本不兼容:protoc-gen-go 和 grpc 版本需要匹配,否则可能生成错误代码。
  • 未实现所有方法:嵌入 UnimplementedUserServiceServer 以避免未实现的方法导致 panic。
  • 连接失败:检查服务端地址和端口,确保网络可达。

9. 进阶:gRPC-Web 与前端集成

对于前端,gRPC 不能直接在浏览器中使用,需要 gRPC-Web 代理。可以使用 Envoy 或 gRPC-Web 库。这里简要介绍:

  1. 部署一个 gRPC-Web 代理(如 Envoy)将 HTTP/1.1 转换为 HTTP/2。
  2. 前端使用 @grpc/grpc-js 的 Web 版本或 grpc-web 库。

示例(使用 grpc-web 库):


// 前端代码示例
const { GetUserRequest } = require('./user_pb.js');
const { UserServiceClient } = require('./user_grpc_web_pb.js');

const client = new UserServiceClient('http://localhost:8080');
const req = new GetUserRequest();
req.setId(1);
client.getUser(req, {}, (err, response) => {
  if (err) console.error(err);
  else console.log(response.getUser());
});

总结

通过本文的实战,你已经掌握了 gRPC 的核心概念和四种调用模式,并成功实现了一个用户服务。gRPC 的高性能和强类型特性使其成为微服务通信的理想选择。下一步,你可以尝试集成 gRPC 拦截器、添加 TLS 加密、或使用服务发现(如 etcd、Consul)。

延伸阅读:

希望这篇文章能帮助你在实际项目中更好地应用 gRPC!