网页IM:网页聊天室
框架:不严谨MVC框架
M-model:数据管理
V-view:界面
C-controller:业务控制
详细设计
数据管理模块:
-
数据存储的管理:使用mysql数据库进行存储管理
-
数据访问的管理
- 用户信息:用户ID,用户名,密码(被加密的字符串),昵称,创建时间
- 聊天信息:消息ID,用户ID,消息内容,消息时间
-
对信息进行的操作
-
用户信息操作
- 新增用户
- 用户名密码的验证
- 修改密码
- 删除用户
- 修改昵称
-
聊天信息操作
- 新增消息
- 获取历史消息
-
业务处理模块
-
网络通信服务器模块:使用第三方库(mongoose)来实现,搭建HTTP服务器,实现网络通信。
-
业务处理模块:分析请求,明确客户端请求目的,然后根据具体的业务功能处理。
网络通信接口设计:约定的是请求格式
restful风格的请求(规范性)
restful一套基于HTTP协议的API接口风格
-
GET请求表示获取资源
-
POST请求表示新增资源
-
PUT请求表示修改资源
-
DELETE请求表示删除资
资源路径:要操作的资源
JSON:一种轻量的数据交换格式
-
字符串:使用双引号括起来的数据
-
数字:连续的数字字符
-
数组:使用[]括起来的元素
-
对象:使用{}括起来的元素
示例:小明,男,18岁,大一,数学32,英语54,语文98
{
“姓名”:“小明”,
“年龄”:18,
“年级”:“大一”,
“成绩”:[32,54,98],
“对象”:{“姓名”:“小红”,“年龄”:18…}
}
接口设计:
1.新增用户
2.用户名密码验证—使用常规的html表单数据格式
3.修改密码
4.修改昵称
5.删除用户
因为HTTP协议是一个请求-相应协议:是客户端请求了服务器,服务器进行业务处理返回结果。
但是这种模式不适合在群聊天,因为一个客户端发送的信息,需要服务器主动的推送给其他所有的客户端。
但HTTP并不支持主动推送,无法主动给客户端发送信息。除非客户端不断的请求服务器,每隔1s请求服务器获取一下新消息。
但是这样有缺陷:
1.消息不够及时
2.对服务器的消耗负载过大(不管有无消息,总有大量的客户端与服务器进行通信)
因此,一旦用户成功登录,群聊天就不在使用HTTP协议了,使用websocket协议。websocket协议就是在此需求的基础上,在HTTP协议之上设计的双向长连接通信协议。
服务器的搭建,并不自己实现,使用第三方库:mongoose
消息操作格式约定
1.客户端将要发送的消息发送给服务器
2.历史消息的获取
session管理
在HTTP通信中,维护客户端状态信息,让服务器能够知道当前通信的连接对应的是哪一个客户端。
服务器上为每一个客户端都创建一个session:sid,user,conn…客户端登录成功后,将对应创建的session的ssid通过cookie发送个客户端。
cookie:让客户端保存的信息,在下一次请求服务器的时候发送给服务器,客户端在下一次通信的时候,将ssid通过cookie发送给服务器,服务器在管理所有的session中,找到对应的session中,通过session就能知道当前对应的是哪一个客户端。
session管理:
- 客户端登录成功则建立session
- 客户端关闭连接时删除session
- 获取连接对应的session
前端模块
拿到一个网页模板,然后进行修改,最终完成所需的界面和操作。
开发环境搭建:
-
编译器升级
yum install centos-release-scl-rh centos-release-scl
yum install devtoolset-7-gcc devtoolset-gcc-c++\
source/opt/rh/devtoolset-7/enable
echo"source /opt/rh/devtoolset-7/enable">>~/.bashrc
-
安装jsoncpp
yum -y install epel-release
yum install jsoncpp-devel
-
安装mariadb数据库
sudo yum install -y mariadb mariadb-server mariadb-client mariadb-devel
vi /etc/my.cnf.d/server.cnf
[mysqld]
collation-server = utf8 general ci
character-set-server = utf8
init-connect='SET NAMES utf8!
sql-mode = TRADITIONALvi /etc/my.cnf.d/mysql-clients.cnf
[mysql]
default-character-set=utf8vi /etc/my.cnf.d/client.cnf
[client]
default-character-set = utf8
systemctl restart mariad -
安装mongoose库
第三方库的认识:
jsoncpp:
Json::Value类:json的数据类,用于实例化一个管理所有序列化数据的对象
-
Json::Value val;
[]的重载:val[“姓名”]=“张三”;val[“年龄”]=18
-
append(Value&);数组元素的添加
val[“成绩”].apend(55); val[“成绩”].apend(88);
-
asString()将保存的元素以string形式返回
std:cout<<val[“姓名”].asString()<<std::endl;
-
alsnt()
std::cout<<val[“年龄”].aslnt()<<std::endl;
-
size() 数组成员的元素个数
for(int i=0;i<val[“成绩”].size();i++)
std::cout<<val[“成绩”] [i].asFloat()<<std::endl;
Json序列化:
Json::StreamWriter:
int write(Value const &root,std::ostream *sout)
Json::StreamWriterBuilder
Json::StreamWriter * newStreamWriter();
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
//序列化
void serialize(Json::Value &value, std::string &body)
{
Json::StreamWriterBuilder swb;
// 输出风格
swb["commentStyle"] = "None";
// Json::StreamWriter *sw = swb.newStreamWriter();
// 智能指针
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";
// delete sw;
return;
}
body = ss.str();
// delete sw;
return;
}
int main()
{
Json::Value val;
val["姓名"] = "小明";
val["年龄"] = 18;
val["成绩"].append(45);
val["成绩"].append(79);
val["成绩"].append(67);cl
std::string body;
serialize(val, body);
std::cout << body << std::endl;
return 0;
}
反序列化:
Json::CharReader
bool parse(char *beginDoc,char *endDoc,Value *root,std::string *errs)
Json::CharReaderBuilder
chatReader* newCharReader();
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
// 反序列化
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;
}
int main()
{
Json::Value val;
val["姓名"] = "小明";
val["年龄"] = 18;
val["成绩"].append(45);
val["成绩"].append(79);
val["成绩"].append(67);
std::string body;
serialize(val, body);
std::cout << body << std::endl;
Json::Value stu;
unserialize(body, stu);
std::cout << stu["姓名"].asString() << std::endl;
std::cout << stu["年龄"].asInt() << std::endl;
if (stu["成绩"].isArray())
{
for (int i = 0; i < stu["成绩"].size(); i++)
{
std::cout << stu["成绩"][i].asFloat() << std::endl;
}
}
return 0;
}
mysql数据库的操作
mysql数据库采用的是C/S架构
mysql的操作都是通过sql(structure query language)语句完成的。
库的操作:
在mysql数据库中可以管理多个库,每个库可以单独存储自己的数据,mysql数据库是一个关系型数据库–以行列关系模型管理数据,在每一个库中以表作为存储单元。
-
创建一个库
create database if not exists My_Test;
-
查看mysql都管理哪些库
show databases;
-
选择要操作的库
use My_Test;
-
删除一个库
drop database My_Test;
数据类型:
int 整形
varchar(255) 字符串类型,中间的数字表示字段最大存储的字符个数。
decimal(4,2) 浮点型 4表示总的数字个数,2表示小数点后数字的个数 8.888->8.89 88.888->88.89 888.888->888.8
datatime 日期类型 “2023-04-11 11:38:03” now()表示当前时间
表的操作:
创建表:creat table ifnot exists tbstu()
create table if not exists tbstu(
sn int,(primary key 主键约束:非空且唯一,创建主键索引(加快查询速度)),(auto_increment 自增属性:默认从1开始)
name varchar(32),
age int,
ch decimal(4,2),
en decimal(4,2));
查看库中都有什么表:show tables;
查看指定表的结构:desc tbstu;
删除表:drop table tbstu;
数据操纵:增删改查
增:insert into
insert into tbstu values(null,'张三',18,99.5,39,"2005-6-1 16:58:39");
insert into tbstu values(null,'李四',0,0,0,now());
删:delete
delete from tbstu where name="张三";
改:update
update tbstu set age=40;(默认为所有成员进行修改)
update tbstu set age=20 where name="张三";
查:select
select* from tbstu;
select sn,name,from tbstu;
查询60s内添加的用户
select *from tbstu where timestampdiff(second,brith,now())<60;
通过mysql的API接口,自己实现一个自己所需的客户端,完场数据库操作。
看手册
初始化:
mysql_init
mysql_real_connect
mysql_set_character_set
mysql_select_db
执行语句:
mysql_query
查询结果获取:
mysql_store_result
mysql_num_rows
mysql_num_fields
mysql_num_row
mysql_fecth_row
mysql_free_result
释放:
mysql_close
错误原因获取:
mysql_error
MYSQL *mysql_init(MYSQL *mysql);
功能:初始化mysql操作句柄,如果参数为NULL,则会动态申请空间然后进行初始化,并返回首地址。
返回值:成功返回句柄空间首地址,失败返回NULL
MYSQL* mysql_real_connect(
MYSQL* mysql//mysql初始化返回的句柄首地址,
char *host,//mysql服务器IP地址
char *user,//访问mysql服务器的用户名,通常为root
char *passwd,//访问mysqld服务器的用户密码,""
char *dbname,//默认选择库的名称
int port,//mysql服务器的端口号,默认为0,表示3306
char* unix_socket,//可以为自己指定的套接字文件或管道,通常为NULL
int client_flag//选择标准,通常为0
)
功能:连接mysql服务器
返回值:成功返回句柄首地址,失败返回NULL
int mysql_select(MYSQL *mysql,char* dbname);
功能:选择要操作的库
返回值:成功返回0,失败返回错误编号
int mysql_set_character_set(MYSQL *mysql, char *encode);
功能:设置客户端字符集,最好与服务器保持一致 “utf8”
返回值:成功返回0,失败返回非0
int mysql_query(MYSQL,char* sql);
功能:执行sql语句
返回值:成功返回0,失败返回非0
MYSQL_RES* mysql_store_result(MYSQL* mysql);
功能:将查询结果,保存到本地
返回值:成功返回结果首地址,失败返回NULL
int mysql_num_rows(MYSQL_RES* res);
功能:获取结果集中所有查询结果的调速
返回值:查询结果条数
int mysql_num_fields(MYSQL_RES* res);
功能:获取结果集中一条数据有多少列
MYSQL_ROW mysql_fetch_row(MYSQL_RES* res)
功能:逐条取出结果集中每一条数据的查询结果
MYSQL_ROW char **(二级指针)
int mysql_free_result(MYSQL_RES *res);
功能:释放结果集
11…
int mysql_close(MYSQL *mysql);
释放mysql的操作句柄
char *mysql_error(MYSQL *mysql);
功能:通过句柄获取将上一次mysql接口操作失败的原因字符串
#include <iostream>
#include <mysql/mysql.h>
int main()
{
// 初始化句柄
MYSQL *mysql = mysql_init(NULL);
if (mysql == NULL)
{
std::cout << "mysql init failed!\n";
return -1;
}
// 连接服务器
// mysql_real_connect(mysql,host,user,passwd,dbname,port,socket,flag)
if (mysql_real_connect(mysql, "127.0.0.1", "root", "", "db888", 0, NULL, 0) == NULL)
{
std::cout << "connect server failed: " << mysql_errno(mysql) << std::endl;
return -1;
}
// 设置字符集
mysql_set_character_set(mysql, "utf8");
// 选择数据库
// mysql_select_db(mysql,"db888");
// 执行语句
// 插入://const char *sql = "insert into tbstu values(null,'孙八',19,65,73,now());";
// 修改://const char* sql="update tbstu set age=100 where sn =2";
// 删除://const char *sql = "delete from tbstu where name='李四'";
// 查询:
const char *sql = "select * from tbstu;";
int ret = mysql_query(mysql, sql);
if (ret != 0)
{
std::cout << sql << std::endl;
std::cout << "query faild!"
<< " " << mysql_error(mysql) << std::endl;
return -1;
}
// 保存结果集到本地
MYSQL_RES *res = mysql_store_result(mysql);
if (res == NULL)
{
std::cout << "store result failed!"
<< " " << mysql_error(mysql) << std::endl;
return -1;
}
// 获取结果集条数,列数,
int num_row = mysql_num_rows(res);
int num_col = mysql_num_fields(res);
// 根据条数和列数,遍历结果集
for (int i = 0; i < num_row; i++)
{
MYSQL_ROW row = mysql_fetch_row(res);
for (int j = 0; j < num_col; j++)
{
printf("%s\t", row[j]);
}
printf("\n");
}
// 释放结果集
mysql_free_result(res);
mysql_close(mysql);
return 0;
}
makefile文件要链接库
mysql:mysql.cpp
g++ -g -std=c++11 $^ -o $@ -L/usr/lib64/mysql/ -ljsoncpp -lmysqlclient
json:json.cpp
g++ -std=c++11 $^ -o $@ -ljsoncpp
使用mongoose库搭建HTTP服务器以及Websocket服务器
http服务器收到的请求有两种:
静态资源请求:首页index.html
动态的功能请求:登录验证
一个用于搭建服务器的库:
不需要让用户关系服务器如何搭建,用户只关心针对收到的请求如何处理。
mongoose库中只需要设置的点主要有两个:
-
静态资源根目录:告诉服务器静态资源文件存放在哪个目录下
当服务器收到了静态资源请求,就会到目录下找到文件,读取数据进行响应。
-
功能回调函数:告诉服务器功能性请求使用哪个函数进行处理
当服务器收到功能性请求,只需要调用回调函数即可
enum {
MG_EV_ERROR, // Error char *error_message
MG_EV_OPEN, // Connection created NULL
MG_EV_POLL, // mg_mgr_poll iteration uint64_t *uptime_millis
MG_EV_RESOLVE, // Host name is resolved NULL
MG_EV_CONNECT, // Connection established NULL
MG_EV_ACCEPT, // Connection accepted NULL
MG_EV_TLS_HS, // TLS handshake succeeded NULL
MG_EV_READ, // Data received from socket long *bytes_read
MG_EV_WRITE, // Data written to socket long *bytes_written
MG_EV_CLOSE, // Connection closed NULL
MG_EV_HTTP_MSG, // HTTP request/response struct mg_http_message *
MG_EV_HTTP_CHUNK, // HTTP chunk (partial msg) struct mg_http_message *
MG_EV_WS_OPEN, // Websocket handshake done struct mg_http_message *
MG_EV_WS_MSG, // Websocket msg, text or bin struct mg_ws_message *
MG_EV_WS_CTL, // Websocket control msg struct mg_ws_message *
MG_EV_MQTT_CMD, // MQTT low-level command struct mg_mqtt_message *
MG_EV_MQTT_MSG, // MQTT PUBLISH received struct mg_mqtt_message *
MG_EV_MQTT_OPEN, // MQTT CONNACK received int *connack_status_code
MG_EV_SNTP_TIME, // SNTP time received uint64_t *epoch_millis
MG_EV_USER // Starting ID for user events
};
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 head; // Request + headers
struct mg_str chunk; // Chunk for chunked encoding, or partial body
struct mg_str message; // Request + headers + body
};
mongoose库:
单线程+多路转接模型实现的服务器
提前给监听连接设置好回调函数–业务处理回调函数
开始监控事件
1.如果监听连接触发了可读事件,–过去新连接,给新连接设置回调函数(就是给监听套接字设置回调函数–业务处理函数),然后给新连接有加入到事件监控中
2.如果通信套接字触发了可读事件–
1.接受socket数据
2.对数据进行解析
3.调用设置的回调函数进行
数据库访问操作的封装:
思想:针对每一张表,都封装一个类
一个类实例化的对象,可以提供对指定表中的数据的访问操作
表:
用户信息表:ID,用户名,昵称,密码,创建时间
新增用户,用户登录,密码修改,昵称修改,删除用户,
用户信息的获取:通过用户名,通过用户ID
聊天信息表:
新增聊天信息:
获取历史聊天信息(30s):
数据库访问操作的封装:
连接成功并初始化,执行语句,关闭句柄
JsonUtil
{
serialize(value,body);//序列化
unserialize(value,bdy);//反序列化
}
MysqlUtil
{
mysql_create(host,user,pass,db,port)
{
//1.初始化
mysql_real_connect();//与系统上的MYSQL服务器建立连接
mysql_ecex(mysql,sql);
mysql_query(mysql,sql.c_ctr());//查询
//成功返回0,失败返回错误编号
//2.设置字符集
}
}
UserTable
{
private:
MYSQL* _mysql;
mutex _mutex;
public:
UserTable(host,user,pass,db,port)
{
_mysql=MysqlUtil::mysql_create();
}
~UserTable();
insert(const::Vaule &user)
{
//将格式化的输出写入缓冲区中
snprintf(sql,4095,INSERT_USER,
user["..."].aCSting()/*获取c语言字符串集*/...);
return MysqlUtil::mysql_esec(mysql,sql);
}
select_by_id()
{
snprintf(...);
{
//加锁,lock对象被释放后,会自动给解锁
//加锁的意义:下面两步操作不是原子操作,二义性
std::unique_lock<std::mutex> lock(_mutex);
/*1.*/
bool ret=MysqlUtil::mysql_exec(_mysql,sql);
/*2.*/
//返回MYSQL_RES结果集,失败返回NULL
res=mysql_store_result(_mysql);
}
}
}
MsgTable
{
//操作与UserTable类似
}
bt 调用函数栈
break 设置断点 run
p显示变量内容
业务处理模块
-
服务器的搭建:mogoose
-
定义服务器句柄
struct mg_mgr mgr;
-
对句柄进行初始化
mg_mgr_init(struct mg_mgr* mgr);
-
给句柄设置要绑定的监听地址,以及业务处理的回调函数
mg_http_listen(struct mg_mgr* mgr,const char* addr,void(*entry)(struct mg_connection *c,int ev,void *msg,void *data),void *data);
-
开始服务器监听
mg_mgr_poll(struct mg_mgr *mgr,int ms);
-
监听套接字的连接
HTTP连接
session:
0.登陆时候创建一个session
1.获取用户信息时,获取session(通过session_id来获取用户相关信息)
2.协议切换升级:HTTP-websocket
session的删除:
之前:在连接关闭时删除,但如果是短链接,将session删除后,客户端在建立一个连接,服务器端依然不知道客户端是谁,这样是不合理的,
所以,session与连接是解耦合的,两者是无关的,session创建的目的是让客户端的状态和连接脱离关系,因为http的短连接是无法通过持续的连接获取客户端状态,创建之后,不能再连接断开后。
解决:定时器
=mysql_store_result(_mysql);
}
}
}
MsgTable
{
//操作与UserTable类似
}
bt 调用函数栈
break 设置断点 run
p显示变量内容
------
### 业务处理模块
1. 服务器的搭建:mogoose
1. 定义服务器句柄
```C
struct mg_mgr mgr;
```
2. 对句柄进行初始化
```c
mg_mgr_init(struct mg_mgr* mgr);
```
3. 给句柄设置要绑定的监听地址,以及业务处理的回调函数
```C
mg_http_listen(struct mg_mgr* mgr,const char* addr,void(*entry)(struct mg_connection *c,int ev,void *msg,void *data),void *data);
```
4. 开始服务器监听
```C
mg_mgr_poll(struct mg_mgr *mgr,int ms);
```
监听套接字的连接
HTTP连接
session:
0.登陆时候创建一个session
1.获取用户信息时,获取session(通过session_id来获取用户相关信息)
2.协议切换升级:HTTP-websocket
session的删除:
之前:在连接关闭时删除,但如果是短链接,将session删除后,客户端在建立一个连接,服务器端依然不知道客户端是谁,这样是不合理的,
所以,session与连接是解耦合的,两者是无关的,session创建的目的是让客户端的状态和连接脱离关系,因为http的短连接是无法通过持续的连接获取客户端状态,创建之后,不能再连接断开后。
解决:定时器