C++集群聊天服务器 数据模块+业务模块+CMake构建项目 笔记 (上)

跟着施磊老师做C++项目,施磊老师_腾讯课堂 (qq.com)

本文在此篇博客的基础上继续实现数据模块和业务模块代码:

C++集群聊天服务器 网络模块+业务模块+CMake构建项目 笔记 (上)-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/135991635?spm=1001.2014.3001.5501一、mysql 项目数据库和表的设计

myql 项目数据库和表的设计-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/135981407?spm=1001.2014.3001.5501二、mysql数据库代码封装

  • include/public.hpp
cpp 复制代码
#ifndef PUBLIC_H
#define PUBLIC_H
/*
    server和client的公共文件
*/
enum EnMsgType {
    LOGIN_MSG = 1, // 登录消息
    LOGIN_MSG_ACK, // 登录响应消息
    REG_MSG, // 注册消息
    REG_MSG_ACK // 注册响应消息
};
#endif // PUBLIC_H
  • include/server/db/db.h
cpp 复制代码
#ifndef DB_H
#define DB_H

#include <mysql/mysql.h>
#include <string>
using namespace std;

// 数据库操作类
class Mysql {
public:
    // 初始化数据库连接
    Mysql();
    // 释放数据库连接资源
    ~Mysql();
    // 连接数据库
    bool connect();
    // 更新操作
    bool update(string sql);
    // 查询操作
    MYSQL_RES *query(string sql);
    // 获取连接
    MYSQL *getConnection();
private:
    MYSQL *m_conn;
};

#endif // DB_H

src/server/db/db.cpp

cpp 复制代码
#include "db.h"
#include <muduo/base/Logging.h>
// 数据库配置信息
static string server = "127.0.0.1";
static string user = "root";
static string password = "123456";
static string dbname = "chat";

// 初始化数据库连接
Mysql::Mysql() {
    m_conn = mysql_init(nullptr);
    // 这里相当于只是给它开辟了一块存储连接数据的资源空间
}

// 释放数据库连接资源
Mysql::~Mysql() {
    if(m_conn != nullptr) {
        mysql_close(m_conn);
    }
    // 析构的时候把这块资源空间用mysql_close掉
}

// 连接数据库
bool Mysql::connect() {
    MYSQL *p = mysql_real_connect(m_conn,server.c_str(),user.c_str(),
    password.c_str(),dbname.c_str(),3306,nullptr,0);
    if(p!=nullptr) {
        // C和C++代码默认的编码字符是ASCII,如果不设置,
        // 从MYSQL上拉下来的中文显示?
        mysql_query(m_conn, "set names gbk");
        LOG_INFO << "connect mysql success!!!";
    } else{
        LOG_INFO << "connect mysql failed!!!";
    }
    return p;
}

// 更新操作
bool Mysql::update(string sql) {
    if(mysql_query(m_conn, sql.c_str())) {
        LOG_INFO << __FILE__ << ":" << __LINE__ << ":" 
            << sql <<"更新失败!";
        return false;
    }
    return true;
}

// 查询操作
MYSQL_RES* Mysql::query(string sql) {
    if(mysql_query(m_conn, sql.c_str())) {
        LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
            << sql <<"查询失败!";   
        return nullptr;
    }
    return mysql_use_result(m_conn);
}

// 获取连接
MYSQL* Mysql::getConnection() {
    return m_conn;
}

三、Model数据层代码框架设计

  • include/server/user.hpp
cpp 复制代码
#ifndef USER_H
#define USER_H

#include <string>
using namespace std;

// 匹配User表的ORM类
class User {
public:
    User(int id=-1, string name="", string password="", string state="offline") {
        m_id = id;
        m_name = name;
        m_password = password;
        m_state = state;
    }
    void setId(int id) { m_id = id; }
    void setName(string name) { m_name = name; }
    void setPwd(string pwd) { m_password = pwd; }   
    void setState(string state) { m_state = state; }
    
    int getId() const { return m_id; }
    string getName() const { return m_name; }
    string getPwd() const { return m_password; }
    string getState() const { return m_state; }
private:
    int m_id;
    string m_name;
    string m_password;
    string m_state;
};
#endif // USER_H
  • include/server/usermodel.hpp
cpp 复制代码
#ifndef USERMODEL_H
#define USERMODEL_H
#include "user.hpp"
// User表的数据操作类
class UserModel {
public:
    // user表的增加方法
    bool insert(User& user); 
    // 根据用户号码查询用户信息
    User query(int id);
    // 更新用户的状态信息
    bool updateState(User user);
};

#endif // USERMODEL_H
  • src/server/usermodel.cpp
cpp 复制代码
#include "usermodel.hpp"
#include "db.h"
#include <iostream>
// User表的增加方法
bool UserModel::insert(User &user) {
    // 1.组装sql语句
    char sql[1024] = {0};
    std::sprintf(sql,"insert into user(name,password,state) values('%s','%s', '%s')",
         user.getName().c_str(), user.getPwd().c_str(), user.getState().c_str());
    // 2.执行sql语句
    Mysql mysql;
    if(mysql.connect()) {
        if(mysql.update(sql)) {
            // 获取插入成功的用户数据生成的主键id
            user.setId(mysql_insert_id(mysql.getConnection()));
            return true;
        }
    }
    return false;
}

// 根据用户号码查询用户信息
User UserModel::query(int id) {
    // 1.组装sql语句
    char sql[1024] = {0};
    sprintf(sql,"select * from user where id = %d", id);
    // 2.执行sql语句
    Mysql mysql;
    if(mysql.connect()) {
        MYSQL_RES* res = mysql.query(sql);
        if(res != nullptr) {
            MYSQL_ROW row = mysql_fetch_row(res);
            if(row != nullptr) {
                User user;
                user.setId(atoi(row[0]));
                user.setName(row[1]);
                user.setPwd(row[2]);
                user.setState(row[3]);
                // 释放资源
                mysql_free_result(res);
                return user;
            }
        }
    }
    return User();
}

// 更新用户的状态信息
bool UserModel::updateState(User user) {
    // 1.组装sql语句
    char sql[1024] = {0};
    sprintf(sql,"update user set state = '%s' where id = %d",
         user.getState().c_str(), user.getId());
    // 2.执行sql语句
    Mysql mysql;
    if(mysql.connect()) {
        if(mysql.update(sql)) {
            return true;
        }
    }
    return false;
}

四、CMake 构建项目

  • src/server/CMakeLists.txt
cpp 复制代码
# 定义了一个SRC_LIST变量 包含了该目录下所有的源文件
aux_source_directory(. SRC_LIST)
aux_source_directory(./db DB_LIST)

# 指定生成可执行文件
add_executable(ChatServer ${SRC_LIST} ${DB_LIST})

# 指定可执行文件链接时需要依赖的库文件
target_link_libraries(ChatServer muduo_net muduo_base mysqlclient pthread)
  • src/CMakeLists.txt
cpp 复制代码
add_subdirectory(server)
  • 和src,include,thirdparty同级目录的CMakeLists.txt
cpp 复制代码
cmake_minimum_required(VERSION 3.28.0)
project(chat)

# 配置编译选项
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -g)

# 配置可执行文件生成路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

# 配置头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_SOURCE_DIR}/include/server)
include_directories(${PROJECT_SOURCE_DIR}/include/server/db)
include_directories(${PROJECT_SOURCE_DIR}/thirdparty)

# 加载子目录
add_subdirectory(src)

bash 复制代码
cmake -B build
cmake --build build

1.测试注册:

cpp 复制代码
telnet 127.0.0.1 6000
cpp 复制代码
{"msgid":3,"name":"heheda","password":"1024"} // 注册
cpp 复制代码
{"msgid":3,"name":"Tom","password":"520"} // 注册
cpp 复制代码
{"msgid":3,"name":"Jerry","password":"1314"} // 注册

2.测试登录:

(1)未登录

(2) 已经登录

cpp 复制代码
telnet 127.0.0.1 6000
{"msgid":1,"id":4,"password":"1024"}
cpp 复制代码
telnet 127.0.0.1 6000
{"msgid":1,"id":4,"password":"1024"}

(3)登录失败

3.gdb排错练习

比如输入以下这句,其实**"id":5** 才对,但是如果误输入的会引起核心中断,如何排查错误呢?

cpp 复制代码
{"msgid":1,"id":"5","password":"520"}

>>gdb调试,比如我们怀疑可能是chatservice.cpp的20行出错了

cpp 复制代码
heheda@linux:~/Linux/Server$ gdb ./bin/ChatServer
(gdb) break chatservice.cpp 20
(gdb) run
cpp 复制代码
telnet 127.0.0.1 6000

输入:

cpp 复制代码
{"msgid":1,"id":"5","password":"520"}

检查出错误了:

cpp 复制代码
reason: [json.exception.type_error.302] type must be number, but is string

故我们把

cpp 复制代码
{"msgid":1,"id":"5","password":"520"}

修改为以下:
​
{"msgid":1,"id":5,"password":"520"}

总结:客户端发送过来一个注册的业务,先从最开始的网络,再通过事件的分发,到业务层的相关的handler处理注册,接着访问底层的model。其中在业务类设计,这里看到的都是对象,方便你把底层的数据模块改成你想要的,例如mysql,sql,oracle,mongoDB等都行。实现了网络模块,业务模块以及数据模块的低耦合。

五、记录用户的连接信息以及线程安全问题

  • 在ChatService.hpp文件中,private处添加
cpp 复制代码
private:
    // 存储在线用户的通信连接
    unordered_map<int,TcpConnectionPtr> m_userConnMap;
    // 定义互斥锁,保证m_userConnMap的线程安全
    mutex m_connMutex;
  • 修改ChatService.cpp中的login函数,在登录成功,记录用户连接信息,将id和conn数据信息插入m_userConnMap,使用lock_guard使得线程安全
cpp 复制代码
// 处理登录业务  user表:id password
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
    int id = js["id"].get<int>();
    string pwd = js["password"];

    User user = m_userModel.query(id);
    if(user.getId() == id && user.getPwd() == pwd) {
        if(user.getState() == "online") {
            //该用户已经登录,不允许重复登录
            json response;
            response["msgid"] = LOGIN_MSG_ACK;
            response["errno"] = 2;
            response["errmsg"] = "该账号已经登录,请重新输入新账号";
            conn->send(response.dump());
        }
        else{
            // 登录成功,记录用户连接信息
            {
                lock_guard<mutex> lock(m_connMutex);
                m_userConnMap.insert({id, conn});
            }
            // 登录成功,更新用户状态信息 state: offline => online
            user.setState("online");
            m_userModel.updateState(user);

            json response;
            response["msgid"] = LOGIN_MSG_ACK;
            response["errno"] = 0;
            response["id"] = user.getId();
            response["name"] = user.getName();
            conn->send(response.dump());
        }
    }
    else {
        // 该用户不存在/用户存在但是密码错误,登录失败
        json response;
        response["msgid"] = LOGIN_MSG_ACK;
        response["errno"] = 1;
        response["errmsg"] = "该用户不存在,您输入用户名或者密码可能错误!";
        conn->send(response.dump());
    }
}

六、客户端异常退出业务代码和测试

  • 在ChatService.hpp中添加处理客户端异常退出的函数声明
cpp 复制代码
public:
    // 处理客户端异常退出
    void clientCloseException(const TcpConnectionPtr& conn);
  • 在ChatService.cpp中编写处理客户端异常退出的函数
cpp 复制代码
// 处理客户端异常退出
void ChatService::clientCloseException(const TcpConnectionPtr &conn) {
    User user;
    {
        lock_guard<mutex> lock(m_connMutex);   
        for(auto it = m_userConnMap.begin();it!=m_userConnMap.end();++it) {
            if(it->second == conn) {
                // 从map表删除用户的链接信息
                user.setId(it->first);
                m_userConnMap.erase(it);
                break;
            }
        }
    }
    // 更新用户的状态信息
    if(user.getId() != -1) {
        user.setState("offline");
        m_userModel.updateState(user);
    }
   
}

​​​

表里原先有Tom登录用户的信息,然后我们登录了该账号,就从offline状态更新为online状态

按下ctrl+],切换到telnet>,输入quit,此时客户端异常退出,也就执行了从online更新为offline

七、离线消息业务代码实现和测试

  • 如果用户登录成功的话,查询该用户是否有离线消息,desc offlinemessage
  • offlinemessagemodel.hpp
cpp 复制代码
#ifndef OFFLINEMESSAGEMODEL_H
#define OFFLINEMESSAGEMODEL_H
#include <string>
#include <vector>
using namespace std;

// 提供离线消息表的操作接口方法
class OfflineMsgModel {
public:
    // 存储用户的离线消息
    void insert(int userid, string msg);
    // 删除用户的离线消息
    void remove(int userid);
    // 查询用户的离线消息
    vector<string> query(int userid);
};

#endif // OFFLINEMESSAGEMODEL_H
  • offlinemessagemodel.cpp
cpp 复制代码
#include "offlinemessagemodel.hpp"
#include "db.h"
// 存储用户的离线消息
void OfflineMsgModel::insert(int userid, string msg) {
    // 1.组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into offlinemessage values(%d, '%s')", userid, msg.c_str());
    // 2.执行sql语句
    Mysql mysql;
    if(mysql.connect()) {
        mysql.update(sql);
    }
}

// 删除用户的离线消息
void OfflineMsgModel::remove(int userid) {
    // 1.组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "delete from offlinemessage where userid = %d", userid);
    // 2.执行sql语句
    Mysql mysql;
    if(mysql.connect()) {
        mysql.update(sql);
    }
}

// 查询用户的离线消息
vector<string> OfflineMsgModel::query(int userid) {
    // 1.组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "select message from offlinemessage where userid = %d", userid);
    // 2.执行sql语句
    Mysql mysql;
    vector<string> vec;
    if(mysql.connect()) {
        MYSQL_RES *res = mysql.query(sql);
        if(res != nullptr) {
            // 把userid用户的所有离线消息放入vec中返回
            MYSQL_ROW row;
            while((row = mysql_fetch_row(res)) != nullptr) {
                vec.push_back(row[0]);
            }
            mysql_free_result(res);
            return vec;
        }
    }
    return vec;
}
  • 在chatservice.hpp中添加
cpp 复制代码
#include "offlinemessagemodel.hpp"

// 聊天服务器业务类
class ChatService {
private:   
    OfflineMsgModel m_offlineMsgModel;
}
  • chatservice.cpp
cpp 复制代码
// 处理登录业务  user表:id password
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
    int id = js["id"].get<int>();
    string pwd = js["password"];

    User user = m_userModel.query(id);
    if(user.getId() == id && user.getPwd() == pwd) {
        if(user.getState() == "online") {
            //该用户已经登录,不允许重复登录
            json response;
            response["msgid"] = LOGIN_MSG_ACK;
            response["errno"] = 2;
            response["errmsg"] = "该账号已经登录,请重新输入新账号";
            conn->send(response.dump());
        }
        else{
            // 登录成功,记录用户连接信息
            {
                lock_guard<mutex> lock(m_connMutex);
                m_userConnMap.insert({id, conn});
            }
            // 登录成功,更新用户状态信息 state: offline => online
            user.setState("online");
            m_userModel.updateState(user);

            json response;
            response["msgid"] = LOGIN_MSG_ACK;
            response["errno"] = 0;
            response["id"] = user.getId();
            response["name"] = user.getName();
            
            // 查询该用户是否有离线消息
            vector<string> vec = m_offlineMsgModel.query(id);
            if(!vec.empty()) {
                response["offlinemsg"] = vec;
                // 读取该用户的离线消息后,把该用户的所有离线消息删除掉
                m_offlineMsgModel.remove(id);
            }
            conn->send(response.dump());
        }
    }
    else {
        // 该用户不存在/用户存在但是密码错误,登录失败
        json response;
        response["msgid"] = LOGIN_MSG_ACK;
        response["errno"] = 1;
        response["errmsg"] = "该用户不存在,您输入用户名或者密码可能错误!";
        conn->send(response.dump());
    }
}


// 一对一聊天业务
void ChatService::oneChat(const TcpConnectionPtr &conn, json &js, Timestamp time) {
    int toid = js["to"].get<int>();
    {
        lock_guard<mutex> lock(m_connMutex);
        auto it = m_userConnMap.find(toid);
        if(it != m_userConnMap.end()) {
            // toid在线,转发消息  服务器主动推送消息给toid用户
            it->second->send(js.dump());
            return;
        }
    }
    // toid不在线,存储离线消息
    m_offlineMsgModel.insert(toid, js.dump());
}

八、服务器异常退出处理代码和测试

  • main.cpp
cpp 复制代码
#include "chatserver.hpp"
#include "chatservice.hpp"
#include <iostream>
#include <signal.h>
using namespace std;

// 处理服务器ctrl+c结束后,重置user的状态信息
void resetHandler(int) {
    ChatService::getInstance()->reset();
    exit(0);
}

int main() {
    signal(SIGINT,resetHandler);
    ...
}
  • 在chatservice.hpp添加reset()方法声明,服务器异常,业务重置方法
cpp 复制代码
// 服务器异常,业务重置方法
void reset();
  • 在chatservice.cpp中编写reset()方法
cpp 复制代码
// 服务器异常,业务重置方法
void ChatService::reset() {
    // 把online状态的用户,设置成offline
    m_userModel.resetState();
}
  • 在usermodel.hpp中添加重置用户的状态信息resetState方法声明
cpp 复制代码
// 重置用户的状态信息
void resetState();
  • 在usermodel.cpp中编写resetState()方法
cpp 复制代码
// 重置用户的状态信息
void UserModel::resetState() {
    // 1.组装sql语句
    char sql[1024] = "update user set state = 'offline' where state = 'online'";
    // 2.执行sql语句
    Mysql mysql;
    if(mysql.connect()) {
        mysql.update(sql);
    }
}

  • ctrl+c终止服务

九、添加好友业务代码和测试

  • public.hpp
cpp 复制代码
#ifndef PUBLIC_H
#define PUBLIC_H
/*
    server和client的公共文件
*/
enum EnMsgType {
    LOGIN_MSG = 1, // 登录消息
    LOGIN_MSG_ACK, // 登录响应消息
    REG_MSG, // 注册消息
    REG_MSG_ACK, // 注册响应消息
    ONE_CHAT_MSG, // 聊天消息
    ADD_FRIEND_MSG, // 添加好友消息
};
#endif // PUBLIC_H
  • friendmodel.hpp
cpp 复制代码
#ifndef FRIENDMODEL_H
#define FRIENDMODEL_H

#include "user.hpp"
#include <vector>
using namespace std;

// 维护好友信息的操作接口方法
class FriendModel {
public:
    // 添加好友关系
    void insert(int userid, int friendid);
    // 返回用户好友列表 friendid 
    vector<User> query(int userid);
};

#endif // FRIENDMODEL_H
  • friendmodel.cpp
cpp 复制代码
#include "friendmodel.hpp"
#include "db.h"
// 添加好友关系
void FriendModel::insert(int userid, int friendid) {
    // 1.组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into friend values (%d, %d)", userid, friendid);
    // 2.执行sql语句
    Mysql mysql;
    if(mysql.connect()) {
        mysql.update(sql);
    }
}
// 返回用户好友列表 friendid 
vector<User> FriendModel::query(int userid) {
    // 1.组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "select a.id, a.name, a.state from user a inner join friend b on b.friendid = a.id where b.userid = %d", userid);      
    
    vector<User> vec;
    Mysql mysql;
    if(mysql.connect()) {
        MYSQL_RES * res = mysql.query(sql);
        if(res != nullptr) {
            // 把userid用户的所有离线消息放入vec中返回
            MYSQL_ROW row;
            while((row = mysql_fetch_row(res)) != nullptr) {
                User user;
                user.setId(atoi(row[0])); // id
                user.setName(row[1]);     // name
                user.setState(row[2]);    // state
                vec.push_back(user);
            }
            mysql_free_result(res);       // 释放资源
            return vec;
        }
    }
    return vec;
}

// select a.id,a.name,a.state from user a inner join 
// friend b on b.friendid = a.id 
// where b.userid = %d
  • chatservice.hpp
cpp 复制代码
// 聊天服务器业务类
class ChatService {
public:
    // 添加好友业务
    void addFriend(const TcpConnectionPtr& conn,json& js,Timestamp time);
private:
    FriendModel m_friendModel;
}
  • chatservice.cpp
cpp 复制代码
// 注册消息以及对应的Handler回调操作
ChatService::ChatService() {
    m_msgHandlerMap.insert({LOGIN_MSG,std::bind(&ChatService::login, this, _1, _2, _3)});  
    m_msgHandlerMap.insert({REG_MSG,std::bind(&ChatService::reg, this, _1, _2, _3)});  
    m_msgHandlerMap.insert({ONE_CHAT_MSG,std::bind(&ChatService::oneChat, this, _1, _2, _3)});
    m_msgHandlerMap.insert({ADD_FRIEND_MSG,std::bind(&ChatService::addFriend, this, _1, _2, _3)});  
}

// 处理登录业务  user表:id password
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
    int id = js["id"].get<int>();
    string pwd = js["password"];

    User user = m_userModel.query(id);
    if(user.getId() == id && user.getPwd() == pwd) {
        if(user.getState() == "online") {
            //该用户已经登录,不允许重复登录
            json response;
            response["msgid"] = LOGIN_MSG_ACK;
            response["errno"] = 2;
            response["errmsg"] = "该账号已经登录,请重新输入新账号";
            conn->send(response.dump());
        }
        else{
            // 登录成功,记录用户连接信息
            {
                lock_guard<mutex> lock(m_connMutex);
                m_userConnMap.insert({id, conn});
            }
            // 登录成功,更新用户状态信息 state: offline => online
            user.setState("online");
            m_userModel.updateState(user);

            json response;
            response["msgid"] = LOGIN_MSG_ACK;
            response["errno"] = 0;
            response["id"] = user.getId();
            response["name"] = user.getName();
            
            // 查询该用户是否有离线消息
            vector<string> vec = m_offlineMsgModel.query(id);
            if(!vec.empty()) {
                response["offlinemsg"] = vec;
                // 读取该用户的离线消息后,把该用户的所有离线消息删除掉
                m_offlineMsgModel.remove(id);
            }
            // 查询该用户的好友信息并返回
            vector<User>userVec = m_friendModel.query(id);
            if(!userVec.empty()) {
                vector<string> vec2;
                for(User &user : userVec) {
                    json js;
                    js["id"] = user.getId();
                    js["name"] = user.getName();
                    js["state"] = user.getState();
                    vec2.push_back(js.dump());
                }
                response["friends"] = vec2;
            }
            conn->send(response.dump());
        }
    }
    else {
        // 该用户不存在/用户存在但是密码错误,登录失败
        json response;
        response["msgid"] = LOGIN_MSG_ACK;
        response["errno"] = 1;
        response["errmsg"] = "该用户不存在,您输入用户名或者密码可能错误!";
        conn->send(response.dump());
    }
}

// 添加好友业务 msgid id friendid
void ChatService::addFriend(const TcpConnectionPtr &conn, json &js, Timestamp time) {
    int userid = js["id"].get<int>();
    int friendid = js["friendid"].get<int>();
    // 存储好友信息
    m_friendModel.insert(userid, friendid);
}
相关推荐
Fleshy数模19 小时前
CentOS7 安装配置 MySQL5.7 完整教程(本地虚拟机学习版)
linux·mysql·centos
az44yao19 小时前
mysql 创建事件 每天17点执行一个存储过程
mysql
秦老师Q21 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
橘子131 天前
MySQL用户管理(十三)
数据库·mysql
Dxy12393102161 天前
MySQL如何加唯一索引
android·数据库·mysql
我真的是大笨蛋1 天前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怣501 天前
MySQL数据检索入门:从零开始学SELECT查询
数据库·mysql
人道领域1 天前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql
千寻技术帮1 天前
10404_基于Web的校园网络安全防御系统
网络·mysql·安全·web安全·springboot
spencer_tseng1 天前
MySQL table backup
mysql