微服务即时通讯系统的实现(客户端)----(2)

时间:2024-11-18 07:38:32

目录

  • 1. 将protobuf引入项目当中
  • 2. 前后端交互接口定义
    • 2.1 核心PB类
    • 2.2 HTTP接口定义
    • 2.3 websocket接口定义
  • 3. 核心数据结构和PB之间的转换
  • 4. 设计数据中心DataCenter类
  • 5. 网络通信
    • 5.1 定义NetClient类
    • 5.2 引入HTTP
    • 5.3 引入websocket
  • 6. 小结
  • 7. 搭建测试服务器
    • 7.1 创建项目
    • 7.2 服务器引入http
    • 7.3 服务器引入websocket
    • 7.4 服务器引protobuf
    • 7.5 编写工具函数和构造数据函数
    • 7.6 验证网络连通性
    • 7.7 网络通信注意事项
  • 8. 主界面逻辑的实现
    • 8.1 获取个人信息
    • 8.2 获取好友列表
    • 8.3 获取会话列表
    • 8.4 获取好友申请列表
    • 8.5 获取指定会话的近期消息
    • 8.6 点击某个好友项
  • 9. 小结

1. 将protobuf引入项目当中

(1)创建 proto 目录, 并把服务器提供的 proto 拷贝过来:

(2)proto文件链接:https://gitee.com/liu-yechi/new_code/tree/master/chat_system/client/ChatClient/proto

2. 前后端交互接口定义

2.1 核心PB类

(1)用户信息:

//用户信息结构
message UserInfo {
    string user_id = 1;//用户ID
    string nickname = 2;//昵称
    string description = 3;//个人签名/描述
    string phone = 4; //绑定手机号
    bytes  avatar = 5;//头像照片,文件内容使用二进制
}

(2)会话信息:

//聊天会话信息
message ChatSessionInfo {
    optional string single_chat_friend_id = 1;//群聊会话不需要设置,单聊会话设置为对方ID
    string chat_session_id = 2; //会话ID
    string chat_session_name = 3;//会话名称git 
    optional MessageInfo prev_message = 4;//会话上一条消息,新建的会话没有最新消息
    optional bytes avatar = 5;//会话头像 --群聊会话不需要,直接由前端固定渲染,单聊就是对方的头像
}

(3)消息信息:

//消息类型
enum MessageType {
    STRING = 0;
    IMAGE = 1;
    FILE = 2;
    SPEECH = 3;
}
message StringMessageInfo {
    string content = 1;//文字聊天内容
}
message ImageMessageInfo {
    optional string file_id = 1;//图片文件id,客户端发送的时候不用设置,由transmit服务器进行设置后交给storage的时候设置
    optional bytes image_content = 2;//图片数据,在ES中存储消息的时候只要id不要文件数据, 服务端转发的时候需要原样转发
}
message FileMessageInfo {
    optional string file_id = 1;//文件id,客户端发送的时候不用设置
    int64 file_size = 2;//文件大小
    string file_name = 3;//文件名称
    optional bytes file_contents = 4;//文件数据,在ES中存储消息的时候只要id和元信息,不要文件数据, 服务端转发的时候也不需要填充
}
message SpeechMessageInfo {
    optional string file_id = 1;//语音文件id,客户端发送的时候不用设置
    optional bytes file_contents = 2;//文件数据,在ES中存储消息的时候只要id不要文件数据, 服务端转发的时候也不需要填充
}
message MessageContent {
    MessageType message_type = 1; //消息类型
    oneof msg_content {
        StringMessageInfo string_message = 2;//文字消息
        FileMessageInfo file_message = 3;//文件消息
        SpeechMessageInfo speech_message = 4;//语音消息
        ImageMessageInfo image_message = 5;//图片消息
    };
}
//消息结构
message MessageInfo {
    string message_id = 1;//消息ID
    string chat_session_id = 2;//消息所属聊天会话ID
    int64 timestamp = 3;//消息产生时间
    UserInfo sender = 4;//消息发送者信息
    MessageContent message = 5;
}

message Message {
    string request_id = 1;
    MessageInfo message = 2;
}

message FileDownloadData {
    string file_id = 1;
    bytes file_content = 2;
}

message FileUploadData {
    string file_name = 1;
    int64 file_size = 2;
    bytes file_content = 3;
}

2.2 HTTP接口定义

(1)请求响应基本格式:

//通信接口统一采用POST请求实现,正文采用protobuf协议进行组织
/*  
    HTTP HEADER:
    POST /service/xxxxx
    Content-Type: application/x-protobuf
    Content-Length: 123

    xxxxxx

    -------------------------------------------------------

    HTTP/1.1 200 OK 
    Content-Type: application/x-protobuf
    Content-Length: 123

    xxxxxxxxxx
*/

(2)约定路径:每个接口都提供对应的请求响应的 proto 对象:

//在客户端与网关服务器的通信中,使用HTTP协议进行通信
//  通信时采用POST请求作为请求方法
//  通信时,正文采用protobuf作为正文协议格式,具体内容字段以前边各个文件中定义的字段格式为准
/*  以下是HTTP请求的功能与接口路径对应关系:
    SERVICE HTTP PATH:
    {
        获取随机验证码                  /service/user/get_random_verify_code
        获取短信验证码                  /service/user/get_phone_verify_code
        用户名密码注册                  /service/user/username_register
        用户名密码登录                  /service/user/username_login
        手机号码注册                    /service/user/phone_register
        手机号码登录                    /service/user/phone_login
        获取个人信息                    /service/user/get_user_info
        修改头像                        /service/user/set_avatar
        修改昵称                        /service/user/set_nickname
        修改签名                        /service/user/set_description
        修改绑定手机                    /service/user/set_phone

        获取好友列表                    /service/friend/get_friend_list
        获取好友信息                    /service/friend/get_friend_info
        发送好友申请                    /service/friend/add_friend_apply
        好友申请处理                    /service/friend/add_friend_process
        删除好友                        /service/friend/remove_friend
        搜索用户                        /service/friend/search_friend
        获取指定用户的消息会话列表       /service/friend/get_chat_session_list
        创建消息会话                    /service/friend/create_chat_session
        获取消息会话成员列表             /service/friend/get_chat_session_member
        获取待处理好友申请事件列表       /service/friend/get_pending_friend_events

        获取历史消息/离线消息列表        /service/message_storage/get_history
        获取最近N条消息列表             /service/message_storage/get_recent
        搜索历史消息                    /service/message_storage/search_history
        
        发送消息                        /service/message_transmit/new_message

        获取单个文件数据                /service/file/get_single_file
        获取多个文件数据                /service/file/get_multi_file
        发送单个文件                    /service/file/put_single_file
        发送多个文件                    /service/file/put_multi_file

        语音转文字                     /service/speech/recognition
    }
    
*/

2.3 websocket接口定义

(1)身份认证:

/*
    消息推送使用websocket长连接进行
    websocket长连接转换请求:ws://host:ip/ws
    长连建立以后,需要客户端给服务器发送一个身份验证信息
*/
message ClientAuthenticationReq {
    string request_id = 1;
    string session_id = 2;
}
message ClientAuthenticationRsp {
    string request_id = 1;
    bool success = 2;
    string errmsg = 3;
}

(2)消息推送。当前存在五种消息推送:

  • 申请好友通知。
  • 好友申请处理通知 (同意/拒绝)。
  • 创建消息会话通知。
  • 收到消息通知。
  • 删除好友通知。
enum NotifyType {
    FRIEND_ADD_APPLY_NOTIFY = 0;
    FRIEND_ADD_PROCESS_NOTIFY = 1;
    CHAT_SESSION_CREATE_NOTIFY = 2;
    CHAT_MESSAGE_NOTIFY = 3;
    FRIEND_REMOVE_NOTIFY = 4;
}

message NotifyFriendAddApply {
    UserInfo user_info = 1;  //申请人信息
}
message NotifyFriendAddProcess {
    bool agree = 1;
    UserInfo user_info = 2;  //处理人信息
}
message NotifyFriendRemove {
    string user_id = 1; //删除自己的用户ID
}
message NotifyNewChatSession {
    ChatSessionInfo chat_session_info = 1; //新建会话信息
}
message NotifyNewMessage {
    MessageInfo message_info = 1; //新消息
}


message NotifyMessage {
    optional string notify_event_id = 1;//通知事件操作id(有则填无则忽略)
    NotifyType notify_type = 2;//通知事件类型
    oneof notify_remarks {      //事件备注信息
        NotifyFriendAddApply friend_add_apply = 3;
        NotifyFriendAddProcess friend_process_result = 4;
        NotifyFriendRemove friend_remove = 7;
        NotifyNewChatSession new_chat_session_info = 5;//会话信息
        NotifyNewMessage new_message_info = 6;//消息信息
    }
}

3. 核心数据结构和PB之间的转换

(1)以下是protobuf数据和QString的数据转化函数:(类里面的成员变量没有写出来):

//
/// 用户信息
//
class UserInfo
{
public:
    // 该类的成员变量没有写出来。。。

    // 从 protobuffer 的 UserInfo 对象, 转成当前代码的 UserInfo 对象
    void load(const bite_im::UserInfo& userInfo)
    {
        this->userId = userInfo.userId();
        this->nickname = userInfo.nickname();
        this->description = userInfo.description();
        this->phone = userInfo.phone();
        if(userInfo.avatar().isEmpty())
        {
            // 使用默认头像即可
            this->avatar = QIcon(":/resource/image/defaultAvatar.png");
        }
        else
        {
            this->avatar = makeIcon(userInfo.avatar());
        }
    }
};

//
/// 消息信息
//
enum MessageType
{
    TEXT_TYPE,		// 文本消息
    IMAGE_TYPE, 	// 图片消息
    FILE_TYPE, 		// 文件消息
    SPEECH_TYPE 	// 语音消息
};

class Message
{
public:
    // 该类的成员变量没有写出来。。。

    // 此处 extraInfo 目前只是在消息类型为文件消息时, 作为 "文件名" 补充.
    static Message makeMessage(MessageType messageType, const QString& chatSessionId,
                               const UserInfo& sender, const QByteArray& content,
                               const QString& extraInfo)
    {
        if(messageType == TEXT_TYPE)
        {
            return makeTextMessage(chatSessionId, sender, content);
        }
        else if(messageType == IMAGE_TYPE)
        {
            return makeImageMessage(chatSessionId, sender, content);
        }
        else if(messageType == FILE_TYPE)
        {
            return makeFileMessage(chatSessionId, sender, content, extraInfo);
        }
        else if(messageType == SPEECH_TYPE)
        {
            return makeSpeechMessage(chatSessionId, sender, content);
        }
        else
        {
            // 触发了未知的消息类型
            return Message();
        }
    }

    void load(const bite_im::MessageInfo& messageInfo)
    {
        this->messageId = messageInfo.messageId();
        this->chatSessionId = messageInfo.chatSessionId();
        this->time = formatTime(messageInfo.timestamp());
        this->sender.load(messageInfo.sender());

        // 设置消息类型
        auto type = messageInfo.message().messageType();
        if(type == bite_im::MessageTypeGadget::MessageType::STRING)
        {
            this->messageType = TEXT_TYPE;
            this->content = messageInfo.message().stringMessage().content().toUtf8();
        }
        else if(type == bite_im::MessageTypeGadget::MessageType::IMAGE)
        {
            this->messageType = IMAGE_TYPE;
            if(messageInfo.message().imageMessage().hasImageContent())
            {
                this->content = messageInfo.message().imageMessage().imageContent();
            }

            if(messageInfo.message().imageMessage().hasFileId())
            {
                this->fileId = messageInfo.message().imageMessage().fileId();
            }
        }
        else if(type == bite_im::MessageTypeGadget::MessageType::FILE)
        {
            this->messageType = FILE_TYPE;
            if(messageInfo.message().fileMessage().hasFileContents())
            {
                this->content = messageInfo.message().fileMessage().fileContents();
            }

            if(messageInfo.message().fileMessage().hasFileId())
            {
                this->fileId = messageInfo.message().fileMessage().fileId();
            }

            this->fileName = messageInfo.message().fileMessage().fileName();
        }
        else if(type == bite_im::MessageTypeGadget::MessageType::SPEECH)
        {
            this->messageType = SPEECH_TYPE;
            if(messageInfo.message().speechMessage().hasFileContents())
            {
                this->content = messageInfo.message().speechMessage().fileContents();
            }

            if(messageInfo.message().speechMessage().hasFileId())
            {
                this->fileId = messageInfo.message().speechMessage().fileId();
            }
        }
        else
        {
            // 错误的类型, 啥都不做了, 只是打印一个日志
            LOG() << "非法的消息类型! type=" << type;
        }
    }


private:
    // 通过这个方法生成唯一的 messageId
    static QString makeId()
    {
        return "M" + QUuid::createUuid().toString().sliced(25, 12);
    }

    static Message makeTextMessage(const QString& chatSessionId,
                                   const UserInfo& sender, const QByteArray& content)
    {
        Message message;
        message.messageId = makeId();
        message.chatSessionId = chatSessionId;
        message.messageType = TEXT_TYPE;
        message.content = content;
        message.sender = sender;
        message.time = formatTime(getTime()); // 生成一个格式化时间

        // 对于文本消息来说, 这俩属性不使用, 设为 ""
        message.fileId = "";
        message.fileName = "";

        return message;
    }

    static Message makeImageMessage(const QString& chatSessionId,
                                   const UserInfo& sender, const QByteArray& content)
    {
        Message message;
        message.messageId = makeId();
        message.chatSessionId = chatSessionId;
        message.messageType = IMAGE_TYPE;
        message.content = content;
        message.sender = sender;
        message.time = formatTime(getTime()); // 生成一个格式化时间

        // fileId 后续使用的时候再进一步设置
        message.fileId = "";
        // fileName 不使用, 直接设为 ""
        message.fileName = "";
        return message;
    }

    static Message makeFileMessage(const QString& chatSessionId, const UserInfo& sender,
                                   const QByteArray& content, const QString& fileName)
    {
        Message message;
        message.messageId = makeId();
        message.chatSessionId = chatSessionId;
        message.messageType = FILE_TYPE;
        message.content = content;
        message.sender = sender;
        message.time = formatTime(getTime()); // 生成一个格式化时间

        // fileId 后续使用的时候进一步设置
        message.fileId = "";
        message.fileName = fileName;

        return message;
    }

    static Message makeSpeechMessage(const QString& chatSessionId,
                                   const UserInfo& sender, const QByteArray& content)
    {
        Message message;
        message.messageId = makeId();
        message.chatSessionId = chatSessionId;
        message.messageType = SPEECH_TYPE;
        message.content = content;
        message.sender = sender;
        message.time = formatTime(getTime()); // 生成一个格式化时间

        // fileId 后续使用的时候进一步设置
        message.fileId = "";
        // fileName 不使用, 直接设为 ""
        message.fileName = "";

        return message;
    }
};

//
/// 会话信息
//
class ChatSessionInfo
{
public:
   	// 该类的成员变量没有写出来。。。

    void load(const bite_im::ChatSessionInfo& chatSessionInfo)
    {
        this->chatSessionId = chatSessionInfo.chatSessionId();
        this->chatSessionName = chatSessionInfo.chatSessionName();
        if(chatSessionInfo.hasSingleChatFriendId())
        {
            this->userId = chatSessionInfo.singleChatFriendId();
        }

        if(chatSessionInfo.hasPrevMessage())
        {
            lastMessage.load(chatSessionInfo.prevMessage());
        }

        if(chatSessionInfo.hasAvatar() && !chatSessionInfo.avatar().isEmpty())
        {
            // 已经有头像了, 直接设置这个头像
            this->avatar = makeIcon(chatSessionInfo.avatar());
        }
        else
        {
            // 如果没有头像, 则根据当前会话是单聊还是群聊, 使用不同的默认头像.
            if(userId != "")
            {
                // 单聊
                this->avatar = QIcon(":/resource/image/defaultAvatar.png");
            }
            else
            {
                // 群聊
                this->avatar = QIcon(":/resource/image/groupAvatar.png"

相关文章