Help us understand the problem. What is going on with this article?

Go + gRPCでzipファイルを作成する

More than 1 year has passed since last update.

gRPCは、googleが開発したRPCライブラリ+フレームワークです。
通信はHTTP/2、IDL(interface description language)はProtocol Buffers、というあたりが特徴になります。
とはいえ、お試しレベルで使う分には「RPCである」という以上の特徴はないかもしれません。

もっと詳しく知りたい人は、以下を見ていただくのがよいと思います。

Goal

Go 1.9.2 + gRPCで動作するサンプルを紹介する。

環境構築

Windows 10 Pro 64bit を使った例として記載します。
何か不明点がある場合は、オフィシャルの Quick Start - Go を見るとよいです。

  1. gRPCをインストールする
    $ go get -u google.golang.org/grpc
  2. Protocol Buffers v3 の protoc compiler をインストールする
    • 以下のURLから、プラットフォームに合わせたコンパイル済みバイナリをダウンロードする
      protoc-<version>-<platform>.zip のようなファイル名になっています
      必要なのはprotoc.exeのみなので、適当にPATHの通った場所に置くなどしてください
  3. protoc plugin for Go をインストールする
    $ go get -u github.com/golang/protobuf/protoc-gen-go

基本的な開発の流れ;

以下の流れで開発を行います。
変更などがある場合は、1~3を繰り返す形となります。

  1. gRPCのサービスを*.protoにより定義する
  2. protocにより*.protoから*.pb.goを生成する
    *.pb.goは、自動生成されるソースコードなので編集不要
  3. サービスを実装する

なお、*.protoはprotocによりJavaやC#等の各種言語用ソースにコンパイルできるため、serverとclientは異なる言語での記述が可能となります。

作るもの

題材は、gRPCでzipファイルを作成するサービスgrpczipの作成とします。

  • client
    以下のように指定することで、aaa.txtとbbb.txtを圧縮してoutput.zipを作成する
    $ client.exe output.zip aaa.txt bbb.txt
  • server
    gRPCによって受け取った引数をzip圧縮し、clientに返す

サービスを定義する

grpczip.protoを作成します。
Goの作法に従い、今回は%GOPATH%\src\github.com\sago35\grpczip-sample以下で作業を行います。
以下が作成するサービスの全体像になります。

syntax = "proto3";

package grpczip;

message File {
    string Filename = 1;
    bytes Data = 2;
}

message Request {
    string ZipFilename = 1;
    repeated File Files = 2;
}

message Response {
    File ZipFile = 1;
}

service Grpczip {
    rpc Grpczip (Request) returns (Response) {}
}

上記を作成したら以下のコマンドでgrpczip.pb.zipを作成します。
この時点では、2ファイルが存在します。

$ protoc --go_out=plugins=grpc:. grpczip.proto

$ tree /f
フォルダー パスの一覧:  ボリューム Windows
ボリューム シリアル番号は 0000008E AEC0:D95D です
C:.
    grpczip.pb.go
    grpczip.proto

サブフォルダーは存在しません

サービスの詳細

以下の部分が、RPCのための関数定義となります。
clientはGrpczip(Reuest)をコールして、Responseを受け取る事ができます。

service Grpczip {
    rpc Grpczip (Request) returns (Response) {}
}

Requestは以下で定義しています。
RequestZipFilenamerepeated File(Fileの配列のようなイメージ)のFilesを持ちます。
Fileは、FilenameData(実際のバイナリデータ)を持ちます。

Goの構造体とほぼ同じイメージで定義できるため特に迷うことはないと思います。
構造体の入れ子を作れるので、分かりやすい定義ができていいですね。

string Filename = 1;1 の部分は unique numbered tag と呼ばれ、バイナリにエンコードした時の識別に使われます。
1~15は1byteにエンコードされるため頻繁に使うフィールドに、16以降は2byte以上にエンコードされるためそれ以外のフィールドに使うべきです。
また、フィールドは追加はOKだが変更や削除はしないほうが良い、というのが基本となっています。

message File {
    string Filename = 1;
    bytes Data = 2;
}

message Request {
    string ZipFileName = 1;
    repeated File Files = 2;
}

最後にResponseを定義します。

message Response {
    File ZipFile = 1;
}

*.protoの記述について、より詳細に知りたい場合は以下を見てください。

Server側を作成する

./cmd/server/server.go を作成します。

ある程度長くなってしまうのですが、実体は関数Grpczip()となります。
r.GetFiles()により、gRPCの引数のFilesを取り出すことができます。
同様に、r.GetZipFilename()によりZipFilenameを取り出すことができます。
ここでは、archize/zip使ってFilesを圧縮しpb.Responseにセットしています。

package main

import (
    "archive/zip"
    "bytes"
    "log"
    "net"
    "path/filepath"

    pb "github.com/sago35/grpczip-sample"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
)

type grpcServer struct {
}

func (s *grpcServer) Grpczip(ctx context.Context, r *pb.Request) (*pb.Response, error) {
    buf := new(bytes.Buffer)
    w := zip.NewWriter(buf)

    for _, file := range r.GetFiles() {
        f, err := w.Create(filepath.Base(file.GetFilename()))
        if err != nil {
            return nil, err
        }
        _, err = f.Write(file.GetData())
        if err != nil {
            return nil, err
        }
    }
    err := w.Close()
    if err != nil {
        return nil, err
    }

    res := &pb.Response{
        ZipFile: &pb.File{
            Filename: r.GetZipFilename(),
            Data:     buf.Bytes(),
        },
    }
    return res, nil
}

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

    srv := grpc.NewServer(grpc.MaxMsgSize(0xFFFFFFFF))

    pb.RegisterGrpczipServer(srv, &grpcServer{})
    srv.Serve(lis)
}

grpc.NewServer()で作成されるサーバーはデフォルトでは最大4096KBのMessageしか受け付けてくれません。
今回はzipを作成するサービスなので、以下のようにして大きい値で設定しています。

    srv := grpc.NewServer(grpc.MaxMsgSize(0xFFFFFFFF))

Client側を作成する

./cmd/client/client.go を作成します。

またもや結構な長さになってしまうのですが、基本的にはpb.Requestを作成してclient.Grpczip()をコールしているだけです。

package main

import (
    "bytes"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"

    "golang.org/x/net/context"

    pb "github.com/sago35/grpczip-sample"

    "google.golang.org/grpc"
)

func main() {
    if len(os.Args) < 2 {
        log.Fatalf("usage : %s [zipFilename] [input...]", os.Args[0])
    }
    zipFilename := os.Args[1]
    files := os.Args[2:]

    conn, err := grpc.Dial("127.0.0.1:11111", grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(0xFFFFFFFF)))
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    client := pb.NewGrpczipClient(conn)

    req, err := makeRequest(zipFilename, files)
    if err != nil {
        log.Fatal(err)
    }

    resp, err := client.Grpczip(context.Background(), req)
    if err != nil {
        log.Fatal(err)
    }

    z := resp.GetZipFile()
    w, err := os.Create(z.GetFilename())
    if err != nil {
        log.Fatal(err)
    }
    defer w.Close()

    r := bytes.NewReader(z.GetData())
    size, err := io.Copy(w, r)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s (%d bytes)\n", z.GetFilename(), size)
}

func makeRequest(zipFilename string, files []string) (*pb.Request, error) {
    ret := &pb.Request{
        ZipFilename: zipFilename,
        Files:       nil,
    }

    for _, file := range files {
        content, err := ioutil.ReadFile(file)
        if err != nil {
            return nil, err
        }
        ret.Files = append(ret.Files, &pb.File{
            Filename: file,
            Data:     content,
        })
    }
    return ret, nil
}

grpc.WithInsecure()は、grpc: no transport security setというエラーへの対策です。
grpc.MaxCallRecvMsgSize(0xFFFFFFFF)は、Responseサイズの上限を4096KBから増やすための設定になります。

    conn, err := grpc.Dial("127.0.0.1:11111", grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(0xFFFFFFFF)))

実行してみる

Buildは以下の通りです。

$ go build ./cmd/server

$ go build ./cmd/client

この時点で、以下のようなフォルダ構成となります。

$ tree /f
フォルダー パスの一覧:  ボリューム Windows
ボリューム シリアル番号は 0000000B AEC0:D95D です
C:.
│  client.exe
│  grpczip.pb.go
│  grpczip.proto
│  server.exe
│
└─cmd
    ├─client
    │      client.go
    │
    └─server
            server.go

先に、Serverを立ち上げておき、

$ server

Clientを実行すると、zipファイルが作成されます。
正しくzipファイルが作成されました。

$ client grpczip.proto.zip grpczip.proto
grpczip.proto.zip (307 bytes)

$ client output.zip grpczip.pb.go grpczip.proto server.exe client.exe
output.zip (7071632 bytes)

まとめ

Go + gRPCの簡単なサンプルを作成しました。
それなりのコード量を書かないといけないのですが、それほど難しい内容ではないので写経等のトライをするとすぐに理解できると思います。

まだgRPCを触っていない人は、是非試してみてください。

今回のコードは以下にあります。

リンク等

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away