⽹⻚版聊天室
项⽬背景
实现⼀个⽹⻚版的聊天室程序.
类似于⽹⻚版微信, 可以直接在⽹⻚上进⾏聊天.
需求分析
用户管理模块
注册功能
实现⼀个注册⻚⾯. 注册⻚⾯上包含了⼀个输⼊框, 输⼊⽤⼾名和密码.
注册成功后可以跳转到登录⻚⾯.
登录功能
实现⼀个登录⻚⾯. 登录⻚⾯上包含⼀个输⼊框. 输⼊⽤⼾名和密码.
登录成功后可以跳转到主⻚⾯.
主界面
消息传输功能
如果对⽅在线, 就会即刻提⽰实时消息.
如果对⽅不在线, 后续上线后就会看到历史消息.
创建项⽬
前端—-网络(mongoose网络库)—-server(服务端)—–数据库
void server_test(int port)
{
im::Server server(HOST, USER, PASS, DB, PORT);
server.Start(port);
}
int main()
{
// user_table_test();
// msg_table_test();
server_test(9000);
return 0;
}
server服务端是开始,实例化server类,其中有成员
class Server
{
private:
struct mg_mgr _mgr; //mongoose库 对客户端进行监听通过handler回调函数对不同的事件进行处理
UserTable *_user_table; //将网络中接受到关于用户的数据发送到数据库中的一个类(其中包含一系列函数)
MsgTable *_msg_table; //将网络中接受到关于消息的数据发送到数据库中的一个类(其中包含一系列函数)
SessionManager *_ss_manager;//对当前服务端监听到的连接(从HTTP到websocket的连接)进行管理
会话信息也就是在客户端与服务端传输的cookie(会话号)
在用户注册是没有cookie的,也就没有会话
在用户登录时,创建该http通话会话 (会话信息的管理只管理http协议)
在用户获取历史消息之前要先进行会话的确认,也就是看当前cookie是否在当前的会话管理中
mongoose库起主导作用通过
void Start(int port)
{
std::string address = "0.0.0.0:" + std::to_string(port);
mg_http_listen(&_mgr, address.c_str(), handler, this);
for (;;)
mg_mgr_poll(&_mgr, 1000);
}
对客户端进行监听通过handler回调函数对不同的事件进行处理
static void handler(struct mg_connection *c, int ev, void *ev_data, void *fn_data)
{
Server *srv = (Server *)fn_data;
if (ev == MG_EV_HTTP_MSG)
{
struct mg_http_message *hm = (struct mg_http_message *)ev_data;
std::string method(hm->method.ptr, hm->method.len);
std::string uri(hm->uri.ptr, hm->uri.len);
if (method == "POST" && uri == "/user")
{
// 新增用户
user_add(c, hm, srv);
}
else if (method == "POST" && uri == "/login")
{
// 登录验证
user_login(c, hm, srv);
}
else if (method == "PUT" && uri == "/user/passwd")
{
// 密码修改
}
将server::handler设置为静态成员函数,防止隐含传参this,因为handler回调函数本身就固定好了参数
通过对不同事件进行处理,来调用不同的函数
对于不同事件有
新增用户:(此时还是http连接,信息保存在ev_data
struct mg_http_message *hm = (struct mg_http_message *)ev_data;
std::string method(hm->method.ptr, hm->method.len);
std::string uri(hm->uri.ptr, hm->uri.len);
也就是注册用户,也就是在数据库中添加一个用户的信息(用户名,密码)
static bool user_add(struct mg_connection *c,
struct mg_http_message *hm, Server *srv)
{
std::string req_body(hm->body.ptr, hm->body.len);
Json::Value user_json;
Json::Value resp_json;
bool ret = JsonUtil::unserialize(req_body, &user_json);
if (ret == false)
{
resp_json["result"] = false;
resp_json["reason"] = "用户信息解析失败";
std::string body;
JsonUtil::serialize(resp_json, &body);
mg_http_reply(c, 400, NULL, "%s", body.c_str());
return false;
}
UserTable *table = srv->get_user_table();
ret = table->insert(user_json);
if (ret == false)
{
resp_json["result"] = false;
resp_json["reason"] = "向服务器插入数据出错";
std::string body;
JsonUtil::serialize(resp_json, &body);
mg_http_reply(c, 500, NULL, "%s", body.c_str());
return false;
}
resp_json["result"] = true;
resp_json["reason"] = "新增用户成功";
std::string body;
JsonUtil::serialize(resp_json, &body);
mg_http_reply(c, 200, NULL, "%s", body.c_str());
}
区分注册用户与登录验证:因为二者都是通过Http连接事件来触发事件的,但二者的ev_data不同,获取method和uri来进行区别
添加数据……这些信息都是通过json进行数据组织的,将数据进行反序列化
登录验证(http连接)
else if (method == "POST" && uri == "/login")
{
// 登录验证
user_login(c, hm, srv);
}
static bool user_login(struct mg_connection *c,
struct mg_http_message *hm, Server *srv)
{
// 1.从请求正文中获取提交的用户名,密码信息
std::string req_body(hm->body.ptr, hm->body.len);
// 2.进行json反序列化
Json::Value user_json;
Json::Value resp_json;
bool ret = JsonUtil::unserialize(req_body, &user_json);
if (ret == false)
{
resp_json["result"] = false;
resp_json["reason"] = "用户信息解析失败";
std::string body;
JsonUtil::serialize(resp_json, &body);
mg_http_reply(c, 400, NULL, "%s", body.c_str());
return false;
}
// 3.在数据库中进行验证
UserTable *table = srv->get_user_table();
ret = table->check_login(user_json);
if (ret == false)
{
resp_json["result"] = false;
resp_json["reason"] = "用户名密码错误";
std::string body;
JsonUtil::serialize(resp_json, &body);
mg_http_reply(c, 401, NULL, "%s", body.c_str());
return false;
}
// 4.为客户端新增会话
SessionManager *ssm = srv->get_ss_manager();
uint64_t ssid = ssm->insert(user_json);
// Set-cookie: SSID=1
std::string cookie = "Set-cookie: SSID=" + std::to_string(ssid) + "\r\n";
// 4.返回200
resp_json["result"] = true;
resp_json["reason"] = "用户登录验证成功";
std::string body;
JsonUtil::serialize(resp_json, &body);
mg_http_reply(c, 200, cookie.c_str(), "%s", body.c_str());
}
登录验证,将数据反序列化之后在数据库中进行查询,如果成功,则新增会话,并将cookie信息返回客户端
这里cookie信息就是创建的会话号
获取聊天信息(HTTP连接)
else if (method == "GET" && uri == "/message")
{
// 获取历史聊天消息
get_history_msg(c, hm, srv);
}
static bool get_history_msg(struct mg_connection *c,
struct mg_http_message *hm, Server *srv)
{
struct mg_str *cookie = mg_http_get_header(hm, "Cookie");
if (cookie == NULL)
{
mg_http_reply(c, 401, "", "%s", "401 Unauthorized");
return false;
}
std::string cookie_str(cookie->ptr, cookie->len);
size_t pos = cookie_str.find("=");
std::string ssid_str = cookie_str.substr(pos + 1);
uint64_t ssid = std::stol(ssid_str);
SessionManager *ssm = srv->get_ss_manager();
if (ssm->exists(ssid) == false)
{
std::cout << ssid << "没有对应的session!\n";
mg_http_reply(c, 401, "", "%s", "401 Unauthorized");
return false;
}
MsgTable *table = srv->get_msg_table();
Json::Value msgs;
bool ret = table->select_part(30000, &msgs);
if (ret == false)
{
Json::Value resp_json;
resp_json["result"] = true;
resp_json["reason"] = "获取历史聊天消息失败";
std::string body;
JsonUtil::serialize(resp_json, &body);
mg_http_reply(c, 500, NULL, "%s", body.c_str());
return false;
}
std::string body;
JsonUtil::serialize(msgs, &body);
ssm->remove(ssid);
Json::Value user;
ssm->get_session_user(ssid, &user);
Json::Value new_user;
UserTable *u_table = srv->get_user_table();
u_table->select_by_name(user["username"].asString(), &new_user);
std::string user_str;
JsonUtil::serialize(new_user, &user_str);
ssid = ssm->insert(new_user);
std::string header = "Content-Type: application/json\r\n";
header += "Set-Cookie: SSID=" + std::to_string(ssid) + "\r\n";
mg_http_reply(c, 200, header.c_str(), "%s", body.c_str());
}
获取历史聊天记录,切换协议,还是通过HTTP连接来获取的
首先是判断是否有cookie信息,并且其中的会话号在当前的会话管理中是否存在,因为上一步是用户登录,只有登录成功后才能创建会话并将会话号作为cookie信息返回客户端,防止没有登录就获取到聊天信息
然后通过查询聊天信息表进行信息获取,并将获取到的信息返回客户端
接着就是在网页聊天室中聊天(广播聊天),这是就需要切换协议(从HTTP切换到websocket)
协议切换(http连接)
else if (method == "GET" && uri == "/ws")
{
// 协议切换请求---切换到websocket协议
struct mg_str *cookie = mg_http_get_header(hm, "Cookie");
if (cookie == NULL)
{
mg_http_reply(c, 401, "", "%s", "401 Unauthorized");
return;
}
std::string cookie_str(cookie->ptr, cookie->len);
size_t pos = cookie_str.find("=");
std::string ssid_str = cookie_str.substr(pos + 1);
uint64_t ssid = std::stol(ssid_str);
SessionManager *ssm = srv->get_ss_manager();
if (ssm->exists(ssid) == false)
{
mg_http_reply(c, 401, "", "%s", "401 Unauthorized");
return;
}
mg_ws_upgrade(c, hm, NULL);
ssm->remove(ssid);
}
切换协议,首先还是要在http协议中进行,先判断cookie的会话号是否在当前的会话管理中
如果在,则切换协议并将该会话号从会话管理中删除
请求静态资源(http连接)
else
{
// 除了以上请求以外,其他请求统一认为是静态资源请求
// 通过ops设置静态资源根目录
struct mg_http_serve_opts opts = {.root_dir = srv->rootdir().c_str()};
// 让连接的处理,到静态资源根目录下找对应文件,进行响应
// 找不到,最终返回404
mg_http_serve_dir(c, hm, &opts);
}
class Server
{
private:
struct mg_mgr _mgr;
UserTable *_user_table;
MsgTable *_msg_table;
SessionManager *_ss_manager;
std::string _rootdir = "./wwwroot";
广播聊天(websocket连接)
else if (ev == MG_EV_WS_MSG)
{
// websocket,数据通信---客户端发送了一条聊天消息---广播这条消息
struct mg_ws_message *wsmsg = (struct mg_ws_message *)ev_data;
std::string msg(wsmsg->data.ptr, wsmsg->data.len);
std::cout << msg << std::endl;
broadcast_msg(srv->get_mgr(), msg);
Json::Value msg_json;
JsonUtil::unserialize(msg, &msg_json);
srv->get_msg_table()->insert(msg_json);
}
static void broadcast_msg(struct mg_mgr *mgr, const std::string &msg)
{
struct mg_connection *conn = mgr->conns;
for (; conn != NULL; conn = conn->next)
{
if (conn->is_websocket)
{
mg_ws_send(conn, msg.c_str(), msg.size(), WEBSOCKET_OP_TEXT);
}
}
}
如果该事件是websocket连接出发的事件,则说明是客户端中有用户发送了一条消息
- 将该消息广播到所有是websosket连接的客户端中(将所有连接一一遍历是websocket连接的则发送这条消息)
- 并将这条消息加入到数据库中的消息表中
工具类
json工具类
/*json工具类:json序列化,json反序列化*/
class JsonUtil {
public:
static bool serialize(Json::Value & value ,std::string * body){
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
int ret=sw->write(value,&ss);
if (ret!=0)
{
std::cout<<"serialize failed!!\n";
return false;
}
*body=ss.str();
return true;
}
static bool unserialize(std::string &body,Json::Value * value){
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
bool ret = cr->parse(body.c_str(),body.c_str()+body.size(),value,&err);
if (ret==false)
{
std::cout<<"unserialize failed!!!"<<std::endl;
return false;
}
return true;
}
};
mysql工具类
class MysqlUtil{
public:
//创建初始化,连接服务器,设置字符集,选择数据库
static MYSQL *mysql_create(const std :: string & host,
const std :: string &user,
const std :: string &pass,
const std :: string &db,
int port ){
MYSQL * mysql=mysql_init(NULL);
if (mysql==NULL){
std::cout<<"mysql init failed!!!"<<std::endl;
return NULL;
}
if(mysql_real_connect(mysql,host.c_str(),
user.c_str(),pass.c_str(),db.c_str(),port,NULL,0)==NULL){
std::cout<<"connect server failed!!!"<<std::endl;
mysql_close(mysql);
return NULL;
}
mysql_set_character_set(mysql,"utf8");
return mysql;
}
static bool mysql_exec(MYSQL *mysql,const std::string &sql){
int ret=mysql_query(mysql,sql.c_str());
if(ret!=0){
std::cout<<mysql<<"mysql query failed !!!"<<mysql_error(mysql);
return false;
}
return true;
}
static void mysql_destroy(MYSQL *mysql){
if (mysql==NULL){
return;
}
mysql_close(mysql);
}
};