Golang开发gRPC服务入门介绍

Golang开发gRPC服务入门介绍

什么是gRPC?

gRPC是一种高性能、开源和通用的RPC框架,由Google推出,基于ProtoBuf序列化协议来实现,具有简单易用、跨语言、高效快速等特点。

gRPC工作原理是什么?

gRPC基于HTTP/2协议,利用protobuf进行序列化,传输效率极高,具体实现原理请参考官方文档

gRPC的优点

  • 性能高:采用protobuf序列化协议和HTTP/2协议,传输效率极高。
  • 跨语言:支持多种语言,如Go、Java、C++等。
  • 易用性好:gRPC支持自动生成客户端和服务端的代码,用户开发时无需关心网络编程。

如何使用gRPC?

gRPC的使用分为三个步骤:

  1. 定义一个.proto文件来描述你要实现的服务。该文件定义了服务的接口、消息类型等信息。
  2. 根据.proto文件生成客户端和服务端的代码。gRPC支持多种代码生成语言,如Go、Java、C++等。
  3. 在服务端实现.proto文件中定义的接口,客户端调用生成的代码,即可实现远程调用。

示例一:使用gRPC实现简易计算器

我们将使用gRPC实现一个简易计算器服务,支持加、减、乘、除四种运算。

定义.proto文件

syntax = "proto3";

package calculator;

service Calculator {
  rpc Add(Params) returns (Result) {}
  rpc Subtract(Params) returns (Result) {}
  rpc Multiply(Params) returns (Result) {}
  rpc Divide(Params) returns (Result) {}
}

message Params {
  int32 num1 = 1;
  int32 num2 = 2;
}

message Result {
  int32 res = 1;
}

根据.proto文件生成代码

使用protoc工具生成对应的Go代码。

protoc -I calculator/ calculator/calculator.proto --go_out=plugins=grpc:calculator

实现服务端

服务端代码如下:

package main

import (
    "context"
    "log"
    "net"

    pb "calculator"

    "google.golang.org/grpc"
)

type server struct{}

func (s *server) Add(ctx context.Context, in *pb.Params) (*pb.Result, error) {
    log.Printf("Add Received: %v\n", in)
    return &pb.Result{Res: in.Num1 + in.Num2}, nil
}

func (s *server) Subtract(ctx context.Context, in *pb.Params) (*pb.Result, error) {
    log.Printf("Subtract Received: %v\n", in)
    return &pb.Result{Res: in.Num1 - in.Num2}, nil
}

func (s *server) Multiply(ctx context.Context, in *pb.Params) (*pb.Result, error) {
    log.Printf("Multiply Received: %v\n", in)
    return &pb.Result{Res: in.Num1 * in.Num2}, nil
}

func (s *server) Divide(ctx context.Context, in *pb.Params) (*pb.Result, error) {
    log.Printf("Divide Received: %v\n", in)
    return &pb.Result{Res: in.Num1 / in.Num2}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    srv := grpc.NewServer()
    pb.RegisterCalculatorServer(srv, &server{})
    log.Printf("Listen to :8080...")
    if err := srv.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

实现客户端

客户端代码如下:

package main

import (
    "context"
    "log"
    "os"
    "strconv"

    pb "calculator"
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial(":9000", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewCalculatorClient(conn)

    cmd := os.Args[1]
    num1, err := strconv.Atoi(os.Args[2])
    num2, err := strconv.Atoi(os.Args[3])
    if err != nil {
        log.Fatalf("failed to parse args: %v", err)
    }

    var res *pb.Result
    switch cmd {
    case "add":
        res, err = client.Add(context.Background(), &pb.Params{Num1: int32(num1), Num2: int32(num2)})
    case "subtract":
        res, err = client.Subtract(context.Background(), &pb.Params{Num1: int32(num1), Num2: int32(num2)})
    case "multiply":
        res, err = client.Multiply(context.Background(), &pb.Params{Num1: int32(num1), Num2: int32(num2)})
    case "divide":
        res, err = client.Divide(context.Background(), &pb.Params{Num1: int32(num1), Num2: int32(num2)})
    default:
        log.Fatalf("invalid command: %v", cmd)
    }

    if err != nil {
        log.Fatalf("failed to call: %v", err)
    }

    log.Printf("Result: %v", res.Res)
}

运行

先启动服务端:

go run server.go

然后在另一个终端启动客户端:

go run client.go add 1 2

注意事项

  • 代码中的grpc.WithInsecure()表示使用非安全的连接方式,实际应用中应使用安全的方式。
  • 服务端代码启动时监听的端口号应与客户端代码中连接的端口号一致。
  • 在调用远程函数时需要传入context.Context参数。

示例二:使用gRPC实现文件传输

这里介绍如何使用gRPC实现文件传输。

定义.proto文件

syntax = "proto3";

package filetransfer;

service FileTransfer {
  rpc Upload(stream Chunk) returns (Response) {}
  rpc Download(Request) returns (stream Chunk) {}
}

message Request {
  string filename = 1;
}

message Chunk {
  bytes content = 1;
}

message Response {
  bool ok = 1;
}

根据.proto文件生成代码

使用protoc工具生成对应的Go代码。

protoc -I filetransfer/ filetransfer/filetransfer.proto --go_out=plugins=grpc:filetransfer

实现服务端

服务端代码如下:

package main

import (
    "context"
    "io"
    "log"
    "net"

    pb "filetransfer"

    "google.golang.org/grpc"
)

type server struct{}

func (s *server) Upload(stream pb.FileTransfer_UploadServer) error {
    log.Printf("Upload Request\n")
    dst, err := CreateFile("/tmp/"+stream.Context().Value("filename").(string), false)
    if err != nil {
        return err
    }
    for {
        chunk, err := stream.Recv()
        if err == io.EOF {
            log.Printf("Upload Done\n")
            return stream.SendAndClose(&pb.Response{Ok: true})
        }
        data := chunk.Content
        dst.Write(data)
    }

    return nil
}

func (s *server) Download(req *pb.Request, stream pb.FileTransfer_DownloadServer) error {

    log.Printf("Download Request, filename: %s\n", req.Filename)
    file, err := OpenFile("/tmp/"+req.Filename, true)
    if err != nil {
        return err
    }

    defer file.Close()

    for {
        chunk := make([]byte, 1024)
        n, err := file.Read(chunk)
        if err == io.EOF {
            log.Printf("Download Done\n")
            break
        } else if err != nil {
            return err
        }
        stream.Send(&pb.Chunk{Content: chunk[:n]})
    }

    return nil
}

func OpenFile(filepath string, readonly bool) (*os.File, error) {
    if readonly {
        return os.Open(filepath)
    } else {
        return os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0666)
    }
}

func CreateFile(filepath string, readonly bool) (*os.File, error) {
    if readonly {
        return os.Create(filepath)
    } else {
        return os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
    }
}

func main() {
    lis, err := net.Listen("tcp", ":9000")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    srv := grpc.NewServer()

    pb.RegisterFileTransferServer(srv, &server{})

    log.Printf("Listen to :9000...")
    if err := srv.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

实现客户端

客户端代码如下:

package main

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

    pb "filetransfer"
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial(":9000", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewFileTransferClient(conn)

    filename := os.Args[2]
    if os.Args[1] == "upload" {
        stream, err := client.Upload(
            context.Background(),
            grpc.PerRPCCredentials(credentials.NewStaticCredentials("","***")),//添加Token
            grpc.Header(&metadata.MD{"filename": []string{filename}}),
            )
        if err != nil {
            log.Fatalf("failed to upload: %v", err)
        }

        file, err := os.Open(filename)
        if err != nil {
            log.Fatalf("failed to open file: %v", err)
        }
        defer file.Close()

        for {
            chunk := make([]byte, 1024)
            n, err := file.Read(chunk)
            if err == io.EOF {
                break
            } else if err != nil {
                log.Fatalf("failed to read file: %v", err)
            }
            if err := stream.Send(&pb.Chunk{Content: chunk[:n]}); err != nil {
                log.Fatalf("failed to send chunk: %v", err)
            }
        }

        resp, err := stream.CloseAndRecv()
        if err != nil {
            log.Fatalf("failed to receive response: %v", err)
        }

        log.Printf("Upload Response: %v", resp.Ok)
    } else if os.Args[1] == "download" {
        stream, err := client.Download(context.Background(), &pb.Request{Filename: filename})
        if err != nil {
            log.Fatalf("failed to download: %v", err)
        }

        file, err := os.Create(filename)
        if err != nil {
            log.Fatalf("failed to create file: %v", err)
        }
        defer file.Close()

        for {
            chunk, err := stream.Recv()
            if err == io.EOF {
                break
            } else if err != nil {
                log.Fatalf("failed to receive chunk: %v", err)
            }
            _, err = file.Write(chunk.Content)
            if err != nil {
                log.Fatalf("failed to write file: %v", err)
            }
        }

        log.Printf("Download Done")
    } else {
        log.Fatalf("invalid command: %v", os.Args[1])
    }
}

运行

先启动服务端:

go run server.go

然后在另一个终端启动客户端上传文件:

go run client.go upload /path/to/file

或者下载文件:

go run client.go download filename

注意事项

  • 异步流式传输可以大幅提高文件传输效率。
  • 为了支持异步传输,在服务端和客户端的方法参数中,需要使用流式传输。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Golang开发gRPC服务入门介绍 - Python技术站

(0)
上一篇 2023年6月27日
下一篇 2023年6月27日

相关文章

  • 如何设置mysqldatetime列的默认值?

    如何设置MySQL datetime列的默认值? 在MySQL中,datetime是一种常用的数据类型,用于存储日期和时间。在创建表时,我们可以为datetime列指定默认值,以确保在插入新行时,该列始终具有一个值。本攻略将介绍如何设置MySQL datetime列的默认值。 方法一:使用NOW()函数 在MySQL中,可以使用NOW()函数来获取当前日期和…

    other 2023年5月9日
    00
  • Angular中使用嵌套Form的详细步骤

    Angular中使用嵌套Form的详细步骤 在Angular中,使用嵌套表单可以更好地组织和管理复杂的表单结构。下面是使用嵌套表单的详细步骤: 步骤1:导入必要的模块 首先,确保你的Angular项目中已经导入了ReactiveFormsModule模块。在你的模块文件(通常是app.module.ts)中添加以下代码: import { ReactiveF…

    other 2023年7月28日
    00
  • Thinkphp5 自定义上传文件名的实现方法

    下面是详细讲解“Thinkphp5 自定义上传文件名的实现方法”的完整攻略: 1. 简介 在Thinkphp5框架中,上传文件后一般会生成一个默认的文件名来保存上传文件。但是,有时我们希望自定义上传文件名,比如为了更好地管理文件或者为了更好地提供下载服务等。 本文将介绍如何在Thinkphp5中实现自定义上传文件名。 2. 实现方法 实现自定义上传文件名可以…

    other 2023年6月27日
    00
  • iOS13.4正式版固件下载地址 iOS13.4正式版下载

    iOS 13.4正式版固件下载地址 苹果公司发布了iOS 13.4正式版固件,这是一次重要的更新,带来了许多新功能和改进。如果你想下载并安装这个版本,下面是一份完整的攻略。 步骤一:备份你的设备 在开始更新之前,强烈建议你备份你的设备。这样可以确保你的数据在更新过程中不会丢失。你可以使用iCloud或iTunes进行备份。 步骤二:检查设备兼容性 确保你的设…

    other 2023年8月4日
    00
  • android自定义popupwindow仿微信右上角弹出菜单效果

    Android自定义PopupWindow仿微信右上角弹出菜单效果攻略 在本攻略中,我将详细介绍如何实现一个仿微信右上角弹出菜单效果的自定义PopupWindow。这个效果通常用于显示更多选项或操作,类似于微信中的右上角菜单。 步骤一:创建PopupWindow布局 首先,我们需要创建一个自定义的PopupWindow布局。这个布局将包含菜单项和其他必要的U…

    other 2023年8月25日
    00
  • C语言数组超详细讲解上

    C语言数组超详细讲解 概述 C语言中的数组是一种数据结构,可以用于存储一组相同的数据类型。数组可以容纳大量数据,可以通过下标来访问数组中的特定元素。数组在程序中的应用非常广泛,特别是在处理大量数据和进行数值计算的时候。 创建数组 要创建数组,首先需要定义数组的长度和数据类型。数组的长度表示数组可以容纳多少个元素,数据类型表示这些元素的类型。例如,下面的代码定…

    other 2023年6月25日
    00
  • QQ7.1 安全防护版发布 QQ7.1 安全防护版下载地址

    QQ7.1 安全防护版发布攻略 1. 简介 QQ7.1 安全防护版是一款专注于用户隐私和安全的即时通讯软件。它提供了一系列的安全功能和防护措施,以保护用户的个人信息和通信内容。本攻略将详细介绍 QQ7.1 安全防护版的发布和下载过程。 2. 发布信息 版本号:QQ7.1 安全防护版 发布日期:待定 主要特性: 强化用户隐私保护 加密通信内容 防止恶意软件攻击…

    other 2023年8月4日
    00
  • Java编程Socket实现多个客户端连接同一个服务端代码

    需要实现Java编程Socket实现多个客户端连接同一个服务端的功能,通常需要遵循以下步骤: 1. 创建服务端Socket在服务端,我们需要创建一个ServerSocket对象。这个对象可以监听客户端连接请求,并为每个新的连接创建一个Socket对象。以下是示例代码: ServerSocket serverSocket = new ServerSocket(…

    other 2023年6月27日
    00
合作推广
合作推广
分享本页
返回顶部