基于Mongoose框架的网页通讯

以前学习C++和Linux的时候,只是单纯的写简单的小程序,所以打算做个项目练习一下,了解一个项目的开发过程和各个组件之间的组合过程。

准备

Mongoose框架:这个框架用起来很方便,只要把.c文件和.h文件拷到项目文件夹下就可以使用了。我用的6.14版本。
MySql数据库:用的5.6.45版本,因为使用C++来做这个项目,所以数据库链接选择了C connector。
JsonCpp:JsonCpp是一个基于C++的开源库,用来存储和传输对应的文本信息,实现前后端数据交互。
前端网页:这个可以在网上下载一个模板。
jQuery库:可以下载使用,也可以通过CDN(内容分发网络)来引用。
websocket协议:可以在一个独立的持久连接上提供全双工通信。客户端和服务器可以主动向对方发送和接受数据。

开发

首先先做出一个基于Mongoose框架的服务器,关于Mongoose的基本接口参考别人的博客:Mongoose6.11官方手册原版
在Mongoose库下面也有example可以参考。

项目功能梳理:

  1. 登录注册功能。
  2. 聊天功能。
  3. 缓存功能。

核心代码

项目文件树:

.
├── ImServer
├── ImServer.cc
├── ImServer.hpp
├── Makefile
├── mongoose
│   ├── mongoose.c
│   └── mongoose.h
├── mysql
│   ├── include
│   └── lib
├── tags
├── Util.hpp
└── web
    ├── bak
    ├── css
    ├── images
    ├── index.html
    ├── js
    ├── login.html
    └── register.html -> login.html

首先引入Mongoose框架,核心代码框架如下:

class ImServer{
    private:
        std::string m_port;
        struct mg_mgr mgr;
        struct mg_connection *nc;
        volatile bool quit;
    public:
        ImServer(std::string port = "8080"):m_port(port),quit(false)
        {}
    
        static void EventHandler(mg_connection *nc, int ev, void *data)
        {
            switch(ev){
                case MG_EV_HTTP_REQUEST:
                    break;
                case MG_EV_WEBSOCKET_HANDSHAKE_DONE:
                    break;
                case MG_EV_WEBSOCKET_FRAME:
                	break;
                case MG_EV_CLOSE:
                    break;
                default:                    
                break;
            }
        }
   
        void InitServer()
        {
            mg_mgr_init(&mgr, NULL);
            nc = mg_bind(&mgr, m_port.c_str(), EventHandler);
            mg_set_protocol_http_websocket(nc);
            s_http_server_opts.document_root = "web";
        }
   
        void Start()
        {
            int timeout = 1000000;
            while (!quit){
                mg_mgr_poll(&mgr, timeout);
            }
        }
   
        ~ImServer()
        {
            mg_mgr_free(&mgr);
        }
};

struct mg_mgr结构体是事件管理器,里面包含一个struct mg_connection,struct mg_connection 用于描述连接的状态。
struct mg_connection中的部分内容:

  struct mg_connection *next, *prev; /* mg_mgr::active_connections linkage */
  struct mg_connection *listener;    /* Set only for accept()-ed connections */
  struct mg_mgr *mgr;                /* Pointer to containing manager */
 
  sock_t sock; /* Socket to the remote peer */

InitServer()中:
先对事件管理器mgr进行初始化,接着与事件触发时进行事件处理的回调函数和端口进行绑定,并创建监听连接。最后则指定web根目录路径。
mg_mgr_poll底层使用select监听事件的到来,并调用回调函数来处理。
EventHandler中针对监听到的事件进行处理。
以上就是基于Mongoose框架的核心代码,接下来的工作就是对这个框架进行填充,一一实现设计功能。

聊天功能实现

当进入到Mongoose的聊天页面时,发送聊天消息时会触发事件MG_EV_WEBSOCKET_FRAME,然后对mgr进行遍历,广播这条websocket_message:

case MG_EV_WEBSOCKET_FRAME:{                                                                                                                   
                    struct websocket_message *wm = (struct websocket_message*)data;
                    struct mg_str ms = {(const char*)wm->data, wm->size};
                    std::string msg = Util::mgStrToString(&ms);
                    Broadcast(nc, msg);
                    }  
                    break;

登录注册

登录注册功能需要把用户信息存储在本地,这个工作由Mysql来完成。

数据库连接

class MysqlClient{
    private:    
        MYSQL *my;
    private:    
        bool ConnectMysql()
        {       
            my = mysql_init(NULL);
            mysql_set_character_set(my, "utf8");
            if (!mysql_real_connect(my, "localhost", "root", "", IM_DB, DB_PORT, NULL, 0)){
                std::cerr << "connection mysql error" << std::endl;
                return false;
            }   
            std::cout << "connect mysql success" << std::endl;
                
            return true;
        }       
                
    public:     
        MysqlClient()
        {}  
                
        bool InsertUser(std::string name, std::string passwd)
        {      
            ConnectMysql();
            std::string sql = "INSERT INTO user values(\"";
            sql += name;
            sql += "\", \"";
            sql += passwd;
            sql += "\")";
            std::cout << sql << std::endl;
            int ret = mysql_query(my, sql.c_str());
            if (0 == ret)
            {   
                std::cout << "insert success:  " << ret << std::endl;
                return true;
            }   
                
            std::cout << "insert fail:  " << ret << std::endl;
            return false;
        }       
                
        bool SelectUser(std::string name, std::string passwd)
        {       
            ConnectMysql();
            std::string sql = "SELECT * FROM user WHERE name=\"";
            sql += name;
            sql += "\" AND passwd=\"";
            sql += passwd;
            sql += "\"";
            std::cout << sql << std::endl;
            bool result = false;
            if (0 == mysql_query(my, sql.c_str())){
                MYSQL_RES *res = mysql_store_result(my);
                if (mysql_num_rows(res) > 0){
                    std::cout << "debug...:" << mysql_num_rows(res) << std::endl;
                    result = true;
                }
                free(res);
            }   
                
            mysql_close(my);
            return result;
        }       
                
        ~MysqlClient()
        {       
            mysql_close(my);    
        }       
};              

关于Mysql的基本接口参考别人的博客:C语言连接mysql简单查询实例入门

登录注册功能实现

登陆注册是特定的事件,所以用mg_register_http_endpoint()来为它们注册特定的回调函数。

		static void RegisterHandler(mg_connection *nc, int ev, void* data)
        {              
            std::string code = "0";
            std::string echo_json = "{\"result\": ";
            struct http_message *hm = (struct http_message*)data;
            std::string method = Util::mgStrToString(&(hm->method));
            if (method == "POST"){
                std::string body = Util::mgStrToString(&hm->body);
                std::string name, passwd;
                if (Util::GetNameAndPasswd(body, name, passwd) && !name.empty() && !passwd.empty()){
                    if (mc.InsertUser(name, passwd)){
                        code = "0";
                    }else{
                        code = "1";
                    }   
                }       
                else{   
                    code = "2"; 
                }       
                echo_json += code;
                echo_json += "}";
                mg_printf(nc, "HTTP/1.1 200 OK\r\n");
                mg_printf(nc, "Content-Length: %lu\r\n\r\n", echo_json.size());
                mg_printf(nc, echo_json.data());
            }          
            else{       
                mg_serve_http(nc, hm, s_http_server_opts);
            }       
                       
            nc->flags |= MG_F_SEND_AND_CLOSE;
        }              
                       
        static void LoginHandler(mg_connection *nc, int ev, void *data)
        {              
            if (ev == MG_EV_CLOSE){
                return;
            }         
                       
            std::string code = "0";
            std::string echo_json = "{\"result\": ";
            std::string shead = "";
            struct http_message *hm = (struct http_message*)data;
                       
            std::cout << "loginHandler ev: " << ev << std::endl;
            mg_printf(nc, "HTTP/1.1 200 OK\r\n");
            std::string method = Util::mgStrToString(&(hm->method));
            if (method == "POST"){
                std::string body = Util::mgStrToString(&hm->body);
                //std::cout << "login handler" << body << std::endl;
                std::string name,passwd;
                if (Util::GetNameAndPasswd(body, name, passwd) && !name.empty() && !passwd.empty()){
                    if (mc.SelectUser(name, passwd)){
                        uint64_t id = 0;
                        if (sn.CreateSession(name, id)){
                            std::stringstream ss;
                            ss << "Set-Cookie: " << SESSION_ID << "=" << id << "; path = /\r\n";
                            ss << "Set-Cookie: " << SESSION_NAME << "=" << name << "; path=/\r\n";
                            shead = ss.str();
                            mg_printf(nc, shead.data());
                            code = "0";
                        }else{
                            code = "3";
                        }
                    }  
                    else{
                        code = "1";
                    }  
                }      
                    else{
                        code = "2";
                    }  
                    echo_json += code;
                    echo_json += "}";
                    mg_printf(nc, "Content-Length: %lu\r\n\r\n", echo_json.size());
                    mg_printf(nc, echo_json.data());
                //mg_serve_http(nc, hm, s_http_server_opts);
            }else{     
                    mg_serve_http(nc, hm, s_http_server_opts);
                }      
                nc->flags |= MG_F_SEND_AND_CLOSE;
            }          

缓存机制

缓存机制的实现过程:登录时的请求报文头中如果有cookie,则将其中的SSESSION_ID与本地sessions进行匹配,匹配成功就进入到聊天页面并更新session状态,否则重定向到登录页面。当第一次登录成功时,就生成一份session保留到本地sessions。如果某个session的最近使用时间超过了一定的时间,就将该session销毁。

typedef struct session{     
    uint64_t id;                    
    std::string name;               
    double created;                 
    double last_used;               
}session_t;                        
                               
class Session{  
    private:
        session_t sessions[NUM];   
    public:             
        Session()       
        {                           
            for (auto i = 0; i < NUM; i++){
                sessions[i].id = 0;
                sessions[i].name = "";
                sessions[i].created = 0.0;
                sessions[i].last_used = 0.0;
            }            
        }       
            
        bool IsLogin(http_message *hm)
        {                    
            return GetSession(hm); 
        }                           
                                    
        bool GetSession(http_message *hm)
        {                          
            uint64_t sid;      
            char ssid[64];     
            char *s_ssid = ssid;   
        
            struct mg_str *cookie_header = mg_get_http_header(hm, "cookie");
            if (nullptr == cookie_header){
                return false;      
            }        
        
            if (!mg_http_parse_header2(cookie_header, SESSION_ID, &s_ssid, sizeof(ssid))){
                return false;      
            }                       
                                    
            sid = strtoull(ssid, NULL, 10);
            for (auto i = 0; i < NUM; i++){
                if (sessions[i].id == sid){
                    sessions[i].last_used = mg_time();
                    return true;   
                }                  
            }   
                  
            return false;          
        }        
           
        bool CreateSession(std::string name, uint64_t &id)
        {  
            int i = 0;             
            for (; i < NUM; i++){  
                if (sessions[i].id == 0){
                    break;         
                }
            }    
                 
            if (i == NUM){         
                return false;      
            }    
            sessions[i].id = (uint64_t)(mg_time()*1000000L);
            sessions[i].name = name;
            sessions[i].last_used = sessions[i].created = mg_time();
            id = sessions[i].id;   
                 
            return true;           
        }        
                 
        void DestroySession(session_t *s)
        {        
            s->id = 0;             
        }        
                 
        void CheckSession()        
        {        
            double threadhold = mg_time() - SESSION_TTL;
            for (auto i = 0; i < NUM; i++)
            {    
                if (sessions[i].id > 0 && sessions[i].last_used < threadhold){
                    DestroySession(sessions+i); 
                }
            }    
        }        
                 
        ~Session()
        {        
                 
        }        
};   

jQuery代码

    var user = document.getElementById("name");
    var passwd = document.getElementById("password");
                 
    var ruser = document.getElementById("Name");
    var rpasswd = document.getElementById("rPassword");
    var rrpasswd = document.getElementById("rrPassword");
                 
    function login(){
        $.ajax({ 
            url: "/LH",
            type: "POST",
            data: JSON.stringify({
                name: user.value,
                passwd: passwd.value
            }),  
            dataType: "json",
            contentType: "application/x-www-form-urlencoded; charset=UTF-8",
            success: function(data){
                if (data.result == 1 ){
                    alert("名字已经被占用,请更换名字再试!")
                }
                else if(data.result == 2){
                    alert("请求方法错误!")
                }
                else{
                    window.location.href='index.html'
                }
            },  
            error: function(e){
                alert("请求失败,请稍后再试!")
            }   
        }); 
    } 
    function register(){
        if(rpasswd.value != rrpasswd.value){
            alert("两次密码不一致,请重新输入!")
            return;
        } 
        $.ajax({
            url: "/RH",
            type: "POST",
            data: JSON.stringify({
                name: ruser.value,
                passwd: rpasswd.value
            }), 
            dataType: "json",
            contentType: "application/x-www-form-urlencoded; charset=UTF-8",
            success: function(data){
                if (data.result == 1 ){
                    alert("名字已经被占用,请更换名字再试!")
                }
                else if(data.result == 2){
                    alert("请求方法错误!")
                }
                else{
                    alert("注册成功,请直接登录!")
                    window.location.href='login.html'
                }
            },  
            error: function(e){
                alert("请求失败,请稍后再试!")
            }
        });
      
    } 

扩展性

该项目还可以添加其它的功能,比如说私发消息,朋友圈等等,不过这就对前端要求就很高了,因为我使用的聊天页面是Mongoose库里面自带的,如果要扩展这些功能,就要对聊天的网页进行重构了。服务器这方面基本上就是针对这个事件注册专门的回调函数(就像登录事件和注册事件)。私发消息的处理是建立一个一对一的连接,不再进行消息的广播。朋友圈和群聊有点相似,只不过是把消息写到网页上而不是文本聊天窗口。

项目中遇到的问题

  1. 数据库连接问题,对数据库进行插入时,发现怎么都插不进去数据,查找问题发现数据库连接失败了。但是后台却能连接上数据库,也能对数据库进行操作。另外用指令sudo netstat -nlt查找确定mysql端口是正确的,mysql服务业也开启了。于是把mysql_real_connect中的“localhost”替换为“127.0.0.1”后也不行。最后用gdb调试,到mysql_real_connect这一步,提示找不到client.c文件。就觉得数据库有问题。CentOS默认安装的数据库是mariadb,mariadb是mysql的一个分支。于是把mariadb删掉重新在线安装了mysql。然而问题还是没有解决,网上查看博客,发觉安装的步骤不正确。就把mysql重新卸载了按mysql离线安装的步骤重新安装了一次。运行时成功插入数据到数据库里。
  2. 前端代码问题,jQuery和C++来回切换,jQuery多打了分号。进入开发者模式,点击代码模块,就能在运行时看到报错了,这个问题解决起来还是相对比较容易点。
  3. 这个项目是在虚拟机上完成的,但是windows主机上的浏览器不能访问虚拟机上的web服务器,在主机上发现能ping通虚拟机ip,但在虚拟机上却ping不通主机。上网查找,发现是主机防火墙的配置问题,按照配置步骤配置后,虚拟机能够ping通主机,但主机上的浏览器还是无法访问虚拟机上的web服务器。我想可能是虚拟机上的防火墙问题,于是用指令sudo systemctl stop firewalld关掉了防火墙。主机上的浏览器成功访问虚拟机上的web服务器。

你可能感兴趣的:(基于Mongoose框架的网页通讯)