⽹⻚版聊天室

76 阅读8分钟

⽹⻚版聊天室

项⽬背景

实现⼀个⽹⻚版的聊天室程序.

类似于⽹⻚版微信, 可以直接在⽹⻚上进⾏聊天.

需求分析

用户管理模块

注册功能

实现⼀个注册⻚⾯. 注册⻚⾯上包含了⼀个输⼊框, 输⼊⽤⼾名和密码.

注册成功后可以跳转到登录⻚⾯.

登录功能

实现⼀个登录⻚⾯. 登录⻚⾯上包含⼀个输⼊框. 输⼊⽤⼾名和密码.

登录成功后可以跳转到主⻚⾯.

主界面

消息传输功能

如果对⽅在线, 就会即刻提⽰实时消息.

如果对⽅不在线, 后续上线后就会看到历史消息.

创建项⽬

前端—-网络(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);
            }
    };