基于嵌入式C++、SQLite、MQTT、Modbus和Web技术的工业物联网网关:从边缘计算到云端集成的全栈解决方案设计与实现

一、项目概述

1.1 项目目标与用途

随着工业4.0时代的到来,传统工业设备与现代信息技术的结合越来越紧密。物联网工业网关作为连接工业设备与云端平台的桥梁,在工业自动化、设备监控、远程运维等方面发挥着至关重要的作用。本项目旨在设计并实现一个能够适用于工业环境的物联网工业网关,支持多种通信协议和云平台集成,满足工业环境中的数据采集、边缘计算、安全管理等需求。

项目目标:

  • 设计一个稳定、可靠的工业网关设备,用于工业数据采集与处理。

  • 支持多种工业通信协议(如Modbus TCP/RTU, CAN, RS-232/485等)。

  • 提供边缘计算能力,实时处理和分析数据。

  • 支持主流云平台的集成,便于远程监控与管理。

  • 确保设备具备高安全性,能够抵御常见的网络威胁。

项目用途:

  • 实现工业设备的远程监控与管理,提高生产效率。

  • 通过边缘计算减少对云端的依赖,降低延迟和带宽消耗。

  • 提供灵活的接口,便于集成到现有的工业自动化系统中。

二、系统架构

2.1 系统架构设计

系统架构的设计需要充分考虑工业环境的需求,包括硬件平台的选择、通信协议的支持、数据处理与存储、安全性、云服务集成等。以下是该系统的整体架构图。
Modbus TCP/RTU, CAN, RS-232/485 边缘计算与数据处理 MQTT, HTTP/HTTPS, OPC UA RESTful API, gRPC Web管理 状态监控 工业设备 物联网工业网关 本地存储 云平台 第三方系统 远程管理界面 日志与监控

系统组件说明:

  • 工业设备: 通过不同的工业通信协议连接到网关,传输数据。

  • 物联网工业网关: 核心组件,负责与工业设备通信、进行数据处理和边缘计算,并将数据传输到云平台或第三方系统。

  • 本地存储: 用于存储经过处理的数据,或在网络连接中断时提供数据缓冲。

  • 云平台: 用于远程数据存储、分析和可视化。支持MQTT、HTTP/HTTPS、OPC UA等协议。

  • 第三方系统: 通过RESTful API或gRPC与网关进行数据交互,实现更多的集成应用。

  • 远程管理界面: 提供Web管理界面,便于用户对网关进行配置和管理。

  • 日志与监控: 实时监控网关状态,并记录系统日志以供故障排查。

2.2 硬件平台选择

网关的硬件平台选择需要考虑工业环境的可靠性和扩展性,通常包括以下组件:

  • 处理器: 选择工业级的ARM Cortex-A系列处理器,能够支持边缘计算需求。

  • 通信接口: 提供丰富的接口如RS-232/485、CAN、以太网等,以支持多种工业协议。

  • 存储: 配备足够的闪存和RAM,用于操作系统、应用程序和数据缓存。

  • 电源管理: 考虑到工业环境的电源条件,选择支持宽电压输入的电源管理模块。

2.3 软件技术栈

  • 操作系统: 选择Linux或实时操作系统(如FreeRTOS),根据实际需求决定。

  • 通信协议栈: 提供对Modbus TCP/RTU, CAN, MQTT, OPC UA等协议的支持。

  • 编程语言: 使用C/C++实现底层驱动与通信协议;Python或JavaScript用于Web管理界面开发。

  • 数据库: 使用SQLite或嵌入式NoSQL数据库用于本地数据存储。

  • 安全性: 实现TLS/SSL加密,使用防火墙和入侵检测系统(IDS)来保护系统安全。

三、环境搭建

3.1 软件环境

在开发和部署物联网工业网关的过程中,需要设置适当的开发环境和工具链,以确保项目的顺利进行。以下是所需的软件环境及其配置步骤:

3.1.1 操作系统
  • 主机操作系统: 建议使用Ubuntu 20.04或更高版本的Linux系统,因其对嵌入式开发和开源工具的广泛支持。

  • 目标操作系统: 物联网工业网关选择运行嵌入式Linux(如Yocto或Buildroot生成的定制Linux系统)或实时操作系统(如FreeRTOS)。

3.1.2 开发工具链
  • 编译器: 安装GCC交叉编译工具链,适用于ARM架构的设备。

    bash 复制代码
    sudo apt-get install gcc-arm-linux-gnueabi
  • 构建系统: 使用CMake或Makefile管理项目构建。

    bash 复制代码
    sudo apt-get install cmake
  • 调试工具: 安装GDB调试器和OpenOCD,用于调试嵌入式设备。

    bash 复制代码
    sudo apt-get install gdb openocd
3.1.3 必要库和依赖
  • 通信协议库: 安装libmodbus、can-utils等库,用于实现Modbus和CAN通信。

    bash 复制代码
    sudo apt-get install libmodbus-dev can-utils
  • 网络库: 安装libcurl、libmosquitto等库,用于HTTP/HTTPS和MQTT通信。

    bash 复制代码
    sudo apt-get install libcurl4-openssl-dev libmosquitto-dev
  • 数据库: 安装SQLite库,用于本地数据存储。

    bash 复制代码
    sudo apt-get install libsqlite3-dev
3.1.4 集成开发环境(IDE)
  • 推荐使用VS Code或CLion作为开发环境,配合适当的插件进行远程开发和调试。

  • VS Code: 可通过Remote-SSH插件在嵌入式设备上进行远程开发。

  • CLion: 支持CMake项目,可以直接配置远程开发和调试。

3.2 硬件环境

3.2.1 开发板选择
  • 开发板类型: 选择Raspberry Pi 4、BeagleBone Black或类似的ARM Cortex-A系列开发板进行原型开发。

  • 接口模块: 根据需求选择带有RS-232/485、CAN、以太网等接口的扩展板。

3.2.2 电源
  • 电源规格: 开发板通常需要5V或12V电源,建议使用稳定的工业级电源适配器,确保设备长时间稳定运行。
3.2.3 传感器与外设
  • 传感器连接: 根据实际应用选择温度、湿度、压力等传感器,并通过RS-485或CAN总线与网关连接。

  • 存储设备: 使用工业级SD卡或eMMC模块,确保数据存储的可靠性和持久性。

3.3 环境安装步骤与配置

3.3.1 编译工具链安装
  1. 下载并安装适用于目标硬件平台的交叉编译工具链。

    sudo apt-get install gcc-arm-linux-gnueabi
    
  2. 设置交叉编译环境变量,确保编译器能够正确找到目标平台的库和头文件。

    export CC=arm-linux-gnueabi-gccexport CXX=arm-linux-gnueabi-g++
    
3.3.2 开发环境配置
  1. 配置CMake: 在项目根目录中创建CMakeLists.txt文件,配置项目的编译选项和依赖库。

    txt 复制代码
    cmake\_minimum\_required(VERSION 3.10)
    project(IndustrialGateway)
    
    set(CMAKE\_CXX\_STANDARD 14)
    
    include_directories(/usr/include/modbus)
    include_directories(/usr/include/mosquitto)
    
    add_executable(IndustrialGateway main.cpp)
    target\_link\_libraries(IndustrialGateway modbus mosquitto sqlite3)

3.3.2 开发环境配置

  1. 编译与运行: 使用CMake生成Makefile,然后编译项目。
  • 首先,在项目的根目录下创建一个名为 build 的目录,用于存放编译生成的文件。

    bash 复制代码
    mkdir buildcd build
  • 接着,使用CMake命令生成Makefile文件:

    bash 复制代码
    cmake ..

    cmake .. 命令会根据你在 CMakeLists.txt 中的配置生成Makefile文件。如果一切配置正确,你应该会看到CMake成功生成构建文件的消息。

  • 生成Makefile后,使用 make 命令开始编译项目:

    bash 复制代码
    make

    编译成功后,会在 build 目录下生成一个可执行文件(假设在 CMakeLists.txt 中指定了 IndustrialGateway 作为输出文件名)。

  • 编译完成后,可以将生成的可执行文件部署到目标设备(如Raspberry Pi或BeagleBone Black)上,或者直接在开发板上运行。如果是在开发主机上进行的交叉编译,则需要将可执行文件通过 scp 命令复制到目标设备上:

    bash 复制代码
    scp IndustrialGateway user@target\_device\_ip:/path/to/deploy
  • 在目标设备上,进入部署目录并运行可执行文件:

    bash 复制代码
    ./IndustrialGateway
  • 运行后,网关程序会开始与连接的工业设备进行通信,处理数据,并将数据上传到配置的云平台或第三方系统。

3.3.3 远程调试配置(可选)

远程调试对于嵌入式开发非常重要,以下是如何通过GDB和VS Code进行远程调试的步骤:

  1. 在目标设备上启动GDB Server:
  • 在目标设备上,使用GDB启动调试服务:

    bash 复制代码
    gdbserver :1234 /path/to/IndustrialGateway
  • 这将启动GDB Server,监听目标设备上的1234端口。

  1. 在开发主机上配置VS Code进行远程调试:
  • 打开VS Code,安装 C/C++Remote - SSH 插件。

  • 在VS Code中配置 .vscode/launch.json 以支持远程调试:

    bash 复制代码
    {
    "version": "0.2.0",
    "configurations": [
    {
      "name": "Remote Debug",
      "type": "cppdbg",
      "request": "launch",
      "program": "/path/to/IndustrialGateway",
      "args": [],
      "stopAtEntry": false,
      "cwd": "${workspaceFolder}",
      "environment": [],
      "externalConsole": false,
      "MIMode": "gdb",
      "setupCommands": [
        {
          "description": "Enable pretty-printing for gdb",
          "text": "-enable-pretty-printing",
          "ignoreFailures": true
        }
      ],
      "miDebuggerServerAddress": "target_device_ip:1234",
      "miDebuggerPath": "/usr/bin/gdb-multiarch",
      "serverReadyAction": {
        "pattern": "Listening on port (\\d+)",
        "uriFormat": "http://localhost:%s",
        "action": "openExternally"
      },
      "preLaunchTask": "build",
      "pipeTransport": {
        "pipeProgram": "ssh",
        "pipeArgs": ["user@target_device_ip"],
        "debuggerPath": "/usr/bin/gdb",
        "pipeCwd": ""
      },
      "sourceFileMap": {
        "/build": "${workspaceFolder}"
      }
    }
     ]
    }
    • 配置完成后,在VS Code中启动调试,会连接到目标设备的GDB Server,允许你在开发主机上进行断点调试、变量查看等操作。

3.3.4 配置示例和注意事项

3.3.4.1 配置示例

以下是一个 CMakeLists.txt 配置示例,用于编译一个集成了Modbus和MQTT通信的网关应用:

cpp 复制代码
cmake_minimum_required(VERSION 3.10)
project(IndustrialGateway)

# 设置C++标准
set(CMAKE_CXX_STANDARD 14)

# 包含头文件路径
include_directories(/usr/include/modbus)
include_directories(/usr/include/mosquitto)

# 添加源文件
add_executable(IndustrialGateway main.cpp modbus_handler.cpp mqtt_handler.cpp)

# 链接库
target_link_libraries(IndustrialGateway modbus mosquitto sqlite3)

在这个配置示例中:

  • include_directories 用于指定项目需要的头文件路径,如Modbus和Mosquitto库的头文件。

  • add_executable 用于添加项目的源代码文件,这里包括 main.cppmodbus_handler.cppmqtt_handler.cpp

  • target_link_libraries 用于指定项目需要链接的库,如 modbusmosquittosqlite3

3.3.4.2 注意事项

在搭建开发环境和配置项目时,有若干关键点需要注意,以避免常见的错误和问题。

  1. 交叉编译工具链的选择:
  • 确保选择的交叉编译工具链与目标硬件平台的架构相匹配。如果目标设备是ARM架构,使用 gcc-arm-linux-gnueabigcc-arm-linux-gnueabihf 之类的工具链。

  • 如果目标设备使用64位架构(如ARM64),请使用相应的工具链如 gcc-aarch64-linux-gnu

  1. 依赖库的安装:
  • 在开发主机上安装的库版本应与目标设备上的库版本匹配,避免出现运行时库版本不兼容的问题。

  • 如果在目标设备上运行时出现库缺失问题,检查库是否已正确安装,并确保其路径在运行时可访问。

  1. 路径配置:
  • CMakeLists.txt 中指定的头文件路径和库路径应与实际安装路径一致。可以使用 find_packagepkg-config 等工具自动查找依赖库,避免手动指定路径时出错。

  • 如果使用了自定义路径安装的库,确保在 CMakeLists.txt 中正确配置了 include_directorieslink_directories

  1. 调试配置:
  • 在远程调试配置中,确保正确配置了 miDebuggerServerAddresspipeTransport,以便VS Code能够正确连接到目标设备上的GDB Server。

  • 如果调试连接失败,检查目标设备上的GDB Server是否正在运行,并确保网络连接正常。

  1. 安全性:
  • 在生产环境中,确保所有网络通信使用加密协议(如TLS/SSL)以保护数据安全。

  • 定期更新操作系统和库,修补已知的安全漏洞。

  1. 部署和测试:
  • 在部署到生产环境之前,务必在开发环境中进行充分的测试,特别是在不同网络条件下的稳定性测试。

  • 使用日志和监控工具实时监控系统状态,迅速发现并处理潜在问题。

四、代码实现

4.1 功能模块设计

根据前面的系统架构设计,物联网工业网关的代码实现将按照以下几个主要功能模块进行开发:

  1. Modbus通信模块:负责与工业设备进行Modbus TCP/RTU通信,采集设备数据。

  2. MQTT通信模块:负责将采集的数据发布到云平台或其他系统,支持MQTT协议。

  3. 边缘计算模块:在网关设备本地对采集的数据进行初步处理和分析。

  4. 存储模块:将数据存储到本地SQLite数据库中,支持断网情况下的数据缓存。

  5. 远程管理模块:提供Web管理界面,用于配置和监控网关状态。

4.2 关键代码示例

4.2.1 Modbus通信模块

以下是一个简单的Modbus TCP通信的示例代码,用于从工业设备读取数据:

cpp 复制代码
#include <modbus.h>
#include <iostream>

class ModbusHandler {
public:
    ModbusHandler(const std::string& ip, int port) {
        ctx = modbus_new_tcp(ip.c_str(), port);
        if (ctx == nullptr) {
            std::cerr << "Unable to create Modbus context" << std::endl;
            exit(1);
        }
    }

    ~ModbusHandler() {
        modbus_close(ctx);
        modbus_free(ctx);
    }

    void connect() {
        if (modbus_connect(ctx) == -1) {
            std::cerr << "Connection failed: " << modbus_strerror(errno) << std::endl;
            exit(1);
        }
    }

    int readRegisters(int addr, int nb, uint16_t* dest) {
        int rc = modbus_read_registers(ctx, addr, nb, dest);
        if (rc == -1) {
            std::cerr << "Failed to read registers: " << modbus_strerror(errno) << std::endl;
        }
        return rc;
    }

private:
    modbus_t* ctx;
};

int main() {
    ModbusHandler modbus("192.168.0.1", 502);
    modbus.connect();

    uint16_t data[10];
    int rc = modbus.readRegisters(0, 10, data);
    if (rc > 0) {
        for (int i = 0; i < rc; i++) {
            std::cout << "Register " << i << ": " << data[i] << std::endl;
        }
    }

    return 0;
}

在这个示例中,ModbusHandler 类封装了Modbus TCP的连接和数据读取操作。main 函数中实例化该类并读取设备的寄存器数据。

4.2.2 MQTT通信模块

以下是一个简单的MQTT客户端示例代码,用于将数据发布到MQTT Broker:

cpp 复制代码
#include <mosquitto.h>
#include <iostream>
#include <cstring>

class MQTTHandler {
public:
    MQTTHandler(const std::string& broker, int port) {
        mosquitto_lib_init();
        mosq = mosquitto_new(NULL, true, NULL);

        if (!mosq) {
            std::cerr << "Failed to create mosquitto instance" << std::endl;
            exit(1);
        }

        if (mosquitto_connect(mosq, broker.c_str(), port, 60) != MOSQ_ERR_SUCCESS) {
            std::cerr << "Failed to connect to broker" << std::endl;
            exit(1);
        }
    }

    ~MQTTHandler() {
        mosquitto_destroy(mosq);
        mosquitto_lib_cleanup();
    }

    void publish(const std::string& topic, const std::string& message) {
        int ret = mosquitto_publish(mosq, NULL, topic.c_str(), message.size(), message.c_str(), 0, false);
        if (ret != MOSQ_ERR_SUCCESS) {
            std::cerr << "Failed to publish message: " << mosquitto_strerror(ret) << std::endl;
        }
    }

private:
    mosquitto* mosq;
};

int main() {
    MQTTHandler mqtt("broker.hivemq.com", 1883);

    mqtt.publish("industrial/data", "Sensor data: 1234");

    return 0;
}

4.2.3 边缘计算模块

边缘计算模块的主要任务是在网关设备上对采集到的数据进行初步处理和分析,以减少上行数据量,降低云平台的负载,并实现实时性要求。以下是一个简单的边缘计算示例代码,用于对采集到的传感器数据进行过滤和平均计算:

cpp 复制代码
#include <iostream>
#include <vector>
#include <numeric>

class EdgeCompute {
public:
    // 简单的移动平均滤波器
    double movingAverageFilter(const std::vector<double>& data, int windowSize) {
        if (data.size() < windowSize) {
            std::cerr << "Insufficient data for the specified window size" << std::endl;
            return 0.0;
        }
        
        double sum = 0.0;
        for (size_t i = data.size() - windowSize; i < data.size(); ++i) {
            sum += data[i];
        }
        
        return sum / windowSize;
    }

    // 数据范围检测
    bool isDataInRange(double value, double min, double max) {
        return value >= min && value <= max;
    }
};

int main() {
    EdgeCompute edgeCompute;

    // 模拟一组传感器数据
    std::vector<double> sensorData = {23.5, 24.0, 23.8, 24.2, 23.9, 24.1, 23.7};

    // 使用移动平均滤波器处理数据,窗口大小为3
    double filteredData = edgeCompute.movingAverageFilter(sensorData, 3);
    std::cout << "Filtered Data (Moving Average): " << filteredData << std::endl;

    // 判断过滤后的数据是否在合理范围内
    double minThreshold = 23.0;
    double maxThreshold = 25.0;

    if (edgeCompute.isDataInRange(filteredData, minThreshold, maxThreshold)) {
        std::cout << "Data is within the acceptable range." << std::endl;
    } else {
        std::cout << "Data is out of range!" << std::endl;
    }

    return 0;
}

说明:

  • 移动平均滤波器:这个简单的算法用于平滑传感器数据,减少噪声。movingAverageFilter 函数接受一组数据和一个窗口大小,计算该窗口内数据的平均值。

  • 数据范围检测:isDataInRange 函数用于检测数据是否在预定的范围内,如果数据超出范围,可以触发告警或进一步处理。

在实际应用中,边缘计算模块可以更复杂,包括但不限于:

  • 数据聚合:对多个传感器的数据进行聚合处理(如求和、求平均等)。

  • 异常检测:使用机器学习模型或统计方法检测异常数据。

  • 事件驱动处理:根据特定的条件触发事件(如设备故障报警)。

4.2.4 存储模块

存储模块的任务是将采集到的数据存储到本地数据库中,以便在网络连接中断或者需要历史数据时进行访问。以下是一个使用SQLite数据库存储传感器数据的简化示例:

cpp 复制代码
#include <sqlite3.h>
#include <iostream>
#include <string>

class Storage {
public:
    Storage(const std::string& dbname) {
        int rc = sqlite3_open(dbname.c_str(), &db);
        if (rc) {
            std::cerr << "Can't open database: " << sqlite3_errmsg(db) << std::endl;
            exit(1);
        }
    }

    ~Storage() {
        sqlite3_close(db);
    }

    void createTable() {
        const char* sql = "CREATE TABLE IF NOT EXISTS SensorData ("
                          "ID INTEGER PRIMARY KEY AUTOINCREMENT,"
                          "Timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,"
                          "Value REAL);";
        executeSQL(sql);
    }

    void insertData(double value) {
        std::string sql = "INSERT INTO SensorData (Value) VALUES (" + std::to_string(value) + ");";
        executeSQL(sql.c_str());
    }

private:
    sqlite3* db;

    void executeSQL(const char* sql) {
        char* errMsg = 0;
        int rc = sqlite3_exec(db, sql, 0, 0, &errMsg);
        if (rc != SQLITE_OK) {
            std::cerr << "SQL error: " << errMsg << std::endl;
            sqlite3_free(errMsg);
        }
    }
};

int main() {
    Storage storage("industrial_gateway.db");

    storage.createTable();
    double sensorValue = 24.5; // 模拟读取到的传感器数据
    storage.insertData(sensorValue);

    std::cout << "Sensor data inserted into the database successfully." << std::endl;

    return 0;
}

说明:

  • SQLite 数据库:Storage 类封装了SQLite数据库的基本操作,包括数据库连接、表创建和数据插入。

  • 数据表:createTable 方法用于创建一个名为 SensorData 的表,其中包含 ID(主键)、Timestamp(自动生成的时间戳)和 Value(传感器数据)。

  • 数据插入:insertData 方法用于将传感器数据插入到 SensorData 表中。

在实际应用中,存储模块可能包含更多功能,如:

  • 数据查询:支持按时间范围查询历史数据。

  • 数据清理:定期清理旧数据以节省存储空间。

  • 事务处理:确保在写入数据时的原子性和一致性,避免数据损坏。

4.2.5 远程管理模块

远程管理模块提供Web管理界面,允许用户通过网络远程配置和监控网关设备的状态。以下是一个简单的Web服务器示例,使用C++的cpp-httplib库来实现一个基本的HTTP服务器:

cpp 复制代码
#include <httplib.h>
#include <iostream>

class WebServer {
public:
    WebServer(int port) : server(), port(port) {}

    void start() {
        server.Get("/", [](const httplib::Request& req, httplib::Response& res) {
            res.set_content("Welcome to the Industrial Gateway Web Interface!", "text/plain");
        });

        server.Get("/status", [](const httplib::Request& req, httplib::Response& res) {
            res.set_content("Gateway Status: Running", "text/plain");
        });

        std::cout << "Starting web server on port " << port << std::endl;
        server.listen("0.0.0.0", port);
    }

private:
    httplib::Server server;
    int port;
};

int main() {
    WebServer webServer(8080);
    webServer.start();

    return 0;
}

说明:

  • Web 服务器:WebServer 类使用 cpp-httplib 库实现了一个简单的HTTP服务器,监听指定端口。

  • 路由处理:在代码中定义了两个简单的路由,一个是根路径 /,另一个是 /status,分别返回欢迎信息和网关状态。

  • 启动服务器:start 方法启动Web服务器,监听所有网络接口上的指定端口。

在实际应用中,远程管理模块可能需要更复杂的功能,如:

  • 用户认证:通过身份验证和授权确保只有授权用户才能访问管理界面。

  • 设备配置:提供界面供用户配置网关的各种参数(如通信协议、云平台连接信息等)。

  • 实时监控:通过Web界面实时显示设备的运行状态和数据,如CPU使用率、内存占用、网络流量等。

五、项目总结

5.1 项目主要功能

本项目通过设计并实现一个物联网工业网关,成功集成了多种功能模块,具体包括:

  • 工业设备通信:支持多种工业通信协议(如Modbus TCP/RTU、CAN、RS-232/485),实现了工业设备的数据采集。

  • 边缘计算:在网关设备上实现了基本的边缘计算能力,对采集的数据进行初步处理和分析。

  • 数据存储:使用本地SQLite数据库存储采集的数据,支持断网情况下的数据缓存和历史数据查询。

  • 云平台集成:通过MQTT协议将数据上传到云平台,实现远程监控和数据分析。

  • 远程管理:提供了一个简单的Web管理界面,支持远程配置和监控网关状态。

相关推荐
逐·風4 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
Devil枫4 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
尚梦5 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子5 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山6 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青花瓷6 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
毕业设计制作和分享6 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis