gRPC 其他功能
数据压缩
数据压缩可以减少数据在网络传输中的体积,减少数据存储空间。从而提高数据传输效率,加快系统响应速度。常用的数据压缩算法有:Gzip、Zlib、Deflater、Lz4。本人在工作中使用Gzip算法比较多,经过实测经过压缩后的数据比原始数据缩小10倍左右。
数据压缩需要客户端、服务端算法配套使用,通常情况下分为两类:
- 客户端压缩数据 服务端解压数据
- 服务端对Response进行压缩 客户端进行解压
接下来看下gRPC-go 是如何进行开启数据压缩功能:
客户端代码
package main
import (
"context"
"flag"
"fmt"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/encoding/gzip" // Install the gzip compressor
pb "google.golang.org/grpc/examples/features/proto/echo"
)
var addr = flag.String("addr", "localhost:50051", "the address to connect to")
func main() {
flag.Parse()
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewEchoClient(conn)
const msg = "compress"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// RPC调用 指定gzip压缩算法
res, err := c.UnaryEcho(ctx, &pb.EchoRequest{Message: msg}, grpc.UseCompressor(gzip.Name))
fmt.Printf("UnaryEcho call returned %q, %v\n", res.GetMessage(), err)
if err != nil || res.GetMessage() != msg {
log.Fatalf("Message=%q, err=%v; want Message=%q, err=<nil>", res.GetMessage(), err, msg)
}
}
服务端代码
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"google.golang.org/grpc"
// 自动调用gzip init方法 注册算法
_ "google.golang.org/grpc/encoding/gzip"
pb "google.golang.org/grpc/examples/features/proto/echo"
)
var port = flag.Int("port", 50051, "the port to serve on")
type server struct {
pb.UnimplementedEchoServer
}
func (s *server) UnaryEcho(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
fmt.Printf("UnaryEcho called with message %q\n", in.GetMessage())
return &pb.EchoResponse{Message: in.Message}, nil
}
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
fmt.Printf("server listening at %v\n", lis.Addr())
s := grpc.NewServer()
pb.RegisterEchoServer(s, &server{})
s.Serve(lis)
}
数据加密
构建数据安全传输通道,保证两个通信应用程序之间数据安全性、完整性,有非常重要的作用。gRPC-go支持不同的算法对数据通道进行加密传输,如TLS、ALTS、MTLS。下面以TLS为例介绍下 gRPC-go API的使用。
客户端代码
package main
import (
"context"
"flag"
"fmt"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/examples/data"
ecpb "google.golang.org/grpc/examples/features/proto/echo"
)
var addr = flag.String("addr", "localhost:50051", "the address to connect to")
func callUnaryEcho(client ecpb.EchoClient, message string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, err := client.UnaryEcho(ctx, &ecpb.EchoRequest{Message: message})
if err != nil {
log.Fatalf("client.UnaryEcho(_) = _, %v: ", err)
}
fmt.Println("UnaryEcho: ", resp.Message)
}
func main() {
flag.Parse()
// 基于证书 创建 TLS
creds, err := credentials.NewClientTLSFromFile(data.Path("x509/ca_cert.pem"), "x.test.example.com")
if err != nil {
log.Fatalf("failed to load credentials: %v", err)
}
// 基于TLS算法 创建grpc 底层链接
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
rgc := ecpb.NewEchoClient(conn)
callUnaryEcho(rgc, "hello world")
}
服务端代码
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/examples/data"
pb "google.golang.org/grpc/examples/features/proto/echo"
)
var port = flag.Int("port", 50051, "the port to serve on")
type ecServer struct {
pb.UnimplementedEchoServer
}
func (s *ecServer) UnaryEcho(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) {
return &pb.EchoResponse{Message: req.Message}, nil
}
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
//使用公钥(server_cert.pem)、私钥(server_key.pem) 进行加密
creds, err := credentials.NewServerTLSFromFile(data.Path("x509/server_cert.pem"), data.Path("x509/server_key.pem"))
if err != nil {
log.Fatalf("failed to create credentials: %v", err)
}
// 基于加密算法 初始化grpc 服务端
s := grpc.NewServer(grpc.Creds(creds))
// Register EchoServer on the server.
pb.RegisterEchoServer(s, &ecServer{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
keepalive
gRPC发送HTTP2 pings指令以检测连接是否关闭。如果另一方在一定时间内未确认ping,则连接将被关闭。需要注意的是,只有在连接上没有活动时才需要ping。
keepalive机制用于检测TCP级别连接故障。典型应用场景:TCP连接丢包(包含 FIN)。检测此故障需要依赖系统TCP超时设置(可能为30分钟)。keepalive允许gRPC更快的检测到此故障。另一个非常有用的作用是保证connection 连接处于活跃状态,实现长连接。
流程细节
当connection连接没有活动时(没有正常的数据发送),等待指定时间后,客户端将发送ping,服务器收到pinng时发送ping ack。客户端将等待超时,并检查在此期间连接上是否有任何活动。
服务端的机制跟客户端类似,也需要设置time、timeout参数,并配置连接 max-age属性。
客户端代码
package main
import (
"context"
"flag"
"fmt"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "google.golang.org/grpc/examples/features/proto/echo"
"google.golang.org/grpc/keepalive"
)
var addr = flag.String("addr", "localhost:50052", "the address to connect to")
var kacp = keepalive.ClientParameters{
Time: 10 * time.Second, // send pings every 10 seconds if there is no activity
Timeout: time.Second, // wait 1 second for ping ack before considering the connection dead
PermitWithoutStream: true, // send pings even without active streams
}
func main() {
flag.Parse()
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithKeepaliveParams(kacp))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewEchoClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
fmt.Println("Performing unary request")
res, err := c.UnaryEcho(ctx, &pb.EchoRequest{Message: "keepalive demo"})
if err != nil {
log.Fatalf("unexpected error from UnaryEcho: %v", err)
}
fmt.Println("RPC response:", res)
select {} // Block forever; run with GODEBUG=http2debug=2 to observe ping frames and GOAWAYs due to idleness.
}
服务端代码
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
pb "google.golang.org/grpc/examples/features/proto/echo"
)
var port = flag.Int("port", 50052, "port number")
var kaep = keepalive.EnforcementPolicy{
MinTime: 5 * time.Second, // If a client pings more than once every 5 seconds, terminate the connection
PermitWithoutStream: true, // Allow pings even when there are no active streams
}
// 以下参数 用于保护服务器免受恶意或者行为不当的客户端攻击
var kasp = keepalive.ServerParameters{
MaxConnectionIdle: 15 * time.Second, // If a client is idle for 15 seconds, send a GOAWAY
MaxConnectionAge: 30 * time.Second, // If any connection is alive for more than 30 seconds, send a GOAWAY
MaxConnectionAgeGrace: 5 * time.Second, // Allow 5 seconds for pending RPCs to complete before forcibly closing connections
Time: 5 * time.Second, // Ping the client if it is idle for 5 seconds to ensure the connection is still active
Timeout: 1 * time.Second, // Wait 1 second for the ping ack before assuming the connection is dead
}
// server implements EchoServer.
type server struct {
pb.UnimplementedEchoServer
}
func (s *server) UnaryEcho(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) {
return &pb.EchoResponse{Message: req.Message}, nil
}
func main() {
flag.Parse()
address := fmt.Sprintf(":%v", *port)
lis, err := net.Listen("tcp", address)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 设置keep alive 参数
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
pb.RegisterEchoServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}