3.5 进行服务间内调
在上一个章节中,我们运行了一个最基本的 gRPC 服务,那么在实际上的应用场景,我们的服务是会有多个的,并且随着需求的迭代拆分重合,服务会越来越多,到上百个也是颇为常见的。因此在这么多的服务中,最常见的就是 gRPC 服务间的内调行为,再细化下来,其实就是客户端如何调用 gRPC 服务端的问题,那么在本章节我们将会进行使用和做一个深入了解。
3.5.1 进行 gRPC 调用
理论上在任何能够执行 Go 语言代码,且网络互通的地方都可以进行 gRPC 调用,它并不受限于必须在什么类型应用程序下才能够调用。接下来我们在项目下新建 client
目录,创建 client.go 文件,编写一个示例来调用我们先前所编写的 gRPC 服务,如下代码:
package main
import (
...
pb "github.com/go-programming-tour-book/tag-service/proto"
)
func main() {
ctx := context.Background()
clientConn, _ := GetClientConn(ctx, "localhost:8004", nil)
defer clientConn.Close()
tagServiceClient := pb.NewTagServiceClient(clientConn)
resp, _ := tagServiceClient.GetTagList(ctx, &pb.GetTagListRequest{Name: "Go"})
log.Printf("resp: %v", resp)
}
func GetClientConn(ctx context.Context, target string, opts []grpc.DialOption) (*grpc.ClientConn, error) {
opts = append(opts, grpc.WithInsecure())
return grpc.DialContext(ctx, target, opts...)
}
在上述 gRPC 调用的示例代码中,一共分为三大步,分别是:
- grpc.DialContext:创建给定目标的客户端连接,另外我们所要请求的服务端是非加密模式的,因此我们调用了
grpc.WithInsecure
方法禁用了此 ClientConn 的传输安全性验证。 - pb.NewTagServiceClient:初始化指定 RPC Proto Service 的客户端实例对象。
- tagServiceClient.GetTagList:发起指定 RPC 方法的调用。
3.5.2 grpc.Dial 做了什么
常常有的人会说在调用 grpc.Dial
或 grpc.DialContext
方法时,客户端就已经与服务端建立起了连接,但这对不对呢,这是需要细心思考的一个点,客户端真的是一调用 Dial
相关方法就马上建立了可用连接吗,我们一起尝试一下,示例代码:
func main() {
ctx := context.Background()
clientConn, _ := GetClientConn(ctx, "localhost:8004", nil)
defer clientConn.Close()
}
在上述代码中,我们只保留了创建给定目标的客户端连接的部分代码,然后执行该程序,接着马上查看抓包工具的情况下,竟然提示一个包都没有,那么这算真正连接了吗?
实际上,如果你真的想在调用 DialContext
方法时就马上打通与服务端的连接,那么你需要调用 WithBlock
方法来进行设置,那么它在发起拨号连接时就会阻塞等待连接完成,并且最终连接会到达 Ready
状态,这样子在此刻的连接才是正式可用的,代码如下:
func main() {
ctx := context.Background()
clientConn, _ := GetClientConn(
ctx,
"localhost:8004",
[]grpc.DialOption{grpc.WithBlock()},
)
defer clientConn.Close()
}
再次进行抓包,查看效果,如下:
3.5.2.1 源码分析
那么在调用 grpc.Dial
或 grpc.DialContext
方法时,到底做了什么事情呢,为什么还要调用 WithBlock
方法那么“麻烦”,接下来我们一起看看正在调用时运行的 goroutine 情况,如下:
我们可以看到有几个核心方法一直在等待/处理信号,通过分析底层源码可得知。涉及如下:
func (ac *addrConn) connect()
func (ac *addrConn) resetTransport()
func (ac *addrConn) createTransport(addr resolver.Address, copts transport.ConnectOptions, connectDeadline time.Time)
func (ac *addrConn) getReadyTransport()
在这里主要分析所提示的 resetTransport
方法,看看都做了什么。核心代码如下:
func (ac *addrConn) resetTransport() {
for i := 0; ; i++ {
if ac.state == connectivity.Shutdown {
return
}
...
connectDeadline := time.Now().Add(dialDuration)
ac.updateConnectivityState(connectivity.Connecting)
newTr, addr, reconnect, err := ac.tryAllAddrs(addrs, connectDeadline)
if err != nil {
if ac.state == connectivity.Shutdown {
return
}
ac.updateConnectivityState(connectivity.TransientFailure)
timer := time.NewTimer(backoffFor)
select {
case <-timer.C:
...
}
continue
}
if ac.state == connectivity.Shutdown {
newTr.Close()
return
}
...
if !healthcheckManagingState {
ac.updateConnectivityState(connectivity.Ready)
}
...
if ac.state == connectivity.Shutdown {
return
}
ac.updateConnectivityState(connectivity.TransientFailure)
}
}
通过上述代码可得知,在该方法中会不断地去尝试创建连接,若成功则结束。否则不断地根据 Backoff
算法的重试机制去尝试创建连接,直到成功为止。
3.5.2.2 小结
因此单纯调用 grpc.DialContext
方法是异步建立连接的,并不会马上就成为可用连接了,仅处于 Connecting
状态(需要多久则取决于外部因素,例如:网络),正式要到达 Ready
状态,这个连接才算是真正的可用。
我们再回顾到前面的示例中,为什么抓包时一个包都抓不到,实际上连接立即建立了,但 main 结束的很快,因此可能刚建立就被销毁了,也可能还处于 Connecting
状态,没来得及产生具体的网络活动,自然也就抓取不到任何包了。