gRPC 简单使用

简介

RPC 的全称是 Remote Procedure Call(远程过程调用), 即可以在客户端应用程序中直接调用其他计算机(服务端)上定义的方法.

gRPC 是一个 RPC 框架, 使用 protobuf 作为数据交换协议.

定义服务

既然是 RPC 系统, 主要的目的在于定义方法, 或者说服务 service.

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

在 gRPC 中可以定义四种类型的服务:

  • 一元 RPC: 客户端向服务器发送单个请求并获取单个响应, 类似普通函数调用
  • 服务器流式 RPC: 客户端发送单个请求, 服务端返回流式响应
  • 客户端流式 RPC: 客户端发送流式请求, 服务端返回单个响应
  • 双向流式 RPC: 使用两个独立的流, 客户端发送流式请求, 服务端返回流式响应
rpc SayHello(HelloRequest) returns (HelloResponse){
}
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

Golang 下使用

定义 protobuf 文件 hello.proto 的内容为:

syntax = "proto3";

import "google/protobuf/any.proto";

package hello;
option go_package = "hello";

message HelloReq {
  string name = 1;
}

message HelloResp {
  int32 code = 1;
  string greet = 2;
  google.protobuf.Any details = 3;
}

service HelloService {
  rpc Greet(HelloReq) returns (HelloResp) {};
  rpc GreetWithServerStream(HelloReq) returns (stream HelloResp) {};
  rpc GreetWithClientStream(stream HelloReq) returns (HelloResp) {};
  rpc GreetWithBidirectionalStream(stream HelloReq) returns (stream HelloResp) {};
}

初始化项目, 安装必要的依赖:

go mod init tzh.com/app
go get -u github.com/golang/protobuf/protoc-gen-go
go get -u google.golang.org/grpc
mkdir hello
# 假设 protoc3 已经解压好了
.\protoc3\bin\protoc.exe --proto_path=. hello.proto --go_out=plugins=grpc:./hello

生成代码的时候, 注意指定插件 plugins=grpc.

main.go 如下:

package main

import (
    "context"
    "flag"
    "fmt"
    "io"
    "log"
    "net"
    "strings"

    "google.golang.org/grpc"
    pb "tzh.com/app/hello"
)

const port = ":5000"

type server struct {
    pb.UnimplementedHelloServiceServer
}

func (s *server) Greet(ctx context.Context, in *pb.HelloReq) (*pb.HelloResp, error) {
    return &pb.HelloResp{Code: 0, Greet: "hello " + in.GetName()}, nil
}

// 对于空格分隔的 name, 使用 stream 发送多次数据
func (s *server) GreetWithServerStream(in *pb.HelloReq, stream pb.HelloService_GreetWithServerStreamServer) error {
    names := strings.Split(in.GetName(), " ")
    for i, name := range names {
        err := stream.Send(&pb.HelloResp{
            Code:  int32(i),
            Greet: fmt.Sprintf("part %d: hello %s", i, name),
        })
        if err != nil {
            return err
        }
    }
    return nil
}

// 对于客户端发送的多个 name, 合并后发送单条响应
func (s *server) GreetWithClientStream(stream pb.HelloService_GreetWithClientStreamServer) error {
    names := make([]string, 0)
    for {
        msg, err := stream.Recv()
        if err != nil {
            break
        }
        names = append(names, msg.GetName())
    }
    stream.SendAndClose(&pb.HelloResp{
        Code:  0,
        Greet: fmt.Sprintf("hello %s count: %d", strings.Join(names, " "), len(names)),
    })
    return nil
}

// 双向流, 对于每个请求, 一一响应
func (s *server) GreetWithBidirectionalStream(stream pb.HelloService_GreetWithBidirectionalStreamServer) error {
    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        if err := stream.Send(&pb.HelloResp{
            Code:  0,
            Greet: "hello " + msg.GetName(),
        }); err != nil {
            return err
        }
    }
}

func runServer() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen on %s: %v", port, err)
    }
  s := grpc.NewServer()
  // 注册服务
    pb.RegisterHelloServiceServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to server: %v", err)
    }
}

func run1(client pb.HelloServiceClient) {
    log.Println("############ Greet ")
    r, err := client.Greet(context.Background(), &pb.HelloReq{
        Name: "tt",
    })
    if err != nil {
        log.Fatalf("failed to get greet resp: %v", err)
    }
    log.Printf("get code : %d, get greet: %s \n", r.GetCode(), r.GetGreet())
}

func run2(client pb.HelloServiceClient) {
    log.Println("############ GreetWithServerStream")
    serverStream, err := client.GreetWithServerStream(context.Background(), &pb.HelloReq{
        Name: "tt aa xx ff",
    })
    if err != nil {
        log.Fatalf("failed with GreetWithServerStream: %v", err)
    }

    for {
        r, err := serverStream.Recv()
        if err != nil {
            break
        }
        log.Printf("get code : %d, get greet: %s \n", r.GetCode(), r.GetGreet())
    }
}

func run3(client pb.HelloServiceClient) {
    log.Println("############ GreetWithClientStream ")
    clientStream, err := client.GreetWithClientStream(context.Background())
    if err != nil {
        log.Fatalf("failed with GreetWithClientStream: %v", err)
    }

    for _, name := range []string{"tt", "qq", "aa", "yy"} {
        err := clientStream.Send(&pb.HelloReq{
            Name: name,
        })
        if err != nil {
            log.Fatalf("failed with GreetWithClientStream during send request: %v", err)
            break
        }
    }
    r, err := clientStream.CloseAndRecv()
    if err != nil {
        log.Fatalf("failed with GreetWithClientStream when get response: %v", err)
    }
    log.Printf("get code : %d, get greet: %s \n", r.GetCode(), r.GetGreet())
}

func run4(client pb.HelloServiceClient) {
    log.Println("############ GreetWithBidirectionalStream ")
    stream, err := client.GreetWithBidirectionalStream(context.Background())
    if err != nil {
        log.Fatalf("failed with GreetWithBidirectionalStream: %v", err)
    }

    for _, name := range []string{"tt", "qq", "aa", "yy"} {
        err := stream.Send(&pb.HelloReq{
            Name: name,
        })
        if err != nil {
            log.Fatalf("failed with GreetWithClientStream during send request: %v", err)
            break
        }
    }
    stream.CloseSend()

    for {
        r, err := stream.Recv()
        if err != nil {
            break
        }
        log.Printf("get code : %d, get greet: %s \n", r.GetCode(), r.GetGreet())
    }
}

func runClient() {
    address := "localhost" + port
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("failed to connect %s: %v", address, err)
    }
    defer conn.Close()
    client := pb.NewHelloServiceClient(conn)

    run1(client)
    run2(client)
    run3(client)
    run4(client)
}

func main() {
    isClient := flag.Bool("client", false, "run client")
    flag.Parse()
    if *isClient {
        runClient()
    } else {
        runServer()
    }
}

代码有点长, 因为将服务端代码和客户端代码都混合在同一个文件中, 使用下面的命令分别启动服务端和运行客户端.

# 运行服务端
go run main.go
# 运行客户端
go run main.go --client

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • 领导力体现出来的首先是责任。一个管理者在团队成员面前的形象,决定了他是否能够被人追随。在一定程度上,团队成员在寻找...
    w小郭阅读 233评论 0 0
  • 一、需求的背景: 当网站页面内容过多且比较长的时候,用户需要在网页里上下来回的翻滚,查阅页面的信息内容时,那使用浏...
    nicn阅读 2,499评论 0 0
  • 《彖》曰:屯,刚柔始交而难生;动乎险中,大亨贞。雷雨之动满盈,天造草昧;宜建侯而不宁。 屯卦,由坎和震组成,在八卦...
    陈锦权阅读 730评论 1 1
  • 泉 城* 全市国槐花儿艳, 七十二处泉眼蹿。 大明湖畔乾荷月, 千佛山下舜耕田。 古色古香大观园, 芙...
    憨牛_713c阅读 241评论 0 1