2017/08/25


gRPC は型の強い RPC を色々な言語を使って実装できる仕組みとライブラリです。

Big Sky :: Protocol Buffers を利用した RPC、gRPC を golang から試してみた。

grpc/grpc · GitHub gRPC - An RPC library and framework https://github.com/grpc/grpc gRPC は Google が開...

https://mattn.kaoriya.net/software/lang/go/20150227144125.htm

とても便利なのですが幾分手数が多いのが難点で、ちょっとしたサービスを gRPC で実装したいと思っていてもそう簡単に作る事が出来ませんでした。

ところが今回ご紹介する lile を使うと、とても簡単に gRPC を使った golang の実装を作れてしまいます。

GitHub - lileio/lile: Easily create gRPC services in Go

readme.md ALPHA: Lile is currently considered "Alpha" in that things may change. Currently I am gath...

https://github.com/lileio/lile

lile は gRPC のスケルトンを生成するコマンドとライブラリセットです。今日はこれを使って簡単に gRPC のサービスを作ってみます。お題は GENE95 辞書 を gRPC 経由で照会するサービスです。

まず lile をインストールするには以下のコマンドを実行します。

$ go get github.com/lileio/lile/...

Windows の人は今 pull-request を作ってるのでそちらを使って下さい。執筆時点でまだマージされてませんが。

lile をインストールしたらまずスケルトンを生成します。

$ lile new gene9go
Creating project in /home/mattn/go/src/github.com/mattn/lile-example/gene9go
Is this OK? [y]es/[n]o
yes
.
├── server
│   ├── server.go
│   └── server_test.go
├── subscribers
│   └── subscribers.go
├── gene9go
│   ├── cmd
│       ├── root.go
│       ├── serve.go
│       ├── subscribe.go
│       └── up.go
│   └── main.go
├── gene9go.proto
├── Makefile
├── Dockerfile
├── .travis.yml
└── .gitignore

git push できるくらいの物が生成されています。次に proto ファイルを編集して独自のインタフェースを作成します。元の proto ファイルは以下の様になっています。

syntax = "proto3";
option go_package = "github.com/mattn/lile-example/gene9go";
package gene9go;

message Request {
  string id = 1;
}

message Response {
  string id = 1;
}

service Gene9go {
  rpc Read (Request) returns (Response) {}
}

これを以下の様に編集しました。

syntax = "proto3";
option go_package = "github.com/mattn/lile-example/gene9go";
package gene9go;

message Request {
  string Word = 1;
}

message Response {
  string Text = 1;
}

service Gene9go {
  rpc Translate (Request) returns (Response) {}
}

辞書引きなので Translate メソッドを追加しています。編集し終えたら Makefile のある場所で make を実行します。するとこの proto ファイルから Translate メソッドのスケルトンが生成されます。

package server

import (
    "errors"

    "github.com/mattn/lile-example/gene9go"
    context "golang.org/x/net/context"
)

func (s Gene9goServer) Translate(ctx context.Context, r *gene9go.Request) (*gene9go.Response, error) {
    return nil, errors.New("not yet implemented")
}
尚、この時点でテストコードのスタブも生成されます。とても便利です。

package server

import (
    "testing"

    "github.com/mattn/lile-example/gene9go"
    "github.com/stretchr/testify/assert"
    context "golang.org/x/net/context"
)

func TestTranslate(t *testing.T) {
    ctx := context.Background()
    req := &gene9go.Request{}

    res, err := cli.Translate(ctx, req)
    assert.Nil(t, err)
    assert.NotNil(t, res)
}

さてでは Translate メソッドの中身を実装します。gene.txt ファイルを読み込んで単語にマッチする次の行を返しているだけです。gene.txt は utf-8 で保存しておいて下さい。

package server

import (
    "bufio"
    "os"
    "strings"

    "github.com/mattn/lile-example/gene9go"
    context "golang.org/x/net/context"
)

func translate(word string) (stringerror) {
    f, err := os.Open("gene.txt")
    if err != nil {
        return "", err
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)
    found := ""
    for scanner.Scan() {
        first := scanner.Text()
        if !scanner.Scan() {
            break
        }

        if strings.ToLower(first) == strings.ToLower(word) {
            found = scanner.Text()
            break
        }
    }
    return found, scanner.Err()
}

func (s Gene9goServer) Translate(ctx context.Context, r *gene9go.Request) (*gene9go.Response, error) {
    text, err := translate(r.GetWord())
    if err != nil {
        return nil, err
    }
    return &gene9go.Response{Text: text}, nil
}

あとはクライアントとサーバを作ります。といっても Register 関数が用意されているのでこちらも簡単。まずはサーバ。

package main

import (
    "log"
    "net"

    "github.com/mattn/lile-example/gene9go"
    "github.com/mattn/lile-example/gene9go/server"
    "google.golang.org/grpc"
)

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

    gene9go.RegisterGene9goServer(srv, &server.Gene9goServer{})
    srv.Serve(lis)
}
そしてクライアント。 package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/mattn/lile-example/gene9go"
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:11111", grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    client := gene9go.NewGene9goClient(conn)
    req := &gene9go.Request{
        Word: os.Args[1],
    }
    resp, err := client.Translate(context.Background(), req)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(resp.Text)
}
後から気付いたのですが、gene9go の下にサーバコマンドも生成されてました。いたれりつくせり過ぎる。 A gRPC based service

Usage:
  gene9go [command]

Available Commands:
  help        Help about any command
  serve       Run the RPC server
  subscribe   Subscribe to and process queue messages
  up          up runs both RPC and pubub subscribers

Flags:
  -h, --help   help for gene9go

Use "gene9go [command] --help" for more information about a command.

これだけです。なにこれ超簡単じゃん。サーバを起動した状態で、クライアントに単語を付けて起動します。

$ client Go
1.〜に進行する,行く,をしに行く,動く,過ぎる,至る,及ぶ,2.〜と書いてある

これを実装するのにわずか20分程度しか掛かりませんでした。今まで gRPC を使ったサービスに興味があったけど実装難しいと思っていた方はぜひ lile を使ってみて下さい。あっと言う間に実装できるはずです。

この記事で作ったソースファイルは以下に置いておきます。

GitHub - mattn/lile-example
https://github.com/mattn/lile-example
Posted at by



2017/08/21


Windows では子プロセスが出力した標準出力を読み取る際にバッファリングが行われる。これは親プロセスからは無効に出来ない。子プロセスが setbuf を呼び出してバッファを無効にするか、fflush で強制的にバッファをフラッシュする必要がある。

#include <windows.h>
#include <stdio.h>

int
main(int argc, char* argv[]) {
  int n = 0;
  while (1) {
    printf("%d", n++);
    Sleep(1000);
  }
  return 0;
}

Vim に job/channel 機能があるが、これらもこの入出力バッファのせいで改行されるまでの出力を読み取る事が出来ない。:terminal が取り込まれてインタラクティブなプロセスを制御できる様になったけど、Windows の :terminal は winpty を使って画面の要素を読み取って実行されている。なので標準出力そのものをリアルタイム読み取るという事は出来ない。そこで CreateRemoteThread を使ってプロセスに DLL インジェクションし、そこで setbuf を呼び出す物を作った。

GitHub - mattn/vim-iobuf

README.md vim-iobuf On Windows, stdout has a buffer. As you can see, this can be controllable with s...

https://github.com/mattn/vim-iobuf

これを使って以下の様に呼び出すと、子プロセスのバッファリングが無効にされ改行コードを出力しなくても channel を使って読み取る事が出来る。

let job = job_start(['.\\tick.exe'], {
\  'out_mode''raw'
\  'out_cb':{ch,msg->execute('echo msg'1)},
\  'exit_cb':{jo,st->execute('echo "exited"'1)
\}})
call iobuf#nobuffer(job)
iobuf

プラグインから使うプラグインとして、お使い下さい。

setbuf はライブラリ依存なので実行するランタイムに依存します。

Posted at by



2017/08/17


2020/07/20 追記: 最近の Windows 10 ではこの動作が変更されている様です。

https://play.golang.org/p/CHGYhtzRsK Go 1.8.3で『GOOS="darwin"』だと完走するけど、『GOOS=...

Go 1.8.3で『GOOS="darwin"』だと完走するけど、『GOOS="windows"』だと途中でエラーになるプログラム。


osやsyscallパッケージを使っている時、こういう事があって当然なのかパッケージの実装に問題があるのか判断する規準となりそうな記述って公式になにかありますでしょうか。

https://plus.google.com/u/0/103737163485109218200/posts/gfNS2Bz4QrH?cfem=1

UNIX だと以下のコードはパスするのに Windows だとエラーになるという話。

package main

import (
    "fmt"
    "os"
)

func chk(err error) {
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func main() {
    f, err := os.Create("log.txt")
    chk(err)
    defer f.Close()
    err = os.Rename("log.txt""log.txt.bak")
    chk(err)

    os.Remove("log.txt")
    os.Remove("log.txt.bak")
}

UNIX の場合、ファイルを開いている最中にファイルを削除するとファイルシステム上からは unlink されるけど、実際は存在していてファイルの読み込みが出来ます。そして close(2) されたタイミングで削除されます。Windows の FILE_SHARE_DELETE は一見、UNIX のそれっぽく見えるのですが動作が異なります。

例えば test.txt というファイルを作り以下のコードを実行します。

#include <windows.h>
#include <stdio.h>

int
main(int argc, char* argv[]) {
  HANDLE h = CreateFile("test.txt",
      GENERIC_READ | GENERIC_WRITE,
      FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
      NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  getchar();
  char buf[256] = {0};
  DWORD nread;
  printf("%d\n", ReadFile(h, buf, 256, &nread, NULL));
  printf("%s,%d\n", buf, nread);
  return 0;
}

getchar() で処理が停止している間に別のコマンドプロンプトでファイルを消してみて下さい。まずは実行

delete1

そして削除。ファイルが消えると思いきや残っています。そしてアクセスするとエラーになります。

delete2

Windows の FILE_SHARE_DELETE の動作は UNIX のそれとは異なるのです。例えばファイルを消した後で、ファイルが存在しないつもりで処理を続行してしまうと別のエラーが発生する事になります。

package main

import (
    "fmt"
    "os"
)

func chk(err error) {
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func main() {
    f, err := os.Create("log.txt")
    chk(err)
    defer f.Close()
    f.Close()
    err = os.Rename("log.txt""log.txt.bak")
    chk(err)

    os.Remove("log.txt")
    os.Remove("log.txt.bak")
}

この様な OS 間の動作の違いを吸収する事は言語レベルでは出来ません。マルチプラットフォームで動作するコードを書きたいと思われるのであれば、os.Rename や os.Remove の前に Close する事をおすすめします。

ちなみに「みんなのGo言語」にも解説が書いてあります。

Posted at by