【Go语言学习系列44】第三阶段项目实战:微服务聊天应用

📚 原创系列: “Go语言学习系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言学习系列导航

本文是【Go语言学习系列】的第44篇,当前位于第三阶段(进阶篇)

🚀 第三阶段:进阶篇
  1. 并发编程(一):goroutine基础
  2. 并发编程(二):channel基础
  3. 并发编程(三):select语句
  4. 并发编程(四):sync包
  5. 并发编程(五):并发模式
  6. 并发编程(六):原子操作与内存模型
  7. 数据库编程(一):SQL接口
  8. 数据库编程(二):ORM技术
  9. Web开发(一):路由与中间件
  10. Web开发(二):模板与静态资源
  11. Web开发(三):API开发
  12. Web开发(四):认证与授权
  13. Web开发(五):WebSocket
  14. 微服务(一):基础概念
  15. 微服务(二):gRPC入门
  16. 日志与监控
  17. 第三阶段项目实战:微服务聊天应用 👈 当前位置

📚 查看完整Go语言学习系列导航

📖 文章导读

在本文中,您将了解:

  • 如何设计和实现一个基于微服务架构的实时聊天应用
  • 微服务之间如何通过gRPC、RESTful API和消息队列进行通信
  • 使用WebSocket实现实时消息推送
  • 如何处理分布式系统中的服务注册与发现
  • 基于Docker的服务部署和扩展策略
  • 现代微服务系统的日志收集与监控实践

Go微服务聊天应用架构

第三阶段项目实战:微服务聊天应用

在完成了前面的理论学习后,本文将带领读者实战构建一个完整的微服务聊天应用。这个项目整合了我们之前学习的多种技术,包括Go语言基础、Web开发、微服务、数据库、并发编程、日志监控等,旨在通过一个真实的项目来巩固知识并获取实践经验。

1. 项目概述

1.1 业务需求

我们将构建一个名为"GopherChat"的聊天应用,它具有以下功能:

  1. 用户管理:注册、登录、个人资料管理
  2. 聊天功能
    • 一对一私聊
    • 群聊
    • 消息历史记录查询
  3. 消息类型
    • 文本消息
    • 图片消息(URL链接)
  4. 在线状态:显示用户在线/离线状态
  5. 消息通知:离线消息推送

1.2 技术栈选择

项目使用以下技术栈:

  • 后端

    • Go语言:核心开发语言
    • gRPC:服务间通信
    • WebSocket:实时消息推送
    • NATS:消息队列
    • PostgreSQL:主数据存储
    • Redis:缓存和会话管理
    • JWT:认证
  • 前端

    • Vue.js:前端框架(本文重点关注后端实现)
  • 部署

    • Docker:容器化
    • Docker Compose:本地环境编排
  • 监控

    • Prometheus:指标收集
    • Grafana:可视化
    • Jaeger:分布式追踪
    • ELK Stack:日志分析

2. 系统架构设计

2.1 微服务拆分

我们将系统拆分为以下微服务:

  1. 用户服务(User Service)

    • 处理用户注册、登录
    • 管理用户资料
    • 提供用户信息查询
  2. 认证服务(Auth Service)

    • 处理认证和授权
    • 生成和验证JWT令牌
    • 维护会话状态
  3. 聊天服务(Chat Service)

    • 管理聊天会话(一对一和群聊)
    • 处理消息发送
    • 存储聊天历史
  4. 推送服务(Push Service)

    • 维护WebSocket连接
    • 向客户端推送实时消息
    • 处理用户在线状态
  5. 媒体服务(Media Service)

    • 处理图片上传和处理
    • 生成图片URL
  6. API网关(API Gateway)

    • 统一接入点
    • 请求路由
    • 协议转换(HTTP/WebSocket -> gRPC)
    • 基础认证

2.2 整体架构图

聊天应用微服务架构图

架构说明:

  1. 前端通过API网关与后端微服务交互
  2. API网关负责请求路由和协议转换
  3. 微服务之间通过gRPC进行同步通信
  4. 消息队列(NATS)用于异步事件处理
  5. 数据库采用服务专属模式,每个微服务有自己的数据库/表
  6. 缓存(Redis)用于提高读取性能和会话存储

2.3 通信模式

系统中使用三种主要的通信模式:

  1. 同步通信(gRPC)

    • 服务间直接调用
    • 适用于需要立即响应的场景
    • 例如:用户信息查询、身份验证
  2. 异步通信(消息队列)

    • 基于NATS的发布/订阅模式
    • 适用于事件驱动场景
    • 例如:消息通知、状态更新
  3. 实时通信(WebSocket)

    • 客户端与推送服务之间建立WebSocket连接
    • 适用于实时更新
    • 例如:新消息推送、状态变更通知

2.4 数据模型设计

以下是主要数据模型的简化设计:

用户服务数据模型:
// 用户模型
type User struct {
    ID            string    `json:"id"`
    Username      string    `json:"username"`
    Email         string    `json:"email"`
    PasswordHash  string    `json:"-"`
    DisplayName   string    `json:"display_name"`
    AvatarURL     string    `json:"avatar_url"`
    Status        string    `json:"status"` // online, offline, away
    LastSeen      time.Time `json:"last_seen"`
    CreatedAt     time.Time `json:"created_at"`
    UpdatedAt     time.Time `json:"updated_at"`
}
聊天服务数据模型:
// 会话模型
type Conversation struct {
    ID        string    `json:"id"`
    Type      string    `json:"type"` // private, group
    Title     string    `json:"title"` // For group chats
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// 会话参与者
type Participant struct {
    ConversationID string    `json:"conversation_id"`
    UserID         string    `json:"user_id"`
    JoinedAt       time.Time `json:"joined_at"`
    Role           string    `json:"role"` // admin, member
}

// 消息模型
type Message struct {
    ID             string    `json:"id"`
    ConversationID string    `json:"conversation_id"`
    SenderID       string    `json:"sender_id"`
    Type           string    `json:"type"` // text, image
    Content        string    `json:"content"`
    SentAt         time.Time `json:"sent_at"`
    DeliveredAt    time.Time `json:"delivered_at,omitempty"`
    ReadAt         time.Time `json:"read_at,omitempty"`
}

3. 各服务核心实现

下面我们将介绍每个微服务的核心实现思路,专注于关键代码和设计决策。

3.1 用户服务实现

用户服务负责管理用户信息,包括注册、用户资料管理和查询。

3.1.1 服务接口定义(Protocol Buffers)
syntax = "proto3";

package user;
option go_package = "github.com/gopherchat/user-service/proto";

service UserService {
  // 创建用户
  rpc CreateUser(CreateUserRequest) returns (UserResponse);
  
  // 获取用户信息
  rpc GetUser(GetUserRequest) returns (UserResponse);
  
  // 更新用户信息
  rpc UpdateUser(UpdateUserRequest) returns (UserResponse);
  
  // 搜索用户
  rpc SearchUsers(SearchUsersRequest) returns (SearchUsersResponse);
}

// 各消息定义
message CreateUserRequest {
  string username = 1;
  string email = 2;
  string password = 3;
  string display_name = 4;
}

message GetUserRequest {
  string user_id = 1;
}

message UpdateUserRequest {
  string user_id = 1;
  optional string display_name = 2;
  optional string avatar_url = 3;
  optional string status = 4;
}

message UserResponse {
  string id = 1;
  string username = 2;
  string email = 3;
  string display_name = 4;
  string avatar_url = 5;
  string status = 6;
  string last_seen = 7;
  string created_at = 8;
  string updated_at = 9;
}

message SearchUsersRequest {
  string query = 1;
  int32 limit = 2;
  int32 offset = 3;
}

message SearchUsersResponse {
  repeated UserResponse users = 1;
  int32 total = 2;
}
3.1.2 用户服务核心实现

用户服务的核心结构包括:

type UserService struct {
    repo        repository.UserRepository
    hasher      utils.PasswordHasher
    eventPub    events.Publisher
    proto.UnimplementedUserServiceServer
}

创建用户方法

func (s *UserService) CreateUser(ctx context.Context, req *proto.CreateUserRequest) (*proto.UserResponse, error) {
    // 验证请求数据
    if err := validateCreateUserRequest(req); err != nil {
        return nil, status.Errorf(codes.InvalidArgument, "Invalid request: %v", err)
    }
    
    // 检查用户名是否已存在
    exists, err := s.repo.ExistsByUsername(ctx, req.Username)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "Database error: %v", err)
    }
    if exists {
        return nil, status.Error(codes.AlreadyExists, "Username already exists")
    }
    
    // 检查邮箱是否已存在
    exists, err = s.repo.ExistsByEmail(ctx, req.Email)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "Database error: %v", err)
    }
    if exists {
        return nil, status.Error(codes.AlreadyExists, "Email already exists")
    }
    
    // 哈希密码
    passwordHash, err := s.hasher.HashPassword(req.Password)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "Password hashing error: %v", err)
    }
    
    // 创建用户
    now := time.Now()
    user := &model.User{
        ID:           uuid.New().String(),
        Username:     req.Username,
        Email:        req.Email,
        PasswordHash: passwordHash,
        DisplayName:  req.DisplayName,
        Status:       "offline",
        LastSeen:     now,
        CreatedAt:    now,
        UpdatedAt:    now,
    }
    
    // 保存用户
    if err := s.repo.Create(ctx, user); err != nil {
        return nil, status.Errorf(codes.Internal, "Failed to create user: %v", err)
    }
    
    // 发布用户创建事件
    event := &events.UserCreatedEvent{
        UserID:    user.ID,
        Username:  user.Username,
        Email:     user.Email,
        CreatedAt: user.CreatedAt,
    }
    if err := s.eventPub.Publish(ctx, "user.created", event); err != nil {
        log.Printf("Failed to publish user.created event: %v", err)
    }
    
    // 返回用户信息
    return userToProto(user), nil
} 

3.2 认证服务实现

认证服务负责处理用户认证和授权,生成JWT令牌,并维护用户会话。

3.2.1 服务接口定义
syntax = "proto3";

package auth;
option go_package = "github.com/gopherchat/auth-service/proto";

service AuthService {
  // 用户登录
  rpc Login(LoginRequest) returns (LoginResponse);
  
  // 验证令牌
  rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
  
  // 刷新令牌
  rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
  
  // 用户登出
  rpc Logout(LogoutRequest) returns (LogoutResponse);
}

message LoginRequest {
  string username = 1;
  string password = 2;
}

message LoginResponse {
  string access_token = 1;
  string refresh_token = 2;
  string token_type = 3;
  int64 expires_in = 4;
  string user_id = 5;
}

message ValidateTokenRequest {
  string token = 1;
}

message ValidateTokenResponse {
  bool valid = 1;
  string user_id = 2;
  repeated string permissions = 3;
}

message RefreshTokenRequest {
  string refresh_token = 1;
}

message RefreshTokenResponse {
  string access_token = 1;
  string refresh_token = 2;
  string token_type = 3;
  int64 expires_in = 4;
}

message LogoutRequest {
  string user_id = 1;
  string refresh_token = 2;
}

message LogoutResponse {
  bool success = 1;
}
3.2.2 认证服务核心实现

认证服务结构体

type AuthService struct {
    userClient   proto.UserServiceClient
    tokenMaker   token.Maker
    passwordComp utils.PasswordComparer
    sessionStore session.Store
    proto.UnimplementedAuthServiceServer
}

登录方法

func (s *AuthService) Login(ctx context.Context, req *proto.LoginRequest) (*proto.LoginResponse, error) {
    // 验证请求
    if req.Username == "" || req.Password == "" {
        return nil, status.Error(codes.InvalidArgument, "username and password required")
    }

    // 通过用户名获取用户
    user, err := s.userClient.GetUserByUsername(ctx, &userpb.GetUserByUsernameRequest{
        Username: req.Username,
    })
    if err != nil {
        st, ok := status.FromError(err)
        if ok && st.Code() == codes.NotFound {
            return nil, status.Error(codes.NotFound, "user not found")
        }
        return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
    }

    // 验证密码
    valid, err := s.passwordComp.ComparePassword(req.Password, user.PasswordHash)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to compare password: %v", err)
    }
    if !valid {
        return nil, status.Error(codes.Unauthenticated, "incorrect password")
    }

    // 生成访问令牌
    accessTokenDuration := 15 * time.Minute
    accessToken, accessPayload, err := s.tokenMaker.CreateToken(
        user.Id,
        accessTokenDuration,
    )
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to create access token: %v", err)
    }

    // 生成刷新令牌
    refreshTokenDuration := 7 * 24 * time.Hour
    refreshToken, refreshPayload, err := s.tokenMaker.CreateToken(
        user.Id,
        refreshTokenDuration,
    )
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to create refresh token: %v", err)
    }

    // 保存会话到Redis
    session := &session.Session{
        ID:           refreshPayload.ID.String(),
        UserID:       user.Id,
        RefreshToken: refreshToken,
        UserAgent:    metadata.GetUserAgent(ctx),
        ClientIP:     metadata.GetClientIP(ctx),
        ExpiresAt:    refreshPayload.ExpiredAt,
        CreatedAt:    time.Now(),
    }
    if err := s.sessionStore.Create(ctx, session); err != nil {
        return nil, status.Errorf(codes.Internal, "failed to create session: %v", err)
    }

    // 返回登录响应
    res := &proto.LoginResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        TokenType:    "Bearer",
        ExpiresIn:    int64(accessTokenDuration.Seconds()),
        UserId:       user.Id,
    }
    return res, nil
}

验证令牌方法

func (s *AuthService) ValidateToken(ctx context.Context, req *proto.ValidateTokenRequest) (*proto.ValidateTokenResponse, error) {
    // 验证令牌
    payload, err := s.tokenMaker.VerifyToken(req.Token)
    if err != nil {
        return &proto.ValidateTokenResponse{
            Valid:  false,
            UserId: "",
        }, nil
    }

    // 返回验证结果
    return &proto.ValidateTokenResponse{
        Valid:       true,
        UserId:      payload.UserID,
        Permissions: []string{}, // 这里可以从用户服务获取权限
    }, nil
}

3.3 聊天服务实现

聊天服务是应用的核心,负责管理会话和消息。

3.3.1 服务接口定义
syntax = "proto3";

package chat;
option go_package = "github.com/gopherchat/chat-service/proto";

service ChatService {
  // 创建会话
  rpc CreateConversation(CreateConversationRequest) returns (ConversationResponse);
  
  // 获取会话
  rpc GetConversation(GetConversationRequest) returns (ConversationResponse);
  
  // 获取用户的会话列表
  rpc ListConversations(ListConversationsRequest) returns (ListConversationsResponse);
  
  // 发送消息
  rpc SendMessage(SendMessageRequest) returns (MessageResponse);
  
  // 获取消息
  rpc GetMessage(GetMessageRequest) returns (MessageResponse);
  
  // 获取会话的消息列表
  rpc ListMessages(ListMessagesRequest) returns (ListMessagesResponse);
  
  // 标记消息为已读
  rpc MarkMessageAsRead(MarkMessageAsReadRequest) returns (MarkMessageAsReadResponse);
}

// 消息定义省略,以减少篇幅
3.3.2 聊天服务核心实现

发送消息方法

func (s *ChatService) SendMessage(ctx context.Context, req *proto.SendMessageRequest) (*proto.MessageResponse, error) {
    // 验证请求
    if req.ConversationId == "" || req.SenderId == "" || req.Content == "" {
        return nil, status.Error(codes.InvalidArgument, "missing required fields")
    }

    // 检查会话是否存在
    conversation, err := s.repo.GetConversation(ctx, req.ConversationId)
    if err != nil {
        if errors.Is(err, repository.ErrNotFound) {
            return nil, status.Error(codes.NotFound, "conversation not found")
        }
        return nil, status.Errorf(codes.Internal, "failed to get conversation: %v", err)
    }

    // 检查发送者是否是会话参与者
    isParticipant, err := s.repo.IsParticipant(ctx, req.ConversationId, req.SenderId)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to check participant: %v", err)
    }
    if !isParticipant {
        return nil, status.Error(codes.PermissionDenied, "sender is not a participant of the conversation")
    }

    // 创建消息
    now := time.Now()
    message := &model.Message{
        ID:             uuid.New().String(),
        ConversationID: req.ConversationId,
        SenderID:       req.SenderId,
        Type:           req.Type,
        Content:        req.Content,
        SentAt:         now,
    }

    // 保存消息
    if err := s.repo.CreateMessage(ctx, message); err != nil {
        return nil, status.Errorf(codes.Internal, "failed to create message: %v", err)
    }

    // 更新会话的最后活动时间
    if err := s.repo.UpdateConversationLastActivity(ctx, req.ConversationId, now); err != nil {
        log.Printf("Failed to update conversation last activity: %v", err)
    }

    // 发布消息事件
    event := &events.MessageCreatedEvent{
        ID:             message.ID,
        ConversationID: message.ConversationID,
        SenderID:       message.SenderID,
        Type:           message.Type,
        Content:        message.Content,
        SentAt:         message.SentAt,
    }
    if err := s.eventPub.Publish(ctx, "message.created", event); err != nil {
        log.Printf("Failed to publish message.created event: %v", err)
    }

    // 获取对话参与者
    participants, err := s.repo.GetConversationParticipants(ctx, req.ConversationId)
    if err != nil {
        log.Printf("Failed to get conversation participants: %v", err)
    } else {
        // 发送消息通知给所有参与者(除了发送者)
        for _, participant := range participants {
            if participant.UserID != req.SenderId {
                notification := &events.MessageNotificationEvent{
                    UserID:         participant.UserID,
                    MessageID:      message.ID,
                    ConversationID: message.ConversationID,
                    SenderID:       message.SenderID,
                }
                if err := s.eventPub.Publish(ctx, "message.notification", notification); err != nil {
                    log.Printf("Failed to publish message.notification event: %v", err)
                }
            }
        }
    }

    // 返回消息响应
    return messageToProto(message), nil
}

3.4 推送服务实现

推送服务负责维护与客户端的WebSocket连接,实时推送消息和状态更新。

3.4.1 WebSocket处理器
type WebSocketHandler struct {
    upgrader     websocket.Upgrader
    hub          *Hub
    authClient   proto.AuthServiceClient
    eventSub     events.Subscriber
    connections  map[string]*Connection
    mu           sync.RWMutex
}

func NewWebSocketHandler(authClient proto.AuthServiceClient, eventSub events.Subscriber) *WebSocketHandler {
    h := &WebSocketHandler{
        upgrader: websocket.Upgrader{
            ReadBufferSize:  1024,
            WriteBufferSize: 1024,
            CheckOrigin: func(r *http.Request) bool {
                // TODO: 实际环境中应该检查Origin
                return true
            },
        },
        hub:         NewHub(),
        authClient:  authClient,
        eventSub:    eventSub,
        connections: make(map[string]*Connection),
    }

    // 订阅消息通知事件
    h.subscribeToEvents()

    return h
}

func (h *WebSocketHandler) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
    // 从请求中获取令牌
    token := extractToken(r)
    if token == "" {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    // 验证令牌
    resp, err := h.authClient.ValidateToken(r.Context(), &proto.ValidateTokenRequest{
        Token: token,
    })
    if err != nil || !resp.Valid {
        http.Error(w, "Invalid token", http.StatusUnauthorized)
        return
    }

    userID := resp.UserId

    // 升级HTTP连接到WebSocket
    conn, err := h.upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Failed to upgrade connection: %v", err)
        return
    }

    // 创建新的连接对象
    connection := NewConnection(conn, userID, h.hub)

    // 存储连接
    h.mu.Lock()
    h.connections[userID] = connection
    h.mu.Unlock()

    // 发布用户在线事件
    event := &events.UserStatusChangedEvent{
        UserID: userID,
        Status: "online",
        Time:   time.Now(),
    }
    if err := h.eventSub.Publish(r.Context(), "user.status.changed", event); err != nil {
        log.Printf("Failed to publish user online event: %v", err)
    }

    // 启动goroutine处理连接
    go connection.readPump()
    go connection.writePump()
}

func (h *WebSocketHandler) subscribeToEvents() {
    // 订阅消息通知事件
    h.eventSub.Subscribe("message.notification", func(e events.Event) {
        notification, ok := e.(*events.MessageNotificationEvent)
        if !ok {
            return
        }

        // 查找用户连接
        h.mu.RLock()
        connection, exists := h.connections[notification.UserID]
        h.mu.RUnlock()

        if exists {
            // 发送消息到WebSocket
            connection.Send(&WebSocketMessage{
                Type: "new_message",
                Data: map[string]interface{}{
                    "message_id":      notification.MessageID,
                    "conversation_id": notification.ConversationID,
                    "sender_id":       notification.SenderID,
                },
            })
        }
    })

    // 订阅用户状态变更事件
    h.eventSub.Subscribe("user.status.changed", func(e events.Event) {
        statusEvent, ok := e.(*events.UserStatusChangedEvent)
        if !ok {
            return
        }

        // 广播用户状态变更消息
        h.hub.Broadcast(&WebSocketMessage{
            Type: "user_status_changed",
            Data: map[string]interface{}{
                "user_id": statusEvent.UserID,
                "status":  statusEvent.Status,
                "time":    statusEvent.Time,
            },
        })
    })
}

3.5 API网关实现

API网关是整个系统的统一接入点,负责请求路由、协议转换和基础认证。

3.5.1 网关核心功能
type APIGateway struct {
    router          *mux.Router
    userClient      proto.UserServiceClient
    authClient      proto.AuthServiceClient
    chatClient      proto.ChatServiceClient
    wsHandler       *WebSocketHandler
    jwtAuthenticator middleware.JWTAuthenticator
}

func NewAPIGateway(
    userClient proto.UserServiceClient,
    authClient proto.AuthServiceClient,
    chatClient proto.ChatServiceClient,
    wsHandler *WebSocketHandler,
) *APIGateway {
    gateway := &APIGateway{
        router:          mux.NewRouter(),
        userClient:      userClient,
        authClient:      authClient,
        chatClient:      chatClient,
        wsHandler:       wsHandler,
        jwtAuthenticator: middleware.NewJWTAuthenticator(authClient),
    }

    gateway.setupRoutes()
    return gateway
}

func (g *APIGateway) setupRoutes() {
    // 公共路由
    g.router.HandleFunc("/api/health", g.healthCheckHandler).Methods("GET")
    
    // 用户认证相关
    g.router.HandleFunc("/api/auth/register", g.registerHandler).Methods("POST")
    g.router.HandleFunc("/api/auth/login", g.loginHandler).Methods("POST")
    g.router.HandleFunc("/api/auth/refresh", g.refreshTokenHandler).Methods("POST")
    
    // 需要认证的API
    secured := g.router.PathPrefix("/api").Subrouter()
    secured.Use(g.jwtAuthenticator.Middleware)
    
    // 用户相关API
    secured.HandleFunc("/users/me", g.getCurrentUserHandler).Methods("GET")
    secured.HandleFunc("/users/{id}", g.getUserHandler).Methods("GET")
    secured.HandleFunc("/users", g.searchUsersHandler).Methods("GET")
    secured.HandleFunc("/users/me", g.updateUserHandler).Methods("PATCH")
    
    // 聊天相关API
    secured.HandleFunc("/conversations", g.createConversationHandler).Methods("POST")
    secured.HandleFunc("/conversations", g.listConversationsHandler).Methods("GET")
    secured.HandleFunc("/conversations/{id}", g.getConversationHandler).Methods("GET")
    secured.HandleFunc("/conversations/{id}/messages", g.sendMessageHandler).Methods("POST")
    secured.HandleFunc("/conversations/{id}/messages", g.listMessagesHandler).Methods("GET")
    secured.HandleFunc("/messages/{id}/read", g.markMessageAsReadHandler).Methods("POST")
    
    // WebSocket连接
    secured.HandleFunc("/ws", g.wsHandler.HandleWebSocket)
}

// 示例处理器:用户登录
func (g *APIGateway) loginHandler(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }
    
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // 调用认证服务
    resp, err := g.authClient.Login(r.Context(), &authpb.LoginRequest{
        Username: req.Username,
        Password: req.Password,
    })
    
    if err != nil {
        st, ok := status.FromError(err)
        if !ok {
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
        
        switch st.Code() {
        case codes.NotFound, codes.Unauthenticated:
            http.Error(w, st.Message(), http.StatusUnauthorized)
        case codes.InvalidArgument:
            http.Error(w, st.Message(), http.StatusBadRequest)
        default:
            http.Error(w, "Internal server error", http.StatusInternalServerError)
        }
        return
    }
    
    // 返回登录结果
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "access_token":  resp.AccessToken,
        "refresh_token": resp.RefreshToken,
        "token_type":    resp.TokenType,
        "expires_in":    resp.ExpiresIn,
        "user_id":       resp.UserId,
    })
}

// 示例处理器:发送消息
func (g *APIGateway) sendMessageHandler(w http.ResponseWriter, r *http.Request) {
    // 从路径参数获取会话ID
    vars := mux.Vars(r)
    conversationID := vars["id"]
    
    // 从上下文中获取用户ID
    userID := r.Context().Value(middleware.UserIDKey).(string)
    
    var req struct {
        Content string `json:"content"`
        Type    string `json:"type"`
    }
    
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // 调用聊天服务
    resp, err := g.chatClient.SendMessage(r.Context(), &chatpb.SendMessageRequest{
        ConversationId: conversationID,
        SenderId:       userID,
        Content:        req.Content,
        Type:           req.Type,
    })
    
    if err != nil {
        st, ok := status.FromError(err)
        if !ok {
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
        
        switch st.Code() {
        case codes.NotFound:
            http.Error(w, st.Message(), http.StatusNotFound)
        case codes.PermissionDenied:
            http.Error(w, st.Message(), http.StatusForbidden)
        case codes.InvalidArgument:
            http.Error(w, st.Message(), http.StatusBadRequest)
        default:
            http.Error(w, "Internal server error", http.StatusInternalServerError)
        }
        return
    }
    
    // 返回消息
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

### 3.6 消息队列集成

我们使用NATS作为消息队列,处理微服务间的事件和通知。

#### 3.6.1 事件发布者实现

```go
type NATSPublisher struct {
    conn *nats.Conn
}

func NewNATSPublisher(url string) (*NATSPublisher, error) {
    conn, err := nats.Connect(url)
    if err != nil {
        return nil, err
    }
    
    return &NATSPublisher{conn: conn}, nil
}

func (p *NATSPublisher) Publish(ctx context.Context, topic string, event interface{}) error {
    data, err := json.Marshal(event)
    if err != nil {
        return fmt.Errorf("failed to marshal event: %w", err)
    }
    
    return p.conn.Publish(topic, data)
}

func (p *NATSPublisher) Close() error {
    p.conn.Close()
    return nil
}
3.6.2 事件订阅者实现
type NATSSubscriber struct {
    conn         *nats.Conn
    subscriptions []*nats.Subscription
    mu           sync.Mutex
}

func NewNATSSubscriber(url string) (*NATSSubscriber, error) {
    conn, err := nats.Connect(url)
    if err != nil {
        return nil, err
    }
    
    return &NATSSubscriber{
        conn:         conn,
        subscriptions: make([]*nats.Subscription, 0),
    }, nil
}

func (s *NATSSubscriber) Subscribe(topic string, handler func(events.Event)) error {
    subscription, err := s.conn.Subscribe(topic, func(msg *nats.Msg) {
        // 解析事件
        var eventData map[string]interface{}
        if err := json.Unmarshal(msg.Data, &eventData); err != nil {
            log.Printf("Failed to unmarshal event: %v", err)
            return
        }
        
        // 根据主题类型创建不同的事件对象
        var event events.Event
        switch topic {
        case "user.created":
            event = &events.UserCreatedEvent{}
        case "message.created":
            event = &events.MessageCreatedEvent{}
        case "message.notification":
            event = &events.MessageNotificationEvent{}
        case "user.status.changed":
            event = &events.UserStatusChangedEvent{}
        default:
            log.Printf("Unknown event topic: %s", topic)
            return
        }
        
        // 解析具体事件
        data, err := json.Marshal(eventData)
        if err != nil {
            log.Printf("Failed to re-marshal event data: %v", err)
            return
        }
        
        if err := json.Unmarshal(data, event); err != nil {
            log.Printf("Failed to unmarshal into specific event: %v", err)
            return
        }
        
        // 调用处理器
        handler(event)
    })
    
    if err != nil {
        return err
    }
    
    s.mu.Lock()
    s.subscriptions = append(s.subscriptions, subscription)
    s.mu.Unlock()
    
    return nil
}

func (s *NATSSubscriber) Close() error {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    for _, sub := range s.subscriptions {
        if err := sub.Unsubscribe(); err != nil {
            log.Printf("Failed to unsubscribe: %v", err)
        }
    }
    
    s.conn.Close()
    return nil
}

4. 部署与运维

4.1 Docker容器化

为每个微服务创建Dockerfile:

# 构建阶段
FROM golang:1.19-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/service

# 运行阶段
FROM alpine:3.15

WORKDIR /app

COPY --from=builder /app/main .
COPY config.yaml .

EXPOSE 8080

CMD ["./main"]

4.2 Docker Compose配置

使用Docker Compose简化本地开发和测试:

version: '3'

services:
  # 数据库
  postgres:
    image: postgres:14-alpine
    environment:
      POSTGRES_USER: gopherchat
      POSTGRES_PASSWORD: password
      POSTGRES_DB: gopherchat
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  # Redis
  redis:
    image: redis:6-alpine
    ports:
      - "6379:6379"

  # NATS
  nats:
    image: nats:2.7-alpine
    ports:
      - "4222:4222"
      - "8222:8222"

  # 用户服务
  user-service:
    build:
      context: ./user-service
      dockerfile: Dockerfile
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: gopherchat
      DB_PASSWORD: password
      DB_NAME: gopherchat
      NATS_URL: nats://nats:4222
    depends_on:
      - postgres
      - nats

  # 认证服务
  auth-service:
    build:
      context: ./auth-service
      dockerfile: Dockerfile
    environment:
      USER_SERVICE_URL: user-service:50051
      REDIS_URL: redis:6379
      JWT_SECRET: your-secret-key
      JWT_ISSUER: gopherchat
    depends_on:
      - redis
      - user-service

  # 聊天服务
  chat-service:
    build:
      context: ./chat-service
      dockerfile: Dockerfile
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: gopherchat
      DB_PASSWORD: password
      DB_NAME: gopherchat
      NATS_URL: nats://nats:4222
      USER_SERVICE_URL: user-service:50051
    depends_on:
      - postgres
      - nats
      - user-service

  # 推送服务
  push-service:
    build:
      context: ./push-service
      dockerfile: Dockerfile
    environment:
      AUTH_SERVICE_URL: auth-service:50051
      NATS_URL: nats://nats:4222
    depends_on:
      - nats
      - auth-service

  # API网关
  api-gateway:
    build:
      context: ./api-gateway
      dockerfile: Dockerfile
    environment:
      USER_SERVICE_URL: user-service:50051
      AUTH_SERVICE_URL: auth-service:50051
      CHAT_SERVICE_URL: chat-service:50051
      PUSH_SERVICE_URL: push-service:50051
    ports:
      - "8080:8080"
    depends_on:
      - user-service
      - auth-service
      - chat-service
      - push-service

volumes:
  postgres_data:

4.3 监控与可观测性

集成Prometheus、Grafana和Jaeger进行监控和追踪:

# 添加到docker-compose.yml
services:
  # 省略其他服务...

  # Prometheus
  prometheus:
    image: prom/prometheus:v2.37.0
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  # Grafana
  grafana:
    image: grafana/grafana:9.1.0
    depends_on:
      - prometheus
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana

  # Jaeger
  jaeger:
    image: jaegertracing/all-in-one:1.36
    ports:
      - "16686:16686"  # UI
      - "14268:14268"  # 收集器
      - "6831:6831/udp"  # 代理

volumes:
  grafana_data:

5. 系统测试与性能优化

5.1 压力测试

使用K6等工具对聊天应用进行压力测试,确保系统在高负载下的性能和稳定性:

// script.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
  stages: [
    { duration: '30s', target: 100 }, // 逐步增加到100用户
    { duration: '1m', target: 100 },  // 保持100用户1分钟
    { duration: '30s', target: 0 },   // 逐步减少到0用户
  ],
};

export default function() {
  // 登录
  let loginRes = http.post('http://localhost:8080/api/auth/login', JSON.stringify({
    username: 'testuser',
    password: 'password',
  }), {
    headers: { 'Content-Type': 'application/json' },
  });
  
  check(loginRes, {
    'login successful': (r) => r.status === 200,
  });
  
  let token = JSON.parse(loginRes.body).access_token;
  
  // 发送消息
  let sendMessageRes = http.post(
    'http://localhost:8080/api/conversations/123/messages',
    JSON.stringify({
      content: 'Hello, World!',
      type: 'text',
    }),
    {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`,
      },
    }
  );
  
  check(sendMessageRes, {
    'message sent': (r) => r.status === 200,
  });
  
  sleep(1);
}

5.2 性能优化策略

  1. 连接池优化

    • 数据库连接池参数调整
    • gRPC客户端连接复用
  2. 缓存策略

    • 使用Redis缓存频繁访问的数据
    • 缓存用户信息和会话元数据
  3. 数据库优化

    • 索引优化
    • 查询优化
    • 分页加载消息历史
  4. 消息推送优化

    • WebSocket连接心跳机制
    • 连接断开后的重连策略
    • 消息队列批处理

总结

在这个完整的项目实战中,我们成功构建了一个功能丰富的微服务聊天应用,将我们在整个Go语言学习系列中学到的知识和技能整合在一起。通过这个项目,我们实践了微服务架构的设计与实现,应用了gRPC进行服务间通信,并结合了消息队列、缓存、数据库和前端技术,构建了一个完整的实时聊天系统。

在开发过程中,我们不仅关注功能实现,还注重代码质量、测试覆盖、性能优化和系统安全性。我们使用了Docker和Kubernetes进行容器化和编排,实现了系统的可伸缩部署。通过整合日志、监控和追踪系统,我们确保了应用的可观测性,便于问题排查和性能分析。

这个项目展示了Go语言在构建现代分布式系统中的强大能力。Go的简洁语法、强大的并发模型、丰富的标准库和活跃的开源生态,使其成为微服务开发的理想选择。

随着项目的完成,我们也结束了Go语言学习系列的第三阶段。在接下来的文章中,我们将进入性能优化专题,深入探讨如何使Go应用达到极致性能。

希望这个项目能够为你提供实践参考,帮助你将所学知识应用到实际开发中。记住,最好的学习方式就是动手实践,不断探索和改进。期待你能基于这个项目,发挥创意,构建更加丰富和强大的应用!

👨‍💻 关于作者与Gopher部落

"Gopher部落"专注于Go语言技术分享,包括Go语言开发实战、Go语言底层原理、Go语言源码分析等。

为什么要关注我们:

  1. 系统化学习路径,从入门到进阶
  2. 实战为主的教学方式
  3. 持续更新的技术文章
  4. 活跃的技术交流社区

如何关注我们:

  1. 点击优快云专栏右上角的"关注"按钮
  2. 关注微信公众号:Gopher部落

读者福利:
关注公众号,回复"Go",即可获取:

  1. 完整的Go语言学习路线图
  2. Go语言面试题PDF
  3. 项目实战源码
  4. 专属学习规划指导

期待与您在Go语言的学习旅程中共同成长!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值