在ubuntu下开发c++程序,捣鼓开发环境就花了不少时间。功能弄好以后,怎么部署到安装到安装了统信(UOS)的服务器上让我犯难。虽然都是linux操作系统,但极大概率,将ubuntu下发布出来的程序,原原本本拷贝到uos会跑不起来。一想到要在uos上再部署一大堆环境就心烦意乱。
后来想到用docker部署。首先在开发机器上构建docker镜像;然后导出该镜像,拷贝到服务器;导入镜像;创建容器。半天工夫就搞好了。优势十分明显。对服务器来说,无须安装运行环境,污染少,没有副作用;对于程序来说,它运行在容器里,与宿主机有所隔离,受外界影响也小;管理起来也方便,容器设置为开机即启动,通过portainer,在同一网络中任意一台机就能用浏览器重启和观察日志。

一、构建docker镜像
1、代码结构

2、CMakeLists.txt
cpp
cmake_minimum_required (VERSION 3.25)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
cmake_policy(SET CMP0153 OLD)
project ("UnderwtConn")
# 检测当前架构
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64")
set(PLATFORM_X86_64 TRUE)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
set(PLATFORM_ARM64 TRUE)
endif()
endif()
set (SOURCES
"UnderWtConn.cpp"
"controller/HttpRoutes.cpp"
"controller/HttpServer.cpp"
"util/conf_util.cpp"
"util/redis_util.cpp"
"util/path_util.cpp"
"util/time_util.cpp"
"util/json_util.cpp"
"util/hex_util.cpp"
"util/mysqlconn.cpp"
"util/general.cpp"
"util/cbx_operate.cpp"
"global/pools_util.cpp"
"entity/ConnBoxSIIM.cpp"
"entity/Gateway.cpp"
"entity/LimDeviceR.cpp"
"entity/PowerHangYu.cpp"
"config/ConfigManager.cpp"
"lib/canClient.cpp"
)
if(PLATFORM_ARM64)
list(APPEND SOURCES "lib/controlcanArm.cpp")
endif()
add_executable(UnderwtConn ${SOURCES})
find_package(Drogon CONFIG REQUIRED)
find_package(Boost REQUIRED COMPONENTS system thread)
if(PLATFORM_ARM64)
if(NOT TARGET hiredis::hiredis)
add_library(hiredis::hiredis UNKNOWN IMPORTED)
set_target_properties(hiredis::hiredis PROPERTIES
IMPORTED_LOCATION "/usr/local/lib/libhiredis.so"
INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include"
)
endif()
include_directories(/usr/include/mysql)
set(MYSQLCPPCONN_DIR /mnt/sda/lib/mysql-connector)
set(MYSQL_CONNECTOR_CPP_INCLUDE_DIR ${MYSQLCPPCONN_DIR}/include)
set(MYSQL_CONNECTOR_CPP_LIBRARY ${MYSQLCPPCONN_DIR}/lib64/libmysqlcppconnx.so)
if(NOT MYSQL_CONNECTOR_CPP_INCLUDE_DIR OR NOT MYSQL_CONNECTOR_CPP_LIBRARY)
message(FATAL_ERROR "MySQL Connector/C++ not found! Please install libmysqlcppconn-dev.")
endif()
else()
#告诉 CMake 尝试找到版本至少为 1.1.0 的 Hiredis 库,并且要求必须找到这个包才能继续执行后续的构建过程
find_package(Hiredis 1.1 CONFIG REQUIRED) # 指定 hiredis 版本为 1.1.0
#CONFIG:指示 CMake 使用"配置"模式来查找包。这意味着 CMake 期望找到由该软件包提供的配置文件
#REQUIRED:表示这是一个必需的依赖项,如果找不到就抛出错误
# 如果不存在 hiredis::hiredis 目标,则创建一个
if(NOT TARGET hiredis::hiredis)
add_library(hiredis::hiredis UNKNOWN IMPORTED)
set_target_properties(hiredis::hiredis PROPERTIES
IMPORTED_LOCATION "${HIREDIS_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${HIREDIS_INCLUDE_DIRS}"
)
endif()
# 指定额外的头文件搜索路径。
# 具体来说,它告诉编译器在编译过程中除了标准的头文件目录外,还需要检查 /usr/include/mysql 目录下的头文件
# 类似于 target_include_directories,但后者更精细。目前推荐使用 target_include_directories
include_directories(/usr/include/mysql)
# 查找 MySQL Connector/C++ 头文件目录
find_path(MYSQL_CONNECTOR_CPP_INCLUDE_DIR
NAMES mysqlx/xdevapi.h
PATHS /usr/include /usr/local/include /usr/include/mysql-cppconn)
find_library(MYSQL_CONNECTOR_CPP_LIBRARY
NAMES mysqlcppconnx # 直接指定具体版本
PATHS /usr/lib/x86_64-linux-gnu)
if(NOT MYSQL_CONNECTOR_CPP_INCLUDE_DIR OR NOT MYSQL_CONNECTOR_CPP_LIBRARY)
message(FATAL_ERROR "MySQL Connector/C++ not found! Please install libmysqlcppconn-dev.")
endif()
# 设置库文件路径
set(CONTROLCAN_LIB_PATH ${CMAKE_SOURCE_DIR}/lib/libcontrolcan.so)
endif()
#类似于include_directories
target_include_directories(${PROJECT_NAME} PRIVATE ${MYSQL_CONNECTOR_CPP_INCLUDE_DIR})
# 编译过程分为4个阶段,预处理、编译、汇编、链接。
# 所谓链接,是指将多个目标文件以及所需的库文件合并成最终的可执行文件或库
target_link_libraries(UnderwtConn
Boost::system Boost::thread
Drogon::Drogon
hiredis::hiredis
${MYSQL_CONNECTOR_CPP_LIBRARY}
${CONTROLCAN_LIB_PATH}
modbus
-static-libstdc++
-static-libgcc
)
if (CMAKE_VERSION VERSION_GREATER 3.12)
set_property(TARGET UnderwtConn PROPERTY CXX_STANDARD 20)
endif()
# 将配置文件复制到构建目录
add_custom_command(TARGET UnderwtConn POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/config/sys.conf
${CMAKE_CURRENT_BINARY_DIR}/config/sys.conf
COMMENT "Copying config file to build directory"
VERBATIM)
3、Dockerfile
准备Dockerfile特别繁琐,要反复试。我上个项目没有时间在ARM的机器上弄,所以才没有部署在docker里。
cpp
# 第一阶段:构建阶段
FROM ubuntu:24.04 AS builder
# 设置工作目录
WORKDIR /ackctrl
# 更新包列表并安装必要的工具和依赖项
RUN apt-get update && apt-get install -y \
gcc g++ \
wget \
make \
cmake \
libboost-system-dev \
libboost-thread-dev \
libssl-dev \
zlib1g-dev \
uuid-dev \
libjsoncpp-dev \
doxygen \
graphviz \
dia \
pkg-config \
libvisa-dev \
libmodbus-dev \
&& rm -rf /var/lib/apt/lists/*
# 将工作目录设为tmp
# 此命令有2个作用:1是之后的操作都从这里出发,而是如果没有这个目录的话则创建一个
WORKDIR /tmp
# Hiredis
# 复制解压后的 Hiredis 源码到容器中
COPY ./docker/hiredis-1.2.0 /tmp/hiredis
# 编译并安装 Hiredis
RUN cd /tmp/hiredis && \
mkdir build && cd build && \
cmake .. && \
make -j$(nproc) && \
make install && \
ldconfig && \
cd / && rm -rf /tmp/hiredis
# 验证 Hiredis 安装
# RUN pkg-config --modversion hiredis && \
# find /usr/local -name "HiredisConfig.cmake"
# MySQL
# 复制 MySQL Connector/C++ 的 DEB 包到容器中
COPY ./docker/libmysqlcppconn10_9.2.0-1ubuntu24.04_amd64.deb /tmp/libmysqlcppconn10.deb
COPY ./docker/libmysqlcppconn-dev_9.2.0-1ubuntu24.04_amd64.deb /tmp/libmysqlcppconn-dev.deb
COPY ./docker/libmysqlcppconnx2_9.2.0-1ubuntu24.04_amd64.deb /tmp/libmysqlcppconnx2.deb
# 安装 MySQL Connector/C++ 的运行时和开发库
RUN dpkg -i /tmp/libmysqlcppconn10.deb /tmp/libmysqlcppconn-dev.deb /tmp/libmysqlcppconnx2.deb && \
apt-get install -f -y && \
rm -rf /tmp/*.deb
# 验证安装
# RUN dpkg -l | grep mysqlcppconn && \
# find /usr -name "mysql" && \
# ls /usr/lib/x86_64-linux-gnu | grep mysqlcppconn
# Drogon
# 将本地的 drogon 源码复制到容器中
COPY ./docker/drogon-master /tmp/drogon-src
# 初始化子模块(如果需要)
WORKDIR /tmp/drogon-src
RUN mkdir -p build && cd build && \
cmake .. -DBUILD_SHARED_LIBS=ON && \
make -j$(nproc) && \
make install && \
cd / && rm -rf /tmp/drogon-src
# 验证 Drogon 和 Trantor 是否安装成功
# RUN find /usr -name "libdrogon.so*" && \
# find /usr/local -name "libdrogon.so*"
# RUN find /usr -name "libtrantor.so*" && \
# find /usr/local -name "libtrantor.so*"
# 回到工作目录
WORKDIR /ackctrl
# 将 AckCtrl 源码复制到容器中
COPY . /ackctrl
# 创建构建目录并编译应用代码
RUN mkdir -p build && cd build && \
cmake -DCMAKE_PREFIX_PATH=/usr/local /ackctrl && \
make
# 第二阶段:运行时(多阶段构建)
#FROM debian:stable-slim
FROM ubuntu:24.04
WORKDIR /ackctrl
# 删除可能存在的旧版本动态库
RUN rm -f /usr/local/lib/libdrogon.so* && \
rm -f /usr/lib/x86_64-linux-gnu/libdrogon.so*
# 从构建阶段复制动态链接库
COPY --from=builder /usr/local/lib/libhiredis.so* /usr/local/lib/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libmysqlcppconnx.so* /usr/local/lib/
COPY --from=builder /usr/local/lib/libdrogon.so* /usr/local/lib/
COPY --from=builder /usr/local/lib/libtrantor.so* /usr/local/lib/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libssl.so.3 /usr/local/lib/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libcrypto.so.3 /usr/local/lib/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libjsoncpp.so.25 /usr/local/lib/
# 从构建阶段复制可执行文件到运行时镜像
COPY --from=builder /ackctrl/build/UnderwtConn .
COPY --from=builder /ackctrl/build/config ./config
# 创建符号链接
RUN ln -sf libdrogon.so.1.9.10 /usr/local/lib/libdrogon.so.1 && \
ln -sf libdrogon.so.1 /usr/local/lib/libdrogon.so && \
ln -sf /usr/local/lib/libtrantor.so.1 /usr/local/lib/libtrantor.so
# 更新动态链接库缓存
RUN ldconfig
# 暴露默认端口
EXPOSE 10992
# 运行程序
CMD ["./UnderwtConn"]
#CMD ["sh", "-c", "./UnderwtConn"]
4、构建命令
进入程序根目录,执行
bash
sudo docker build -t ackctrl:1.0 .
二、导出镜像
在开发机器上导出刚构建好的镜像。
bash
sudo docker save -o ackctrl.tar ackctrl:1.0
修改权限,否则无法下载
bash
sudo chmod 644 ackctrl.tar
三、导入镜像
将开发机器上导出的镜像ackctrl.tar下载,拷贝到服务器,然后导入
bash
sudo docker load -i ackctrl.tar
四、创建容器
bash
mkdir /home/docker/ackctrl/config -p
sudo docker run --name ackctrl \
--restart=always -d -it \
-v /home/docker/ackctrl/config:/ackctrl/config \
-p 10992:10992 \
ackctrl:1.0
五、小结
其实我在上一个项目就想着用docker部署了。那个项目,也是要将C++部署到linux。但生产服务器的CPU是ARM架构的,而开发环境的CPU是X86的。这意味着无法使用开发机器构建镜像的方式,必须要拿到生产服务器上编译代码、构建镜像。项目时间赶,就在一台跟服务器配置相同的测试机上部署了开发环境,编译、发布,拷贝到其他服务器上运行。还要写脚本设置开机自动运行。查看日志还要敲命令。主要还是不熟悉docker,构建docker镜像时特别麻烦。
感谢发明了docker或者容器概念的人。