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);
}
相关推荐
i道i4 小时前
MySQL win安装 和 pymysql使用示例
数据库·mysql
Oak Zhang5 小时前
sharding-jdbc自定义分片算法,表对应关系存储在mysql中,缓存到redis或者本地
redis·mysql·缓存
久醉不在酒7 小时前
MySQL数据库运维及集群搭建
运维·数据库·mysql
WindFutrue7 小时前
使用Mybatis向Mysql中的插入Point类型的数据全方位解析
数据库·mysql·mybatis
一只爱撸猫的程序猿8 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
计算机毕设源码qq-38365310418 小时前
(附项目源码)Java开发语言,215 springboot 大学生爱心互助代购网站,计算机毕设程序开发+文案(LW+PPT)
java·开发语言·spring boot·mysql·课程设计
袁庭新8 小时前
Cannal实现MySQL主从同步环境搭建
java·数据库·mysql·计算机·java程序员·袁庭新
爱学习的白杨树9 小时前
MySQL中有哪几种锁?
数据库·mysql
Stara051112 小时前
Git推送+拉去+uwsgi+Nginx服务器部署项目
git·python·mysql·nginx·gitee·github·uwsgi
不爱学习的啊Biao12 小时前
初识mysql数据库
数据库·mysql·oracle