微服务即时通讯系统(5)用户管理子服务,网关子服务
- 用户管理子服务(user文件)
用户管理子服务也是这个项目中的一个业务最多的子服务,接口多,但是主要涉及的数据表只有user表,Redis的键值对和ES的一个搜索引擎,主要功能是对用户的个人信息进行修改管理,对用户的登录进行管理,用户注册进行管理
用户管理子服务的所有接口
service UserService {
rpc UserRegister(UserRegisterReq) returns (UserRegisterRsp);
rpc UserLogin(UserLoginReq) returns (UserLoginRsp);
rpc GetPhoneVerifyCode(PhoneVerifyCodeReq) returns
(PhoneVerifyCodeRsp);
rpc PhoneRegister(PhoneRegisterReq) returns
(PhoneRegisterRsp);
rpc PhoneLogin(PhoneLoginReq) returns (PhoneLoginRsp);
rpc GetUserInfo(GetUserInfoReq) returns (GetUserInfoRsp);
rpc GetMultiUserInfo(GetMultiUserInfoReq) returns
(GetMultiUserInfoRsp);
rpc SetUserAvatar(SetUserAvatarReq) returns
(SetUserAvatarRsp);
rpc SetUserNickname(SetUserNicknameReq) returns
(SetUserNicknameRsp);
rpc SetUserDescription(SetUserDescriptionReq) returns
(SetUserDescriptionRsp);
rpc SetUserPhoneNumber(SetUserPhoneNumberReq) returns
(SetUserPhoneNumberRsp);
}
- 用户昵称加密码的方式进行注册接口:先从request里面接收用户的nickname和密码,然后对nickname和密码进行检查,看看是不是符合长度要求,如果符合,为其生成用户ID后添加到user数据库和ES的user搜索引擎里面,设置response为成功,如果不符合,则不添加,并且设置response为失败,添加失败信息,结束
virtual void UserRegister(::google::protobuf::RpcController* controller,
const ::zhou::UserRegisterReq* request,
::zhou::UserRegisterRsp* response,
::google::protobuf::Closure* done) {
LOG_DEBUG("收到用户注册请求!");
brpc::ClosureGuard rpc_guard(done);
//定义一个错误处理函数,当出错的时候被调用
auto err_response = [this, response](const std::string &rid,
const std::string &errmsg) -> void {
response->set_request_id(rid);
response->set_success(false);
response->set_errmsg(errmsg);
return;
};
//1. 从请求中取出昵称和密码
std::string nickname = request->nickname();
std::string password = request->password();
//2. 检查昵称是否合法(只能包含字母,数字,连字符-,下划线_,长度限制 3~15 之间)
bool ret = nickname_check(nickname);
if (ret == false) {
LOG_ERROR("{} - 用户名长度不合法!", request->request_id());
return err_response(request->request_id(), "用户名长度不合法!");
}
//3. 检查密码是否合法(只能包含字母,数字,长度限制 6~15 之间)
ret = password_check(password);
if (ret == false) {
LOG_ERROR("{} - 密码格式不合法!", request->request_id());
return err_response(request->request_id(), "密码格式不合法!");
}
//4. 根据昵称在数据库进行判断是否昵称已存在
auto user = _mysql_user->select_by_nickname(nickname);
if (user) {
LOG_ERROR("{} - 用户名被占用- {}!", request->request_id(), nickname);
return err_response(request->request_id(), "用户名被占用!");
}
//5. 向数据库新增数据
std::string uid = uuid();
user = std::make_shared<User>(uid, nickname, password);
ret = _mysql_user->insert(user);
if (ret == false) {
LOG_ERROR("{} - Mysql数据库新增数据失败!", request->request_id());
return err_response(request->request_id(), "Mysql数据库新增数据失败!");
}
//6. 向 ES 服务器中新增用户信息
ret = _es_user->appendData(uid, "", nickname, "", "");
if (ret == false) {
LOG_ERROR("{} - ES搜索引擎新增数据失败!", request->request_id());
return err_response(request->request_id(), "ES搜索引擎新增数据失败!");
}
//7. 组织响应,进行成功与否的响应即可。
response->set_request_id(request->request_id());
response->set_success(true);
}
- 用户昵称加密码登录接口设计:先从request里面获取nickname和密码,然后进行判断是否合法,然后从Redis的状态键值对里面判断用户是否已经登录,然后从数据库中获取nickname对应的密码,如果不存在则设置response为失败,检查密码是否和传入的密码相同,如果是则登录成功,为用户创建一个登录会话ID,对Redis的状态键值对和会话键值对进行添加,然后对response设置对应的回话ID,然后返回
virtual void UserLogin(::google::protobuf::RpcController* controller,
const ::zhou::UserLoginReq* request,
::zhou::UserLoginRsp* response,
::google::protobuf::Closure* done){
LOG_DEBUG("收到用户登录请求!");
brpc::ClosureGuard rpc_guard(done);
auto err_response = [this, response](const std::string &rid,
const std::string &errmsg) -> void {
response->set_request_id(rid);
response->set_success(false);
response->set_errmsg(errmsg);
return;
};
//1. 从请求中取出昵称和密码
std::string nickname = request->nickname();
std::string password = request->password();
//2. 通过昵称获取用户信息,进行密码是否一致的判断
auto user = _mysql_user->select_by_nickname(nickname);
if (!user || password != user->password()) {
LOG_ERROR("{} - 用户名或密码错误 - {}-{}!", request->request_id(), nickname, password);
return err_response(request->request_id(), "用户名或密码错误!");
}
//3. 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。
bool ret = _redis_status->exists(user->user_id());
if (ret == true) {
LOG_ERROR("{} - 用户已在其他地方登录 - {}!", request->request_id(), nickname);
return err_response(request->request_id(), "用户已在其他地方登录!");
}
//4. 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息
std::string ssid = uuid();
_redis_session->append(ssid, user->user_id());
//5. 添加用户登录信息
_redis_status->append(user->user_id());
//5. 组织响应,返回生成的会话 ID
response->set_request_id(request->request_id());
response->set_login_session_id(ssid);
response->set_success(true);
}
- 手机号验证码发送接口:从request里面获取电话号码,判断电话号码是否合法,然后生成四位随机验证码和事件ID,将事件ID和验证码一起放入Redis的验证码键值对缓存里面,然后调用验证码发送的SDK服务向对应的手机号发送验证码,对response进行设置此次发送事件的ID,结束
virtual void GetPhoneVerifyCode(::google::protobuf::RpcController* controller,
const ::zhou::PhoneVerifyCodeReq* request,
::zhou::PhoneVerifyCodeRsp* response,
::google::protobuf::Closure* done){
LOG_DEBUG("收到短信验证码获取请求!");
brpc::ClosureGuard rpc_guard(done);
auto err_response = [this, response](const std::string &rid,
const std::string &errmsg) -> void {
response->set_request_id(rid);
response->set_success(false);
response->set_errmsg(errmsg);
return;
};
// 1. 从请求中取出手机号码
std::string phone = request->phone_number();
// 2. 验证手机号码格式是否正确(必须以 1 开始,第二位 3~9 之间,后边 9 个数字字符)
bool ret = phone_check(phone);
if (ret == false) {
LOG_ERROR("{} - 手机号码格式错误 - {}!", request->request_id(), phone);
return err_response(request->request_id(), "手机号码格式错误!");
}
// 3. 生成 4 位随机验证码
std::string code_id = uuid();
std::string code = vcode();
// 4. 基于短信平台 SDK 发送验证码
ret = _dms_client->send(phone, code);
if (ret == false) {
LOG_ERROR("{} - 短信验证码发送失败 - {}!", request->request_id(), phone);
return err_response(request->request_id(), "短信验证码发送失败!");
}
// 5. 构造验证码 ID,添加到 redis 验证码映射键值索引中
_redis_codes->append(code_id, code);
// 6. 组织响应,返回生成的验证码 ID
response->set_request_id(request->request_id());
response->set_success(true);
response->set_verify_code_id(code_id);
LOG_DEBUG("获取短信验证码处理完成!");
}
- 手机号注册接口实现:先从request里面获取验证码事件ID和验证码,手机号,用户名,然后从Redis里面的验证码键值对里面取出对应的验证码,判断是否一致,如果一致则进行注册,就是向use表和ES所对应的搜索引擎里面添加对应数据(用户名和手机号码),然后设置response成功,结束,(5)并且手机号登录接口就是在nickname加密码登录的逻辑下加入了手机号判断和验证码判断,其余都一样,就不在赘述
virtual void PhoneRegister(::google::protobuf::RpcController* controller,
const ::zhou::PhoneRegisterReq* request,
::zhou::PhoneRegisterRsp* response,
::google::protobuf::Closure* done){
LOG_DEBUG("收到手机号注册请求!");
brpc::ClosureGuard rpc_guard(done);
auto err_response = [this, response](const std::string &rid,
const std::string &errmsg) -> void {
response->set_request_id(rid);
response->set_success(false);
response->set_errmsg(errmsg);
return;
};
// 1. 从请求中取出手机号码和验证码,验证码ID
std::string phone = request->phone_number();
std::string code_id = request->verify_code_id();
std::string code = request->verify_code();
// 2. 检查注册手机号码是否合法
bool ret = phone_check(phone);
if (ret == false) {
LOG_ERROR("{} - 手机号码格式错误 - {}!", request->request_id(), phone);
return err_response(request->request_id(), "手机号码格式错误!");
}
// 3. 从 redis 数据库中进行验证码 ID-验证码一致性匹配
auto vcode = _redis_codes->code(code_id);
if (vcode != code) {
LOG_ERROR("{} - 验证码错误 - {}-{}!", request->request_id(), code_id, code);
return err_response(request->request_id(), "验证码错误!");
}
// 4. 通过数据库查询判断手机号是否已经注册过
auto user = _mysql_user->select_by_phone(phone);
if (user) {
LOG_ERROR("{} - 该手机号已注册过用户 - {}!", request->request_id(), phone);
return err_response(request->request_id(), "该手机号已注册过用户!");
}
// 5. 向数据库新增用户信息
std::string uid = uuid();
user = std::make_shared<User>(uid, phone);
ret = _mysql_user->insert(user);
if (ret == false) {
LOG_ERROR("{} - 向数据库添加用户信息失败 - {}!", request->request_id(), phone);
return err_response(request->request_id(), "向数据库添加用户信息失败!");
}
// 6. 向 ES 服务器中新增用户信息
ret = _es_user->appendData(uid, phone, uid, "", "");
if (ret == false) {
LOG_ERROR("{} - ES搜索引擎新增数据失败!", request->request_id());
return err_response(request->request_id(), "ES搜索引擎新增数据失败!");
}
//7. 组织响应,进行成功与否的响应即可。
response->set_request_id(request->request_id());
response->set_success(true);
}
- 获取用户个人信息接口:先从request里面获取用户的uid,然后从user表中查询获取用户的nickname,description,phone,头像ID,然后根据头像ID向文件子服务发起请求,获取头像文件数据,然后将这些放入response中,结束。批量获取用户信息的接口就是在这个接口的基础上取出的是用户ID列表,然后再根据用户ID列表获取用户信息列表,得到用户头像列表,然后下载用户头像,后面就不对这个接口进行赘述
virtual void GetUserInfo(::google::protobuf::RpcController* controller,
const ::zhou::GetUserInfoReq* request,
::zhou::GetUserInfoRsp* response,
::google::protobuf::Closure* done){
LOG_DEBUG("收到获取单个用户信息请求!");
brpc::ClosureGuard rpc_guard(done);
auto err_response = [this, response](const std::string &rid,
const std::string &errmsg) -> void {
response->set_request_id(rid);
response->set_success(false);
response->set_errmsg(errmsg);
return;
};
// 1. 从请求中取出用户 ID
std::string uid = request->user_id();
// 2. 通过用户 ID,从数据库中查询用户信息
auto user = _mysql_user->select_by_id(uid);
if (!user) {
LOG_ERROR("{} - 未找到用户信息 - {}!", request->request_id(), uid);
return err_response(request->request_id(), "未找到用户信息!");
}
// 3. 根据用户信息中的头像 ID,从文件服务器获取头像文件数据,组织完整用户信息
UserInfo *user_info = response->mutable_user_info();
user_info->set_user_id(user->user_id());
user_info->set_nickname(user->nickname());
user_info->set_description(user->description());
user_info->set_phone(user->phone());
if (!user->avatar_id().empty()) {
//从信道管理对象中,获取到连接了文件管理子服务的channel
auto channel = _mm_channels->choose(_file_service_name);
if (!channel) {
LOG_ERROR("{} - 未找到文件管理子服务节点 - {} - {}!",
request->request_id(), _file_service_name, uid);
return err_response(request->request_id(), "未找到文件管理子服务节点!");
}
//进行文件子服务的rpc请求,进行头像文件下载
zhou::FileService_Stub stub(channel.get());
zhou::GetSingleFileReq req;
zhou::GetSingleFileRsp rsp;
req.set_request_id(request->request_id());
req.set_file_id(user->avatar_id());
brpc::Controller cntl;
stub.GetSingleFile(&cntl, &req, &rsp, nullptr);
if (cntl.Failed() == true || rsp.success() == false) {
LOG_ERROR("{} - 文件子服务调用失败:{}!", request->request_id(), cntl.ErrorText());
return err_response(request->request_id(), "文件子服务调用失败!");
}
user_info->set_avatar(rsp.file_data().file_content());
}
// 4. 组织响应,返回用户信息
response->set_request_id(request->request_id());
response->set_success(true);
}
- 修改用户头像接口:从request里面取出用户ID和头像数据,然后通过文件子服务将头像数据进行上传,得到头像ID,再对user表里面的用户ID对应的数据进行头像ID的修改,然后再对ES表中搜索引擎的数据进行修改,结束
virtual void SetUserAvatar(::google::protobuf::RpcController* controller,
const ::zhou::SetUserAvatarReq* request,
::zhou::SetUserAvatarRsp* response,
::google::protobuf::Closure* done){
LOG_DEBUG("收到用户头像设置请求!");
brpc::ClosureGuard rpc_guard(done);
auto err_response = [this, response](const std::string &rid,
const std::string &errmsg) -> void {
response->set_request_id(rid);
response->set_success(false);
response->set_errmsg(errmsg);
return;
};
// 1. 从请求中取出用户 ID 与头像数据
std::string uid = request->user_id();
// 2. 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
auto user = _mysql_user->select_by_id(uid);
if (!user) {
LOG_ERROR("{} - 未找到用户信息 - {}!", request->request_id(), uid);
return err_response(request->request_id(), "未找到用户信息!");
}
// 3. 上传头像文件到文件子服务,
auto channel = _mm_channels->choose(_file_service_name);
if (!channel) {
LOG_ERROR("{} - 未找到文件管理子服务节点 - {}!", request->request_id(), _file_service_name);
return err_response(request->request_id(), "未找到文件管理子服务节点!");
}
zhou::FileService_Stub stub(channel.get());
zhou::PutSingleFileReq req;
zhou::PutSingleFileRsp rsp;
req.set_request_id(request->request_id());
req.mutable_file_data()->set_file_name("");
req.mutable_file_data()->set_file_size(request->avatar().size());
req.mutable_file_data()->set_file_content(request->avatar());
brpc::Controller cntl;
stub.PutSingleFile(&cntl, &req, &rsp, nullptr);
if (cntl.Failed() == true || rsp.success() == false) {
LOG_ERROR("{} - 文件子服务调用失败:{}!", request->request_id(), cntl.ErrorText());
return err_response(request->request_id(), "文件子服务调用失败!");
}
std::string avatar_id = rsp.file_info().file_id();
// 4. 将返回的头像文件 ID 更新到数据库中
user->avatar_id(avatar_id);
bool ret = _mysql_user->update(user);
if (ret == false) {
LOG_ERROR("{} - 更新数据库用户头像ID失败 :{}!", request->request_id(), avatar_id);
return err_response(request->request_id(), "更新数据库用户头像ID失败!");
}
// 5. 更新 ES 服务器中用户信息
ret = _es_user->appendData(user->user_id(), user->phone(),
user->nickname(), user->description(), user->avatar_id());
if (ret == false) {
LOG_ERROR("{} - 更新搜索引擎用户头像ID失败 :{}!", request->request_id(), avatar_id);
return err_response(request->request_id(), "更新搜索引擎用户头像ID失败!");
}
// 6. 组织响应,返回更新成功与否
response->set_request_id(request->request_id());
response->set_success(true);
}
- 修改用户nickname接口:从request里面获取用户ID和nickname,然后在从数据库中对用户ID进行查询,获取对应的用户数据,将用户数据里面的nickname进行修改,然后也对ES搜索引擎中对应用户ID的nickname进行修改,结束,同样的,用户description修改的接口也是一样的,而手机号的修改接口,是在此基础上先判断手机号是否正确,然后查询手机号是否已经被绑定,然后再进行验证码判断,如果都没问题就想数据库和ES里面更新数据,所以后面就不再赘述
virtual void SetUserNickname(::google::protobuf::RpcController* controller,
const ::zhou::SetUserNicknameReq* request,
::zhou::SetUserNicknameRsp* response,
::google::protobuf::Closure* done){
LOG_DEBUG("收到用户昵称设置请求!");
brpc::ClosureGuard rpc_guard(done);
auto err_response = [this, response](const std::string &rid,
const std::string &errmsg) -> void {
response->set_request_id(rid);
response->set_success(false);
response->set_errmsg(errmsg);
return;
};
// 1. 从请求中取出用户 ID 与新的昵称
std::string uid = request->user_id();
std::string new_nickname = request->nickname();
// 2. 判断昵称格式是否正确
bool ret = nickname_check(new_nickname);
if (ret == false) {
LOG_ERROR("{} - 用户名长度不合法!", request->request_id());
return err_response(request->request_id(), "用户名长度不合法!");
}
// 3. 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
auto user = _mysql_user->select_by_id(uid);
if (!user) {
LOG_ERROR("{} - 未找到用户信息 - {}!", request->request_id(), uid);
return err_response(request->request_id(), "未找到用户信息!");
}
// 4. 将新的昵称更新到数据库中
user->nickname(new_nickname);
ret = _mysql_user->update(user);
if (ret == false) {
LOG_ERROR("{} - 更新数据库用户昵称失败 :{}!", request->request_id(), new_nickname);
return err_response(request->request_id(), "更新数据库用户昵称失败!");
}
// 5. 更新 ES 服务器中用户信息
ret = _es_user->appendData(user->user_id(), user->phone(),
user->nickname(), user->description(), user->avatar_id());
if (ret == false) {
LOG_ERROR("{} - 更新搜索引擎用户昵称失败 :{}!", request->request_id(), new_nickname);
return err_response(request->request_id(), "更新搜索引擎用户昵称失败!");
}
// 6. 组织响应,返回更新成功与否
response->set_request_id(request->request_id());
response->set_success(true);
}
- 网关子服务(gateway文件)
网关子服务的作用是对客户端发来的请求(httplib库进行实现)序列化为rpc请求进行转发,发送到对应的微服务器,然后将接收到的微服务器处理好的响应序列化为http报文返回客户端,并且对于需要服务端主动传输到客户端的通知采用websocket来管理链接进行传输
先介绍对于长连接管理的类:
主要含有的成员变量是这三个
std::mutex _mutex;
std::unordered_map<std::string, server_t::connection_ptr> _uid_connections;
std::unordered_map<server_t::connection_ptr, Client> _conn_clients;
其中的uid_connect是管理用户ID和对应的socket的链接
_conn_connect是通过链接来找到对应客户端的
下面是对websocket关键初始化的动作的代码解释
_ws_server.set_open_handler(std::bind(&GatewayServer::onOpen, this, std::placeholders::_1));
_ws_server.set_close_handler(std::bind(&GatewayServer::onClose, this, std::placeholders::_1));
auto wscb = std::bind(&GatewayServer::onMessage, this,
std::placeholders::_1, std::placeholders::_2);
_ws_server.set_message_handler(wscb);
Open函数只是简单的显示连接的产生,这里不做赘述
Close函数是长连接断开后,也就是客户端下线后,将Redis里面的状态键值对和会话键值对进行删除然后移除管理的长连接
void onClose(websocketpp::connection_hdl hdl) {
//长连接断开时做的清理工作
//0. 通过连接对象,获取对应的用户ID与登录会话ID
auto conn = _ws_server.get_con_from_hdl(hdl);
std::string uid, ssid;
bool ret = _connections->client(conn, uid, ssid);
if (ret == false) {
LOG_WARN("长连接断开,未找到长连接对应的客户端信息!");
return ;
}
//1. 移除登录会话信息
_redis_session->remove(ssid);
//2. 移除登录状态信息
_redis_status->remove(uid);
//3. 移除长连接管理数据
_connections->remove(conn);
LOG_DEBUG("{} {} {} 长连接断开,清理缓存数据!", ssid, uid, (size_t)conn.get());
}
而当websocket接收到消息以后就会实现对长连接的管理和保活,就是将其添加到Redis上面的两个键值对中和connect类里面,实现长连接和客户端的对应,长连接保活主要是通过对客户端定期检查,判断客户端的链接情况是否正常,如果正常则进行ping,维护长连接,如果有问题,则断开
auto conn = _ws_server.get_con_from_hdl(hdl);
//2. 针对消息内容进行反序列化 -- ClientAuthenticationReq -- 提取登录会话ID
ClientAuthenticationReq request;
bool ret = request.ParseFromString(msg->get_payload());
if (ret == false) {
LOG_ERROR("长连接身份识别失败:正文反序列化失败!");
_ws_server.close(hdl, websocketpp::close::status::unsupported_data, "正文反序列化失败!");
return;
}
//3. 在会话信息缓存中,查找会话信息
std::string ssid = request.session_id();
auto uid = _redis_session->uid(ssid);
//4. 会话信息不存在则关闭连接
if (!uid) {
LOG_ERROR("长连接身份识别失败:未找到会话信息 {}!", ssid);
_ws_server.close(hdl, websocketpp::close::status::unsupported_data, "未找到会话信息!");
return;
}
//5. 会话信息存在,则添加长连接管理
_connections->insert(conn, *uid, ssid);
LOG_DEBUG("新增长连接管理:{}-{}-{}", ssid, *uid, (size_t)conn.get());
keepAlive(conn);
}
void keepAlive(server_t::connection_ptr conn) {
if (!conn || conn->get_state() != websocketpp::session::state::value::open) {
LOG_DEBUG("非正常连接状态,结束连接保活");
return;
}
conn->ping("");
_ws_server.set_timer(60000, std::bind(&GatewayServer::keepAlive, this, conn));
}
下面是httplib的请求路径和对应函数,虽然看上去接口众多,但是多数的接口逻辑都是一样的,都是将http报文序列化为对应的微服务器的请求报文,然后进行rpc调用,获得对应微服务器的响应以后将响应序列化为对应的http响应报文,不过部分会影响其他用户的操作,比如添加好友的行为,就会采用websocket的链接管理对被影响的用户发送对应的通知
_http_server.Post(GET_PHONE_VERIFY_CODE , (httplib::Server::Handler)std::bind(&GatewayServer::GetPhoneVerifyCode , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(USERNAME_REGISTER , (httplib::Server::Handler)std::bind(&GatewayServer::UserRegister , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(USERNAME_LOGIN , (httplib::Server::Handler)std::bind(&GatewayServer::UserLogin , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(PHONE_REGISTER , (httplib::Server::Handler)std::bind(&GatewayServer::PhoneRegister , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(PHONE_LOGIN , (httplib::Server::Handler)std::bind(&GatewayServer::PhoneLogin , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(GET_USERINFO , (httplib::Server::Handler)std::bind(&GatewayServer::GetUserInfo , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(SET_USER_AVATAR , (httplib::Server::Handler)std::bind(&GatewayServer::SetUserAvatar , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(SET_USER_NICKNAME , (httplib::Server::Handler)std::bind(&GatewayServer::SetUserNickname , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(SET_USER_DESC , (httplib::Server::Handler)std::bind(&GatewayServer::SetUserDescription , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(SET_USER_PHONE , (httplib::Server::Handler)std::bind(&GatewayServer::SetUserPhoneNumber , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FRIEND_GET_LIST , (httplib::Server::Handler)std::bind(&GatewayServer::GetFriendList , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FRIEND_APPLY , (httplib::Server::Handler)std::bind(&GatewayServer::FriendAdd , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FRIEND_APPLY_PROCESS , (httplib::Server::Handler)std::bind(&GatewayServer::FriendAddProcess , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FRIEND_REMOVE , (httplib::Server::Handler)std::bind(&GatewayServer::FriendRemove , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FRIEND_SEARCH , (httplib::Server::Handler)std::bind(&GatewayServer::FriendSearch , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FRIEND_GET_PENDING_EV , (httplib::Server::Handler)std::bind(&GatewayServer::GetPendingFriendEventList , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(CSS_GET_LIST , (httplib::Server::Handler)std::bind(&GatewayServer::GetChatSessionList , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(CSS_CREATE , (httplib::Server::Handler)std::bind(&GatewayServer::ChatSessionCreate , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(CSS_GET_MEMBER , (httplib::Server::Handler)std::bind(&GatewayServer::GetChatSessionMember , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(MSG_GET_RANGE , (httplib::Server::Handler)std::bind(&GatewayServer::GetHistoryMsg , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(MSG_GET_RECENT , (httplib::Server::Handler)std::bind(&GatewayServer::GetRecentMsg , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(MSG_KEY_SEARCH , (httplib::Server::Handler)std::bind(&GatewayServer::MsgSearch , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(NEW_MESSAGE , (httplib::Server::Handler)std::bind(&GatewayServer::NewMessage , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FILE_GET_SINGLE , (httplib::Server::Handler)std::bind(&GatewayServer::GetSingleFile , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FILE_GET_MULTI , (httplib::Server::Handler)std::bind(&GatewayServer::GetMultiFile , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FILE_PUT_SINGLE , (httplib::Server::Handler)std::bind(&GatewayServer::PutSingleFile , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(FILE_PUT_MULTI , (httplib::Server::Handler)std::bind(&GatewayServer::PutMultiFile , this, std::placeholders::_1, std::placeholders::_2));
_http_server.Post(SPEECH_RECOGNITION , (httplib::Server::Handler)std::bind(&GatewayServer::SpeechRecognition , this, std::placeholders::_1, std::placeholders::_2));
_http_thread = std::thread([this, http_port](){
_http_server.listen("0.0.0.0", http_port);
});
- 获取手机验证码接口:得到httplib包request报头,然后自己实例化一个phoneVerifyCodeReq报头,这结构体在PROTOBUF文件里序列化的,可以直接使用,目的就是为了进行微服务器之间的网络传输,然后通过request报头来初始化我们自己定义的那个通过rpc调用的请求,然后再获取用户子服务的信道,对信道进行,将请求和响应放入,调用对应的验证码发送接口,实现验证码的发送,得到对应的响应后,再将这个用来微服务调用的响应初始化httplib的response报文,返回给客户端,便结束了从客户端接收响应后传到对应的微服务处理器,然后接收微服务处理器响应,在将响应转发给客户端的过程。后面的userRegister,UserLogin,PhoneRegister,PhoneLogind这四个接口都是这样的逻辑,就不在赘述
void GetPhoneVerifyCode(const httplib::Request &request, httplib::Response &response) {
//1. 取出http请求正文,将正文进行反序列化
PhoneVerifyCodeReq req;
PhoneVerifyCodeRsp rsp;
auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
rsp.set_success(false);
rsp.set_errmsg(errmsg);
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
};
bool ret = req.ParseFromString(request.body);
if (ret == false) {
LOG_ERROR("获取短信验证码请求正文反序列化失败!");
return err_response("获取短信验证码请求正文反序列化失败!");
}
//2. 将请求转发给用户子服务进行业务处理
auto channel = _mm_channels->choose(_user_service_name);
if (!channel) {
LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
return err_response("未找到可提供业务处理的用户子服务节点!");
}
zhou::UserService_Stub stub(channel.get());
brpc::Controller cntl;
stub.GetPhoneVerifyCode(&cntl, &req, &rsp, nullptr);
if (cntl.Failed()) {
LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
return err_response("用户子服务调用失败!");
}
//3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}
- 获取用户身份信息接口:在获取用户信息接口这里,我们得到了httplib的报文以后,就不能再直接原封不动的将报文转发到对应的用户子服务处理器了,应该要判断是否用户是处于上线状态,已经用户是否有权利查看这个用户的身份信息,发现有这样的权力以后再将报文转发到对应的用户子服务的接口,同理,后面的SetUserAvatar,SetUserName,SetUserDiscription,SetUserNumberPhone,GetFriendList都是这个逻辑,故后面不在赘述
void GetFriendList(const httplib::Request &request, httplib::Response &response) {
//1. 取出http请求正文,将正文进行反序列化
GetFriendListReq req;
GetFriendListRsp rsp;
auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
rsp.set_success(false);
rsp.set_errmsg(errmsg);
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
};
bool ret = req.ParseFromString(request.body);
if (ret == false) {
LOG_ERROR("获取好友列表请求正文反序列化失败!");
return err_response("获取好友列表请求正文反序列化失败!");
}
//2. 客户端身份识别与鉴权
std::string ssid = req.session_id();
auto uid = _redis_session->uid(ssid);
if (!uid) {
LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
return err_response("获取登录会话关联用户信息失败!");
}
req.set_user_id(*uid);
//2. 将请求转发给好友子服务进行业务处理
auto channel = _mm_channels->choose(_friend_service_name);
if (!channel) {
LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
return err_response("未找到可提供业务处理的用户子服务节点!");
}
zhou::FriendService_Stub stub(channel.get());
brpc::Controller cntl;
stub.GetFriendList(&cntl, &req, &rsp, nullptr);
if (cntl.Failed()) {
LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
return err_response("好友子服务调用失败!");
}
//3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}
- 好友添加接口实现:在上述的接口中,都是简单的对httplib请求进行序列化解析后转发给对应的微服务器,然后从微服务器中得到对应的响应再转发给客户端的过程,并没有对websocket的连接使用,也就是并没有在没有接收到请求的情况下将响应发出的行为,但是加好友是一个双向的过程,不仅需要发送者接收到对应的响应,也需要被申请者接受到发送者的好友申请,下面是对好友添加接口逻辑的描述和代码实现,首先还是得将http报文转变为我们设计的PROTOBUF的结构,然后再将报文通过好友子服务进行处理,得到对应的PROTOBUF报文响应,然后将响应转发给对应的客户端的对象,但是不同的是,如果发送成功后,我门还需要从websocket管理里面取出被申请者ID对应的长连接,然后主动实例化一个结构体通过websocket的长连接管理将包含着新朋友添加通知的消息告诉对应的被申请人,这个通知的结构为User_info,也就是申请者的个人信息
void FriendAdd(const httplib::Request &request, httplib::Response &response) {
// 好友申请的业务处理中,好友子服务其实只是在数据库创建了申请事件
// 网关需要做的事情:当好友子服务将业务处理完毕后,如果处理是成功的--需要通知被申请方
// 1. 正文的反序列化,提取关键要素:登录会话ID
FriendAddReq req;
FriendAddRsp rsp;
auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
rsp.set_success(false);
rsp.set_errmsg(errmsg);
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
};
bool ret = req.ParseFromString(request.body);
if (ret == false) {
LOG_ERROR("申请好友请求正文反序列化失败!");
return err_response("申请好友请求正文反序列化失败!");
}
// 2. 客户端身份识别与鉴权
std::string ssid = req.session_id();
auto uid = _redis_session->uid(ssid);
if (!uid) {
LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
return err_response("获取登录会话关联用户信息失败!");
}
req.set_user_id(*uid);
// 3. 将请求转发给好友子服务进行业务处理
auto channel = _mm_channels->choose(_friend_service_name);
if (!channel) {
LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
return err_response("未找到可提供业务处理的用户子服务节点!");
}
zhou::FriendService_Stub stub(channel.get());
brpc::Controller cntl;
stub.FriendAdd(&cntl, &req, &rsp, nullptr);
if (cntl.Failed()) {
LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
return err_response("好友子服务调用失败!");
}
// 4. 若业务处理成功 --- 且获取被申请方长连接成功,则向被申请放进行好友申请事件通知
auto conn = _connections->connection(req.respondent_id());
if (rsp.success() && conn) {
LOG_DEBUG("找到被申请人 {} 长连接,对其进行好友申请通知", req.respondent_id());
auto user_rsp = _GetUserInfo(req.request_id(), *uid);
if (!user_rsp) {
LOG_ERROR("{} 获取当前客户端用户信息失败!", req.request_id());
return err_response("获取当前客户端用户信息失败!");
}
NotifyMessage notify;
notify.set_notify_type(NotifyType::FRIEND_ADD_APPLY_NOTIFY);
notify.mutable_friend_add_apply()->mutable_user_info()->CopyFrom(user_rsp->user_info());
conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
}
- 好友添加处理事件结果接口:上面我们将好友申请发送以后,接下来就是事件的处理了,我们添加好友的结果无非就两种,第一种是对方同意,然后你的好友列表多出一位新朋友,同时也建立起新的会话列表,如果对方没有同意,则你就会收到对方拒绝你加好友的消息,除此之外,无事发生。我们这边也是这个逻辑,通过request里面取出申请人和被申请人的ID以及结果(是否同意添加联系人),然后通过ID得到对应的身份信息以及对应的长连接,先向申请人通过长连接通知申请结果(同意或者拒绝),如果是同意,则需要为双方创建一个会话,会话的头像就是双方的头像,会话的名字就是双方的名字,然后再将这个会话放入通知里面,通过双方对应的链接传输这个通知,使得客户端建立相应会话。在好友删除接口中,除了对relation里面的二者关系进行删除,还要对message表里面二者发送的所有信息进行删除,然后再删除对应的回话表和会话成员表,最后向被删除者发送一个通知,让被删除者得知消息,虽然有点残忍,但是我觉得知道还是比不知道好,后面的许多接口都是和前面的流程差不多,都是将http报文转换成对应的PROTOBUF报文,然后再进行鉴权后转发给对应的微服务器,微服务器对于每一个请求报文的处理在前面讲述完成,也不在此讲述,接收到对应微服务器的响应报文以后再进行转换成http报文以后转发给客户端
void FriendAddProcess(const httplib::Request &request, httplib::Response &response) {
//好友申请的处理-----
FriendAddProcessReq req;
FriendAddProcessRsp rsp;
auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
rsp.set_success(false);
rsp.set_errmsg(errmsg);
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
};
bool ret = req.ParseFromString(request.body);
if (ret == false) {
LOG_ERROR("好友申请处理请求正文反序列化失败!");
return err_response("好友申请处理请求正文反序列化失败!");
}
// 2. 客户端身份识别与鉴权
std::string ssid = req.session_id();
auto uid = _redis_session->uid(ssid);
if (!uid) {
LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
return err_response("获取登录会话关联用户信息失败!");
}
req.set_user_id(*uid);
// 3. 将请求转发给好友子服务进行业务处理
auto channel = _mm_channels->choose(_friend_service_name);
if (!channel) {
LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
return err_response("未找到可提供业务处理的用户子服务节点!");
}
zhou::FriendService_Stub stub(channel.get());
brpc::Controller cntl;
stub.FriendAddProcess(&cntl, &req, &rsp, nullptr);
if (cntl.Failed()) {
LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
return err_response("好友子服务调用失败!");
}
if (rsp.success()) {
auto process_user_rsp = _GetUserInfo(req.request_id(), *uid);
if (!process_user_rsp) {
LOG_ERROR("{} 获取用户信息失败!", req.request_id());
return err_response("获取用户信息失败!");
}
auto apply_user_rsp = _GetUserInfo(req.request_id(), req.apply_user_id());
if (!process_user_rsp) {
LOG_ERROR("{} 获取用户信息失败!", req.request_id());
return err_response("获取用户信息失败!");
}
auto process_conn = _connections->connection(*uid);
if (process_conn) LOG_DEBUG("找到处理人的长连接!");
else LOG_DEBUG("未找到处理人的长连接!");
auto apply_conn = _connections->connection(req.apply_user_id());
if (apply_conn) LOG_DEBUG("找到申请人的长连接!");
else LOG_DEBUG("未找到申请人的长连接!");
//4. 将处理结果给申请人进行通知
if (apply_conn) {
NotifyMessage notify;
notify.set_notify_type(NotifyType::FRIEND_ADD_PROCESS_NOTIFY);
auto process_result = notify.mutable_friend_process_result();
process_result->mutable_user_info()->CopyFrom(process_user_rsp->user_info());
process_result->set_agree(req.agree());
apply_conn->send(notify.SerializeAsString(),
websocketpp::frame::opcode::value::binary);
LOG_DEBUG("对申请人进行申请处理结果通知!");
}
//5. 若处理结果是同意 --- 会伴随着单聊会话的创建 -- 因此需要对双方进行会话创建的通知
if (req.agree() && apply_conn) { //对申请人的通知---会话信息就是处理人信息
NotifyMessage notify;
notify.set_notify_type(NotifyType::CHAT_SESSION_CREATE_NOTIFY);
auto chat_session = notify.mutable_new_chat_session_info();
chat_session->mutable_chat_session_info()->set_single_chat_friend_id(*uid);
chat_session->mutable_chat_session_info()->set_chat_session_id(rsp.new_session_id());
chat_session->mutable_chat_session_info()->set_chat_session_name(process_user_rsp->user_info().nickname());
chat_session->mutable_chat_session_info()->set_avatar(process_user_rsp->user_info().avatar());
apply_conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
LOG_DEBUG("对申请人进行会话创建通知!");
}
if (req.agree() && process_conn) { //对处理人的通知 --- 会话信息就是申请人信息
NotifyMessage notify;
notify.set_notify_type(NotifyType::CHAT_SESSION_CREATE_NOTIFY);
auto chat_session = notify.mutable_new_chat_session_info();
chat_session->mutable_chat_session_info()->set_single_chat_friend_id(req.apply_user_id());
chat_session->mutable_chat_session_info()->set_chat_session_id(rsp.new_session_id());
chat_session->mutable_chat_session_info()->set_chat_session_name(apply_user_rsp->user_info().nickname());
chat_session->mutable_chat_session_info()->set_avatar(apply_user_rsp->user_info().avatar());
process_conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
LOG_DEBUG("对处理人进行会话创建通知!");
}
}
//6. 对客户端进行响应
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}
- 最后一个接口,新消息转发接口:先将http报文序列化为对应的消息转发接口的报文,然后调用消息转发子服务,将报文进行处理,得到消息转发子服务传来的响应以后,说明消息已经被永久化到数据库和ES搜索引擎里面,就对被发送消息的用户发送一个通知,通知就是有一条新消息,通知的发送也是通过用户ID找到对应的长连接,进行发送通知。
void NewMessage(const httplib::Request &request, httplib::Response &response) {
NewMessageReq req;
NewMessageRsp rsp;//这是给客户端的响应
GetTransmitTargetRsp target_rsp;//这是请求子服务的响应
auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
rsp.set_success(false);
rsp.set_errmsg(errmsg);
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
};
bool ret = req.ParseFromString(request.body);
if (ret == false) {
LOG_ERROR("新消息请求正文反序列化失败!");
return err_response("新消息请求正文反序列化失败!");
}
// 2. 客户端身份识别与鉴权
std::string ssid = req.session_id();
auto uid = _redis_session->uid(ssid);
if (!uid) {
LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
return err_response("获取登录会话关联用户信息失败!");
}
req.set_user_id(*uid);
// 3. 将请求转发给好友子服务进行业务处理
auto channel = _mm_channels->choose(_transmite_service_name);
if (!channel) {
LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
return err_response("未找到可提供业务处理的用户子服务节点!");
}
zhou::MsgTransmitService_Stub stub(channel.get());
brpc::Controller cntl;
stub.GetTransmitTarget(&cntl, &req, &target_rsp, nullptr);
if (cntl.Failed()) {
LOG_ERROR("{} 消息转发子服务调用失败!", req.request_id());
return err_response("消息转发子服务调用失败!");
}
// 4. 若业务处理成功 --- 且获取被申请方长连接成功,则向被申请放进行好友申请事件通知
if (target_rsp.success()){
for (int i = 0; i < target_rsp.target_id_list_size(); i++) {
std::string notify_uid = target_rsp.target_id_list(i);
if (notify_uid == *uid) continue; //不通知自己
auto conn = _connections->connection(notify_uid);
if (!conn) { continue;}
NotifyMessage notify;
notify.set_notify_type(NotifyType::CHAT_MESSAGE_NOTIFY);
auto msg_info = notify.mutable_new_message_info();
msg_info->mutable_message_info()->CopyFrom(target_rsp.message());
conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
}
}
// 5. 向客户端进行响应
rsp.set_request_id(req.request_id());
rsp.set_success(target_rsp.success());
rsp.set_errmsg(target_rsp.errmsg());
response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}
相关文章:
微服务即时通讯系统(5)用户管理子服务,网关子服务
用户管理子服务(user文件) 用户管理子服务也是这个项目中的一个业务最多的子服务,接口多,但是主要涉及的数据表只有user表,Redis的键值对和ES的一个搜索引擎,主要功能是对用户的个人信息进行修改管理&#…...
docker.io连接超时的处理,用代理网站
docker pull的时候会超时: Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 这时可以找一些代理网站,比如…...
【测试工具JMeter篇】JMeter性能测试入门级教程(四):JMeter中BeanShell内置方法使用
一、什么是BeanShell BeanShell是一种完全符合Java语法规范的脚本语言,并且又拥有自己的一些语法和方法;BeanShell是一种松散类型的脚本语言(这点和JS类似);BeanShell是用Java写成的,一个小型的、免费的、可以下载的、嵌入式的Java源代码解释器,具有对象脚本语言特性,非常精简…...
JavaScript 键盘控制移动
如果你想通过 JavaScript 实现键盘控制对象(比如一个方块)的移动,下面是一个简单的示例,展示如何监听键盘事件并根据按下的键来移动一个元素。 HTML 和 CSS: <!DOCTYPE html> <html lang"en">…...
如何预防服务器后台爆破攻击
服务器后台爆破(Brute Force Attack)是一种通过反复尝试用户名和密码组合,以非法获取系统访问权限的攻击方式。这种攻击不仅会消耗服务器资源,还可能导致合法用户被锁定或敏感数据泄露。为了有效预防服务器后台爆破攻击࿰…...
AI 写作(一):开启创作新纪元(1/10)
一、AI 写作:重塑创作格局 在当今数字化高速发展的时代,AI 写作正以惊人的速度重塑着创作格局。AI 写作在现代社会中占据着举足轻重的地位,发挥着不可替代的作用。 随着信息的爆炸式增长,人们对于内容的需求日益旺盛。AI 写作能够…...
【HarmonyOS】鸿蒙应用使用lottie动画
【HarmonyOS】鸿蒙应用使用lottie动画 一、lottie动画是什么? https://airbnb.design/lottie Lottie是由Airbnb团队开发的一个适用于iOS、Android、React Native、Web和Windows的开源动画库,用于解析使用Bodymovin导出为JSON的Adobe After Effects动…...
SQL面试题——腾讯SQL面试题 合并连续支付订单
合并连续支付订单 现有一张用户支付表:user_pay包含字段订单ID,用户ID,商户ID,支付时间,支付金额。如果同一用户在同一商户存在多笔订单,且中间该用户没有其他商户的支付记录,则认为是连续订单,请把连续订单进行合并,时间取最早支付时间,金额求和。 +----------+------…...
【docker】10. 容器操作案例
容器操作案例 容器基本操作 • 通过 nginx 镜像文件创建容器 • 容器的列举(包含正在运行的容器) # 发现此时 e7c33d9f5c61 这个容器运行的状态为 Up,即运行状态 rootLAPTOP-H2EI4I6A:~# docker container ls CONTAINER ID IMAGE COMMAND CREATED …...
postman测试
当然,以下是针对你提供的API层和Service层代码中涉及到的各个接口,如何使用 Postman 进行详细测试的指南。这个指南将帮助你理解如何配置 Postman 来测试这些接口,包括请求的构造、认证的处理、以及如何解读响应。 目录 准备工作接口测试指…...
【攻防实验】溯源与取证分析实验
溯源与取证分析实验 溯源取证分析作为网络攻防过程中重要环节,准确找到攻击者的入侵线索(尤其是攻击突破口、攻击IP地址、域名、工具等信息),对于企业或者团队安全运营团队来说都是必备技能。常规攻击取证过程中往往会结合流量、Web访问日志、终端系统或…...
【测试工具JMeter篇】JMeter性能测试入门级教程(七):JMeter断言
一、前言 在 JMeter 中,断言元件(Assertion)用于验证测试结果是否符合预期。断言元件可以检查服务器的响应数据,以确保它们符合期望的模式或值,从而验证性能测试脚本的正确性。断言元件通常在每个请求的响应中添加&am…...
Linux 常用命令
目录 一、ls 指令 二、pwd命令 三、cd 指令 1、cd 目录名 2、cd .. 返回上级目录 3、cd ~ 进入用户家目 4、cd - 返回最近访问目录 5、cd相对路径&&cd绝对路径 四、touch指令 五、mkdir指令 1、mkdir 目录名 创建一个目录 2、mkdir -p 递归创建多…...
汽车IVI中控OS Linux driver开发实操(二十八):回声消除echo cancellation和噪声消除Noise reduction
概述: 在当今高度互联的世界中,清晰的实时通信比以往任何时候都更重要。在远程团队会议期间,没有什么能像回声一样打断对话。当说话者听到他们的声音回响时,可能会分散注意力,甚至无法理解对话。即使是很小的回声也会产生很大的影响,仅仅25毫秒的振幅就足以造成声音干扰…...
003-SpringBoot整合Pagehelper
SpringBoot整合Pagehelper 一、引入依赖二、配置 application.yml三、配置 MybatisPlusConfig四、Controller五、ServiceImpl 一、引入依赖 <dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</ar…...
零基础学安全--shell练习
目录 用shell写一个计算器 测试 一些小问题 n阶乘数 测试 拓展 写⼀个Shell脚本去筛选出eth0⽹卡的ipv4地址,并赋值⼀个变量输出 测试 无限重启 用shell写一个计算器 read -p "请输入数字a: " number1 read -p "请输入操作符…...
【专题】计算机网络之运输层(传输层)
1. 运输层协议概述 1.1 进程之间的通信 (1) 运输层的作用 运输层提供进程间的逻辑通信。 运输层的屏蔽作用: 运输层向高层用户屏蔽了下面网络核心的细节(如网络拓扑、所采用的路由选择协议等),使应用进程看见的就是好像在两个运…...
【leetcode100】旋转图像
1、题目描述 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1: 输入:matrix [[1,2,3],[4,5,6],…...
软件工程——期末复习(1)
名词解释: 名词解释--人月 答案:人月是软件开发工作量的单位,1人月表示1个程序员1个月的工作时间所开发的代码量。 请解释软件缺陷、错误和失败,并简单举例说明。 答案:缺陷(defect)指系统代…...
HTML5系列(3)--多媒体标签详解
前端技术探索系列:HTML5 多媒体标签详解 🎥 开篇寄语 👋 前端开发者们, 在前三篇文章中,我们探讨了 HTML5 的语义化和表单特性。今天,让我们深入了解 HTML5 的多媒体能力,看看如何构建强大的…...
Spring Boot 3.4.0 发布:功能概览与示例
Spring Boot 3.4.0 带来了许多增强功能,使现代应用开发更加高效、便捷和强大。以下是最新功能的完整概述,以及一些帮助您快速入门的代码示例。 1. 应用程序版本管理 Spring Boot 引入了 spring.application.version 属性,方便开发者设置和访…...
【Vue3】【Naive UI】<n-upload>标签
【Vue3】【Naive UI】标签 基本设置 【VUE3】【Naive UI】<NCard> 标签 【VUE3】【Naive UI】<n-button> 标签 【VUE3】【Naive UI】<a> 标签 【VUE3】【Naive UI】<NDropdown> 标签…...
7.代理模式(Proxy Pattern)
古朗月行 代理模式JDK动态代理代码示例原码分析 cglib动态代理代码示例源码分析 JDK cglib动态代理对比ClassLoader类的生命周期: 参考资料 唐 李白 小时不识月,呼作白玉盘。 又疑瑶台镜,飞在青云端。 仙人垂两足,桂树何团团。…...
【效果】回到顶部功能实现
实现效果: 相关代码: <template><div class"cats" :style"{ top: catsTop }" ref"cats" click"catTop"></div> </template> 样式: /* 回到顶部 - 小猫咪 */ .cats {posi…...
项目搭建+修改
一 : 在列表成功回调函数,追加数据中,添加修改的按钮 for (let x of res) {//追加数据$("#table").append(<tr><td><input type"checkbox" class"ck" value"\${x.uid}"></td><td>\${x.uid}</td>…...
GD库如何根据颜色生成纯色背景图
GD库是一个用于图像处理的PHP扩展模块,它提供了一系列函数来创建、编辑和操作图像。要使用GD库根据颜色生成纯色背景图,可以按照以下步骤进行: 一、检查并安装GD库 检查GD库是否已安装: 可以通过运行phpinfo();或在命令行中使用p…...
Python 网络爬虫入门全知道
一、引言 在当今数字化时代,网络上的数据量呈爆炸式增长。无论是进行数据分析、市场调研,还是开发智能应用,获取网络数据都变得极为重要。而 Python 网络爬虫就是一把打开网络数据宝库的利器。它能够自动地从网页中抓取我们需要的信息&#…...
MATLAB期末复习笔记(下)
目录 五、数据和函数的可视化 1.MATLAB的可视化对象 2.二维图形的绘制 3.图形标识 4.多子图绘图 5.直方图的绘制 (1)分类 (2)垂直累计式 (3)垂直分组式 (4)水平分组式 &…...
基于大数据爬虫数据挖掘技术+Python的网络用户购物行为分析与可视化平台(源码+论文+PPT+部署文档教程等)
#1024程序员节|征文# 博主介绍:CSDN毕设辅导第一人、全网粉丝50W,csdn特邀作者、博客专家、腾讯云社区合作讲师、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老…...
【go】只读通道和只写通道
文章目录 概述1. 通道的方向2. 为什么会有只读通道和只写通道?3. 总结 概述 在 Go 中,只读通道和只写通道的概念通过通道的方向来实现。Go 语言允许你在函数参数中指定通道的方向,从而限制通道的使用方式,这样可以确保代码的清晰…...
带Burst AOT Settings移植问题
报错 burst问题 Burst AOT Settings 是 Unity 的 Burst Compiler 的一部分,用于预编译程序集(AOT,Ahead-Of-Time Compilation),以便在不支持 JIT(即时编译)的平台上运行,例如 iOS 和…...
Debezium日常分享系列之:Debezium Engine
Debezium日常分享系列之:Debezium Engine 依赖打包项目在代码中输出消息格式消息转换消息转换谓词高级记录使用引擎属性异步引擎属性数据库模式历史属性处理故障 Debezium连接器通常通过部署到Kafka Connect服务来运行,并配置一个或多个连接器来监视上游…...
运行 GreatSQL 时为什么要求关闭透明大页
在大部分运维规范中,一般都会要求在运行 GreatSQL/MySQL 的环境中要关闭透明大页,那么到底什么是透明大页,为什么要关闭,打开有什么风险吗? 在此之前,我也是有点懵的,本文试着回答这个疑问&…...
【Rive】Rive在Android上的简单应用
1 前言 Rive 是一款强大的矢量图编辑器,可以设计图形、也可以制作动画。Rive 提供了矩形、圆形、三角形、多边形、星形、钢笔、文字等工具来绘制各式各样的矢量图形;提供了平移、旋转、缩放等工具对矢量图形进行各种变换;提供了骨骼、约束、时…...
Base 崛起,SynFutures 或成生态系统中最具潜力应用
10月份的 Unchained Crypto 采访中,Solana 联合创始人 Anatoly 表示,通过观察活跃地址数、TVL、DeFi 版块、Meme 热潮和开发者生态等多个关键指标,察觉到 Base 势头正猛,成为以太坊生态最强劲的 L2。 11月下旬,小狐狸创…...
探索Go语言中的循环双向链表
简介 循环双向链表将双向链表的灵活性与循环结构相结合,使得每个节点都有一个指向前一个节点和后一个节点的指针,并且最后一个节点的Next指针指向头节点,形成一个闭环。本文将深入探讨如何在Go语言中实现和操作这种数据结构。 循环双向链表…...
Leetcode617.合并二叉树(HOT100)+Leetcode79. 单词搜索(HOT100)
链接 代码: class Solution { public:TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {if(!root1)return root2;if(!root2)return root1;root1->valroot2->val;root1->left mergeTrees(root1->left,root2->left);root1->right merg…...
亚马逊云(AWS)使用root用户登录
最近在AWS新开了服务器(EC2),用于学习,遇到一个问题就是默认是用ec2-user用户登录,也需要密钥对。 既然是学习用的服务器,还是想直接用root登录,下面开始修改: 操作系统是࿱…...
使用Docker在Ubuntu 22.04上部署MySQL数据库的完整指南
使用Docker在Ubuntu 22.04上部署MySQL数据库的完整指南 在现代应用开发中,使用Docker来部署数据库已成为一种流行的做法。本文将详细介绍如何在Ubuntu 22.04系统上使用Docker部署最新版本的MySQL数据库,包括关键注意事项、详细步骤、闭坑指南以及总结。…...
算法笔记:力扣15、三数之和
思路: 实现代码 class Solution {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> result new ArrayList<>(); Arrays.sort(nums); // 先对数组进行排序 for (int i 0; i < nums.length - 2; i) { /…...
perf list PMU 缓存事件
事件标识事件解释PMU事件路径l1d_cacheL1数据缓存的访问次数,L1缓存是CPU内部最快的缓存,位于距离CPU核心非常近的位置。armv8_pmuv3/l1d_cache/l1d_cache_lmiss_rd表示从L1数据缓存读取数据时发生缓存未命中的次数。armv8_pmuv3/l1d_cache_lmiss_rd/l1d…...
使用C#开发VTK笔记(一)-VTK开发环境搭建
一.使用C#开发VTK的背景 因为C#开发的友好性,一直都比较习惯于从C#开发程序。而长期以来,都希望有一个稳定可靠的三位工程数模的开发演示平台,经过多次对比之后,感觉VTK和OpenCasCade这两个开源项目是比较好的,但它们都是用C++编写的,我用C#形式开发,只能找到发布的C#组…...
2024Selenium自动化常见问题!
"NoSuchElementException"异常: 确保使用了正确的选择器来定位元素。可以使用id、class、XPath或CSS选择器等。 可以尝试使用find_elements方法来查找元素列表,并检查列表的长度来判断元素是否存在。 使用显式等待(WebDriverWait…...
考研英语翻译与大小作文
名词动化词 1 持有 harbor2 2 反映 mirror 3 缩短 bridge 4 使用 harness 5 掩饰 mask/veil 6 修改 tailor 7 汇集 pool 8 控制 curb 9 想象 picture 10 激发 trigger 拉丁…...
详解Rust异步编程
文章目录 多线程编程与异步编程对比并发模型对比分析异步编程基础概念及用法 Rust的异步编程通过async/await语法和Future特性提供了一种高效的方式来处理并发任务,尤其在I/O密集型操作中表现出色。async/await异步编程模型性能高,还能支持底层编程&…...
Vue + Element UI 实战技巧:如何实现 el-table 重新加载数据后折叠所有展开行
在 Vue 中使用 Element UI 的 el-table 组件时,如果你想要在数据重新加载后折叠所有行的展开状态,你可以通过维护一个数据属性来追踪哪些行是展开的,并在数据更新时重置这个属性。 以下是一个简单的示例来说明如何实现这个功能: …...
linux静态链接和动态链接
静态链接的特点 程序独立性高 静态链接是在程序编译时,将所有需要的目标文件以及它们所依赖的库文件中的代码和数据链接成一个可执行文件。一旦链接完成,这个可执行文件就包含了运行所需的全部内容,不依赖外部的库文件。例如,一个…...
计算机网络学习资料全攻略
计算机网络是计算机科学中一个非常重要的分支,它涉及到数据在计算机系统之间的传输和通信。随着互联网的快速发展,对计算机网络知识的掌握变得越来越重要。本文将为您提供一份全面的计算机网络学习资料指南,帮助您从基础到高级逐步深入学习。…...
第七课 Unity编辑器创建的资源优化_UI篇(UGUI)
上期我们学习了简单的Scene优化,接下来我们继续编辑器创建资源的UGUI优化 UI篇(UGUI) 优化UGUI应从哪些方面入手? 可以从CPU和GPU两方面考虑,CPU方面,避免触发或减少Canvas的Rebuild和Rebatch,…...
Go的简单问题问答
基础问题回答 Go 的主要特点是什么? 简洁:语法简化,减少复杂性。并发:内置 Goroutine 和 Channel,支持轻量级并发。静态类型:强类型语言,编译时检查错误。跨平台:编译生成独立的二进…...