docker 化程式設計

alpine 映像檔介紹

由於直接使用別人設定好的映像檔,檔案會太大,因次建議採用:Alpine映像檔,大小只有約5MB。其使用上需注意:
* 無法使用apt-get命令
* apt-get update 和 apt-get install 命令要寫在同一行

目錄架構

README.md
---api
----- say.proto
----- makefile
---backend
----- Dockerfile
----- makefile
----- main.go
---say
----- main.go

使用docker建立執行環境(backend 後端)

  • 建立 Dockerfile
FROM alpine

RUN apk update && apk add flite
ADD app /app
ENTRYPOINT ["/app"]

// 先期測試
docker build -t justjii/say .
make data
docker run --rm -v $(PWD)/data:/data -w /data justjii/say flite -t "Hello world" -o output.wav
afplay data/output.wav

  • main.go
package main

import (
        pb "../api"
        "flag"
        "fmt"
        "github.com/sirupsen/logrus"
        "golang.org/x/net/context"
        "google.golang.org/grpc"
        "google.golang.org/grpc/reflection"
        "io/ioutil"
        "net"
        // "os"
        "os/exec"
)

type server struct{}

func (server) Say(ctx context.Context, text *pb.Text) (*pb.Speech, error) {
        f, err := ioutil.TempFile("", "")
        if err != nil {
                return nil, fmt.Errorf("cound not create tmp file: %v", err)
        }
        if err := f.Close(); err != nil {
                return nil, fmt.Errorf("Could not close %s: %v", f.Name(), err)
        }
        cmd := exec.Command("flite", "-t", text.Text, "-o", f.Name())
        if data, err := cmd.CombinedOutput(); err != nil {
                return nil, fmt.Errorf("flite failed: %s", data)
        }

        data, err := ioutil.ReadFile(f.Name())
        if err != nil {
                return nil, fmt.Errorf("Could not read tmp file: %v", err)
        }
        return &pb.Speech{Audio: data}, nil
}

func main() {
        port := flag.Int("p", 8080, "port to listen to")
        flag.Parse()

        logrus.Infof("Listening to port %d", *port)
        lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
        if err != nil {
                logrus.Fatalf("Could not listen to port %d: %v", *port, err)
        }

        s := grpc.NewServer()
        pb.RegisterTextToSpeechServer(s, &server{})
        // reflection.Register(s)
        if err = s.Serve(lis); err != nil {
                logrus.Fatalf("無法提供服務:%v", err)
        }
}
  • 建立makefile
build:
        GOOS=linux go build -o app   // 將程式打包成app後複製進docker
        docker build -t justjii/say .
        rm -f app
  • step2 加入main.go 後測試
make build
docker run --rm -v $(PWD)/data:/data -w /data justjii/say "How are you"

// 重新更新docker images
make build
docker run -p 8080:8080 justjii/say
afplay output.wav

建立 gRPC 程序

  • say.proto
syntax = "proto3";

package say;

message Text {
   string text = 1;
}

message Speech {
   bytes Audio = 1;
}

service TextToSpeech {
   rpc Say(Text) returns(Speech) {}
}
  • makefile
build:
        protoc --go_out=plugins=grpc:. *.proto
  • 測試
make build
go doc

前端測試程式(say)

  • main.go
package main

import (
        pb "../api"
        "flag"
        "fmt"
        // "golang.org/x/net/context"
        "context"
        "google.golang.org/grpc"
        "io/ioutil"
        "log"
        "os"
)

func main() {
        backend := flag.String("b", "140.109.6.150:8080", "address of the say backend")
        output := flag.String("o", "output.wav", "wave file where the output")
        flag.Parse()

        if len(os.Args) < 2 {
                fmt.Println("usage: \n\t%s \"text to speak\"", os.Args[0])
                os.Exit(1)
        }

        conn, err := grpc.Dial(*backend, grpc.WithInsecure())
        if err != nil {
                log.Fatalf("連線失敗: %v", err)
        }
        defer conn.Close()

        client := pb.NewTextToSpeechClient(conn)
        text := &pb.Text{Text: os.Args[1]}
        res, err := client.Say(context.Background(), text)
        if err != nil {
                log.Fatalf("執行錯誤:%v", err)
        }
        if err := ioutil.WriteFile(*output, res.Audio, 0666); err != nil {
                log.Fatalf("Could not write to %s: %v", *output, err)
        }
}
  • 執行 client 端程式
$>go run main.go "Hello world"

參考資料