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日

相关文章

  • juc面试题目

    JUC面试题目攻略 JUC(Java Util Concurrent)是Java中用于并发编程的工具包,包含了许多用于多线程编程的类口。在JUC面试中,常见的问题包括线程池、锁、原子类等。本攻略将详细介绍JUC面试题目的解答方法,并提供两个示例说明。 线程池 问题1:线程池的作用是什么? 答:线程池一种用于管理程的机制,它可以在需要时创建线程,并在不需要时用…

    other 2023年5月7日
    00
  • 百度ocr文字识别在线

    百度OCR文字识别在线 百度OCR文字识别在线是一款免费的在线文字识别工具,使用百度超级计算集群作为技术支撑,能够快速而精确地将图片中的文字识别出来。这个工具可以非常方便地解决文字录入的繁琐问题,比如需要将纸质文件转化为电子文档、需要把图片中的文字提取出来等。 优点 免费:百度OCR文字识别在线工具是免费的,无需任何费用,只需要注册一个账户即可使用。 精确度…

    其他 2023年3月28日
    00
  • 获取Android签名MD5的方式实例详解

    以下是使用标准的Markdown格式文本,详细讲解获取Android签名MD5的方式的实例详解的完整攻略: 获取Android签名MD5的方式 打开终端或命令提示符窗口,并导航到包含应用签名文件的目录。 使用以下命令获取应用签名的MD5值: shell keytool -list -v -keystore your_keystore_file.keystor…

    other 2023年10月14日
    00
  • 服务器技术全面解析

    服务器技术全面解析 前言 服务器技术是一项广泛的技术领域,涉及到多种方面的知识。了解服务器技术对于每一个Web开发者都是必要的,因为它是支撑所有网站、应用程序和Web服务的基石。在这篇文章中,我们将对服务器技术进行全面的解析。我们将从什么是服务器开始,逐步介绍服务器的相关知识,并且提供两个示例来说明服务器的运作方式。 什么是服务器? 服务器是指一台专门用于提…

    other 2023年6月26日
    00
  • mongodb的ttl索引介绍(超时索引)

    MongoDB的TTL索引介绍(超时索引) MongoDB是一种NoSQL数据库系统,它支持多种类型的索引,其中一种常见的索引是TTL索引(超时索引)。在这篇文章中,我将介绍TTL索引的基本概念、使用场景和实现方法。 TTL索引是什么? TTL是”Time to Live”的缩写,它代表了某个对象的存活时间。在MongoDB中,TTL索引即为超时索引,它是一…

    其他 2023年3月29日
    00
  • 网站访问慢的排查方法及解决方案

    网站访问慢的排查方法及解决方案 排查方法 1. 确定问题范围 首先需要明确问题的具体表现,例如是整个网站慢还是只有某个页面慢,是移动端还是PC端访问慢等等。通过定位问题的具体表现,可以明确排查范围,缩小问题的影响范围从而更加高效地排查问题。 2. 基础排查 基础排查包括检查网站服务器、网络连接、DNS解析等基本内容,以下是一些基础排查的方法: 通过ping命…

    other 2023年6月26日
    00
  • 一个手机号可以注册几个b站账号?B站可以同手机号多账号吗

    根据B站的官方规定,一个手机号只能用来注册一个B站账号。当手机号已经被注册过之后,再用它注册新的账号将会失败。 同一手机号注册多个B站账号的方法有两种: 绑定已有的其他社交账号 B站支持绑定其他社交账号,如微信、QQ等,这些账号与手机号绑定后再使用可视为与同一手机号关联的其它账号,可以使用不同的账号发表评论,上传视频或直播等操作。「B站账号中心→社交账号」即…

    other 2023年6月27日
    00
  • 如何在sqlite中创建自增字段

    如何在SQLite中创建自增字段 在SQLite中,我们可以使用自增字段实现自动编号,该字段可以避免插入重复的数据记录,并且方便我们进行数据管理和查询。本文将简单介绍如何在SQLite中创建自增字段。 1. 建立数据表 首先,我们需要建立一张数据表,例如: CREATE TABLE users ( id INTEGER PRIMARY KEY, name T…

    其他 2023年3月28日
    00
合作推广
合作推广
分享本页
返回顶部