4. 第一个gRPC服务:定义proto文件、生成Go代码、实现服务端、实现客户端、运行测试

本章是gRPC开发的“Hello World”实战环节。你将亲手完成从契约定义到代码运行的全流程:编写.proto文件、使用protoc工具生成Go桩代码、实现服务端与客户端逻辑,并验证双向通信。这是后续所有审计与安全加固的基础。

4.1 整体流程概览

一个完整的gRPC服务开发遵循“契约优先”原则,流程分为五个步骤:

  1. 定义契约:编写.proto文件,声明服务接口与消息结构。
  2. 生成代码:使用protoc编译器生成Go语言的客户端桩代码和服务端接口。
  3. 实现服务端:编写Go结构体,实现生成的接口方法。
  4. 实现客户端:使用生成的客户端桩代码发起RPC调用。
  5. 运行测试:启动服务端,运行客户端,验证通信是否正常。
关键原则:.proto文件是团队协作的“单一事实来源”。服务端与客户端必须基于同一份proto文件生成代码,否则契约断裂将导致通信失败。
gRPC服务开发流程(契约优先) 1. 定义契约 .proto文件 2. 生成代码 protoc + protoc-gen-go 3. 实现服务端 Go struct + 接口方法 4. 实现客户端 调用桩代码 5. 运行测试 验证通信 ⚠ 契约一致性要求:服务端与客户端必须基于同一份 .proto 文件生成代码 任何一方修改 .proto 文件后,另一方必须重新生成代码,否则 RPC 调用将失败 gRPC服务端 监听 :50051 HTTP/2 双向流 gRPC客户端 调用 SayHello()

4.2 定义proto文件

创建一个名为hello.proto的文件,定义Greeter服务及其消息类型。这是整个系统的契约基础。

syntax = "proto3";

package hello;

option go_package = "github.com/example/hello/proto/hello";

// Greeter 服务:提供问候功能
service Greeter {
  // 一元RPC:发送一个请求,接收一个响应
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// 请求消息:包含用户名
message HelloRequest {
  string name = 1;
}

// 响应消息:包含问候语
message HelloReply {
  string message = 1;
}
字段编号规则:消息中的每个字段必须有一个唯一的编号(1-15占用1字节,16-2047占用2字节)。频繁使用的字段应使用小编号以节省带宽。

4.3 生成Go代码

使用protoc编译器生成Go桩代码。确保已安装protoc、protoc-gen-go和protoc-gen-go-grpc插件。

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

# 生成代码
protoc --go_out=. --go-grpc_out=. hello.proto

执行后生成两个文件:

生成文件 内容说明
hello.pb.go 消息类型的Go结构体(HelloRequest、HelloReply)及序列化/反序列化方法
hello_grpc.pb.go 服务端接口(GreeterServer)、客户端桩代码(GreeterClient)及注册函数
审计关注点:生成的hello_grpc.pb.go中定义了GreeterServer接口。任何服务端实现都必须完整实现该接口的所有方法,否则编译会报错。这是契约强制性的体现。

4.4 实现服务端

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

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "github.com/example/hello/proto/hello"
)

// server 结构体实现 GreeterServer 接口
type server struct {
    pb.UnimplementedGreeterServer
}

// SayHello 实现接口方法
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("收到请求: %v", req.GetName())
    return &pb.HelloReply{Message: "Hello " + req.GetName()}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("监听失败: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})

    log.Println("服务端启动,监听 :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("服务启动失败: %v", err)
    }
}
安全基线:生产环境中grpc.NewServer()默认不启用TLS。必须显式配置grpc.Creds()选项启用TLS加密,否则所有通信将以明文传输,违反安全审计要求。

4.5 实现客户端

创建client/main.go,使用生成的客户端桩代码发起RPC调用:

package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    pb "github.com/example/hello/proto/hello"
)

func main() {
    // 连接服务端(不启用TLS,仅用于演示)
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("连接失败: %v", err)
    }
    defer conn.Close()

    client := pb.NewGreeterClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    // 调用 SayHello
    resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: "gRPC"})
    if err != nil {
        log.Fatalf("调用失败: %v", err)
    }

    log.Printf("收到响应: %v", resp.GetMessage())
}

4.6 运行测试

启动服务端和客户端,验证通信是否正常:

# 终端1:启动服务端
go run server/main.go
# 输出: 服务端启动,监听 :50051

# 终端2:运行客户端
go run client/main.go
# 输出: 收到响应: Hello gRPC
gRPC调用时序图(一元RPC) gRPC客户端 gRPC服务端 SayHello(HelloRequest{name:"gRPC"}) 处理请求 HelloReply{message:"Hello gRPC"} 收到响应

4.7 常见问题与排查

问题现象 可能原因 解决方案
protoc: command not found 未安装protoc或未加入PATH 从protobuf releases下载并安装,确保protoc可执行
undefined: pb.RegisterGreeterServer 未正确导入生成的pb包 检查go_package路径与import路径是否一致
connection refused 服务端未启动或端口错误 确认服务端已运行且监听端口与客户端一致
rpc error: code = Unimplemented 服务端未注册服务或方法未实现 检查RegisterGreeterServer调用及接口方法实现
调试技巧:使用grpcurl工具可以无需编写客户端代码即可测试gRPC服务:grpcurl -plaintext localhost:50051 list 查看已注册的服务列表。

4.8 本章小结

通过本章实战,你完成了第一个gRPC服务的完整开发流程:

  • 编写.proto文件定义服务契约
  • 使用protoc生成Go桩代码
  • 实现服务端接口并启动gRPC服务
  • 实现客户端并成功调用远程方法
  • 验证了契约一致性对通信成功的关键作用