實做一個可以寄信的 gRPC

Proto

寫一個寄發信件服務所需要的資料格式

Proto 是一個文件用來儲存 gRPC server 與 client 交換資料時鎖需要的資料格式,建議可以看看它與 JSON 的對照表來迅速了解需要怎樣寫

syntax = "proto3"; // use proto version 3

package pb; // package name

/*
Add the Send function for use
*/
service Mail{
    rpc Send (MailRequest) returns (MailStatus) {}
}

/*
Declare what data you need to let server know
and server will use it to send a mail
*/
message MailRequest{
    string from = 1;
    repeated string to = 2;
    repeated string cc = 3;
    string subject = 4;
    string body = 5;
    string type = 6;
}

/*
Means what the mail status
be send or not
*/
message MailStatus{
    int32 status = 1;
    string code = 2;
}

產生 golang 的程式

go get -u github.com/golang/protobuf/protoc-gen-go
protoc --go_out=plugins=grpc:. *.proto

觀察產生出來的檔案

可以看到 MailRequest 直接幫你轉換成 golang 的 struct,還多了一些奇怪的東西,但是我們只要知道以後不管是 client 還是 server 都可以用這一些定義好的 protocol 來 import 來使用

type MailRequest struct {
	From                 string   `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"`
	To                   []string `protobuf:"bytes,2,rep,name=to,proto3" json:"to,omitempty"`
	Cc                   []string `protobuf:"bytes,3,rep,name=cc,proto3" json:"cc,omitempty"`
	Subject              string   `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"`
	Body                 string   `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
	Type                 string   `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

Server

這邊我們就可以實做一個可以寄信的服務 (此處使用 gomail 套件))

package main

import (
	"log"
	"net"
	"os"
	"time"

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

	"myMail/pb"

	gomail "gopkg.in/gomail.v2"
)

type server struct{}

var ch = make(chan *gomail.Message)

/*
Send is a simple function for send email
*/
func (s *server) Send(ctx context.Context, mail *pb.MailRequest) (*pb.MailStatus, error) {
	m := gomail.NewMessage()
	m.SetHeader("From", mail.From)
	m.SetHeader("To", mail.To...)
	m.SetHeader("Subject", mail.Subject)
	m.SetBody(mail.Type, mail.Body)
	ch <- m
	return &pb.MailStatus{Status: int32(0), Code: ""}, nil
}

func main() {
    // 監聽 50051 port
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("無法監聽該埠口:%v", err)
	}
	s := grpc.NewServer()
	pb.RegisterMailServer(s, &server{})
	reflection.Register(s)
	go func() {
		d := gomail.NewDialer("smtp.gmail.com", 587, os.Getenv("GMAIL_ACC"), os.Getenv("GMAIL_PASS"))

		var s gomail.SendCloser
		var err error
		open := false
		for {
			select {
			case m, ok := <-ch:
				if !ok {
					return
				}
				if !open {
					if s, err = d.Dial(); err != nil {
						panic(err)
					}
					open = true
				}
				if err := gomail.Send(s, m); err != nil {
					log.Print(err)
				}
			// Close the connection to the SMTP server if no email was sent in
			// the last 30 seconds.
			case <-time.After(30 * time.Second):
				if open {
					if err := s.Close(); err != nil {
						panic(err)
					}
					open = false
				}
			}
		}
	}()
	if err := s.Serve(lis); err != nil {
		log.Fatalf("無法提供服務:%v", err)
		close(ch)
	}
}

Client

我們希望 client 模擬一般會用到的 http 服務,但是我們不寫邏輯在裡面,就瀏覽就呼叫 server 寄信了

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"net/http"

	"myMail/pb"

	"google.golang.org/grpc"
)

func main() {
	// 連線到遠端 gRPC 伺服器。
	conn, err := grpc.Dial("server:50051", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("連線失敗:%v", err)
	}
	defer conn.Close()

	// 建立新的 Mail 客戶端,所以等一下就能夠使用 Mail 的所有方法。
	c := pb.NewMailClient(conn)

	// 傳送新請求到遠端 gRPC 伺服器 Mail 中,並呼叫 Send 函式
	mr := pb.MailRequest{
		From:    os.Getenv("MAIL_FROM"),
		To:      []string{"abc@example.com"},
		Cc:      []string{},
		Subject: "How to use gRPC",
		Body:    "Just done",
		Type:    "text/html",
	}
	http.HandleFunc("/send", func(w http.ResponseWriter, r *http.Request) {
		ret, err := c.Send(context.Background(), &mr)
		if err != nil {
			log.Fatalf("無法執行 Send 函式:%v", err)
		} else {
			fmt.Fprintf(w, "Send %s", ret.Code)
		}
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}

中場休息

看看我們目錄們現在長怎樣

tree myMail
.
├── client
│   └── main.go
├── pb
│   ├── mail.pb.go
│   └── mail.proto
└── server
    └── main.go

我們可能有幾種管理方式

  • 可以看出來我們現在其實可以拆開成為三個 git repo 一個是 client 一個是 server 一個是 pb 由團隊們共同協同修改 pb 由個人或團隊維護單一個或多個 client(可能是某商業應用),再由個人或一個團隊維護 server (實做單純的 mail service),此時 pb 的修改將會關忽到所有人、client 的修改不會動到 mail serice,此時 mail service 團隊如果想要修改訊息格式必須要交給 pb 團隊去實現或是交付 PR 給 pb 團隊
  • 但是如果把 mail service 的 pb 單獨綁到 server 這個專案,使得最後只有 client(1~*) & server(1) 個 repo 將會發生以下事情,client 只需要安裝 pb 但是卻要把整個 service 下載下來,就算 golang 不會幫你 compiled 沒用到的東西,但是在 CI/CD 時還是會去下載那些用不到的玩意兒,在第一次使用以及後來 service 有更新時都會影響整個部屬時間

故在此我覺得第一個方案比較妥當,也比較符合 micro service 的感覺

Dockerize

其實就是包成 Docker

Dockerfile

這裡很懶惰的接受參數後 build client or server

cat Dockerfile
FROM golang AS build-env
ARG BUILD_PATH
ADD . /go/src/myMail
RUN cd /go/src/myMail/$BUILD_PATH && go get && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o app

FROM alpine
ARG BUILD_PATH
ARG EXPOSE_PROT
WORKDIR /app
COPY --from=build-env /go/src/myMail/$BUILD_PATH/app /app/
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
EXPOSE ${EXPOSE_PROT}
# 50051, 8080

Build

docker build \
--build-arg BUILD_PATH=server \
--build-arg EXPOSE_PROT=50051 \
-t mailserver .
docker build \
--build-arg BUILD_PATH=client \
--build-arg EXPOSE_PROT=8080 \
-t mailclient .

K8s

拆拆拆,拆成 micro service 以後部屬變成麻煩,資料傳遞的網路也變成麻煩,K8s 可能可以幫我們少點這種麻煩,但是還不夠,這裡只的範例只其實架構還是爛爛的,少了很多微服務必要的元件,如: message queue, circuit breaker, API route…

kind: Service
apiVersion: v1
metadata:
  name:  server
spec:
  selector:
    app:  server
  ports:
  - port:  50051
    protocol: TCP

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: server-depolyment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: server
  template:
    metadata:
      labels:
        app: server
    spec:
      containers:
      - name: server
        image: mailserver
        ports:
        - containerPort: 50051
        env:
	- name: GMAIL_ACC
          valueFrom:
            secretKeyRef:
              name: gmail-acc
              key: env
        - name: GMAIL_PASS
          valueFrom:
            secretKeyRef:
              name: gmail-pass
              key: env
kind: Service
apiVersion: v1
metadata:
  name:  client
spec:
  selector:
    app:  client
  type:  NodePort
  ports:
  - port:  8080
    nodePort: 30290
    protocol: TCP
    targetPort:  8080

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-depolyment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      containers:
      - name: client
        image: mailclient
        ports:
        - containerPort: 8080
        env:
	- name: MAIL_FROM
          valueFrom:
            secretKeyRef:
              name: gmail-acc
              key: env

Try it

kubectl -f server.yaml
kubectl -f client.yaml