gRPC在Go语言微服务通信中凭借Protobuf序列化效率和HTTP/2多路复用能力,已成为高性能RPC框架的首选。双向流式通信(Bidirectional Streaming)是gRPC四种调用模式中灵活性最高的模式,适合实时聊天、文件传输、日志收集等需要持续双向交互的场景。拦截器(Interceptor)机制则为gRPC提供了类似HTTP中间件的横切关注点处理能力。
gRPC双向流式通信Proto定义
使用stream关键字定义双向流式RPC方法,客户端和服务器端均可独立发送和接收消息流:
syntax = "proto3";
package chatpb;
option go_package = "chatpb/chatpb";
message ChatMessage {
string user_id = 1;
string content = 2;
int64 timestamp = 3;
string room_id = 4;
}
message JoinRequest {
string user_id = 1;
string room_id = 2;
}
message ServerResponse {
string message_id = 1;
bool ack = 2;
string error = 3;
}
service ChatService {
rpc JoinChat(stream ChatMessage) returns (stream ServerResponse);
}
JoinChat方法的请求和返回都是stream类型,客户端连接后可持续发送ChatMessage,服务端持续返回ServerResponse,双向流独立控制发送节奏。
服务端双向流实现
服务端实现核心是维护流的生命周期和消息收发循环:
type ChatServer struct {
chatpb.UnimplementedChatServiceServer
rooms map[string]map[string]chatpb.ChatService_JoinChatServer
roomsMutex sync.RWMutex
}
func (s *ChatServer) JoinChat(stream chatpb.ChatService_JoinChatServer) error {
// 读取客户端首条消息获取用户信息和房间号
firstMsg, err := stream.Recv()
if err != nil {
return fmt.Errorf("读取首条消息失败: %w", err)
}
userID := firstMsg.UserId
roomID := firstMsg.RoomId
// 注册当前流到房间
s.registerStream(roomID, userID, stream)
defer s.unregisterStream(roomID, userID)
// 启动发送协程
sendErr := make(chan error, 1)
go func() {
sendErr <- s.forwardMessages(roomID, userID, stream)
}()
// 接收循环
recvErr := make(chan error, 1)
go func() {
for {
msg, err := stream.Recv()
if err != nil {
recvErr <- err
return
}
// 广播消息到房间内其他用户
s.broadcastMessage(roomID, userID, msg)
}
}()
// 等任一协程退出即结束
select {
case err := <-sendErr:
return err
case err := <-recvErr:
return err
}
}
registerStream和unregisterStream使用sync.RWMutex保护并发访问的rooms map。broadcastMessage遍历房间内所有活跃流转发消息。select等待任一方向出错时清理资源退出,避免goroutine泄漏。
客户端双向流实现
客户端使用goroutine分离发送和接收逻辑,实现全双工通信:
func runChatClient(addr, userID, roomID string) error {
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return err
}
defer conn.Close()
client := chatpb.NewChatServiceClient(conn)
stream, err := client.JoinChat(context.Background())
if err != nil {
return err
}
// 发送首条消息注册房间
stream.Send(&chatpb.ChatMessage{
UserId: userID,
RoomId: roomID,
Content: "__join__",
Timestamp: time.Now().Unix(),
})
// 接收协程
done := make(chan struct{})
go func() {
defer close(done)
for {
resp, err := stream.Recv()
if err != nil {
log.Printf("接收结束: %v", err)
return
}
log.Printf("收到消息: ack=%v msg_id=%s", resp.Ack, resp.MessageId)
}
}()
// 主协程读取标准输入并发送
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
text := scanner.Text()
if text == "quit" {
stream.CloseSend()
break
}
err := stream.Send(&chatpb.ChatMessage{
UserId: userID,
RoomId: roomID,
Content: text,
Timestamp: time.Now().Unix(),
})
if err != nil {
log.Printf("发送失败: %v", err)
break
}
}
<-done
return nil
}
CloseSend()通知服务端客户端不再发送消息,服务端Recv()返回io.EOF。客户端接收协程在服务端关闭连接后退出,主协程通过done channel等待接收协程完成。
拦截器中间件链式设计
gRPC拦截器分为Unary Interceptor(一元RPC)和Stream Interceptor(流式RPC),通过链式组合实现日志、认证、限流、重试等横切关注点:
// Unary拦截器链
func ChainUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
n := len(interceptors)
if n == 0 {
return nil
}
if n == 1 {
return interceptors[0]
}
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 构建从后往前的调用链
chainer := func(currentInter grpc.UnaryServerInterceptor, currentHandler grpc.UnaryHandler) grpc.UnaryHandler {
return func(currentCtx context.Context, currentReq interface{}) (interface{}, error) {
return currentInter(currentCtx, currentReq, info, currentHandler)
}
}
chainedHandler := handler
for i := n - 1; i >= 0; i-- {
chainedHandler = chainer(interceptors[i], chainedHandler)
}
return chainedHandler(ctx, req)
}
}
// 日志拦截器
func LoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
duration := time.Since(start)
log.Printf("method=%s duration=%s error=%v", info.FullMethod, duration, err)
return resp, err
}
// 认证拦截器
func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "缺少元数据")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "缺少认证token")
}
userID, err := validateToken(tokens[0])
if err != nil {
return nil, status.Error(codes.Unauthenticated, "token无效")
}
// 注入用户ID到context
ctx = context.WithValue(ctx, "userID", userID)
return handler(ctx, req)
}
// 限流拦截器
func RateLimitInterceptor(limiter *rate.Limiter) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if !limiter.Allow() {
return nil, status.Error(codes.ResourceExhausted, "请求频率超限")
}
return handler(ctx, req)
}
}
拦截器注册与服务器启动
将链式拦截器注册到gRPC Server,按顺序执行各中间件逻辑:
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
limiter := rate.NewLimiter(rate.Every(time.Second/100), 200)
server := grpc.NewServer(
grpc.UnaryInterceptor(
ChainUnaryInterceptors(
LoggingInterceptor,
AuthInterceptor,
RateLimitInterceptor(limiter),
),
),
grpc.MaxRecvMsgSize(16 * 1024 * 1024),
grpc.MaxSendMsgSize(16 * 1024 * 1024),
)
chatpb.RegisterChatServiceServer(server, &ChatServer{
rooms: make(map[string]map[string]chatpb.ChatService_JoinChatServer),
})
log.Println("gRPC服务器启动 :50051")
if err := server.Serve(lis); err != nil {
log.Fatalf("启动失败: %v", err)
}
}
拦截器执行顺序:LoggingInterceptor → AuthInterceptor → RateLimitInterceptor → handler。日志拦截器在最外层记录完整耗时,认证拦截器在限流之前执行避免无效请求消耗令牌。MaxRecvMsgSize和MaxSendMsgSize控制消息体上限,防止大消息占用过多内存。
Stream拦截器实现与连接生命周期管理
流式RPC需要Stream Interceptor处理连接级别的横切关注点:
func StreamLoggingInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
start := time.Now()
ctx := ss.Context()
// 从context获取peer信息
peerInfo, _ := peer.FromContext(ctx)
clientAddr := "unknown"
if peerInfo != nil {
clientAddr = peerInfo.Addr.String()
}
err := handler(srv, ss)
duration := time.Since(start)
log.Printf("stream method=%s client=%s duration=%s error=%v",
info.FullMethod, clientAddr, duration, err)
return err
}
// 包装ServerStream注入context值
type wrappedStream struct {
grpc.ServerStream
ctx context.Context
}
func (w *wrappedStream) Context() context.Context {
return w.ctx
}
func StreamAuthInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
md, ok := metadata.FromIncomingContext(ss.Context())
if !ok {
return status.Error(codes.Unauthenticated, "缺少元数据")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return status.Error(codes.Unauthenticated, "缺少认证token")
}
userID, err := validateToken(tokens[0])
if err != nil {
return status.Error(codes.Unauthenticated, "token无效")
}
// 注入认证信息到流context
ctx := context.WithValue(ss.Context(), "userID", userID)
return handler(srv, &wrappedStream{ServerStream: ss, ctx: ctx})
}
wrappedStream包装原始ServerStream,重写Context()方法注入认证信息。流式RPC的context在连接存活期间持续有效,通过wrappedStream传递的userID可在JoinChat等业务方法中直接获取。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-grpc-shuang-xiang-liu-shi-tong-xin-yu-lan-jie-qi/