项目内容
使用开源websocket框架mongoose编写网页版本的群聊即时通信工具,使用http+mongoose+session+mysql+jsoncpp的技术构成,从而实现一个网页版的聊天室。
技术点:C++11 STL、http协议、websocket协议 、session和cookie理解、mysql等
项目的整个逻辑:
这样就可以开始网页版聊天室的聊天了
WebSocket是一种协议,与HTTP协议一样位于应用层,都是TCP/IP协议的子集。HTTP协议是单向通信协议,只有客户端发起HTTP请求,服务端才会返回数据。而WebSocket协议是双向通信协议,在建立连接之后,客户端和服务器都可以主动向对方发送或接受数据。WebSocket协议建立的前提需要借助HTTP协议,建立连接之后,持久连接的双向通信就与HTTP协议无关了。
WebSocket属于服务端推送技术,本质是一种应用层协议,可以实现持久连接的全双工双向通信。
项目目录
IMServer.cc
IMServer.hpp
Util.hpp //工具类
Makefile
mongoose //mongoose框架
mysql
web //前端页面
temp_file //保存临时语音文件目录
主要的逻辑框架
struct session{ //登录用户的session信息数据结构,有部分内容是要写到客户端cookie中的
};
class IM_MysqlClient{
//访问数据库客户端,使用C connect封装 };
class IM_Controller{
//控制器 };
class IM_Server{
//服务器功能 };
下面是源代码:
IMServer.hpp
#pragma once
#include
#include
#include
#include "Util.hpp"
#include "mongoose.h"
#include "mysql.h"
#define IM_DB "im_db" //数据库
#define IM_PORT 3306 //数据库端口号
#define SESSION_TTL 600.0
#define SESSION_CHECK_INTERVAL 5.0
#define NUM 128 //必须是确定大小的数组,不能超过
#define SESSION_COOKIE_NAME "IM"
#define SESSION_COOKIE_USER "NAME"
struct mg_serve_http_opts s_http_server_opts;
struct session{
uint64_t id;
double created;
double last_used;
std::string name;
};
class Session{
private:
session 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 *ssid_p = 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_COOKIE_NAME, &ssid_p, sizeof(ssid))){
return false;
}
//字符串转长整型,16进制
sid = strtoull(ssid, NULL, 16);
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(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].created = sessions[i].last_used = mg_time();
sessions[i].name = name;
sessions[i].id = (uint64_t)(mg_time()*1000000L);//采用时间戳作为session ID
id = sessions[i].id;
return true;
}
void DestroySession(struct session *s)
{
s->id = 0; //只要将id设置为0,即代表该session失效
}
//定期检查session是否超时,超时的话,就移除该session
void CheckSession()
{
//获取当前时间,减去session生命周期
double threadhold = mg_time() - SESSION_TTL;
for(auto i = 0; i < NUM; i++)
{
if(sessions[i].id > 0 && sessions[i].last_used < threadhold)
{
std::cerr << "Sessions: " << sessions[i].id <<" User: " << sessions[i].name << " idle long time ... close " << std::endl;
DestroySession(sessions + i);
}
}
}
~Session()
{
}
};
class MysqlClient{
private:
MYSQL *my;
bool ConnectMysql() //连接数据库
{
mysql_set_character_set(my, "utf8");
if(!mysql_real_connect(my, "localhost", "root", "", IM_DB, IM_PORT, NULL, 0))
//host连接本地主机,user用户,密码,连接数据库名,端口号
{
std::cerr << "connect mysql error " << std::endl;
return false;
}
// mysql_query(my, "setnames 'utf8'");
std::cout << "connect mysql success " << std::endl;
return true;
}
public:
MysqlClient(){
my = mysql_init(NULL); //初始化
// mysql_set_character_set(my, "utf8");
}
bool InsertUser(std::string name, std::string passwd){
ConnectMysql();
std::string sql = "INSERT INTO user (name, passwd) values(\"";
sql += name;
sql += "\",\"";
sql += passwd;
sql += "\")";
std::cout << sql << std::endl;
int ret = mysql_query(my, sql.c_str());
//std::cout << "ret: " << ret << std::endl;
mysql_close(my);
return ret = 0 ? true : 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;
int ret = mysql_query(my, sql.c_str());
bool result = false;
if(ret == 0){
MYSQL_RES *res = mysql_store_result(my); //如果mysql_query返回成功,那么我们就通过mysql_store_result 这个函数来读取结果,res变量主要用于保存查询的结果。同时该函数malloc了一片内存空间来存储查询过来的数据
if(mysql_num_rows(res) > 0){ //获取结果行数
std::cout <<"debug.....: " << mysql_num_rows(res) << std::endl;
result = true;
}
free(res);
}
mysql_close(my);
return result;
}
void InsertMessage();
void SelectMessage();
~MysqlClient()
{
}
};
class IMServer{
private:
std::string port; //服务器端口号
struct mg_mgr mgr; //mongoose 事件管理器 里面有一个链表的头部指针,管理所有的连接,之后可以通过遍历访问
struct mg_connection *nc; //listen socket 对应的connect
volatile bool quit;
static MysqlClient mc;
static Session sn;
public:
IMServer(std::string _port = "8080"):port(_port),quit(false)
{
}
static int IsWebsocket(const struct mg_connection *nc)
{
return nc->flags & MG_F_IS_WEBSOCKET; //检测链接是否是websocket的长链接
}
static void Broadcast(struct mg_connection *nc, std::string msg)
{
struct mg_connection *c;
for(c = mg_next(nc->mgr, NULL); \
c != NULL; c = mg_next(nc->mgr, c)){
//if(c == nc) continue; //不给自己广播
mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.c_str(), msg.size());
}
}
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;
std::string 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\r\n");
mg_printf(nc, "Content-Length: %lu\r\n\r\n", echo_json.size());
mg_printf(nc, echo_json.data());
}
else
{
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 name, passwd;
std::string body = Util::mgStrToString(&(hm->body));
// std::cout << "login handler: " << body << std::endl;
if(Util::GetNameAndPasswd(body, name, passwd) && !name.empty() && !passwd.empty())
{
if(mc.SelectUser(name, passwd))
{
uint64_t id = 0;
if(sn.CreateSession(name, id))
{
stringstream ss;
ss << "Set-Cookie: " << SESSION_COOKIE_NAME << "=" << id << "; path=/\r\n";
ss << "Set-Cookie: " << SESSION_COOKIE_USER << "=" << name << "; path=/\r\n";
std::string 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());
}
else
{
mg_serve_http(nc, hm, s_http_server_opts); //返回登录页面
}
nc->flags |= MG_F_SEND_AND_CLOSE; //相应完毕,完毕链接
}
//回调函数,谁调用谁填充参数
static void EventHandler(mg_connection *nc, int ev, void *data)
{
switch(ev){
//正常的HTTP请求
case MG_EV_HTTP_REQUEST:{
struct http_message *hm = (struct http_message*)data;
std::string uri = Util::mgStrToString(&hm->uri);
if(uri.empty() || uri == "/" || uri == "/index.html"){
if(sn.IsLogin(hm)){
mg_serve_http(nc, hm, s_http_server_opts);
}
else{
mg_http_send_redirect(nc, 302, mg_mk_str("/login.html"), mg_mk_str(NULL));
}
}
else{
mg_serve_http(nc, hm, s_http_server_opts);
}
nc->flags |= MG_F_SEND_AND_CLOSE;
}
break; //重定向完毕,退出事件处理逻辑,关闭链接
//握手成功
case MG_EV_WEBSOCKET_HANDSHAKE_DONE:{
Broadcast(nc, "some body join...");
}
break;
//先提取出内容,再广播给其他人
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;
case MG_EV_CLOSE:
if(IsWebsocket(nc)){
Broadcast(nc, "Someone left...");
}
break;
case MG_EV_TIMER:{
sn.CheckSession();
//能引起超时事件的只有listen绑定的链接
//这里就保证系统能定期检查是否有session超时
mg_set_timer(nc, mg_time() + SESSION_CHECK_INTERVAL);
}
break;
default:
//std::cout << "other ev: " << ev << std::endl;
break;
}
}
void InitServer()
{
signal(SIGPIPE, SIG_IGN);
mg_mgr_init(&mgr, NULL); //初始化事件管理器
nc = mg_bind(&mgr, port.c_str(), EventHandler); //绑定
mg_register_http_endpoint(nc, "/LH", LoginHandler);//注册方法,当访问登录网页时,执行对应动作
mg_register_http_endpoint(nc, "/RH", RegisterHandler); //注册
mg_set_protocol_http_websocket(nc); //让服务器支持websocket
s_http_server_opts.document_root = "web"; //设置属性
mg_set_timer(nc, mg_time() + SESSION_CHECK_INTERVAL);
}
void Start()
{
int timeout = 10000;
while(!quit){
mg_mgr_poll(&mgr, timeout); //监听所关心的事件
// std::cout << "time out ..." << std::endl;
}
}
~IMServer()
{
mg_mgr_free(&mgr);
}
};
MysqlClient IMServer::mc;
//静态变量初始化
Session IMServer::sn;
IMServer.cc
#include "IMServer.hpp"
using namespace std;
static void Usage(string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
std::cout << "Notice: " << "\n\tDefault port: 8080" << "\n\tPort Range : [1024-9999]" << std::endl;
}
static bool IsPortOk(const char* _port, std::string &port_out)
{
int p = atoi(_port);
if(p >= 1024 && p <= 9999){
port_out = _port;
return true;
}
return false;
}
int main(int argc, char *argv[])
{
// printf("mysql client Version: %s\n", mysql_get_client_info());
// MysqlClient *mc = new MysqlClient();
// mc->InsertUser("李四5","1223");
// delete mc;
int ret = 0;
std::string port = "8080";
if((argc == 2 && IsPortOk(argv[1], port)) || argc == 1){
//IMServer *im = new IMServer();
IMServer im(port);
im.InitServer();
im.Start();
}
else{
Usage(argv[0]);
ret = 1;
}
return ret;
}
Util.hpp
#pragma once
#include
#include
#include
#include
#include "mongoose.h"
using namespace std;
class Util{
public:
static string mgStrToString(struct mg_str* str)
{
string msg = "";
for(auto i = 0; i < str->len; i++)
{
msg.push_back(str->p[i]);
}
return msg;
}
static bool GetNameAndPasswd(string info, string &name, string &passwd){
bool result;
JSONCPP_STRING errs;
Json::Value root;
Json::CharReaderBuilder cb;
std::unique_ptr const cr(cb.newCharReader());
result = cr->parse(info.data(), info.data()+info.size(), &root, &errs);
if(!result || !errs.empty())
{
cerr << "parse error" <