一 安装

1
2
3
4
go get -v -u -d github.com/metaverse/truss
cd $GOPATH/src/github.com/metaverse/truss
make dependencies
make

二 proto 内容说明

在 .proto 文件中定义通讯接口。truss 将基于 package 指定的包名称定义服务的名称,为保证代码可读性 package 指定名称应该与 service 指定名称相同。如下:

1
2
3
// echo.proto
package echo;
service Echo{...}

一个 API 服务是通过 rpc 调用以及 rpc 调用的参数消息和响应消息构成。请求和响应消息中的字段是枚举类型(protobuf 序列化要求)。

1
2
3
4
message LouderRequest {
string In = 1; // In is the string to echo back
int32 Loudness = 2; // Loudness is the number of exclamations marks to add to the echoed string
}

在使用 rpc 描述 API 接口时可以添加 HTTP 注解使接口支持 HTTP 调用(需要导入谷歌注解包)。

1
2
3
4
5
6
7
8
9
10
11
12
import "github.com/metaverse/truss/deftree/googlethirdparty/annotations.proto";

service Echo {
...
rpc Echo (EchoRequest) returns (EchoResponse) {
option (google.api.http) = {
// All message fields are query parameters of the http request unless otherwise specified
get: "/echo"
};
}
...
}

除了想上面使用 GET 方法,还可以使用 POST 方法,从 HTTP Body 中获取参数(如果 API 接口中包含 map 类型变量,必须使用 POST 方法,将参数放在 Body 中):

1
2
3
4
5
6
rpc Louder (LouderRequest) returns (EchoResponse) {
option (google.api.http) = {
post: "/louder/{Loudness}" // Loudness is accepted in the http path
body: "*" // All other fields (In) are located in the body of the http/json request
};
}

一个服务(service)可以被分割到多个 proto 文件中定义,但最好在一个文件中。

三 生成目录结构说明

使用 truss *.proto 会在 *.proto 同一目录下生成服务代码目录(echo-service)。服务目录(echo-service 目录)内部结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── echo-service
| ├── cmd
| │ ├── echo
| │ │ └── main.go
| │ └── echo-server
| │ └── main.go
| ├── echo.pb.go
| ├── handlers
| │ ├── handlers.go
| │ ├── hooks.go
| │ └── middlewares.go
| └── svc
| └── ...
└── echo.proto

目录功能说明:

srv :包含服务通讯所需的编解码代码(已生成)
handlers/handlers.go :在其中添加业务逻辑代码,调用桩代码
handlers/middlewares.go : 对端点或服务添加中间件,例如:访问日志、检测
cmd/echo/ :包含客户端代码,测试有用
cmd/echo-server/ :服务入口,在其中构建服务
echo.pb.go :包含已从 echo.proto 转换为 golang 的 RPC 接口定义和支持结构

重新运行 truss 会将除 handlers 目录外的所有文件覆盖。

四 添加业务逻辑

handlers/handlers.go 中添加业务逻辑:

1
2
3
4
5
6
7
func (s echoService) Echo(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
var resp pb.EchoResponse
resp = pb.EchoResponse{
Out: in.In, // 将请求参数回传
}
return &resp, nil
}

可以新建包添加业务逻辑代码,只在 handlers.go 中进行耦合。

五 构建代码

1
go build echo-service/echo

六 truss 命令行参数

1. 指定代码输出目录

使用 --svcout 可以指定代码数据路径,路径必须是 GOPATH 格式,因为在代码中会使用路径引用代码。示例:

1
truss src/git.du.com/audit/audit.proto --svcout git.du.com/audit 

2. 指定 pb 输出目录

使用 --pbout 指定生成的 pb 文件保存目录,同样是 GOPATH 格式。新版本不再支持。

七 更多细节

1. 端口相关

truss 生成的服务同时支持 HTTP 和 gRPC 协议,程序运行是会有如下输出:

1
2
3
4
$ ./audit-server
2019/02/01 22:41:08 transport gRPC addr :5040
2019/02/01 22:41:08 transport HTTP addr :5050
2019/02/01 22:41:08 transport debug addr :5060

服务的地址和端口可以通过命令行或环境变量设置,命令行方法:

1
./audit-server --http.addr ":8080" --grpc.addr ":9090"

2. 请求处理与协程绑定

gRPC 实现中每个请求会单独创建一个协程进行处理。可以在 google.golang.org/grpc/server.go 中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// Serve accepts incoming connections on the listener lis, creating a new
// ServerTransport and service goroutine for each. The service goroutines
// read gRPC requests and then call the registered handlers to reply to them.
// Serve returns when lis.Accept fails with fatal errors. lis will be closed when
// this method returns.
// Serve will return a non-nil error unless Stop or GracefulStop is called.
func (s *Server) Serve(lis net.Listener) error {
s.mu.Lock()
s.printf("serving")
s.serve = true
if s.lis == nil {
// Serve called after Stop or GracefulStop.
s.mu.Unlock()
lis.Close()
return ErrServerStopped
}

s.serveWG.Add(1)
defer func() {
s.serveWG.Done()
select {
// Stop or GracefulStop called; block until done and return nil.
case <-s.quit:
<-s.done
default:
}
}()

ls := &listenSocket{Listener: lis}
s.lis[ls] = true

if channelz.IsOn() {
ls.channelzID = channelz.RegisterListenSocket(ls, s.channelzID, "")
}
s.mu.Unlock()

defer func() {
s.mu.Lock()
if s.lis != nil && s.lis[ls] {
ls.Close()
delete(s.lis, ls)
}
s.mu.Unlock()
}()

var tempDelay time.Duration // how long to sleep on accept failure

for {
rawConn, err := lis.Accept()
if err != nil {
if ne, ok := err.(interface {
Temporary() bool
}); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.mu.Lock()
s.printf("Accept error: %v; retrying in %v", err, tempDelay)
s.mu.Unlock()
timer := time.NewTimer(tempDelay)
select {
case <-timer.C:
case <-s.quit:
timer.Stop()
return nil
}
continue
}
s.mu.Lock()
s.printf("done serving; Accept = %v", err)
s.mu.Unlock()

select {
case <-s.quit:
return nil
default:
}
return err
}
tempDelay = 0
// Start a new goroutine to deal with rawConn so we don't stall this Accept
// loop goroutine.
//
// Make sure we account for the goroutine so GracefulStop doesn't nil out
// s.conns before this conn can be added.
s.serveWG.Add(1)
go func() {
s.handleRawConn(rawConn)
s.serveWG.Done()
}()
}
}

3. graceful shutdown

不支持,需要自己写代码集成到服务中。