文章目录
项目名称:网页公共聊天室
项目简介:搭建HTTP服务器,实现网页版的网络聊天室,具有用户的注册,登录以及聊天功能。
项目实现框架图:
项目概要设计:
首先基于MVC框架实现,设计三个模块:
1、数据管理模块:基于Mysql数据库进行用户信息管理;
2、业务处理模块:基于Mongoose库搭建HTTP服务器,处理登录注册请求以及切换WebSocket协议进行聊天;
3、前端界面模块:基于vue.js以及网页模板完成前端页面的操作功能
/
环境搭建:
1.g++编译器的升级
sudo yum install centos-release-scl-rh centos-release-scl
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-C++
source /opt/rh/devtoolset-7/enable
echo “source /opt/rh/devtoolset-7/enable” > > ~/.bashrc
2.安装Mysql数据库
按照教程自己进行安装即可
3.安装mongoose http库
从github上clone:
git clone https://github.com/cesanta/mongoose.git
(如果克隆不下来就下载压缩包再给虚拟机复制就好)
4.安装jsoncpp库
sudo yum -y install epel-release
sudo yum install jsoncpp-devel
项目的模块设计(详细):
数据管理模块:
.
要管理的数据:用户信息,用户ID,用户名(账号),密码,状态(上线||下线),用户创建时间,最后一次下线时间(最后一次状态改变时间)
.
如何管理:基于Mysql数据库进行管理
表的设计:
creat table if not exists im_user(
id int primary key auto_increment comment '用户ID',
name varchar(32) comment '用户名',
pass varchar(128) comment '加密后的密码串',
status int comment '状态, 0-下线,1-上线',
ctime datetime comment '用户创建时间' ,
stime datetime comment '用户最后一次状态改变时间');
.
要实现的功能:针对数据进行统一管理
用户新增,登录验证,获取用户状态,修改用户状态,修改用户密码
MYSQL *MysqlInit();
void MysqlDestory(MYSQL *mysql);
bool MysqlQuery(MYSQL *mysql,const std::string &sql);
class UserTable{
private:
MYSQL *_mysql;
public:
bool Insert(const std::string &name, const std::string &pass);//新增用户
bool UserPassCheck(const std::string &name, const std::string &pass);//用户名密码验证
int Status(int user_id);//获取用户状态,在线返回true,不在线返回false
bool UpdateStatus(int user_id, int status);//修改用户状态
bool UpdatePasswd(const std::string &name, const std::string &pass);//修改密码
}
业务处理模块
.
1、搭建HTTP服务器,能够接受客户端的请求
基于mongoose库搭建服务器
.
2、针对客户端的请求进行业务处理
0、静态页面请求
1、用户注册功能(用户名是否已占用功能,数据新增功能)
2、用户登录功能(查看用户状态,用户名密码验证,修改用户状态)
3、网络聊天功能(消息广播–将消息逐个发送给所有聊天室客户端)
我们搭建的服务器是一个HTTP服务器,然而HTTP协议是一个简单的请求-响应协议,也就是说服务器是针对请求进行响应的,这时候如果有个客户端发送了一条消息,我们如何将消息发送给其他客户端
.
Ps:
1、每个客户端不断循环向服务器发送获取新消息功能(效率差,占用资源多,对服务器负载大),因此HTTP协议本质上并不适合用于这种广播聊天场景,因为它无法主动推送消息。
2、因此我们要在HTTP协议的基础上进行协议切换,切换为websocket协议进行聊天消息的传输
.
websocket协议是一个基于tcp的全双工通信协议,支持服务器主动推送数据,能更好的节省服务器资源和带宽,并且能够更实时的进行通讯
Websocket通过HTTP/1.1协议的101状态码进行握手
struct mg_mgr{
struct mg_connection *conns;//活跃的连接链表--里边包含了所有的tcp新建套接字以及监听套接字
void *userdata;//用户数据指针
};//事件内务结构体
void mg_mgr_init(struct mg_mgr *mgr);用于初始化事件结构
struct mg_connection{
struct mg_connection *next;//下一个节点的链表指针
struct mg_mgr *mgr;//事件内务结构体指针
void *fd;//套接字信息
mg_event_handler_t fn;//用户指定的时间处理函数
void *fn_data;//用户指定的回调函数参数
unsigned is_websocket:1;//当前连接标志,判断是否是一个websocket连接
};
struct mg_connection*mg_http_listen(struct mg_mgr*mgr,const char*url,mg_event_handler_t fn,void *fn_data);
ngr:mg_mgr_init初始化的句柄
url:服务器监听的地址信息ip:port
fn:事件回调函数---当某个连接接收到了数据,将使用这个函数进行处理
fn_data:给回调函数传入的一个用户数据
返回值:成功返回创建成功的HTTP连接
如果有哪个链接有数据到来就会调用这个函数
void(*mg_event_handler_t)(struct mg_connection *conn, int ev, void*ev_data, void *fn_data);
conn:传入时当前有数据到来的连接
ev:当前连接所触发的事件
ev_data:如果连接是一个http连接,则ev_data存储的就是http请求信息
struct mg_http_message{
struct mg_str method,uri,query,proto; //Request/response line
struct mg_http_header headers[MG_MAX_HTTP_HEADERS]; //Headers
struct mg_str_body; //Body
struct mg_str message; //Request line + headers + body
};
mg_mgr_poll(struct mg_mgr*mgr, int timeout)
开始所有连接的监听,内部使用的是poll完成所有套接字的事件监控,当描述符有数据,则这时候读取数据,进行解析,调入传入的回调函数
mongoose这个库并没有用到多线程和多进程,而是使用多路转接模型,进行IO事件监控,然后逐个连接进行请求处理。
mongoose库的处理流程:
1、初始化struct mg_mgr句柄结构(句柄中包含了一个连接链表,里边就是所有的活跃连接)
2、调用mg_http_listen()接口,内部创建了一个http连接–struct mg_connection,并且设置了fn成员为传入的回调函数,设置了pfn成员为http_cb回调函数
3、调用mg_mgr_poll()接口开始监控,内部是使用poll-多路转接模型进行io事件监控
当某个连接IO就绪:
若连接状态是listen状态,则获取新连接,添加到mg_mgr的链表中,并且给这个连接的fn和pfn成员也赋值回调函数
若状态是可读,非listen状态,则读取数据,放到封装的接收缓冲区中,调用pfn接口进行http解析
然后再调用fn成员函数----用户设置的回调函数(内部就是针对不用请求进行不同处理的过程)
回调函数处理完毕后,将连接的发送缓冲区中的数据进行响应给客户端
网络通信接口的设计:
什么样的数据是一个什么样的请求
HTTP请求类型:
.
静态页面请求
用户注册请求
用户登录请求
协议切换请求
WebSocket通信:
.
接收数据,进行广播
.
业务处理模块代码设计:搭建网络通信服务器,进行业务处理
class IMServer{
private:
struct mg_mgr_mgr; //mongoose服务器句柄
static UserTable *_user_table; //数据管理对象
static void cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) //针对不同请求进行不同处理
public:
IMServer(){
//句柄初始化,数据管理对象的构造
mg_mgr_init(&_mgr);
_user_table = new UserTable();
}
~IMServer(){
//句柄释放,数据管理对象释放
bool RunModule(int port){
mg_http_listen(&mgr, "0.0.0.0:19000", cb, &_mgr);
}
};
}
总结:
1、session的管理:最好实现一个session的单例类,进行session的管理
2、使用ajax进行用户登录,不太合适,因为无法进行页面的正常重定向,需要手动重载chat.html页面,建议使用html的原生表单数据提交功能
项目源码
1、sql文件的导入(mysql-uroot -p):
1 create database if not exists duck;
2
3 use duck;
4
5 create table if not exists im_user(
6 id int primary key auto_increment,
7 name varchar(32) not null,
8 pass varchar(128) not null,
9 status int comment '0-下线,1-在线',
10 ctime datetime,
11 stime datetime
12 );
~
2、序列化反序列化测试:
vi util.hpp
#include<iostream>
#include<sstream>
#include<string>
#include<jsoncpp/json/json.h>
namespace im_sys{
class JsonUtil{
public:
static bool Serialize(Json::Value &value,std::string *jsonstr)
{
std::stringstream ss;
Json::StreamWriterBuilder swb;
Json::StreamWriter *sw = swb.newStreamWriter();
int ret = sw->write(value,&ss);
if(ret !=0)
{
std::cout<<"json writer failed!\n";
delete sw;
return true;
}
*jsonstr = ss.str();
delete sw;
return true;
}
static bool UnSerialize(const std::string &jsonstr,Json::Value *value)
{
Json::CharReaderBuilder crb;
Json::CharReader *cr = crb.newCharReader();
std::string err;
bool ret = cr->parse(jsonstr.c_str(),jsonstr.c_str() + jsonstr.size(),value,&err);
if(ret == false)
{
delete cr;
std::cout<<"json parse failed!"<<err<<std::endl;
return false;
}
delete cr;
return true;
}
};
}
vi json_test.cpp
#include "util.hpp"
int main()
{
std::string str = R"({"username":"wj","password":"1111"})";
Json::Value val;
im_sys::JsonUtil::UnSerialize(str,&val);
std::cout<<val["username"].asString()<<std::endl;
std::cout<<val["password"].asString()<<std::endl;
Json::Value err;
err["result"]=false;
err["reason"] = "用户名已经被占用!";
std::string buf;
im_sys::JsonUtil::Serialize(err,&buf);
std::cout<<buf<<std::endl;
return 0;
}
server.hpp(注册等功能的实现):
#include "data.hpp"
#include "mongoose.h"
#include "util.hpp"
namespace im_sys{
#define SERVER_PORT 20000
class Server
{
private:
static struct mg_mgr *_mgr;
static UserTable *_user;
private:
static void callback(struct mg_connection *c,int ev,void *ev_data,void *fn_data)
{
if(ev == MG_EV_HTTP_MSG)
{
struct mg_http_message *hm = (struct mg_http_message *)ev_data;
if(mg_http_match_uri(hm,"/register"))
{
// mg_http_reply(c,200,"","register success!");
// return;
//拿到请求信息的正文,是json格式的用户信息,进行解析
Json::Value user_info;
JsonUtil::UnSerialize(hm->body.ptr,&user_info);
std::string username = user_info["username"].asString();
std::string password = user_info["password"].asString();
//在数据库中进行查看用户名是否已经被占用
bool ret = _user->Exists(username);
if(ret == true)
{
Json::Value err;
err["result"] = false;
err["reason"] = "用户名已经被占用";
std::string body;
JsonUtil::Serialize(err,&body);
std::string header = "Content-Type:application/json\r\n";
mg_http_reply(c,400,header.c_str(),body.c_str());
return;
}
//返回结果--注册成功或失败
ret = _user->Insert(username,password);
if(ret == false)
{
Json::Value err;
err["result"] = false;
err["reason"] = "用户插入数据库失败!";
std::string body;
JsonUtil::Serialize(err,&body);
std::string header = "Content-Type: application/json\r\n"; mg_http_reply(c,200,header.c_str(),body.c_str());
return;
}
Json::Value err;
err["result"] = true;
err["reason"] = "注册成功!";
std::string body;
JsonUtil::Serialize(err,&body);
std::string header = "Content-Type:application/json\r\n"; mg_http_reply(c,200,header.c_str(),body.c_str());
return;
}
}
}
public:
Server()
{
_mgr = new struct mg_mgr();
_user = new UserTable();
}
~Server()
{
mg_mgr_free(_mgr);
delete _mgr;
delete _user;
}
bool RunModule(int port = SERVER_PORT)
{
std::string addr = "0.0.0.0:";
addr += std::to_string(port);
auto res = mg_http_listen(_mgr,addr.c_str(),callback,_mgr);
if(res ==NULL)
{
std::cout<<"init http listen failed!\n";
return false;
}
while(1)
{
mg_mgr_poll(_mgr,1000);
}
return true;
}
};
struct mg_mgr *Server::_mgr = NULL;
UserTable * Server::_user = NULL;
}
data.hpp:
#ifndef __M_IM_DATA_H__
#define __M_IM_DATA_H__
#include<iostream>
#include<string>
#include<cstdlib>
#include<mutex>
#include<mysql/mysql.h>
namespace im_sys{
#define DB_HOST "127.0.0.1"
#define DB_USER "root"
#define DB_PASS "1111"
#define DB_NAME "duck"
static MYSQL *MysqlInit()
{
MYSQL *mysql = mysql_init(NULL);
if(mysql == NULL)
{
std::cout<<"mysql init failed!\n";
return NULL;
}
if(mysql_real_connect(mysql,DB_HOST,DB_USER,DB_PASS,DB_NAME,0,NULL,0) == NULL)
{
std::cout<<"connect mysql server failed:"<<mysql_error(mysql)<<std::endl;
mysql_close(mysql);
return NULL;
}
if(mysql_set_character_set(mysql,"utf8") != 0)
{
std::cout<<"set mysql client character failed!:"<<mysql_error(mysql)<<std::endl;
mysql_close(mysql);
return NULL;
}
return mysql;
}
static void MysqlDestory(MYSQL *mysql)
{
if(mysql)
{
mysql_close(mysql);
}
return;
}
static bool MysqlQuery(MYSQL *mysql,const std::string &sql)
{
int ret = mysql_query(mysql,sql.c_str());
if(ret != 0)
{
std::cout<<sql<<std::endl;
std::cout<<"query failed:"<<mysql_error(mysql)<<std::endl;
return false;
}
return true;
}
class UserTable{
private:
MYSQL *_mysql;
std::mutex _mutex;
public:
UserTable():_mysql(NULL)
{
_mysql = MysqlInit();
if(_mysql == NULL)
{
exit(-1);
}
}
~UserTable()
{
MysqlDestory(_mysql);
}
bool Insert(const std::string &name,const std::string &pass)
{
#define USER_INSERT "insert im_user values(null,'%s',MD5('%s'),0,now(),now());"
char sql[4096] = {0};
sprintf(sql,USER_INSERT,name.c_str(),pass.c_str());
return MysqlQuery(_mysql,sql);
}
bool Exists(const std::string &name)
{
#define USER_EXISTS "select id from im_user where name='%s';"
char sql[4096] = {0};
sprintf(sql,USER_EXISTS,name.c_str());
bool ret = MysqlQuery(_mysql,sql);
if(ret == false)
{
return false;
}
MYSQL_RES *res = mysql_store_result(_mysql);
int num = mysql_num_rows(res);
if(num!=0)
{
mysql_free_result(res);
return true;
}
mysql_free_result(res);
return false;
}
bool UserPassCheck(const std::string &name,const std::string &pass)
{
#define USER_CHECK "select id from im_user where name='%s' and pass=MD5('%s');"
char sql[4096] = {0};
sprintf(sql,USER_CHECK,name.c_str(),pass.c_str());
bool ret = MysqlQuery(_mysql,sql);
if(ret == false)
{
return false;
}
MYSQL_RES *res = mysql_store_result(_mysql);
int num = mysql_num_rows(res);
if(num!=0)
{
mysql_free_result(res);
return true;
}
mysql_free_result(res);
return false;
}
int Status(const std::string &name)
{
#define USER_STATUS "select status from im_user where name='%s';"
char sql[4096]={0};
sprintf(sql,USER_STATUS,name.c_str());
bool ret = MysqlQuery(_mysql,sql);
if(ret == false)
{
return false;
}
MYSQL_RES *res = mysql_store_result(_mysql);
int num = mysql_num_rows(res);
if(num == 0)
{
mysql_free_result(res);
return -1;
}
MYSQL_ROW row = mysql_fetch_row(res);
int status = std::stoi(row[0]);
mysql_free_result(res);
return status;
}
bool UpdateStatus(const std::string &name,int status)
{
#define USER_UPDATE_STATUS "update im_user set status=%d,stime=now() where name='%s';"
char sql[4096] = {0};
sprintf(sql,USER_UPDATE_STATUS,status,name.c_str());
return MysqlQuery(_mysql,sql);
}
bool UpdatePasswd(const std::string &name,const std::string &pass)
{
#define USER_UPDATE_PASS "update im_user set pass=MD5('%s') where name='%s';"
char sql[4096] = {0};
sprintf(sql,USER_UPDATE_PASS,pass.c_str(),name.c_str());
return MysqlQuery(_mysql,sql);
}
};
}
#endif
main.cpp:
#include "data.hpp"
#include "server.hpp"
#define OFFLINE 0
#define ONLINE 1
void DataTest()
{
im_sys::UserTable * _user = new im_sys::UserTable();
插入用户
_user->Insert("鸭鸭","111111");
测试用户是否存在
bool ret = _user->Exists("鸭鸭");
std::cout<<ret<<std::endl;
判断用户是否在线
int status = _user->Status("鸭鸭");
if(status ==ONLINE)
{
std::cout<<"online user!\n";
}
else if(status == OFFLINE)
{
std::cout<<"offline user!\n";
}
修改用户状态
_user->UpdateStatus("鸭鸭",OFFLINE);
int status = _user->Status("鸭鸭");
if(status ==ONLINE)
{
std::cout<<"online user!\n";
}
else if(status == OFFLINE)
{
std::cout<<"offline user!\n";
}
检验密码
bool ret = _user->UserPassCheck("鸭鸭","111111");
if(ret == false)
{
std::cout<<"用户名密码错误!\n";
}
else
{
std::cout<<"login success!\n";
}
}
修改密码
_user->UpdatePasswd("王五","1111");
int ret =_user->UserPassCheck("王五","1111");
if(ret == false)
{
std::cout<<"用户名密码错误!\n";
}
else
{
std::cout<<"login success!\n";
}
}
void ServerTest()
{
im_sys::Server *server = new im_sys::Server();
server->RunModule();
return;
}
int main()
{
DataTest();
ServerTest();
return 0;
}
makefile:
main:data.hpp main.cpp
g++ -std=c++11 $^ -o $@ -L/usr/lib64/mysql -lmysqlclient -L./lib -lmongoose -ljsoncpp
server.hpp
#include "data.hpp"
#include "mongoose.h"
#include "util.hpp"
#include <unordered_map>
#include <time.h>
namespace im_sys{
#define SERVER_PORT 20000
#define OFFLINE 0
#define ONLINE 1
#define WWWROOT "./wwwroot/"
struct IMSession
{
int status;
std::string session_id;//系统时间
std::string username;
time_t ctime;//会话创建时间
time_t ltime;//最后操作时间
struct mg_connection *conn;//当前用户对一个的mongoose连接
};
class Server
{
private:
static struct mg_mgr *_mgr;
static UserTable *_user;
static std::unordered_map<std::string,IMSession> _session;
private:
static std::string ConResp(bool res,const std::string &info)
{
Json::Value err;
err["result"] = res;
err["reason"] = info;
std::string body;
JsonUtil::Serialize(err,&body);
return body;
}
static std::string CreateIMSession(struct mg_connection *c,const std::string &username)
{
struct IMSession s;
s.conn = c;
s.username = username;
s.status = ONLINE;
s.ctime = time(NULL);
s.ltime = time(NULL);
s.session_id = std::to_string(time(NULL));
_session[s.session_id] = s;
return s.session_id;
}
static void Login(struct mg_connection *c,struct mg_http_message *hm)
{
//拿到请求正文,进行json反序列化得到用户名和密码
Json::Value user_info;
JsonUtil::UnSerialize(hm->body.ptr,&user_info);
std::string username = user_info["username"].asString();
std::string password = user_info["password"].asString();
//在数据库中验证用户名和密码
bool ret = _user->UserPassCheck(username,password);
if(ret == false)
{
std::string body = ConResp(false,"用户名密码错误");
std::string header = "Content-Type: application/json\r\n";
mg_http_reply(c,400,header.c_str(),body.c_str());
return;
}
//判断用户是否已经在线,若已经在线,则不能重复登录
ret = _user->Status(username);
if(ret == ONLINE)
{
std::string body = ConResp(false,"用户已登录");
std::string header = "Content-Type: application/json\r\n";
mg_http_reply(c,400,header.c_str(),body.c_str());
return;
}
//没在线,则修改用户为在线状态,并且返回登录成功,设置SetCookie包括用户名、用户状态
std::string ssid = CreateIMSession(c,username);
ret = _user->UpdateStatus(username,ONLINE);
if(ret == false)
{
std::string body = ConResp(false,"修改用户状态失败");
std::string header = "Content-Type: application/json\r\n";
mg_http_reply(c,500,header.c_str(),body.c_str());
return;
}
//登录成功需要让客户端去请求一个聊天页面
std::stringstream headers;
headers<< "Location: /chat.html\r\n";
headers <<"Set-Cookie: SSID=" <<ssid<<";Path=/\r\n";
std::string body = ConResp(true,"登录成功");
mg_http_reply(c,302,headers.str().c_str(),body.c_str());
return;
}
static void Register(struct mg_connection *c,struct mg_http_message *hm)
{
//拿到请求信息的正文,是json格式的用户信息,进行解析
Json::Value user_info;
JsonUtil::UnSerialize(hm->body.ptr,&user_info);
std::string username = user_info["username"].asString();
std::string password = user_info["password"].asString();
//在数据库中进行查看用户名是否已经被占用
bool ret = _user->Exists(username);
if(ret == true)
{
Json::Value err;
err["result"] = false;
err["reason"] = "用户名已经被占用";
std::string body;
JsonUtil::Serialize(err,&body);
std::string header = "Content-Type:application/json\r\n";
mg_http_reply(c,400,header.c_str(),body.c_str());
return;
}
//返回结果--注册成功或失败
ret = _user->Insert(username,password);
if(ret == false)
{
Json::Value err;
err["result"] = false;
err["reason"] = "用户插入数据库失败!";
std::string body;
JsonUtil::Serialize(err,&body);
std::string header = "Content-Type: application/json\r\n";
mg_http_reply(c,200,header.c_str(),body.c_str());
return;
}
Json::Value err;
err["result"] = true;
err["reason"] = "注册成功!";
std::string body;
JsonUtil::Serialize(err,&body);
std::string header = "Content-Type:application/json\r\n";
mg_http_reply(c,200,header.c_str(),body.c_str());
return;
}
static bool GetSession(struct mg_connection *c,IMSession *session)
{
auto it = _session.begin();
for(;it!=_session.end();++it)
{
if(it->second.conn == c)
{
*session = it->second;
return true;
}
}
return false;
}
static bool DelSession(IMSession &session)
{
auto it = _session.find(session.session_id);
if(it == _session.end())
{
std::cout<<"not find session\n";
return false;
}
_session.erase(it);
return true;
}
static void ConnClose(struct mg_connection *c)
{
//找到连接对应的session
IMSession session;
bool ret = GetSession(c,&session);
if(ret == false)
{
std::cout<<"have no session info!\n";
return;
}
//修改数据库中的用户状态
ret = _user->UpdateStatus(session.username,OFFLINE);
if(ret == false)
{
std::cout<<"update status offline failed!\n";
return;
}
//删除会话信息
DelSession(session);
}
static void callback(struct mg_connection *c,int ev,void *ev_data,void *fn_data)
{
if(ev == MG_EV_HTTP_MSG)
{
struct mg_http_message *hm = (struct mg_http_message *)ev_data;
if(mg_http_match_uri(hm,"/register"))
{
// mg_http_reply(c,200,"","register success!");
// return;
Register(c,hm);
}
else if(mg_http_match_uri(hm,"/login"))
{
//登录请求
Login(c,hm);
}
else if(mg_http_match_uri(hm,"/cws"))
{
//协议切换请求
}
else
{
//静态页面请求
struct mg_http_serve_opts opts = {.root_dir = WWWROOT};
mg_http_serve_dir(c,hm,&opts);
}
}
else if(ev == MG_EV_WS_OPEN)
{
//websocket握手成功
}
else if(ev == MG_EV_WS_MSG)
{
//收到聊天消息
}
else if(ev == MG_EV_CLOSE)
{
//表示连接断开
ConnClose(c);
}
}
public:
Server()
{
_mgr = new struct mg_mgr();
_user = new UserTable();
}
~Server()
{
mg_mgr_free(_mgr);
delete _mgr;
delete _user;
}
bool RunModule(int port = SERVER_PORT)
{
std::string addr = "0.0.0.0:";
addr += std::to_string(port);
auto res = mg_http_listen(_mgr,addr.c_str(),callback,_mgr);
if(res ==NULL)
{
std::cout<<"init http listen failed!\n";
return false;
}
while(1)
{
mg_mgr_poll(_mgr,1000);
}
return true;
}
};
struct mg_mgr *Server::_mgr = NULL;
UserTable * Server::_user = NULL;
std::unordered_map<std::string,IMSession>Server::_session;
}