目录
- 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"相关文章
- 微服务即时通讯系统的实现(客户端)----(2)
- 通过Dapr实现一个简单的基于.net的微服务电商系统(九)——一步一步教你如何撸Dapr之OAuth2授权
- RabbitMQ 优点和缺点- 消息可靠性:RabbitMQ 提供了持久化功能和消息确认机制,确保消息在各种情况下都能可靠地存储和处理。
灵活的路由:通过多种交换机类型和绑定规则,RabbitMQ 能够灵活地路由消息到指定的队列。
支持多种消息协议:实现了 AMQP 等(MQTT、STOMP)标准化、开放的消息队列协议,使其能够与多种语言编写的应用程序进行通信。
插件化扩展:RabbitMQ 提供了丰富的插件系统,可以通过插件扩展功能,如死信队列、压缩、追踪等。
高可用性:支持集群模式和镜像队列,确保服务的可用性
易用性和可管理性:提供了丰富的 API 和管理工具,以及多种客户端库和框架支持,易于集成和使用。
多语言支持:RabbitMQ 支持多种编程语言的客户端,包括 Java、Python、Ruby、C#、Node.js 等,方便开发人员集成到各种应用中。
高性能:在处理大量并发消息时表现出色。
广泛的社区支持:拥有庞大的开发者社区和丰富的文档资源。
劣势:
性能和吞吐量较低:相比于 Apache Kafka 等面向大数据流处理的消息队列系统,RabbitMQ 的吞吐量较低,不适合处理海量的实时数据流。RabbitMQ 的设计更注重消息的可靠性和灵活性,而非极高的吞吐性能。
- 项目总和
多级字典表单的Python实现关于购物车程序的Python实现python实现简单的登陆认证(含简单的文件操作)Python3 文件的重命名Python:员工信息增删改查文件修改的两种方式Fibonacci数列的两种实现方式模拟实现ATM与购物商城一个简单的多用户交互系统的实现模拟远程SSH执行命令的编解码说明optparse模块解析命令行参数的说明及优化利用生成器制作一个简单的客户端接收文件的进度条简单的单进程FTP服务器的实现博客系统之完整的项目文
- 编写一个基于Linux操作系统+C语言的聊天应用程序,使用QT实现两个主机端(服务器和客户端)进行图形化界面通信。
- 实现服务器端与客户端的高频实时通信 SignalR(2)
- windows server 2008 R2 部署NFS,实现多台服务器间、客户端间的共享目录。
- delphi通过修改socket做三层,能实现一个服务器2万个客户端的连接?
- 服务器版本2008 R2,Win8系统的客户端无法连接,显示“数据源打开失败”