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

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

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

 * [gRPCって何？](https://qiita.com/oohira/items/63b5ccb2bf1a913659d6)
 * [What is gRPC?](https://grpc.io/docs/guides/)

# Goal

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

# 環境構築

Windows 10 Pro 64bit を使った例として記載します。
何か不明点がある場合は、オフィシャルの [Quick Start - Go](https://grpc.io/docs/quickstart/go.html#prerequisites) を見るとよいです。

 1. gRPCをインストールする  
```
$ go get -u google.golang.org/grpc
```
 2. Protocol Buffers v3 の protoc compiler をインストールする
   * 以下のURLから、プラットフォームに合わせたコンパイル済みバイナリをダウンロードする  
     `protoc-<version>-<platform>.zip` のようなファイル名になっています  
     必要なのは`protoc.exe`のみなので、適当にPATHの通った場所に置くなどしてください
     * https://github.com/google/protobuf/releases
 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`以下で作業を行います。
以下が作成するサービスの全体像になります。

```protobuf
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`を受け取る事ができます。

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

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

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

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

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

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

最後に`Response`を定義します。

```protobuf
message Response {
    File ZipFile = 1;
}
```

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

 * [Language Guide  |  Protocol Buffers  |  Google Developers](https://developers.google.com/protocol-buffers/docs/proto)

# Server側を作成する

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

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

```go
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を作成するサービスなので、以下のようにして大きい値で設定しています。

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

# Client側を作成する

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

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

```go
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から増やすための設定になります。

```go
	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を触っていない人は、是非試してみてください。

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

 * https://github.com/sago35/grpczip-sample

# リンク等
 * [Big Sky :: Protocol Buffers を利用した RPC、gRPC を golang から試してみた。](https://mattn.kaoriya.net/software/lang/go/20150227144125.htm)
 * [grpc / Go Quick Start](https://grpc.io/docs/quickstart/go.html#go-version)
 * [grpc / gRPC Basics - Go](https://grpc.io/docs/tutorials/basic/go.html)
 * [Language Guide  |  Protocol Buffers  |  Google Developers](https://developers.google.com/protocol-buffers/docs/proto)