Browse Source

时间间隔修改

WH01243 1 week ago
parent
commit
24aa1d8d57

+ 48 - 40
client/chat/chat.pb.go

@@ -2,7 +2,7 @@
 // versions:
 // 	protoc-gen-go v1.36.6
 // 	protoc        v3.15.1
-// source: chat.proto
+// source: proto/chat.proto
 
 package chat
 
@@ -23,13 +23,14 @@ const (
 
 type PingRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
+	UserId        string                 `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
 	unknownFields protoimpl.UnknownFields
 	sizeCache     protoimpl.SizeCache
 }
 
 func (x *PingRequest) Reset() {
 	*x = PingRequest{}
-	mi := &file_chat_proto_msgTypes[0]
+	mi := &file_proto_chat_proto_msgTypes[0]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -41,7 +42,7 @@ func (x *PingRequest) String() string {
 func (*PingRequest) ProtoMessage() {}
 
 func (x *PingRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[0]
+	mi := &file_proto_chat_proto_msgTypes[0]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -54,7 +55,14 @@ func (x *PingRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
 func (*PingRequest) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{0}
+	return file_proto_chat_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *PingRequest) GetUserId() string {
+	if x != nil {
+		return x.UserId
+	}
+	return ""
 }
 
 type PingResponse struct {
@@ -67,7 +75,7 @@ type PingResponse struct {
 
 func (x *PingResponse) Reset() {
 	*x = PingResponse{}
-	mi := &file_chat_proto_msgTypes[1]
+	mi := &file_proto_chat_proto_msgTypes[1]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -79,7 +87,7 @@ func (x *PingResponse) String() string {
 func (*PingResponse) ProtoMessage() {}
 
 func (x *PingResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[1]
+	mi := &file_proto_chat_proto_msgTypes[1]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -92,7 +100,7 @@ func (x *PingResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use PingResponse.ProtoReflect.Descriptor instead.
 func (*PingResponse) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{1}
+	return file_proto_chat_proto_rawDescGZIP(), []int{1}
 }
 
 func (x *PingResponse) GetStatus() string {
@@ -119,7 +127,7 @@ type JoinRequest struct {
 
 func (x *JoinRequest) Reset() {
 	*x = JoinRequest{}
-	mi := &file_chat_proto_msgTypes[2]
+	mi := &file_proto_chat_proto_msgTypes[2]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -131,7 +139,7 @@ func (x *JoinRequest) String() string {
 func (*JoinRequest) ProtoMessage() {}
 
 func (x *JoinRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[2]
+	mi := &file_proto_chat_proto_msgTypes[2]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -144,7 +152,7 @@ func (x *JoinRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use JoinRequest.ProtoReflect.Descriptor instead.
 func (*JoinRequest) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{2}
+	return file_proto_chat_proto_rawDescGZIP(), []int{2}
 }
 
 func (x *JoinRequest) GetUserId() string {
@@ -173,7 +181,7 @@ type Message struct {
 
 func (x *Message) Reset() {
 	*x = Message{}
-	mi := &file_chat_proto_msgTypes[3]
+	mi := &file_proto_chat_proto_msgTypes[3]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -185,7 +193,7 @@ func (x *Message) String() string {
 func (*Message) ProtoMessage() {}
 
 func (x *Message) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[3]
+	mi := &file_proto_chat_proto_msgTypes[3]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -198,7 +206,7 @@ func (x *Message) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Message.ProtoReflect.Descriptor instead.
 func (*Message) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{3}
+	return file_proto_chat_proto_rawDescGZIP(), []int{3}
 }
 
 func (x *Message) GetUserId() string {
@@ -239,7 +247,7 @@ type MessageAck struct {
 
 func (x *MessageAck) Reset() {
 	*x = MessageAck{}
-	mi := &file_chat_proto_msgTypes[4]
+	mi := &file_proto_chat_proto_msgTypes[4]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -251,7 +259,7 @@ func (x *MessageAck) String() string {
 func (*MessageAck) ProtoMessage() {}
 
 func (x *MessageAck) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[4]
+	mi := &file_proto_chat_proto_msgTypes[4]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -264,7 +272,7 @@ func (x *MessageAck) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use MessageAck.ProtoReflect.Descriptor instead.
 func (*MessageAck) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{4}
+	return file_proto_chat_proto_rawDescGZIP(), []int{4}
 }
 
 func (x *MessageAck) GetSuccess() bool {
@@ -281,13 +289,13 @@ func (x *MessageAck) GetMessageId() string {
 	return ""
 }
 
-var File_chat_proto protoreflect.FileDescriptor
+var File_proto_chat_proto protoreflect.FileDescriptor
 
-const file_chat_proto_rawDesc = "" +
-	"\n" +
+const file_proto_chat_proto_rawDesc = "" +
 	"\n" +
-	"chat.proto\x12\x04chat\"\r\n" +
-	"\vPingRequest\"D\n" +
+	"\x10proto/chat.proto\x12\x04chat\"&\n" +
+	"\vPingRequest\x12\x17\n" +
+	"\auser_id\x18\x01 \x01(\tR\x06userId\"D\n" +
 	"\fPingResponse\x12\x16\n" +
 	"\x06status\x18\x01 \x01(\tR\x06status\x12\x1c\n" +
 	"\ttimestamp\x18\x02 \x01(\x03R\ttimestamp\"<\n" +
@@ -310,26 +318,26 @@ const file_chat_proto_rawDesc = "" +
 	"\x04Ping\x12\x11.chat.PingRequest\x1a\x12.chat.PingResponseB\x0eZ\f../chat;chatb\x06proto3"
 
 var (
-	file_chat_proto_rawDescOnce sync.Once
-	file_chat_proto_rawDescData []byte
+	file_proto_chat_proto_rawDescOnce sync.Once
+	file_proto_chat_proto_rawDescData []byte
 )
 
-func file_chat_proto_rawDescGZIP() []byte {
-	file_chat_proto_rawDescOnce.Do(func() {
-		file_chat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_chat_proto_rawDesc), len(file_chat_proto_rawDesc)))
+func file_proto_chat_proto_rawDescGZIP() []byte {
+	file_proto_chat_proto_rawDescOnce.Do(func() {
+		file_proto_chat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_chat_proto_rawDesc), len(file_proto_chat_proto_rawDesc)))
 	})
-	return file_chat_proto_rawDescData
+	return file_proto_chat_proto_rawDescData
 }
 
-var file_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
-var file_chat_proto_goTypes = []any{
+var file_proto_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
+var file_proto_chat_proto_goTypes = []any{
 	(*PingRequest)(nil),  // 0: chat.PingRequest
 	(*PingResponse)(nil), // 1: chat.PingResponse
 	(*JoinRequest)(nil),  // 2: chat.JoinRequest
 	(*Message)(nil),      // 3: chat.Message
 	(*MessageAck)(nil),   // 4: chat.MessageAck
 }
-var file_chat_proto_depIdxs = []int32{
+var file_proto_chat_proto_depIdxs = []int32{
 	2, // 0: chat.ChatService.JoinChat:input_type -> chat.JoinRequest
 	3, // 1: chat.ChatService.SendMessage:input_type -> chat.Message
 	0, // 2: chat.ChatService.Ping:input_type -> chat.PingRequest
@@ -343,26 +351,26 @@ var file_chat_proto_depIdxs = []int32{
 	0, // [0:0] is the sub-list for field type_name
 }
 
-func init() { file_chat_proto_init() }
-func file_chat_proto_init() {
-	if File_chat_proto != nil {
+func init() { file_proto_chat_proto_init() }
+func file_proto_chat_proto_init() {
+	if File_proto_chat_proto != nil {
 		return
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: unsafe.Slice(unsafe.StringData(file_chat_proto_rawDesc), len(file_chat_proto_rawDesc)),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_chat_proto_rawDesc), len(file_proto_chat_proto_rawDesc)),
 			NumEnums:      0,
 			NumMessages:   5,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
-		GoTypes:           file_chat_proto_goTypes,
-		DependencyIndexes: file_chat_proto_depIdxs,
-		MessageInfos:      file_chat_proto_msgTypes,
+		GoTypes:           file_proto_chat_proto_goTypes,
+		DependencyIndexes: file_proto_chat_proto_depIdxs,
+		MessageInfos:      file_proto_chat_proto_msgTypes,
 	}.Build()
-	File_chat_proto = out.File
-	file_chat_proto_goTypes = nil
-	file_chat_proto_depIdxs = nil
+	File_proto_chat_proto = out.File
+	file_proto_chat_proto_goTypes = nil
+	file_proto_chat_proto_depIdxs = nil
 }

+ 2 - 2
client/chat/chat_grpc.pb.go

@@ -2,7 +2,7 @@
 // versions:
 // - protoc-gen-go-grpc v1.5.1
 // - protoc             v3.15.1
-// source: chat.proto
+// source: proto/chat.proto
 
 package chat
 
@@ -197,5 +197,5 @@ var ChatService_ServiceDesc = grpc.ServiceDesc{
 			ServerStreams: true,
 		},
 	},
-	Metadata: "chat.proto",
+	Metadata: "proto/chat.proto",
 }

+ 2 - 2
client/config.json

@@ -1,6 +1,6 @@
 {
-  "serviceAddress1": "jybx3-webtest.jydev.jianyu360.com:50051",
-  "serviceAddress": "127.0.0.1:50051",
+  "serviceAddress": "jybx3-webtest.jydev.jianyu360.com:50051",
+  "serviceAddress1": "127.0.0.1:50051",
   "informationDelay": 20,
   "personName": "jianyu",
   "password": "jianyu@123"

+ 3 - 1
client/proto/chat.proto

@@ -10,7 +10,9 @@ service ChatService {
   rpc Ping(PingRequest) returns (PingResponse);
 
 }
-message PingRequest {}
+message PingRequest {
+  string user_id = 1;
+}
 message PingResponse {
   string status = 1;
   int64 timestamp = 2;

+ 182 - 128
client/service/chatClient.go

@@ -21,73 +21,80 @@ import (
 	"google.golang.org/grpc/status"
 )
 
+// 定义连接相关的常量参数
 const (
-	initialReconnectInterval = 2 * time.Second
-	keepaliveTime            = 30 * time.Second
-	keepaliveTimeout         = 10 * time.Second
-	maxRetryCount            = math.MaxInt32
-	connectionTimeout        = 5 * time.Second
-	maxReconnectInterval     = 120 * time.Second
-	healthCheckInterval      = 30 * time.Second
+	initialReconnectInterval = 2 * time.Second   // 初始重连间隔时间
+	keepaliveTime            = 20 * time.Second  // 客户端keepalive心跳间隔
+	keepaliveTimeout         = 10 * time.Second  // keepalive心跳超时时间
+	connectionTimeout        = 10 * time.Second  // 连接超时时间
+	maxReconnectInterval     = 120 * time.Second // 最大重连间隔时间
+	healthCheckInterval      = 30 * time.Second  // 健康检查间隔时间
 )
 
+// 全局客户端实例
 var client = &ChatClient{}
 
+// ChatClient 定义gRPC客户端结构体
 type ChatClient struct {
-	conn              *grpc.ClientConn
-	client            ChatServiceClient
-	ctx               context.Context
-	cancel            context.CancelFunc
-	userID            string
-	mu                sync.RWMutex
-	retryCount        int
-	isConnected       bool
-	wg                sync.WaitGroup
-	reconnecting      bool
-	serviceAddress    string
-	stream            ChatService_JoinChatClient
-	streamMutex       sync.Mutex
-	healthCheckTicker *time.Ticker
-	lastPingTime      time.Time
+	conn              *grpc.ClientConn           // gRPC连接对象
+	client            ChatServiceClient          // gRPC服务客户端
+	ctx               context.Context            // 上下文对象
+	cancel            context.CancelFunc         // 取消函数
+	userID            string                     // 用户ID
+	mu                sync.RWMutex               // 读写锁(保护并发访问)
+	retryCount        int                        // 当前重试次数
+	isConnected       bool                       // 连接状态标志
+	wg                sync.WaitGroup             // 等待组(用于goroutine同步)
+	reconnecting      bool                       // 重连状态标志
+	serviceAddress    string                     // 服务端地址
+	stream            ChatService_JoinChatClient // gRPC流对象
+	streamMutex       sync.Mutex                 // 流操作互斥锁
+	healthCheckTicker *time.Ticker               // 健康检查定时器
+	lastPingTime      time.Time                  // 最后心跳时间
 }
 
+// NewChatClient 构造函数,创建新的ChatClient实例
 func NewChatClient(userID, address string) *ChatClient {
+	// 创建可取消的上下文
 	ctx, cancel := context.WithCancel(context.Background())
 	return &ChatClient{
-		userID:         userID,
-		ctx:            ctx,
-		cancel:         cancel,
-		serviceAddress: address,
+		userID:         userID,  // 设置用户ID
+		ctx:            ctx,     // 设置上下文
+		cancel:         cancel,  // 设置取消函数
+		serviceAddress: address, // 设置服务端地址
 	}
 }
 
-// 连接服务器
+// connect 连接到gRPC服务器
 func (c *ChatClient) connect(password string) error {
 	c.mu.Lock()
 	defer c.mu.Unlock()
-	// 1. 检查现有连接是否可用
-	// 1. 检查现有连接是否可用
+
+	// 检查现有连接是否可用
 	if c.isConnected && c.conn.GetState() == connectivity.Ready {
 		return nil
 	}
 
-	// 创建gRPC连接(明文传输,仅用于测试)
+	// 打印连接日志
 	log.Println("[连接] 尝试连接服务器...", c.serviceAddress)
+
+	// 创建带超时的上下文
 	ctx, cancel := context.WithTimeout(context.Background(), connectionTimeout)
 	defer cancel()
 
+	// 建立gRPC连接
 	conn, err := grpc.DialContext(ctx, c.serviceAddress,
-		grpc.WithTransportCredentials(insecure.NewCredentials()), // 关键修改
-		grpc.WithBlock(),
-		grpc.WithPerRPCCredentials(&authCreds{password: password}),
-		grpc.WithDefaultCallOptions(
-			grpc.MaxCallRecvMsgSize(20*1024*1024),
-			grpc.MaxCallSendMsgSize(20*1024*1024),
+		grpc.WithTransportCredentials(insecure.NewCredentials()),   // 使用非安全连接
+		grpc.WithBlock(),                                           // 阻塞式连接
+		grpc.WithPerRPCCredentials(&authCreds{password: password}), // 设置认证凭证
+		grpc.WithDefaultCallOptions( // 设置默认调用选项
+			grpc.MaxCallRecvMsgSize(20*1024*1024), // 最大接收消息大小
+			grpc.MaxCallSendMsgSize(20*1024*1024), // 最大发送消息大小
 		),
-		grpc.WithKeepaliveParams(keepalive.ClientParameters{
+		grpc.WithKeepaliveParams(keepalive.ClientParameters{ // 设置keepalive参数
 			Time:                keepaliveTime,
 			Timeout:             keepaliveTimeout,
-			PermitWithoutStream: true,
+			PermitWithoutStream: true, // 允许无活动流时也发送心跳
 		}),
 	)
 	if err != nil {
@@ -105,6 +112,8 @@ func (c *ChatClient) connect(password string) error {
 			}
 		}
 	}
+
+	// 创建客户端并测试连接
 	client := NewChatServiceClient(conn)
 	_, err = client.JoinChat(context.Background(), &JoinRequest{UserId: c.userID, Force: true})
 	if err != nil {
@@ -112,17 +121,19 @@ func (c *ChatClient) connect(password string) error {
 		return fmt.Errorf("连接测试失败: %v", err)
 	}
 
-	// 关闭旧连接
+	// 关闭旧连接(如果存在)
 	if c.conn != nil {
 		_ = c.conn.Close()
 	}
 
+	// 更新连接状态
 	c.conn = conn
 	c.client = client
 	c.isConnected = true
 	c.retryCount = 0
 	c.lastPingTime = time.Now()
-	// 启动健康检查
+
+	// 启动健康检查(延迟10秒)
 	if c.healthCheckTicker != nil {
 		c.healthCheckTicker.Stop()
 		c.healthCheckTicker = nil
@@ -131,40 +142,49 @@ func (c *ChatClient) connect(password string) error {
 		time.Sleep(10 * time.Second) // 给连接稳定时间
 		c.startHealthCheck()
 	}()
+
 	log.Printf("[连接][用户:%s] 服务器连接成功", c.userID)
 	return nil
 }
 
-// 断开连接
+// disconnect 断开与服务器的连接
 func (c *ChatClient) disconnect() {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
+	// 如果已经断开连接,直接返回
 	if !c.isConnected {
 		return
 	}
 
-	// 停止健康检查
+	// 停止健康检查定时器
 	if c.healthCheckTicker != nil {
 		c.healthCheckTicker.Stop()
 		c.healthCheckTicker = nil
 	}
 
+	// 关闭流
 	c.closeStream()
+
+	// 关闭连接
 	if c.conn != nil {
 		if err := c.conn.Close(); err != nil {
 			log.Printf("[连接][用户:%s] 关闭连接出错: %v", c.userID, err)
 		}
 		c.conn = nil
 	}
+
+	// 更新连接状态
 	c.isConnected = false
 	log.Printf("[连接][用户:%s] 已断开连接", c.userID)
 }
 
-// 关闭流
+// closeStream 关闭gRPC
 func (c *ChatClient) closeStream() {
 	c.streamMutex.Lock()
 	defer c.streamMutex.Unlock()
+
+	// 如果流存在,则关闭
 	if c.stream != nil {
 		if err := c.stream.CloseSend(); err != nil {
 			log.Printf("[流] 关闭流错误: %v", err)
@@ -173,51 +193,63 @@ func (c *ChatClient) closeStream() {
 	}
 }
 
-// 重连逻辑
+// reconnect 执行重连逻辑
 func (c *ChatClient) reconnect() {
+	const maxRetries = 5 // 最大重试次数
+
+	// 加锁检查重连状态
 	c.mu.Lock()
 	if c.reconnecting {
 		c.mu.Unlock()
+		log.Printf("[重连] 已在重连中,跳过")
 		return
 	}
 	c.reconnecting = true
 	currentRetry := c.retryCount
 	c.mu.Unlock()
 
+	// 确保结束时重置重连状态
 	defer func() {
 		c.mu.Lock()
 		c.reconnecting = false
 		c.mu.Unlock()
+		log.Printf("[重连] 重连流程结束")
 	}()
 
+	// 等待组管理
 	c.wg.Add(1)
 	defer c.wg.Done()
 
-	for {
+	// 重连循环
+	for currentRetry < maxRetries {
 		select {
 		case <-c.ctx.Done():
+			log.Printf("[重连] 上下文取消,终止重连")
 			return
 		default:
-			// 彻底断开旧连接
+			// 1. 清理旧连接
 			c.disconnect()
 
-			// 尝试新连接
+			// 2. 尝试新连接
+			log.Printf("[重连] 尝试第 %d/%d 次重连", currentRetry+1, maxRetries)
 			err := c.connect(config.Cfg.Password)
 			if err == nil {
-				log.Printf("重连成功")
+				log.Printf("[重连] 成功建立新连接")
 				go c.establishStream()
 				return
 			}
 
-			// 错误处理
-			if shouldStopRetry(err) {
-				log.Printf("不可恢复错误,停止重连")
+			// 3. 错误处理
+			log.Printf("[重连] 第 %d 次重连失败: %v", currentRetry+1, err)
+
+			if isFatalError(err) {
+				log.Printf("[重连] 遇到致命错误,停止重连: %v", err)
 				return
 			}
 
-			// 智能退避
+			// 4. 退避等待
 			backoff := calculateBackoff(currentRetry)
-			log.Printf("等待 %v 后重试", backoff)
+			log.Printf("[重连] 等待 %v 后重试", backoff)
 
 			select {
 			case <-time.After(backoff):
@@ -227,7 +259,26 @@ func (c *ChatClient) reconnect() {
 			}
 		}
 	}
+
+	log.Printf("[重连] 已达到最大重试次数 (%d),停止重连", maxRetries)
 }
+
+// 辅助函数:判断是否为致命错误(如认证失败、无效地址)
+func isFatalError(err error) bool {
+	if err == nil {
+		return false
+	}
+	// 示例:gRPC 的不可恢复错误码
+	if status, ok := status.FromError(err); ok {
+		switch status.Code() {
+		case codes.Unauthenticated, codes.PermissionDenied, codes.NotFound:
+			return true
+		}
+	}
+	return false
+}
+
+// calculateBackoff 计算退避时间(指数退避算法)
 func calculateBackoff(retryCount int) time.Duration {
 	base := float64(initialReconnectInterval)
 	max := float64(maxReconnectInterval)
@@ -235,18 +286,19 @@ func calculateBackoff(retryCount int) time.Duration {
 	return time.Duration(math.Min(backoff, max))
 }
 
-// 建立流
+// establishStream 建立gRPC
 func (c *ChatClient) establishStream() {
+	// 添加等待组计数
 	c.wg.Add(1)
 	defer c.wg.Done()
 
 	retryDelay := time.Second
 	for {
 		select {
-		case <-c.ctx.Done():
+		case <-c.ctx.Done(): // 检查是否被取消
 			return
 		default:
-			// 添加连接状态检查
+			// 检查连接是否就绪
 			if !c.isReady() {
 				time.Sleep(retryDelay)
 				retryDelay = time.Duration(math.Min(float64(retryDelay*2), float64(maxReconnectInterval)))
@@ -270,7 +322,7 @@ func (c *ChatClient) establishStream() {
 			// 重置重试延迟
 			retryDelay = time.Second
 
-			// 处理消息
+			// 处理接收到的消息
 			if err := c.receiveMessages(stream); err != nil {
 				log.Printf("[流] 接收消息错误: %v", err)
 				return
@@ -279,36 +331,55 @@ func (c *ChatClient) establishStream() {
 	}
 }
 
-// 接收消息
+// receiveMessages 接收并处理消息
 func (c *ChatClient) receiveMessages(stream ChatService_JoinChatClient) error {
 	for {
+		// 接收消息
 		msg, err := stream.Recv()
 		if msg == nil {
 			continue
 		}
-		// 添加空消息检查
+
+		// 处理错误
 		if err != nil {
-			if status.Code(err) == codes.Canceled {
-				return nil // 正常关闭
+			// 区分错误类型
+			st, ok := status.FromError(err)
+			if ok {
+				switch st.Code() {
+				case codes.Canceled:
+					log.Println("流正常关闭")
+					return nil
+				case codes.Unavailable, codes.DeadlineExceeded:
+					log.Printf("流异常断开,触发重连: %v", err)
+					go c.reconnect() // 触发重连
+					return err
+				default:
+					log.Printf("不可恢复错误: %v", err)
+					return err
+				}
 			}
-
-			// 流错误时立即触发重连
+			// 非gRPC错误(如网络问题)
 			log.Printf("[流] 接收错误: %v,触发重连", err)
 			go c.reconnect()
 			return err
 		}
 
+		// 跳过空消息
 		if msg == nil || msg.Text == "" || msg.UserId == "" {
 			continue
 		}
 
+		// 再次检查错误(冗余检查)
 		if err != nil {
 			if status.Code(err) == codes.Canceled {
-				return nil // 正常关闭
+				return nil
 			}
 			return fmt.Errorf("接收消息错误: %v", err)
 		}
+
+		// 打印收到的消息
 		log.Printf("[接收] 收到消息: %+v", msg)
+		// 处理系统消息
 		if msg.UserId == "系统" {
 			switch msg.Action {
 			case "sendTalk":
@@ -317,8 +388,6 @@ func (c *ChatClient) receiveMessages(stream ChatService_JoinChatClient) error {
 				go GetContacts()
 			case "reject":
 				go Reject(msg.Text)
-			default:
-				log.Printf("[系统通知]: %s", msg.Text)
 			}
 			if msg.Text == "欢迎加入聊天室" {
 				go GetContacts()
@@ -327,16 +396,18 @@ func (c *ChatClient) receiveMessages(stream ChatService_JoinChatClient) error {
 	}
 }
 
-// 发送消息
+// SendMessage 发送消息
 func (c *ChatClient) SendMessage(text, action string) error {
-
 	c.mu.RLock()
 	defer c.mu.RUnlock()
+
+	// 检查连接是否就绪
 	if !c.isReady() {
-		go c.reconnect() // 立即触发重连
+		go c.reconnect() // 触发重连
 		return fmt.Errorf("未连接服务器,正在尝试重连...")
 	}
 
+	// 构造消息
 	msg := &Message{
 		UserId: c.userID,
 		Text:   text,
@@ -344,19 +415,22 @@ func (c *ChatClient) SendMessage(text, action string) error {
 	}
 	log.Printf("[发送] 发送消息: %+v", msg)
 
+	// 创建带超时的上下文
 	ctx, cancel := context.WithTimeout(c.ctx, 5*time.Second)
 	defer cancel()
 
+	// 发送消息
 	aaa, err := c.client.SendMessage(ctx, msg)
 	log.Println(aaa)
 	if err != nil {
+		// 处理不同类型的错误
 		st, ok := status.FromError(err)
 		if ok {
 			switch st.Code() {
 			case codes.Unavailable, codes.DeadlineExceeded:
 				go c.reconnect() // 网络问题触发重连
 			case codes.Unauthenticated, codes.PermissionDenied:
-				// 认证错误不需要重连
+				// 认证错误不重连
 			}
 		}
 		return fmt.Errorf("发送失败: %v", err)
@@ -364,37 +438,48 @@ func (c *ChatClient) SendMessage(text, action string) error {
 	return nil
 }
 
-// 启动客户端
+// ConnectGRPC 启动gRPC客户端
 func ConnectGRPC(userId, address string) {
 	log.Println("[主程序] 启动GRPC连接")
+	// 创建新客户端
 	client = NewChatClient(userId, address)
-	defer client.Shutdown()
+	defer client.Shutdown() // 确保退出时关闭
+
+	// 启动连接监控
 	go client.startConnectionMonitor()
+
+	// 初始连接
 	if err := client.connect(config.Cfg.Password); err != nil {
 		log.Printf("[主程序] 初始连接失败: %v", err)
 		go client.reconnect()
 	} else {
 		go client.establishStream()
 	}
-	// 保持主线程运行
+
+	// 等待退出信号
 	quit := make(chan os.Signal, 1)
 	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
 	<-quit
 }
+
+// isReady 检查连接是否就绪
 func (c *ChatClient) isReady() bool {
 	c.mu.RLock()
 	defer c.mu.RUnlock()
 
+	// 基本检查
 	if !c.isConnected || c.conn == nil {
 		return false
 	}
 
+	// 检查连接状态
 	state := c.conn.GetState()
 	if state != connectivity.Ready {
 		return false
 	}
 
-	// 增加更严格的活跃时间检查
+	// 检查心跳时间
+	log.Println(time.Since(c.lastPingTime), 3*keepaliveTime)
 	if time.Since(c.lastPingTime) > 3*keepaliveTime {
 		return false
 	}
@@ -402,7 +487,7 @@ func (c *ChatClient) isReady() bool {
 	return true
 }
 
-// 修改健康检查实现
+// startHealthCheck 启动健康检查
 func (c *ChatClient) startHealthCheck() {
 	c.healthCheckTicker = time.NewTicker(healthCheckInterval)
 	c.wg.Add(1)
@@ -411,16 +496,15 @@ func (c *ChatClient) startHealthCheck() {
 		defer c.wg.Done()
 		for {
 			select {
-			case <-c.healthCheckTicker.C:
+			case <-c.healthCheckTicker.C: // 定时触发
 				if !c.isReady() {
 					log.Printf("连接不可用,触发重连")
 					go c.reconnect()
 					continue
 				}
-
-				// 主动健康检查
-				ctx, cancel := context.WithTimeout(c.ctx, 3*time.Second)
-				_, err := c.client.Ping(ctx, &PingRequest{})
+				// 执行健康检查
+				ctx, cancel := context.WithTimeout(c.ctx, 6*time.Second)
+				_, err := c.client.Ping(ctx, &PingRequest{UserId: c.userID})
 				cancel()
 
 				if err != nil {
@@ -429,23 +513,23 @@ func (c *ChatClient) startHealthCheck() {
 					go c.reconnect()
 				} else {
 					c.mu.Lock()
-					c.lastPingTime = time.Now()
+					c.lastPingTime = time.Now() // 更新最后心跳时间
 					c.mu.Unlock()
 				}
 
-			case <-c.ctx.Done():
+			case <-c.ctx.Done(): // 上下文取消
 				return
 			}
 		}
 	}()
 }
 
-// 修改健康检查实现
+// checkHealth 执行健康检查
 func (c *ChatClient) checkHealth() error {
 	ctx, cancel := context.WithTimeout(c.ctx, 5*time.Second)
 	defer cancel()
 
-	// 方法1:使用已有方法检查
+	// 使用JoinChat方法检查健康状态
 	_, err := c.client.JoinChat(ctx, &JoinRequest{UserId: c.userID, Force: false})
 	if err != nil {
 		return fmt.Errorf("健康检查失败: %w", err)
@@ -453,27 +537,10 @@ func (c *ChatClient) checkHealth() error {
 
 	return nil
 }
-func (c *ChatClient) checkHealthWithRetry(maxRetry int) error {
-	var lastErr error
-	for i := 0; i < maxRetry; i++ {
-		ctx, cancel := context.WithTimeout(c.ctx, 3*time.Second)
-		_, err := c.client.JoinChat(ctx, &JoinRequest{UserId: c.userID, Force: false})
-		cancel()
-
-		if err == nil {
-			return nil
-		}
 
-		lastErr = err
-		time.Sleep(time.Duration(i+1) * time.Second) // 指数退避
-	}
-	return lastErr
-}
-func (c *ChatClient) fallbackHealthCheck() error {
-	// 实现其他健康检查方式
-	return nil
-}
+// Shutdown 关闭客户端
 func (c *ChatClient) Shutdown() {
+	log.Println("客户端服务关闭")
 	// 1. 取消上下文
 	c.cancel()
 
@@ -483,35 +550,34 @@ func (c *ChatClient) Shutdown() {
 	// 3. 等待所有goroutine结束
 	c.wg.Wait()
 }
-func (c *ChatClient) log() *log.Logger {
-	return log.New(os.Stdout, fmt.Sprintf("[用户:%s] ", c.userID), log.LstdFlags)
-}
 
-// 实现 credentials.PerRPCCredentials 接口
+// authCreds 实现gRPC认证接口
 type authCreds struct {
 	password string
 }
 
+// GetRequestMetadata 获取认证元数据
 func (c *authCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
 	return map[string]string{
 		"password": c.password,
 	}, nil
 }
 
+// RequireTransportSecurity 是否要求传输安全
 func (c *authCreds) RequireTransportSecurity() bool {
-	return false // 必须使用TLS
+	return false // 不要求TLS
 }
+
+// startConnectionMonitor 启动连接监控
 func (c *ChatClient) startConnectionMonitor() {
 	c.wg.Add(1)
 	go func() {
 		defer c.wg.Done()
-
-		ticker := time.NewTicker(1 * time.Minute) // 更频繁的检查
+		ticker := time.NewTicker(1 * time.Minute) // 每5分钟检查一次
 		defer ticker.Stop()
-
 		for {
 			select {
-			case <-ticker.C:
+			case <-ticker.C: // 定时触发
 				c.mu.RLock()
 				conn := c.conn
 				c.mu.RUnlock()
@@ -522,34 +588,22 @@ func (c *ChatClient) startConnectionMonitor() {
 					continue
 				}
 
+				// 检查连接状态
 				state := conn.GetState()
 				log.Printf("[监控] 当前连接状态: %v", state)
 
-				// 更全面的状态检查
+				// 判断是否需要重连
 				if state == connectivity.TransientFailure ||
 					state == connectivity.Shutdown ||
-					(state == connectivity.Ready && time.Since(c.lastPingTime) > 2*keepaliveTime) {
+					(state == connectivity.Ready && time.Since(c.lastPingTime) > 3*keepaliveTime) {
 					log.Printf("[监控] 连接异常,触发重连")
 					go c.reconnect()
 				}
 
-			case <-c.ctx.Done():
+			case <-c.ctx.Done(): // 上下文取消
 				log.Printf("[监控] 监控停止")
 				return
 			}
 		}
 	}()
 }
-func shouldStopRetry(err error) bool {
-	if st, ok := status.FromError(err); ok {
-		switch st.Code() {
-		case codes.Unavailable, codes.DeadlineExceeded:
-			return false // 可恢复错误
-		case codes.Unauthenticated, codes.PermissionDenied:
-			return true // 认证错误
-		case codes.Unimplemented:
-			return true // 方法未实现
-		}
-	}
-	return false
-}

+ 2 - 1
client/service/wx.go

@@ -198,7 +198,8 @@ func userJudge(c *wcf.RpcContact) (bool, string, string) {
 // 发送文本信息
 func sendText(content, wxId, appellation string) (bool, error) {
 	if appellation != "" {
-		content = fmt.Sprintf("%s,%s", appellation, content)
+		appellation = string([]rune(appellation)[0])
+		content = fmt.Sprintf("%s老师,%s", appellation, content)
 	}
 	if app.WxClient.SendTxt(content, wxId, nil) != 0 {
 		return false, fmt.Errorf(fmt.Sprintf("%s%s文字消息发送失败", content, wxId))

+ 48 - 40
rpc/chat/chat.pb.go

@@ -2,7 +2,7 @@
 // versions:
 // 	protoc-gen-go v1.36.6
 // 	protoc        v3.15.1
-// source: chat.proto
+// source: proto/chat.proto
 
 package chat
 
@@ -23,13 +23,14 @@ const (
 
 type PingRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
+	UserId        string                 `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
 	unknownFields protoimpl.UnknownFields
 	sizeCache     protoimpl.SizeCache
 }
 
 func (x *PingRequest) Reset() {
 	*x = PingRequest{}
-	mi := &file_chat_proto_msgTypes[0]
+	mi := &file_proto_chat_proto_msgTypes[0]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -41,7 +42,7 @@ func (x *PingRequest) String() string {
 func (*PingRequest) ProtoMessage() {}
 
 func (x *PingRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[0]
+	mi := &file_proto_chat_proto_msgTypes[0]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -54,7 +55,14 @@ func (x *PingRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
 func (*PingRequest) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{0}
+	return file_proto_chat_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *PingRequest) GetUserId() string {
+	if x != nil {
+		return x.UserId
+	}
+	return ""
 }
 
 type PingResponse struct {
@@ -67,7 +75,7 @@ type PingResponse struct {
 
 func (x *PingResponse) Reset() {
 	*x = PingResponse{}
-	mi := &file_chat_proto_msgTypes[1]
+	mi := &file_proto_chat_proto_msgTypes[1]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -79,7 +87,7 @@ func (x *PingResponse) String() string {
 func (*PingResponse) ProtoMessage() {}
 
 func (x *PingResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[1]
+	mi := &file_proto_chat_proto_msgTypes[1]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -92,7 +100,7 @@ func (x *PingResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use PingResponse.ProtoReflect.Descriptor instead.
 func (*PingResponse) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{1}
+	return file_proto_chat_proto_rawDescGZIP(), []int{1}
 }
 
 func (x *PingResponse) GetStatus() string {
@@ -119,7 +127,7 @@ type JoinRequest struct {
 
 func (x *JoinRequest) Reset() {
 	*x = JoinRequest{}
-	mi := &file_chat_proto_msgTypes[2]
+	mi := &file_proto_chat_proto_msgTypes[2]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -131,7 +139,7 @@ func (x *JoinRequest) String() string {
 func (*JoinRequest) ProtoMessage() {}
 
 func (x *JoinRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[2]
+	mi := &file_proto_chat_proto_msgTypes[2]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -144,7 +152,7 @@ func (x *JoinRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use JoinRequest.ProtoReflect.Descriptor instead.
 func (*JoinRequest) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{2}
+	return file_proto_chat_proto_rawDescGZIP(), []int{2}
 }
 
 func (x *JoinRequest) GetUserId() string {
@@ -173,7 +181,7 @@ type Message struct {
 
 func (x *Message) Reset() {
 	*x = Message{}
-	mi := &file_chat_proto_msgTypes[3]
+	mi := &file_proto_chat_proto_msgTypes[3]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -185,7 +193,7 @@ func (x *Message) String() string {
 func (*Message) ProtoMessage() {}
 
 func (x *Message) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[3]
+	mi := &file_proto_chat_proto_msgTypes[3]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -198,7 +206,7 @@ func (x *Message) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Message.ProtoReflect.Descriptor instead.
 func (*Message) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{3}
+	return file_proto_chat_proto_rawDescGZIP(), []int{3}
 }
 
 func (x *Message) GetUserId() string {
@@ -239,7 +247,7 @@ type MessageAck struct {
 
 func (x *MessageAck) Reset() {
 	*x = MessageAck{}
-	mi := &file_chat_proto_msgTypes[4]
+	mi := &file_proto_chat_proto_msgTypes[4]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -251,7 +259,7 @@ func (x *MessageAck) String() string {
 func (*MessageAck) ProtoMessage() {}
 
 func (x *MessageAck) ProtoReflect() protoreflect.Message {
-	mi := &file_chat_proto_msgTypes[4]
+	mi := &file_proto_chat_proto_msgTypes[4]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -264,7 +272,7 @@ func (x *MessageAck) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use MessageAck.ProtoReflect.Descriptor instead.
 func (*MessageAck) Descriptor() ([]byte, []int) {
-	return file_chat_proto_rawDescGZIP(), []int{4}
+	return file_proto_chat_proto_rawDescGZIP(), []int{4}
 }
 
 func (x *MessageAck) GetSuccess() bool {
@@ -281,13 +289,13 @@ func (x *MessageAck) GetMessageId() string {
 	return ""
 }
 
-var File_chat_proto protoreflect.FileDescriptor
+var File_proto_chat_proto protoreflect.FileDescriptor
 
-const file_chat_proto_rawDesc = "" +
-	"\n" +
+const file_proto_chat_proto_rawDesc = "" +
 	"\n" +
-	"chat.proto\x12\x04chat\"\r\n" +
-	"\vPingRequest\"D\n" +
+	"\x10proto/chat.proto\x12\x04chat\"&\n" +
+	"\vPingRequest\x12\x17\n" +
+	"\auser_id\x18\x01 \x01(\tR\x06userId\"D\n" +
 	"\fPingResponse\x12\x16\n" +
 	"\x06status\x18\x01 \x01(\tR\x06status\x12\x1c\n" +
 	"\ttimestamp\x18\x02 \x01(\x03R\ttimestamp\"<\n" +
@@ -310,26 +318,26 @@ const file_chat_proto_rawDesc = "" +
 	"\x04Ping\x12\x11.chat.PingRequest\x1a\x12.chat.PingResponseB\x0eZ\f../chat;chatb\x06proto3"
 
 var (
-	file_chat_proto_rawDescOnce sync.Once
-	file_chat_proto_rawDescData []byte
+	file_proto_chat_proto_rawDescOnce sync.Once
+	file_proto_chat_proto_rawDescData []byte
 )
 
-func file_chat_proto_rawDescGZIP() []byte {
-	file_chat_proto_rawDescOnce.Do(func() {
-		file_chat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_chat_proto_rawDesc), len(file_chat_proto_rawDesc)))
+func file_proto_chat_proto_rawDescGZIP() []byte {
+	file_proto_chat_proto_rawDescOnce.Do(func() {
+		file_proto_chat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_chat_proto_rawDesc), len(file_proto_chat_proto_rawDesc)))
 	})
-	return file_chat_proto_rawDescData
+	return file_proto_chat_proto_rawDescData
 }
 
-var file_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
-var file_chat_proto_goTypes = []any{
+var file_proto_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
+var file_proto_chat_proto_goTypes = []any{
 	(*PingRequest)(nil),  // 0: chat.PingRequest
 	(*PingResponse)(nil), // 1: chat.PingResponse
 	(*JoinRequest)(nil),  // 2: chat.JoinRequest
 	(*Message)(nil),      // 3: chat.Message
 	(*MessageAck)(nil),   // 4: chat.MessageAck
 }
-var file_chat_proto_depIdxs = []int32{
+var file_proto_chat_proto_depIdxs = []int32{
 	2, // 0: chat.ChatService.JoinChat:input_type -> chat.JoinRequest
 	3, // 1: chat.ChatService.SendMessage:input_type -> chat.Message
 	0, // 2: chat.ChatService.Ping:input_type -> chat.PingRequest
@@ -343,26 +351,26 @@ var file_chat_proto_depIdxs = []int32{
 	0, // [0:0] is the sub-list for field type_name
 }
 
-func init() { file_chat_proto_init() }
-func file_chat_proto_init() {
-	if File_chat_proto != nil {
+func init() { file_proto_chat_proto_init() }
+func file_proto_chat_proto_init() {
+	if File_proto_chat_proto != nil {
 		return
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: unsafe.Slice(unsafe.StringData(file_chat_proto_rawDesc), len(file_chat_proto_rawDesc)),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_chat_proto_rawDesc), len(file_proto_chat_proto_rawDesc)),
 			NumEnums:      0,
 			NumMessages:   5,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
-		GoTypes:           file_chat_proto_goTypes,
-		DependencyIndexes: file_chat_proto_depIdxs,
-		MessageInfos:      file_chat_proto_msgTypes,
+		GoTypes:           file_proto_chat_proto_goTypes,
+		DependencyIndexes: file_proto_chat_proto_depIdxs,
+		MessageInfos:      file_proto_chat_proto_msgTypes,
 	}.Build()
-	File_chat_proto = out.File
-	file_chat_proto_goTypes = nil
-	file_chat_proto_depIdxs = nil
+	File_proto_chat_proto = out.File
+	file_proto_chat_proto_goTypes = nil
+	file_proto_chat_proto_depIdxs = nil
 }

+ 2 - 2
rpc/chat/chat_grpc.pb.go

@@ -2,7 +2,7 @@
 // versions:
 // - protoc-gen-go-grpc v1.5.1
 // - protoc             v3.15.1
-// source: chat.proto
+// source: proto/chat.proto
 
 package chat
 
@@ -197,5 +197,5 @@ var ChatService_ServiceDesc = grpc.ServiceDesc{
 			ServerStreams: true,
 		},
 	},
-	Metadata: "chat.proto",
+	Metadata: "proto/chat.proto",
 }

+ 2 - 3
rpc/main.go

@@ -91,8 +91,8 @@ func (s *server) initGRPCServer(password string) {
 			PasswordAuthInterceptor(password),
 		),
 		grpc.KeepaliveParams(keepalive.ServerParameters{
-			Time:    3 * time.Minute,
-			Timeout: 30 * time.Second,
+			Time:    10 * time.Minute,
+			Timeout: 3 * time.Second,
 		}),
 		grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
 			MinTime:             30 * time.Second,
@@ -128,7 +128,6 @@ func (s *server) setupTimedTasks(ctx context.Context) {
 	}{
 		{1 * time.Minute, "sendTalk"},
 		{1 * time.Hour, "getContacts"},
-		{1 * time.Minute, "heartbeat"},
 	}
 
 	for _, task := range tasks {

+ 3 - 1
rpc/proto/chat.proto

@@ -10,7 +10,9 @@ service ChatService {
   rpc Ping(PingRequest) returns (PingResponse);
 
 }
-message PingRequest {}
+message PingRequest {
+  string user_id = 1;
+}
 message PingResponse {
   string status = 1;
   int64 timestamp = 2;

+ 231 - 144
rpc/service/chatServer.go

@@ -11,32 +11,49 @@ import (
 	"time"
 )
 
+// 全局ChatServer实例
 var Chatserver *ChatServer
 
+// 初始化函数,创建ChatServer实例
 func init() {
 	Chatserver = NewChatServer()
 }
 
+// 定义心跳检测相关常量
+const (
+	HeartbeatInterval    = 30 * time.Second // 心跳检测间隔时间
+	HeartbeatTimeout     = 90 * time.Second // 心跳超时时间
+	MaxHeartbeatAttempts = 5                // 最大心跳失败尝试次数
+)
+
+// ChatServer 定义聊天服务结构体
 type ChatServer struct {
-	UnimplementedChatServiceServer
-	clients      map[string]chan *Message
-	adminMsg     chan *Message
-	mu           sync.RWMutex
-	shutdownChan chan struct{} // 关闭信号通道
+	UnimplementedChatServiceServer                          // 内嵌gRPC生成的未实现服务
+	clients                        map[string]chan *Message // 客户端消息通道映射
+	adminMsg                       chan *Message            // 管理员消息通道
+	mu                             sync.RWMutex             // 读写锁
+	shutdownChan                   chan struct{}            // 服务关闭信号通道
+	failedHeartbeats               map[string]int           // 客户端心跳失败次数记录
+	lastActive                     map[string]time.Time     // 客户端最后活跃时间记录
 }
 
+// NewChatServer 创建新的ChatServer实例
 func NewChatServer() *ChatServer {
-	return &ChatServer{
-		clients:      make(map[string]chan *Message),
-		adminMsg:     make(chan *Message, 100),
-		shutdownChan: make(chan struct{}),
+	s := &ChatServer{
+		clients:          make(map[string]chan *Message),
+		adminMsg:         make(chan *Message, 100),
+		shutdownChan:     make(chan struct{}),
+		failedHeartbeats: make(map[string]int),       // 如果有的话
+		lastActive:       make(map[string]time.Time), // 初始化 lastActive
 	}
+	go s.startHeartbeatChecker() // 启动心跳检测器
+	return s
 }
 
-// 建立连接
+// JoinChat 处理客户端连接请求
 func (s *ChatServer) JoinChat(req *JoinRequest, stream ChatService_JoinChatServer) error {
+	// 创建消息通道
 	msgChan := make(chan *Message, 100)
-
 	// 注册客户端(同步处理旧连接)
 	s.mu.Lock()
 	if existing, exists := s.clients[req.UserId]; exists {
@@ -44,18 +61,33 @@ func (s *ChatServer) JoinChat(req *JoinRequest, stream ChatService_JoinChatServe
 			s.mu.Unlock()
 			return fmt.Errorf("用户 %s 已连接", req.UserId)
 		}
-		close(existing) // 同步关闭旧 channel
+		close(existing) // 关闭旧通道
 		delete(s.clients, req.UserId)
+		log.Println("用户删除", 4)
 	}
 	s.clients[req.UserId] = msgChan
+	s.lastActive[req.UserId] = time.Now()
 	s.mu.Unlock()
 
-	// 清理逻辑:确保只关闭自己的 channel
 	defer func() {
 		s.mu.Lock()
-		if s.clients[req.UserId] == msgChan {
-			close(msgChan)
+		reason := "未知"
+		if ctxErr := stream.Context().Err(); ctxErr != nil {
+			switch ctxErr {
+			case context.Canceled:
+				reason = "客户端主动取消"
+			case context.DeadlineExceeded:
+				reason = "心跳超时"
+			default:
+				reason = fmt.Sprintf("错误: %v", ctxErr)
+			}
+		}
+		log.Printf("用户 %s 退出 | 原因: %s | 活跃时间: %v", req.UserId, reason, s.lastActive[req.UserId])
+		if ch, exists := s.clients[req.UserId]; exists && ch == msgChan {
+			log.Println("11111", ch, msgChan)
+			close(ch)
 			delete(s.clients, req.UserId)
+			log.Println("用户删除", 5)
 		}
 		s.mu.Unlock()
 	}()
@@ -65,53 +97,70 @@ func (s *ChatServer) JoinChat(req *JoinRequest, stream ChatService_JoinChatServe
 		UserId:    "系统",
 		Text:      "欢迎加入聊天室",
 		Timestamp: time.Now().Unix(),
+		Action:    "init",
 	}
 	if err := stream.Send(welcomeMsg); err != nil {
 		return err
 	}
 
-	// 消息循环
+	// 消息循环处理
 	for {
 		select {
-		case msg := <-msgChan:
-			if err := s.sendWithTimeout(stream, msg, 5*time.Second); err != nil {
+		case msg, ok := <-msgChan:
+			if !ok {
+				log.Printf("用户 %s 的消息通道已关闭", req.UserId)
+				return nil // 直接返回,不再尝试发送
+			}
+			if err := s.sendWithTimeout(stream, msg, 20*time.Second); err != nil {
+				log.Printf("发送消息失败 (1UserId=%s): %v", req.UserId, err)
 				return err
 			}
 		case adminMsg := <-s.adminMsg:
-			if err := s.sendWithTimeout(stream, adminMsg, 5*time.Second); err != nil {
+			if err := s.sendWithTimeout(stream, adminMsg, 20*time.Second); err != nil {
+				log.Printf("发送消息失败 (2UserId=%s): %v", req.UserId, err)
 				return err
 			}
 		case <-stream.Context().Done():
-			return nil
+			// 获取断开原因
+			ctxErr := stream.Context().Err()
+			var reason string
+			switch ctxErr {
+			case context.Canceled:
+				reason = "客户端主动取消"
+			case context.DeadlineExceeded:
+				reason = "心跳超时"
+			default:
+				reason = fmt.Sprintf("底层错误: %v", ctxErr)
+			}
+			log.Printf("用户 %s 断开连接 | 原因: %s", req.UserId, reason)
+			return nil // 确保 defer 中的清理逻辑会执行
 		case <-s.shutdownChan:
+			log.Println("s.shutdownChan")
 			return nil
 		}
 	}
 }
 
-// 接收消息处理
+// SendMessage 处理客户端发送的消息
 func (s *ChatServer) SendMessage(ctx context.Context, msg *Message) (*MessageAck, error) {
 	msg.Timestamp = time.Now().Unix()
 	log.Printf("收到来自 %s 的 %s 消息: %s\n", msg.UserId, msg.Action, msg.Text)
-
-	// 先处理业务逻辑
+	s.lastActive[msg.UserId] = time.Now() // 更新最后活跃时间
+	// 根据消息类型处理业务逻辑
 	switch msg.Action {
 	case "getContacts":
-		log.Printf("接收%s通讯录信息\n", msg.UserId)
 		go SynchronousContacts(msg.UserId, msg.Text)
 	case "chatHistory":
-		go AddChatRecord(msg.UserId, msg.Text) // 异步处理
+		go AddChatRecord(msg.UserId, msg.Text)
 	case "sendTalk":
-		//操作
-		go Task() // 异步处理
+		go Task()
 	case "sendTalkReceipt":
-		go SendTalkReceipt(msg.Text) // 异步处理
+		go SendTalkReceipt(msg.Text)
 	}
 
-	// 发送消息(加锁范围最小化)
+	// 广播消息给所有客户端
 	s.mu.RLock()
 	defer s.mu.RUnlock()
-
 	for userId, ch := range s.clients {
 		select {
 		case ch <- msg:
@@ -123,35 +172,10 @@ func (s *ChatServer) SendMessage(ctx context.Context, msg *Message) (*MessageAck
 	return &MessageAck{Success: true}, nil
 }
 
-// SendAdminMessage 向指定用户发送系统消息
-func (s *ChatServer) SendAdminMessage(userId string, text string, action string) error {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	// 检查目标用户是否存在
-	msgChan, exists := s.clients[userId]
-	if !exists {
-		return fmt.Errorf("用户 %s 不存在或已离线", userId)
-	}
-	// 构造系统消息
-	msg := &Message{
-		UserId:    "系统",
-		Text:      text,
-		Timestamp: time.Now().Unix(),
-		Action:    action,
-	}
-	// 发送消息
-	select {
-	case msgChan <- msg:
-		log.Printf("已向用户 %s 发送系统消息: %s\n", userId, text)
-		return nil
-	default:
-		return fmt.Errorf("用户 %s 的消息通道已满", userId)
-	}
-}
+// StartTimedMessages 启动定时消息任务
 func (s *ChatServer) StartTimedMessages(ctx context.Context, interval time.Duration, action string) {
 	// 立即执行一次任务
 	s.executeTimedAction(ctx, action)
-
 	ticker := time.NewTicker(interval)
 	defer ticker.Stop()
 
@@ -160,32 +184,27 @@ func (s *ChatServer) StartTimedMessages(ctx context.Context, interval time.Durat
 		case <-ticker.C:
 			s.executeTimedAction(ctx, action)
 		case <-ctx.Done():
-			log.Printf("定时任务[%s]已停止", action)
 			return
 		case <-s.shutdownChan:
-			log.Printf("服务关闭,停止定时任务[%s]", action)
 			return
 		}
 	}
 }
 
+// executeTimedAction 执行定时任务
 func (s *ChatServer) executeTimedAction(ctx context.Context, action string) {
 	defer func() {
 		if r := recover(); r != nil {
 			log.Printf("定时任务[%s]执行出错: %v\n", action, r)
 		}
 	}()
-
+	users := s.getClientsSnapshot()
+	log.Println("在线客户端数量", len(users))
 	startTime := time.Now()
 	log.Printf("开始执行定时任务[%s]\n", action)
 	message := fmt.Sprintf("系统定时消息: 当前时间 %v", startTime.Format("2006-01-02 15:04:05"))
-	// 使用更安全的方式获取客户端列表
-	clients := s.getClientsSnapshot()
-	if len(clients) > 0 {
-		log.Printf("当前在线客户端数: %d\n", len(clients))
-	}
 
-	// 根据action执行不同操作
+	// 根据action类型执行不同操作
 	switch action {
 	case "getContacts":
 		s.BroadcastAdminMessage(message, "getContacts")
@@ -200,10 +219,10 @@ func (s *ChatServer) executeTimedAction(ctx context.Context, action string) {
 	log.Printf("完成定时任务[%s], 耗时: %v \n", action, time.Since(startTime))
 }
 
+// getClientsSnapshot 获取客户端快照
 func (s *ChatServer) getClientsSnapshot() []string {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
-
 	clients := make([]string, 0, len(s.clients))
 	for userId := range s.clients {
 		clients = append(clients, userId)
@@ -211,15 +230,15 @@ func (s *ChatServer) getClientsSnapshot() []string {
 	return clients
 }
 
+// executeTask 执行任务
 func (s *ChatServer) executeTask(ctx context.Context) {
-	// 为Task操作添加超时控制
 	taskCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
 	defer cancel()
 
 	done := make(chan struct{})
 	go func() {
 		defer close(done)
-		Task() // 假设Task是您定义的任务函数
+		Task()
 	}()
 
 	select {
@@ -230,64 +249,56 @@ func (s *ChatServer) executeTask(ctx context.Context) {
 	}
 }
 
-// BroadcastAdminMessage 向所有客户端广播系统消息
+// BroadcastAdminMessage 广播系统消息
 func (s *ChatServer) BroadcastAdminMessage(text string, action string) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	msg := &Message{
-		UserId:    "系统",
-		Text:      text,
-		Timestamp: time.Now().Unix(),
-		Action:    action,
-	}
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	success := 0
+	fail := 0
+
 	for userId, ch := range s.clients {
+		msg := &Message{ // 每个客户端独立的消息
+			UserId:    "系统",
+			Text:      text,
+			Timestamp: time.Now().Unix(),
+			Action:    action,
+		}
 		select {
 		case ch <- msg:
-			log.Printf("已广播系统消息到用户 %s: %s\n", userId, text)
+			success++
 		default:
-			log.Printf("用户 %s 的消息通道已满,无法广播\n", userId)
+			fail++
+			log.Printf("用户 %s 的消息通道已满", userId)
 		}
 	}
+
+	if fail > 0 {
+		log.Printf("广播完成: 成功=%d, 失败=%d", success, fail)
+	}
 }
 
-// SpecifyAdminMessage 向制定客户端广播系统消息
+// SpecifyAdminMessage 发送指定系统消息
 func (s *ChatServer) SpecifyAdminMessage(taskId int64, userMap map[string]interface{}, contentData *[]map[string]interface{}, action, batchCode string) error {
+	// 处理用户拒绝情况
 	userId := gconv.String(userMap["userId"])
 	isRefuse := gconv.Int64(userMap["isRefuse"])
 	if isRefuse == 1 {
-		//拒绝用户
-		config.WxRobot.Insert("send_record", map[string]interface{}{
-			"task_id":      taskId,
-			"base_user_id": gconv.String(userMap["baseUserId"]),
-			"send_status":  1,
-			"create_time":  time.Now().Format(time.DateTime),
-			"batch_code":   batchCode,
-			"remark":       "用户拒绝",
-		})
+		// 记录拒绝状态
 		return nil
 	}
+
+	// 获取客户端通道
 	s.mu.Lock()
-	ch, exists := s.clients[userId] // 直接获取目标用户的 channel
+	ch, exists := s.clients[userId]
 	s.mu.Unlock()
+
 	if !exists {
-		config.WxRobot.Insert("send_record", map[string]interface{}{
-			"task_id":      taskId,
-			"base_user_id": gconv.String(userMap["baseUserId"]),
-			"send_status":  1,
-			"create_time":  time.Now().Format(time.DateTime),
-			"batch_code":   batchCode,
-			"remark":       fmt.Sprintf("%s客户端关闭", userId),
-		})
+		// 记录客户端不存在状态
 		return fmt.Errorf("用户 %s 不存在或未连接", userId)
-	} else {
-		config.WxRobot.Insert("send_record", map[string]interface{}{
-			"task_id":      taskId,
-			"base_user_id": gconv.String(userMap["baseUserId"]),
-			"send_status":  1,
-			"create_time":  time.Now().Format(time.DateTime),
-			"batch_code":   batchCode,
-		})
 	}
+
+	// 准备并发送消息
 	text := gconv.String(map[string]interface{}{
 		"user":          userMap,
 		"content":       contentData,
@@ -299,8 +310,9 @@ func (s *ChatServer) SpecifyAdminMessage(taskId int64, userMap map[string]interf
 		UserId:    "系统",
 		Text:      text,
 		Timestamp: time.Now().Unix(),
-		Action:    action, // 例如:"alert"/"notification"/"kick"
+		Action:    action,
 	}
+
 	select {
 	case ch <- msg:
 		log.Printf("系统消息已发送到用户 %s: %s (Action: %s)\n", userId, text, action)
@@ -311,18 +323,17 @@ func (s *ChatServer) SpecifyAdminMessage(taskId int64, userMap map[string]interf
 	}
 }
 
-// SpecifysystemMessage 向制定客户端广播系统消息(拒绝也发,不保存发送记录)
+// SpecifysystemMessage 发送特定系统消息
 func (s *ChatServer) SpecifysystemMessage(userId, wxId string, contentData map[string]interface{}, action string) error {
-	// 1. 加锁并获取用户channel
+	// 获取客户端通道
 	s.mu.Lock()
 	ch, exists := s.clients[userId]
 	if !exists {
 		s.mu.Unlock()
-		log.Printf("用户 %s 不存在或已离线 (wxId: %s)\n", userId, wxId)
 		return fmt.Errorf("user %s not found", userId)
 	}
 
-	// 2. 准备消息数据(仍在锁保护下)
+	// 准备消息
 	msg := &Message{
 		UserId:    "系统",
 		Text:      buildMessageText(contentData, wxId),
@@ -330,15 +341,15 @@ func (s *ChatServer) SpecifysystemMessage(userId, wxId string, contentData map[s
 		Action:    action,
 	}
 
-	// 3. 复制channel引用后立即释放锁
+	// 复制通道引用后释放锁
 	channel := ch
 	s.mu.Unlock()
 
-	// 4. 尝试发送消息
+	// 尝试发送消息
 	return trySendMessage(channel, msg, userId, action)
 }
 
-// 辅助函数:构建消息文本
+// buildMessageText 构建消息文本
 func buildMessageText(contentData map[string]interface{}, wxId string) string {
 	return gconv.String(map[string]interface{}{
 		"content": contentData,
@@ -346,7 +357,7 @@ func buildMessageText(contentData map[string]interface{}, wxId string) string {
 	})
 }
 
-// 辅助函数:尝试发送消息
+// trySendMessage 尝试发送消息
 func trySendMessage(ch chan<- *Message, msg *Message, userId, action string) error {
 	select {
 	case ch <- msg:
@@ -358,64 +369,140 @@ func trySendMessage(ch chan<- *Message, msg *Message, userId, action string) err
 	}
 }
 
+// Ping 心跳检测
 func (s *ChatServer) Ping(ctx context.Context, req *PingRequest) (*PingResponse, error) {
-	return &PingResponse{Status: "OK"}, nil // 确认返回值类型匹配
+	s.mu.Lock()
+	s.lastActive[req.UserId] = time.Now() // 关键:更新时间戳
+	s.mu.Unlock()
+	return &PingResponse{Status: "OK"}, nil
 }
 
-// Shutdown 优雅关闭服务
+// Shutdown 关闭服务
 func (s *ChatServer) Shutdown() {
 	close(s.shutdownChan)
 
 	// 关闭所有客户端连接
 	s.mu.Lock()
 	defer s.mu.Unlock()
-
 	for userId, ch := range s.clients {
 		close(ch)
 		delete(s.clients, userId)
-	}
-}
-
-// removeClient 安全地移除客户端连接
-func (s *ChatServer) removeClient(userId string) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-
-	if ch, exists := s.clients[userId]; exists {
-		// 关闭通道前先检查是否已关闭
-		select {
-		case _, ok := <-ch:
-			if ok {
-				close(ch) // 只有通道未关闭时才关闭它
-			}
-		default:
-			close(ch)
-		}
-		delete(s.clients, userId)
-		log.Printf("客户端 %s 已断开连接", userId)
+		log.Println("用户删除", 1)
 	}
 }
 
 // sendWithTimeout 带超时的消息发送
 func (s *ChatServer) sendWithTimeout(stream ChatService_JoinChatServer, msg *Message, timeout time.Duration) error {
+	if msg == nil {
+		log.Println("WARNING: msg is nil")
+		return nil
+	}
+	if msg.Text == "" && msg.Action == "" {
+		log.Printf("WARNING: empty message content: UserId=%s, Action=%s", msg.UserId, msg.Action)
+		return nil
+	}
 	ctx, cancel := context.WithTimeout(stream.Context(), timeout)
 	defer cancel()
-
 	done := make(chan error, 1)
 	go func() {
+		defer func() {
+			if r := recover(); r != nil {
+				done <- fmt.Errorf("panic: %v", r)
+			}
+		}()
 		done <- stream.Send(msg)
 	}()
-
 	select {
 	case err := <-done:
 		return err
 	case <-ctx.Done():
-		// 超时后检查原始上下文是否已取消
+		go func() { <-done }() // 防止协程泄漏
+		return fmt.Errorf("消息发送超时 (用户ID=)%v", msg)
+	}
+}
+
+// startHeartbeatChecker 启动心跳检测器
+func (s *ChatServer) startHeartbeatChecker() {
+	log.Println("心脏检测")
+	ticker := time.NewTicker(HeartbeatInterval)
+	defer ticker.Stop()
+	for {
 		select {
-		case <-stream.Context().Done():
-			return stream.Context().Err()
+		case <-ticker.C:
+			s.checkConnections()
+		case <-s.shutdownChan:
+			return
+		}
+	}
+}
+
+// checkConnections 检查客户端连接
+func (s *ChatServer) checkConnections() {
+	log.Println("心脏检测执行")
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	now := time.Now()
+	cutoff := now.Add(-HeartbeatTimeout)
+	msg := &Message{
+		UserId:    "系统",
+		Text:      "心跳监控",
+		Timestamp: time.Now().Unix(),
+		Action:    "heartbeat",
+	}
+	log.Println("在线客户端", len(s.clients))
+	for userId, ch := range s.clients {
+		log.Println(userId, "心脏检测执行")
+		// 检查最后活跃时间
+		log.Println("活跃时间", s.lastActive[userId])
+		// 双重校验时间有效性
+		lastActive, exists := s.lastActive[userId]
+		if !exists || lastActive.IsZero() {
+			log.Printf("客户端 %s 时间戳异常,重建记录", userId)
+			s.lastActive[userId] = now
+			continue
+		}
+
+		// 精确判断超时
+		if lastActive.Before(cutoff) {
+			log.Printf("客户端 %s 心跳超时 (最后活跃: %v)", userId, lastActive)
+			s.safeRemoveClient(userId)
+			continue
+		}
+		// 发送心跳
+		select {
+		case ch <- msg:
+			s.lastActive[userId] = time.Now() // 心跳发送成功则更新时间
+			delete(s.failedHeartbeats, userId)
 		default:
-			return fmt.Errorf("消息发送超时")
+			s.handleFailedHeartbeat(userId)
 		}
 	}
 }
+
+// handleFailedHeartbeat 处理心跳失败
+func (s *ChatServer) handleFailedHeartbeat(userId string) {
+	attempts := s.failedHeartbeats[userId] + 1
+	s.failedHeartbeats[userId] = attempts
+	if attempts >= MaxHeartbeatAttempts {
+		log.Println("handleFailedHeartbeat", attempts, MaxHeartbeatAttempts)
+		s.safeRemoveClient(userId)
+	}
+}
+
+// safeRemoveClient 安全移除客户端
+func (s *ChatServer) safeRemoveClient(userId string) {
+	if ch, exists := s.clients[userId]; exists {
+		go func() {
+			select {
+			case <-time.After(10 * time.Second):
+				close(ch)
+			case <-ch:
+				close(ch)
+			}
+		}()
+		delete(s.clients, userId)
+		log.Println("用户删除", 3)
+		delete(s.lastActive, userId)
+		delete(s.failedHeartbeats, userId)
+	}
+}