以前学习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可以参考。
项目文件树:
.
├── 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()
{
}
};
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库里面自带的,如果要扩展这些功能,就要对聊天的网页进行重构了。服务器这方面基本上就是针对这个事件注册专门的回调函数(就像登录事件和注册事件)。私发消息的处理是建立一个一对一的连接,不再进行消息的广播。朋友圈和群聊有点相似,只不过是把消息写到网页上而不是文本聊天窗口。