gRPCの通信をGo(サーバー)とNode.js(クライアント)で実装してみる。

# Go言語を勉強してみる。
JavascriptとかPHPとか自分がよく知っている言語以外で、あたらしいモノに触れてないなと思い、今になってGoogleが開発したというGoに触れてみている。いろいろ調べていく中でおなじくGoogleが開発したというgRPCというのが出てきたので、どんなものか自分でも実装してみる。

サーバー/クライアントともGo(あるいはNode.js)という記事はいくつかあったが、違うサービス間で通信してるっぽくなるように、あえて違う言語で試した。

Nodeは元からホストにインストールしていたものの、Goはインストールしてなかったので、ここではDockerコンテナを用意した。

# 全体の構成

Dockerコンテナ側に配置するサーバープログラム。

```
root/
 ├─docker-compose.yml
 ├─Dockerfile
 ├─.env
 └─src/
    ├─go.mod
    └─gRPC/
       ├─grpc_sample/
       ├─server.go
       └─ex.proto
```

ホスト側に配置するクライアントプログラム。
この2ファイルは同じディレクトリにあればよい

```
ex.proto
client.mjs
```

※ ex.proto の中身は両方とも同じ


# 各種ファイル

```yaml:docker-compose.yml
services:
  app:
    container_name: app
    build:
      context: .
    volumes:
      - ./src:/go/src
    tty: true
    env_file: .env
    environment:
      - TZ=Asia/Tokyo
    working_dir: /go/src
    ports:
      - 9000:9000
```

```Dockerfile:Dockerfile
# goバージョン
FROM golang:1.22.3-alpine

# アップデートとgit,protocのインストール
RUN apk update && apk add git protoc

# ワーキングディレクトリの設定
WORKDIR /go/src

# ホストのファイルをコンテナの作業ディレクトリに移行
ADD . /go/src

# パッケージのインストール
RUN go install golang.org/x/tools/cmd/goimports@latest
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```

```.env
GOPATH=/go
```

```go.mod
module src

go 1.22.3
```

```protobuf:ex.proto
syntax = "proto3";

package grpc_ex;

option go_package = "./grpc_sample";

message RequestMessage {
  string request_id = 1;
}

message UserProfile {
  string user_name = 1;
  int32 user_age = 2;
}

message ResponseMessage {
  repeated UserProfile user_datas = 1;
}

service ExService {
  rpc GetProfile(RequestMessage) returns (ResponseMessage) {}
}
```

### サーバー側(Go)

```golang:server.go
package main

import (
	"fmt"
	"log"
	"net"

	"src/gRPC/grpc_sample"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

type GetProfileStruct struct {
	name string
}

func (g *GetProfileStruct) GetProfile(ctx context.Context, req *grpc_sample.RequestMessage) (*grpc_sample.ResponseMessage, error) {
	fmt.Println("Received request_id:", req.RequestId)

	userDatas := []*grpc_sample.UserProfile{
		// ユーザーデータのサンプル
		{UserName: "ユーザーA", UserAge: 20},
		{UserName: "ユーザーB", UserAge: 25},
		{UserName: "ユーザーC", UserAge: 30},
	}

	// レスポンスを返す。
	response := &grpc_sample.ResponseMessage{
		UserDatas: userDatas,
	}
	return response, nil
}

func main() {
	// 9000番ポートでクライアントからのリクエストを受け付ける。
	listen, err := net.Listen("tcp", ":9000")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer()

	// GetProfileStruct構造体のアドレスを通じてGetProfileメソッドが呼ばれるようになる。
	grpc_sample.RegisterExServiceServer(grpcServer, &GetProfileStruct{})

	fmt.Println("main start")

	// リッスンする。
	if err := grpcServer.Serve(listen); err != nil {
		log.Fatalf("failed to serve: %s", err)
	}

	fmt.Println("main end")
}
```

### クライアント側(Node.js)

```js:client.mjs
import { loadSync } from '@grpc/proto-loader';
import { loadPackageDefinition, credentials } from '@grpc/grpc-js';

import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const protoFile = `${__dirname}/ex.proto`;
const pd = loadSync(protoFile);
const proto = loadPackageDefinition(pd);

const client =
  new proto.grpc_ex.ExService('localhost:9000', credentials.createInsecure());

// 関数の戻り値はコールバックでしか受け取れない(?)のでPromiseにする。
const promisify = (methodName) =>
  args => new Promise((resolve, reject) =>
    client[methodName](args, (err, res) => {
      if (err) { reject(err); }
      else { resolve(res); }
    })
  );

const getProfile = promisify('GetProfile');

try {
  const response = await getProfile({ requestId: '3072' });
  console.log('response:', response);
} catch (e) {
  console.error(e);
}
```

# 実際に動かしてみる

### サーバープログラム

コンテナを立ち上げる。

```shell
# コンテナを起動
docker-compose up -d --build

# 起動したらシェルを実行する。
docker-compose exec app sh
```

↓コンテナ内のシェルで実行する。

```shell
cd /go/src/gRPC/

# プロトコル定義ファイルからGo向けのソースコードを生成する。
protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. ex.proto

# ソースコードがgrpc_sample/直下に生成される。
ls grpc_sample/

go run server.go
# main start が出力されたらOK
```

### クライアントプログラム

client.mjsを配置したディレクトリで、必要なモジュールをインストール

```shell
npm install @grpc/proto-loader @grpc/grpc-js
```

インストール完了したら実行

```shell
node client.mjs
```

# 実行結果

### サーバープログラム

![server.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60846/baa99884-eb3f-a39a-7d2e-05f89ff522b5.png)

### クライアントプログラム

![client.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60846/9272f051-a3bd-cd81-44f9-11cf73e53040.png)

# 参考情報
以下を参考にさせていただきしました。ありがとうございます。

https://qiita.com/totoaoao/items/6bf533b6d2164b74ac09

https://qiita.com/kenji123/items/c9ba70a6734d981055a7

https://fits.hatenablog.com/entry/2020/11/15/222343

# 最後に
身の回りでgRPCが活躍することは当分ないだろうけど、Goは使いこなせるくらいには習熟していきたい。
Webサーバーとしての用途で実装をすすめたら、難しいコトを覚える機会はありそう。