目录
注意:我们这里是使用Docker来进行一键部署的,所以,没有安装Docker的,请去安装好了,再来完成下面这些事情
一.获取各个子服务可执行程序的依赖
我们在部署之前呢,都是需要先将各个子服务的可执行程序先编译出来的。
bash
# 记录起始目录
START_DIR=$(pwd)
echo "========== 开始编译 speech_recognition =========="
cd ../Service/speech_recognition/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 file =========="
cd file/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 user =========="
cd user/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 message_transmite =========="
cd message_transmite/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 message_storage =========="
cd message_storage/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 friend =========="
cd friend/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 gateway =========="
cd gateway/
mkdir -p build
cd build
cmake ..
make
cd ../..
# 返回起始目录
cd "$START_DIR"
echo "========== 所有模块编译完成 =========="
各个子服务编译完成之后,我们就能依据这些子服务的可执行程序获取它们的依赖
首先,我们需要明白,我们的可执行程序无非就是依赖于某些库嘛。
但是库文件又分为动态库和静态库
- 静态库(.a 文件)在编译链接时就已经被直接合并到最终的可执行文件中,成为程序的一部分。运行时,静态库的代码已经存在于可执行文件内部,因此不再需要外部的 .a 文件,也不存在"运行时依赖"的概念。
- 动态库(.so 文件)在编译链接时不会被合并到可执行文件中,仅记录所需的库名及符号信息。程序运行时 ,由动态链接器负责查找并加载这些动态库到内存中。
因此,可执行文件依赖于外部环境中存在对应的 .so 文件,否则会报"找不到共享库"的错误
那么实际上,我们就只需要去寻找到每个可执行程序依赖的动态库即可。
在Linux里面,ldd 命令可以列出一个可执行文件所依赖的动态链接库。
我们就拿用户子服务的可执行程序来看看
bash
ubuntu@10-13-52-255:~/cpp-chatsystem/server/Service/user/build$ ls
CMakeCache.txt Makefile base.pb.h file.pb.cc user-odb.cxx user-odb.ixx user.pb.h user_server
CMakeFiles base.pb.cc cmake_install.cmake file.pb.h user-odb.hxx user.pb.cc user.sql user_test
ubuntu@10-13-52-255:~/cpp-chatsystem/server/Service/user/build$ ldd user_server
linux-vdso.so.1 (0x00007fffe5bc7000)
libgflags.so.2.2 => /lib/x86_64-linux-gnu/libgflags.so.2.2 (0x00007f9a65875000)
libfmt.so.8 => /lib/x86_64-linux-gnu/libfmt.so.8 (0x00007f9a65854000)
libssl.so.3 => /lib/x86_64-linux-gnu/libssl.so.3 (0x00007f9a657b0000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f9a6536c000)
libprotobuf.so.23 => /lib/x86_64-linux-gnu/libprotobuf.so.23 (0x00007f9a650a9000)
libleveldb.so.1d => /lib/x86_64-linux-gnu/libleveldb.so.1d (0x00007f9a6505c000)
libetcd-cpp-api.so => /lib/x86_64-linux-gnu/libetcd-cpp-api.so (0x00007f9a64bd6000)
libcpprest.so.2.10 => /lib/x86_64-linux-gnu/libcpprest.so.2.10 (0x00007f9a64809000)
libcurl.so.4 => /lib/x86_64-linux-gnu/libcurl.so.4 (0x00007f9a64762000)
libodb-mysql-2.5.so => /lib/libodb-mysql-2.5.so (0x00007f9a6472f000)
libodb-2.5.so => /lib/libodb-2.5.so (0x00007f9a64707000)
libjsoncpp.so.19 => /lib/x86_64-linux-gnu/libjsoncpp.so.19 (0x00007f9a646cb000)
libelasticlient.so.2 => /lib/libelasticlient.so.2 (0x00007f9a646af000)
libhiredis.so.0.14 => /lib/x86_64-linux-gnu/libhiredis.so.0.14 (0x00007f9a6469d000)
libredis++.so.1 => /lib/x86_64-linux-gnu/libredis++.so.1 (0x00007f9a645fb000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f9a643cf000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f9a642e8000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f9a642c6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a6409d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9a67067000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9a64098000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f9a6407c000)
libsnappy.so.1 => /lib/x86_64-linux-gnu/libsnappy.so.1 (0x00007f9a64072000)
libgpr.so.10 => /usr/lib/x86_64-linux-gnu/libgpr.so.10 (0x00007f9a64060000)
libgrpc.so.10 => /usr/lib/x86_64-linux-gnu/libgrpc.so.10 (0x00007f9a63e1a000)
libgrpc++.so.1 => /usr/lib/x86_64-linux-gnu/libgrpc++.so.1 (0x00007f9a63da2000)
libbrotlidec.so.1 => /lib/x86_64-linux-gnu/libbrotlidec.so.1 (0x00007f9a63d94000)
libbrotlienc.so.1 => /lib/x86_64-linux-gnu/libbrotlienc.so.1 (0x00007f9a63d03000)
libnghttp2.so.14 => /lib/x86_64-linux-gnu/libnghttp2.so.14 (0x00007f9a63cd9000)
libidn2.so.0 => /lib/x86_64-linux-gnu/libidn2.so.0 (0x00007f9a63cb6000)
librtmp.so.1 => /lib/x86_64-linux-gnu/librtmp.so.1 (0x00007f9a63c97000)
libssh.so.4 => /lib/x86_64-linux-gnu/libssh.so.4 (0x00007f9a63c2a000)
libpsl.so.5 => /lib/x86_64-linux-gnu/libpsl.so.5 (0x00007f9a63c16000)
libgssapi_krb5.so.2 => /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 (0x00007f9a63bc2000)
libldap-2.5.so.0 => /lib/x86_64-linux-gnu/libldap-2.5.so.0 (0x00007f9a63b63000)
liblber-2.5.so.0 => /lib/x86_64-linux-gnu/liblber-2.5.so.0 (0x00007f9a63b50000)
libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007f9a63a81000)
libmysqlclient-21.so => /lib/libmysqlclient-21.so (0x00007f9a634f8000)
libcpr.so.1 => /lib/x86_64-linux-gnu/libcpr.so.1 (0x00007f9a634dd000)
libabsl_strings.so.20210324 => /lib/x86_64-linux-gnu/libabsl_strings.so.20210324 (0x00007f9a634be000)
libabsl_throw_delegate.so.20210324 => /lib/x86_64-linux-gnu/libabsl_throw_delegate.so.20210324 (0x00007f9a634b5000)
libabsl_str_format_internal.so.20210324 => /lib/x86_64-linux-gnu/libabsl_str_format_internal.so.20210324 (0x00007f9a6349a000)
libabsl_time.so.20210324 => /lib/x86_64-linux-gnu/libabsl_time.so.20210324 (0x00007f9a63487000)
libabsl_bad_optional_access.so.20210324 => /lib/x86_64-linux-gnu/libabsl_bad_optional_access.so.20210324 (0x00007f9a63482000)
libcares.so.2 => /lib/x86_64-linux-gnu/libcares.so.2 (0x00007f9a6346b000)
libbrotlicommon.so.1 => /lib/x86_64-linux-gnu/libbrotlicommon.so.1 (0x00007f9a63446000)
libunistring.so.2 => /lib/x86_64-linux-gnu/libunistring.so.2 (0x00007f9a6329c000)
libgnutls.so.30 => /lib/x86_64-linux-gnu/libgnutls.so.30 (0x00007f9a630b0000)
libhogweed.so.6 => /lib/x86_64-linux-gnu/libhogweed.so.6 (0x00007f9a63068000)
libnettle.so.8 => /lib/x86_64-linux-gnu/libnettle.so.8 (0x00007f9a63022000)
libgmp.so.10 => /lib/x86_64-linux-gnu/libgmp.so.10 (0x00007f9a62fa0000)
libkrb5.so.3 => /lib/x86_64-linux-gnu/libkrb5.so.3 (0x00007f9a62ed3000)
libk5crypto.so.3 => /lib/x86_64-linux-gnu/libk5crypto.so.3 (0x00007f9a62ea4000)
libcom_err.so.2 => /lib/x86_64-linux-gnu/libcom_err.so.2 (0x00007f9a62e9e000)
libkrb5support.so.0 => /lib/x86_64-linux-gnu/libkrb5support.so.0 (0x00007f9a62e90000)
libsasl2.so.2 => /lib/x86_64-linux-gnu/libsasl2.so.2 (0x00007f9a62e75000)
libssl-1.1.so => /lib/libssl-1.1.so (0x00007f9a62dd7000)
libcrypto-1.1.so => /lib/libcrypto-1.1.so (0x00007f9a62b46000)
libabsl_strings_internal.so.20210324 => /lib/x86_64-linux-gnu/libabsl_strings_internal.so.20210324 (0x00007f9a62b40000)
libabsl_int128.so.20210324 => /lib/x86_64-linux-gnu/libabsl_int128.so.20210324 (0x00007f9a62b38000)
libabsl_raw_logging_internal.so.20210324 => /lib/x86_64-linux-gnu/libabsl_raw_logging_internal.so.20210324 (0x00007f9a62b33000)
libabsl_time_zone.so.20210324 => /lib/x86_64-linux-gnu/libabsl_time_zone.so.20210324 (0x00007f9a62b17000)
libabsl_base.so.20210324 => /lib/x86_64-linux-gnu/libabsl_base.so.20210324 (0x00007f9a62b11000)
libp11-kit.so.0 => /lib/x86_64-linux-gnu/libp11-kit.so.0 (0x00007f9a629d6000)
libtasn1.so.6 => /lib/x86_64-linux-gnu/libtasn1.so.6 (0x00007f9a629be000)
libkeyutils.so.1 => /lib/x86_64-linux-gnu/libkeyutils.so.1 (0x00007f9a629b7000)
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f9a629a1000)
libabsl_spinlock_wait.so.20210324 => /lib/x86_64-linux-gnu/libabsl_spinlock_wait.so.20210324 (0x00007f9a6299c000)
libffi.so.8 => /lib/x86_64-linux-gnu/libffi.so.8 (0x00007f9a6298f000)
ldd命令会直接列举出链接出的所有的动态库的完整路径,我们只需要将这些这些路径里面的库文件给拷贝过来即可。
那么我们就可以写一个脚本来获取这些库文件
bash
declare depends # 声明全局变量,用于存储依赖库列表
get_depends() {
# 使用 ldd 列出动态链接依赖,用 awk 提取出第三列(库的完整路径),过滤出包含 '/' 的行
depends=$(ldd $1 | awk '{if (match($3,"/")){print $3}}')
# mkdir $2 # 被注释掉,需要手动确保目标目录存在
# 复制依赖库到目标目录(-L:跟随软链接,-r:递归复制)
cp -Lr $depends $2
}
首先我们先来看看
bash
ldd $1 | awk '{if (match($3,"/")){print $3}}
我来帮你拆解一下,尤其适合初学者。
1 和 2 是什么意思?
在 Shell 脚本的函数里:
- $1 表示调用函数时传入的第一个参数
- $2 表示调用函数时传入的第二个参数
- 依次类推
例如你的脚本里调用:
get_depends ../Service/gateway/build/gateway_server ../Service/gateway/depends
那么在 get_depends 函数内部:
- $1 的值就是 ../Service/gateway/build/gateway_server(可执行文件路径)
- $2 的值就是 ../Service/gateway/depends(目标目录)
所以 ldd $1 就是 ldd ../Service/gateway/build/gateway_server,用来查看这个可执行文件依赖哪些动态库。
bash
awk '{if (match($3,"/")){print $3}}'
- awk 会逐行处理输入(这里是 ldd 的每一行输出)。
- 在 awk 中,1、2、$3 表示当前行的第1个字段、第2个字段、第3个字段(字段默认由空格或制表符分隔)。
对于上面示例中的一行:
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f8b1e200000)
- $1 = libselinux.so.1
- $2 = =>
- $3 = /lib/x86_64-linux-gnu/libselinux.so .1
- $4 = (0x00007f8b1e200000)
match($3,"/") 检查第3个字段里是否包含斜杠 /(即判断它是不是一个路径,因为动态库的完整路径包含 /,而像 linux-vdso.so.1 这种虚拟库没有路径,不匹配)。
如果匹配到 /,就执行 {print $3},输出第3个字段(即动态库的完整路径)。
bash
# 函数:获取可执行程序的所有动态依赖库,并复制到指定目录
# 参数1:可执行程序的路径名
# 参数2:目标目录名称(将依赖库拷贝到此目录下)
declare depends # 声明全局变量,用于存储依赖库列表
get_depends() {
# 使用 ldd 列出动态链接依赖,用 awk 提取出第三列(库的完整路径),过滤出包含 '/' 的行
depends=$(ldd $1 | awk '{if (match($3,"/")){print $3}}')
mkdir -p $2 #先创建出目标目录
# 复制依赖库到目标目录(-L:跟随软链接,-r:递归复制)
cp -Lr $depends $2
}
# 为各个服务程序提取依赖库,并复制到各自的 depends 目录
get_depends ../Service/gateway/build/gateway_server ../Service/gateway/depends
get_depends ../Service/file/build/file_server ../Service/file/depends
get_depends ../Service/friend/build/friend_server ../Service/friend/depends
get_depends ../Service/message_storage/build/message_storage_server ../Service/message_storage/depends
get_depends ../Service/speech_recognition/build/speech_recognition_server ../Service/speech_recognition/depends
get_depends ../Service/message_transmite/build/message_transmite_server ../Service/message_transmite/depends
get_depends ../Service/user/build/user_server ../Service/user/depends
# 将 /bin/nc(netcat 工具)复制到各个服务的根目录(用于后续打包进镜像)
cp /bin/nc ../Service/gateway/
cp /bin/nc ../Service/file/
cp /bin/nc ../Service/friend/
cp /bin/nc ../Service/message_storage/
cp /bin/nc ../Service/speech_recognition/
cp /bin/nc ../Service/message_transmite/
cp /bin/nc ../Service/user/
# 再为每个服务目录中的 nc 工具提取其依赖库,并复制到对应的 depends 目录
get_depends /bin/nc ../Service/gateway/depends
get_depends /bin/nc ../Service/file/depends
get_depends /bin/nc ../Service/friend/depends
get_depends /bin/nc ../Service/message_storage/depends
get_depends /bin/nc ../Service/speech_recognition/depends
get_depends /bin/nc ../Service/user/depends
get_depends /bin/nc ../Service/message_transmite/depends
有了这些依赖,我们的程序才可以被打包发送出去。
至此,我们就编写好了这个prepare_for_docker.sh!!!!
bash
#!/bin/bash
# 自动编译所有服务模块的脚本
# 脚本会按照原始命令的相对路径依次进入各个模块进行编译
set -e # 遇到任何错误立即退出
# 记录起始目录
START_DIR=$(pwd)
echo "========== 开始编译 speech_recognition =========="
cd ../Service/speech_recognition/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 file =========="
cd file/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 user =========="
cd user/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 message_transmite =========="
cd message_transmite/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 message_storage =========="
cd message_storage/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 friend =========="
cd friend/
mkdir -p build
cd build
cmake ..
make
cd ../..
echo "========== 开始编译 gateway =========="
cd gateway/
mkdir -p build
cd build
cmake ..
make
cd ../..
# 返回起始目录
cd "$START_DIR"
echo "========== 所有模块编译完成 =========="
# 函数:获取可执行程序的所有动态依赖库,并复制到指定目录
# 参数1:可执行程序的路径名
# 参数2:目标目录名称(将依赖库拷贝到此目录下)
declare depends # 声明全局变量,用于存储依赖库列表
get_depends() {
# 使用 ldd 列出动态链接依赖,用 awk 提取出第三列(库的完整路径),过滤出包含 '/' 的行
depends=$(ldd $1 | awk '{if (match($3,"/")){print $3}}')
mkdir -p $2 #先创建出目标目录
# 复制依赖库到目标目录(-L:跟随软链接,-r:递归复制)
cp -Lr $depends $2
}
# 为各个服务程序提取依赖库,并复制到各自的 depends 目录
get_depends ../Service/gateway/build/gateway_server ../Service/gateway/depends
get_depends ../Service/file/build/file_server ../Service/file/depends
get_depends ../Service/friend/build/friend_server ../Service/friend/depends
get_depends ../Service/message_storage/build/message_storage_server ../Service/message_storage/depends
get_depends ../Service/speech_recognition/build/speech_recognition_server ../Service/speech_recognition/depends
get_depends ../Service/message_transmite/build/message_transmite_server ../Service/message_transmite/depends
get_depends ../Service/user/build/user_server ../Service/user/depends
# 将 /bin/nc(netcat 工具)复制到各个服务的根目录(用于后续打包进镜像)
cp /bin/nc ../Service/gateway/
cp /bin/nc ../Service/file/
cp /bin/nc ../Service/friend/
cp /bin/nc ../Service/message_storage/
cp /bin/nc ../Service/speech_recognition/
cp /bin/nc ../Service/message_transmite/
cp /bin/nc ../Service/user/
# 再为每个服务目录中的 nc 工具提取其依赖库,并复制到对应的 depends 目录
get_depends /bin/nc ../Service/gateway/depends
get_depends /bin/nc ../Service/file/depends
get_depends /bin/nc ../Service/friend/depends
get_depends /bin/nc ../Service/message_storage/depends
get_depends /bin/nc ../Service/speech_recognition/depends
get_depends /bin/nc ../Service/user/depends
get_depends /bin/nc ../Service/message_transmite/depends
二.编写各个子服务的配置文件
我们每个子服务在运行之前都是需要进行一些配置的,比如说这个friend_server.cc
cpp
//主要实现语音识别子服务的服务器的搭建
#include "friend_server.hpp"
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
DEFINE_string(registry_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(instance_name, "/friend_service/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:10006", "当前实例的外部访问地址");
DEFINE_int32(listen_port, 10006, "Rpc服务器监听端口");
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
DEFINE_int32(rpc_threads, 1, "Rpc的IO线程数量");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(user_service, "/service/user_service", "用户管理子服务名称");
DEFINE_string(message_storage_service, "/service/message_storage_service", "消息存储子服务名称");
DEFINE_string(es_host, "http://127.0.0.1:9200/", "ES搜索引擎服务器URL");
DEFINE_string(mysql_host, "127.0.0.1", "Mysql服务器访问地址");
DEFINE_string(mysql_user, "root", "Mysql服务器访问用户名");
DEFINE_string(mysql_pswd, "123456", "Mysql服务器访问密码");
DEFINE_string(mysql_db, "IMS", "Mysql默认库名称");
DEFINE_string(mysql_cset, "utf8", "Mysql客户端字符集");
DEFINE_int32(mysql_port, 0, "Mysql服务器访问端口");
DEFINE_int32(mysql_pool_count, 4, "Mysql连接池最大连接数量");
int main(int argc, char *argv[])
{
google::ParseCommandLineFlags(&argc, &argv, true);
IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
IMS::FriendServerBuilder fsb;
fsb.make_es_object({FLAGS_es_host});
fsb.make_mysql_object(FLAGS_mysql_user, FLAGS_mysql_pswd, FLAGS_mysql_host,
FLAGS_mysql_db, FLAGS_mysql_cset, FLAGS_mysql_port, FLAGS_mysql_pool_count);
fsb.make_discovery_object(FLAGS_registry_host, FLAGS_base_service, FLAGS_user_service, FLAGS_message_storage_service);
fsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);
fsb.make_registry_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
auto server = fsb.build();
server->start();
return 0;
}
上面那些使用gflag来定义的那些,就是我们的配置选项。
那么我们将这些配置全部写入friend_server.conf
bash
-run_mode=true
-log_file=/im/logs/friend.log
-log_level=0
-registry_host=http://106.75.144.240:2379
-instance_name=/friend_service/instance
-access_host=106.75.144.240:10006
-listen_port=10006
-rpc_timeout=-1
-rpc_threads=1
-base_service=/service
-user_service=/service/user_service
-message_storage_service=/service/message_storage_service
-es_host=http://106.75.144.240:9200/
-mysql_host=106.75.144.240
-mysql_user=root
-mysql_pswd=123456
-mysql_db=IMS
-mysql_cset=utf8
-mysql_port=0
-mysql_pool_count=4
这里的配置和上面的思路基本就是一模一样的。
其余子服务的也是如此,我就不多说了。
三.编写各个子服务的Dockerfile
首先,我们需要明白我们上面的动态库文件需要放到哪里去?
/lib/x86_64-linux-gnu/ 只是 Linux 系统上动态库的常见存放路径之一。
所以我们将依赖里面的文件全部放到/lib/x86_64-linux-gnu/里面去。
那么剩余的我也没什么好说的
bash
# 声明基础镜像来源
# 使用 Ubuntu 22.04 作为基础操作系统镜像
FROM ubuntu:22.04
# 声明工作路径
# 设置容器内的工作目录为 /im,后续所有相对路径操作(如 RUN、COPY、CMD)都以此目录为基准
WORKDIR /im
# 创建必要的运行时目录
# - /im/logs: 存放服务产生的日志文件
# - /im/data: 存放服务产生的数据文件(如临时文件、持久化数据等)
# - /im/conf: 存放配置文件(程序启动时会读取这里的 .conf 文件)
# - /bin: 确保 /bin 目录存在
RUN mkdir -p /im/logs &&\
mkdir -p /im/data &&\
mkdir -p /im/conf &&\
mkdir -p /bin
# 将可执行程序文件拷贝进入镜像
# 宿主机上构建上下文中的 ./build/speech_recognition_server 文件(编译好的二进制程序)
# 复制到容器内的 /im/bin/ 目录下
COPY ./build/speech_recognition_server /im/bin/
# 将可执行程序的依赖库拷贝进入镜像
# 宿主机上 ./depends/ 目录下的所有文件(通常是动态链接库 .so 文件)
# 复制到容器内的系统库目录 /lib/x86_64-linux-gnu/,解决程序运行时的库依赖问题
COPY ./depends/ /lib/x86_64-linux-gnu/
# 注意这个./depends/目录是我们借助我们的脚本depends.sh来获取这个speech_recognition_server可执行文件所需的依赖,并且拷贝到当前目录里的depends目录里
# 拷贝 netcat 工具到镜像中
# 宿主机上的 ./nc 文件(nc 命令的二进制)复制到容器内的 /bin/ 目录
# 这样在容器内可以直接使用 nc 命令进行网络调试或端口检查
COPY ./nc /bin/
# 设置容器的启动默认操作 ------ 运行程序
# 容器启动时执行:/im/bin/speech_recognition_server -flagfile=/im/conf/speech_recognition_server.conf
# 即启动语音识别服务,并指定配置文件的路径(从 /im/conf 目录读取)
# 注意:如果在 docker-compose.yml 中指定了 entrypoint,则会覆盖这里的 CMD
CMD /im/bin/speech_recognition_server -flagfile=/im/conf/speech_recognition_server.conf
其他子服务的也是这样子。
四.编写docker-compose.yml
4.1.入口脚本文件编写
我们这里仔细想想
在我们整个项目里面,一共涉及到了12个服务,
- etcd
- mysql
- redis
- elasticsearch
- rabbitmq
- file_server
- friend_server
- gateway_server
- message_storage_server
- speech_recognition_server
- message_transmite_server
- user_server
但是这些服务之间是存在依赖关系的,某些服务必须先运行起来!!!
| 服务 | 对应依赖服务 |
|---|---|
| file_server | etcd |
| friend_server | etcd, mysql, elasticsearch |
| gateway_server | etcd, redis |
| message_storage_server | etcd, mysql, elasticsearch, rabbitmq |
| speech_recognition_server | etcd |
| message_transmite_server | etcd, mysql, rabbitmq |
| user_server | etcd, mysql, redis, elasticsearch |
那么我们怎么保证这些服务比我们的先运行起来呢?
在 Docker Compose 中,如果我们希望某个服务(如 message_transmite_server)在它所依赖的其他服务(如 etcd、mysql、rabbitmq)启动之后再启动,通常会在 docker-compose.yml 中这样配置:
bash
depends_on:
- etcd
- mysql
- rabbitmq
然而,仅靠 depends_on 是有隐患的。
depends_on
- Docker Compose 的原生机制,确保 message_transmite_server 容器会在 etcd、mysql、rabbitmq 这三个容器启动之后才开始创建和运行。
- 但是它只保证依赖容器的进程状态为 running,并不保证容器内的服务进程已经就绪。
例如:MySQL 容器虽然已经启动,但可能还在执行初始化脚本、创建数据库、加载数据,此时它的 3306 端口尚未开始监听。如果业务进程立刻连接,就会收到连接拒绝或超时错误。
为了解决"容器已启动但服务未就绪"的问题,项目引入了一个自定义的启动脚本 entrypoint.sh,并像这样调用:
entrypoint.sh -p 2379,3306,5672
- 这是一个自定义的启动脚本,它会在真正执行业务进程(message_transmite_server)之前,主动轮询检查指定的端口(2379、3306、5672)是否可以建立 TCP 连接。
- 只有当所有端口都成功连通(即依赖服务已经完成初始化、开始监听端口),脚本才会继续执行 -c 后面的命令。
- 这保证了依赖服务不仅容器存在,而且服务本身已经可用。
两者结合的效果
- 第一层保险(depends_on):避免因容器未启动而直接运行端口检查(否则端口检查会立刻失败)。
- 第二层保险(端口等待):解决"容器已启动但服务未就绪"的问题,防止业务进程因为依赖服务还没准备好而报错退出。
那么我们很快就写出了下面这个脚本,
bash
#!/bin/bash
# 脚本用途:在启动主程序前,先检测指定的 IP 和端口是否可连通,
# 所有端口都连通后,再执行主程序命令。
# 使用示例:
# ./entrypoint.sh -h 127.0.0.1 -p 3306,2379,6379 -c '/im/bin/file_server -flagfile=./xx.conf'
# ===================== 1. 端口探测函数 =====================
# 函数功能:循环检测指定 IP 和端口是否可连接(使用 nc 命令)
# 参数1(也就是里面的$1):目标 IP 地址
# 参数2(也就是里面的$2):目标端口号
wait_for() {
# nc -z 测试端口是否可连接(不发送数据,仅检测)
while ! nc -z $1 $2
do
# 连接失败时输出提示,休眠1秒后继续重试
echo "$2 端口连接失败,休眠等待!"
sleep 1
done
# 连接成功后输出成功信息
echo "$1:$2 检测成功!"
}
# ===================== 2. 解析命令行参数 =====================
# 声明变量用于存储解析后的参数值
declare ip # 存储 -h 参数(目标主机 IP)
declare ports # 存储 -p 参数(端口列表,逗号分隔)
declare command # 存储 -c 参数(待执行的命令)
# getopts 解析格式为 -h <ip> -p <ports> -c <command> 的参数
# "h:p:c:" 表示期望接收 -h、-p、-c 三个选项,后面的冒号表示该选项需要参数
while getopts "h:p:c:" arg
do
case $arg in
h)
ip=$OPTARG # 获取 -h 后面的 IP 地址
;;
p)
ports=$OPTARG # 获取 -p 后面的端口列表(如 "2379,3306,5672")
;;
c)
command=$OPTARG # 获取 -c 后面的命令字符串
;;
esac
done
# ===================== 3. 遍历端口列表并逐个检测 =====================
# ${ports//,/ } 的作用:将变量 ports 中的所有逗号(,)替换为空格
# 例如 "2379,3306,5672" 会变成 "2379 3306 5672"
# 然后 for 循环会遍历这个用空格分隔的列表,每个值作为 port 变量
for port in ${ports//,/ }
do
# 调用我们上面写的 wait_for 函数,传入 IP 和当前端口号
wait_for $ip $port
done
# ===================== 4. 执行最终的命令 =====================
# eval 会对字符串进行解析,将其当作 shell 命令执行
# 这里执行 -c 参数传入的命令(通常是启动服务的主程序)
eval $command
可以看到,他是借助了这个nc工具来测试是否联通的。所以我们也需要将这个nc工具及其依赖全部打包进我们的容器内部去,这个我们在第一步:获取各个子服务可执行程序的依赖那里就已经完成了!!!
在这个文件里面,我们特别需要理解下面这个东西
bash
/im/bin/entrypoint.sh -h 106.75.144.240 -p 2379,3306,5672,9200 -c "/im/bin/user_server -flagfile=/im/conf/user_server.conf"
- /im/bin/entrypoint.sh
这是一个脚本文件,它被放在容器的 /im/bin/ 目录下。这个脚本的作用是:
- 接收你传给它的参数(-h、-p、-c)
- 根据 -p 中列出的端口,挨个检查对应的服务是否已经可以连接
- 只有当所有依赖服务都连接成功时,才去执行 -c 后面真正的命令
- -h 106.75.144.240
- -h 表示 host(主机地址)
- 106.75.144.240 是依赖服务的 IP 地址。这里填的是一个公网 IP,但实际上在 Docker Compose 环境中,通常会填其他容器的服务名(如 mysql、etcd)或宿主机 IP。这个脚本会用这个 IP 去连接后面 -p 指定的端口。
- -p 2379,3306,5672,9200
-p 表示 port(端口列表)
这里列出了 4 个端口,用逗号隔开:
- 2379 → etcd 服务的端口
- 3306 → MySQL 数据库的端口
- 5672 → RabbitMQ 消息队列的端口
- 9200 → Elasticsearch 的端口
脚本会依次尝试连接 106.75.144.240:2379、106.75.144.240:3306...... 如果某个端口连不上,就每隔 1 秒重试一次,直到成功。
- -c "/im/bin/user_server -flagfile=/im/conf/user_server.conf"
-c 表示 command(要执行的命令)
双引号里的内容就是真正要启动的程序:
- /im/bin/user_server → 用户服务的主程序
- -flagfile=/im/conf/user_server.conf → 告诉这个程序,它的配置文件放在哪里
等前面的所有端口都检测通过后,entrypoint.sh 就会执行这个命令,启动用户服务。
现在你明白了吧
4.2.docker-compose.yml的编写
这个其实就是很简单!!!写清楚配置文件,依赖啥的就行了
bash
version: "3.8"
services:
# etcd 服务 - 用于服务发现与配置存储
etcd:
image: quay.io/coreos/etcd:v3.3.25
container_name: etcd-service
environment:
- ETCD_NAME=etcd-s1 # 节点名称
- ETCD_DATA_DIR=/var/lib/etcd # 数据存储目录
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 # 监听客户端连接的地址
- ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379 # 对外广播的客户端地址
volumes:
# 将宿主机目录挂载到容器内,实现数据持久化
- ./middle/data/etcd:/var/lib/etcd:rw
ports:
- "2379:2379" # 客户端端口
restart: always
# MySQL 数据库服务
mysql:
image: mysql:8.0.39
container_name: mysql-service
environment:
MYSQL_ROOT_PASSWORD: 123456 # root 用户密码
volumes:
- ./sql:/docker-entrypoint-initdb.d/:rw # 初始化 SQL 脚本目录
- ./middle/data/mysql:/var/lib/mysql:rw # 数据持久化目录
ports:
- "3306:3306"
restart: always
# Redis 缓存服务
redis:
image: redis:6.0.16
container_name: redis-service
volumes:
- ./middle/data/redis:/var/lib/redis:rw # 持久化数据目录
ports:
- "6379:6379"
restart: always
# Elasticsearch 搜索/索引服务
elasticsearch:
image: elasticsearch:7.17.21
container_name: elasticsearch-service
environment:
- "discovery.type=single-node" # 单节点模式
volumes:
- ./middle/data/elasticsearch:/data:rw # 数据持久化
ports:
- "9200:9200" # HTTP API 端口
- "9300:9300" # 集群内部通信端口
restart: always
# RabbitMQ 消息队列服务
rabbitmq:
image: rabbitmq:3.9.13
container_name: rabbitmq-service
environment:
RABBITMQ_DEFAULT_USER: root # 默认用户名
RABBITMQ_DEFAULT_PASS: 123456 # 默认密码
volumes:
- ./middle/data/rabbitmq:/var/lib/rabbitmq:rw # 数据持久化
ports:
- "5672:5672" # AMQP 协议端口
restart: always
# 文件服务 (file_server)
file_server:
build: ../Service/file # 构建上下文路径
container_name: file-service
volumes:
- ./conf/file_server.conf:/im/conf/file_server.conf # 配置文件挂载
- ./middle/data/logs:/im/logs:rw # 日志目录
- ./middle/data/data:/im/data:rw # 数据目录
- ./entrypoint.sh:/im/bin/entrypoint.sh # 启动脚本
ports:
- "10002:10002"
restart: always
entrypoint:
# 启动入口脚本:等待 etcd 就绪,然后启动文件服务
/im/bin/entrypoint.sh -h 106.75.144.240 -p 2379 -c "/im/bin/file_server -flagfile=/im/conf/file_server.conf"
depends_on:
- etcd
# 好友服务 (friend_server)
friend_server:
build: ../Service/friend
container_name: friend-service
volumes:
- ./conf/friend_server.conf:/im/conf/friend_server.conf
- ./middle/data/logs:/im/logs:rw
- ./middle/data/data:/im/data:rw
- ./entrypoint.sh:/im/bin/entrypoint.sh
ports:
- "10006:10006"
restart: always
depends_on:
- etcd
- mysql
- elasticsearch
entrypoint:
# 等待 etcd、MySQL、Elasticsearch 就绪后启动,注意这个2379,3306,9200几个端口分别对应 etcd、MySQL、Elasticsearch 的端口
/im/bin/entrypoint.sh -h 106.75.144.240 -p 2379,3306,9200 -c "/im/bin/friend_server -flagfile=/im/conf/friend_server.conf"
# 网关服务 (gateway_server)
gateway_server:
build: ../Service/gateway
container_name: gateway-service
volumes:
- ./conf/gateway_server.conf:/im/conf/gateway_server.conf
- ./middle/data/logs:/im/logs:rw
- ./middle/data/data:/im/data:rw
- ./entrypoint.sh:/im/bin/entrypoint.sh
ports:
- "8000:8000" # 网关主端口
- "8001:8001" # 管理端口或 WebSocket 端口
restart: always
depends_on:
- etcd
- redis
entrypoint:
# 等待 etcd 和 Redis 就绪后启动
/im/bin/entrypoint.sh -h 106.75.144.240 -p 2379,6379 -c "/im/bin/gateway_server -flagfile=/im/conf/gateway_server.conf"
# 消息存储服务 (message_storage_server)
message_storage_server:
build: ../Service/message_storage
container_name: message_storage-service
volumes:
- ./conf/message_storage_server.conf:/im/conf/message_storage_server.conf
- ./middle/data/logs:/im/logs:rw
- ./middle/data/data:/im/data:rw
- ./entrypoint.sh:/im/bin/entrypoint.sh
ports:
- "10005:10005"
restart: always
depends_on:
- etcd
- mysql
- elasticsearch
- rabbitmq
entrypoint:
# 等待 etcd、MySQL、Elasticsearch、RabbitMQ 就绪后启动
/im/bin/entrypoint.sh -h 106.75.144.240 -p 2379,3306,9200,5672 -c "/im/bin/message_storage_server -flagfile=/im/conf/message_storage_server.conf"
# 语音识别服务 (speech_recognition_server)
speech_recognition_server:
build: ../Service/speech_recognition
container_name: speech_recognition-service
volumes:
- ./conf/speech_recognition_server.conf:/im/conf/speech_recognition_server.conf
- ./middle/data/logs:/im/logs:rw
- ./middle/data/data:/im/data:rw
- ./entrypoint.sh:/im/bin/entrypoint.sh
ports:
- "10001:10001"
restart: always
depends_on:
- etcd
entrypoint:
# 仅等待 etcd 就绪
/im/bin/entrypoint.sh -h 106.75.144.240 -p 2379 -c "/im/bin/speech_recognition_server -flagfile=/im/conf/speech_recognition_server.conf"
# 消息传输服务 (message_transmite_server)
message_transmite_server:
build: ../Service/message_transmite
container_name: message_transmite-service
volumes:
- ./conf/message_transmite_server.conf:/im/conf/message_transmite_server.conf
- ./middle/data/logs:/im/logs:rw
- ./middle/data/data:/im/data:rw
- ./entrypoint.sh:/im/bin/entrypoint.sh
ports:
- "10004:10004"
restart: always
depends_on:
- etcd
- mysql
- rabbitmq
entrypoint:
# 等待 etcd、MySQL、RabbitMQ 就绪后启动
/im/bin/entrypoint.sh -h 106.75.144.240 -p 2379,3306,5672 -c "/im/bin/message_transmite_server -flagfile=/im/conf/message_transmite_server.conf"
# 用户服务 (user_server)
user_server:
build: ../Service/user
container_name: user-service
volumes:
- ./conf/user_server.conf:/im/conf/user_server.conf
- ./middle/data/logs:/im/logs:rw
- ./middle/data/data:/im/data:rw
- ./entrypoint.sh:/im/bin/entrypoint.sh
ports:
- "10003:10003"
restart: always
depends_on:
- etcd
- mysql
- redis
- elasticsearch
entrypoint:
# 等待 etcd、MySQL、Redis、Elasticsearch 就绪后启动
/im/bin/entrypoint.sh -h 106.75.144.240 -p 2379,3306,6379,9200 -c "/im/bin/user_server -flagfile=/im/conf/user_server.conf"
这里面还需要注意一个点,我们仔细看看MySQL这部分
bash
# MySQL 数据库服务
mysql:
image: mysql:8.0.39
container_name: mysql-service
environment:
MYSQL_ROOT_PASSWORD: 123456 # root 用户密码
volumes:
- ./sql:/docker-entrypoint-initdb.d/:rw # 初始化 SQL 脚本目录
- ./middle/data/mysql:/var/lib/mysql:rw # 数据持久化目录
ports:
- "3306:3306"
restart: always
MySQL 官方镜像(mysql:8.0.39)在容器首次启动时,会自动执行 /docker-entrypoint-initdb.d/ 目录下的所有 .sql、.sh、.sql.gz 等文件。执行顺序按文件名排序。
因此,我们这里还需要准备好一个sql目录

这些其实都是.hxx文件编译出来的。
但是,这还不够,我们知道这个ODB编译出来的.sql文件是不会自己去选定这个数据库的,

那么我们就需要手动在这些.sql文件里面的最前面加上下面这2句(注意是所有.sql文件,因为你不知道docker先执行哪一个)
bash
CREATE DATABASE IF NOT EXISTS `IMS`;
USE `IMS`;

注意是全部.sql文件都需要加上哦!!!
五.使用Docker部署项目
如果我们是云服务器,那么我们需要提前去官网那里开放下面这些端口
- 2379
- 3306
- 6379
- 9200
- 5672
- 10001-10006
- 8000,8001
首先,我们直接来到deployment目录下执行这个命令即可
bash
./prepare_for_docker.sh
等待完成
我们直接来到我们的这个docker-compose.yml所在目录里,直接执行下面这个命令
bash
docker compose build --no-cache
这个命令的作用就是根据 docker-compose.yml 中每个服务定义的 build 指令(以及对应的 Dockerfile)构建出 Docker 镜像。

那么我们接下来就来构建我们的项目
注意,在运行起我们的镜像服务之前,我们需要先停止掉运行在我们宿主机上的这些服务,因为这些宿主机的服务和我们的容器里的那些服务绑定的端口是冲突的了
bash
# 停止服务
sudo systemctl stop mysql elasticsearch rabbitmq-server etcd redis-server redis
# 确认已停止
sudo systemctl status mysql elasticsearch rabbitmq-server etcd redis-server redis
停止了这些服务,我们才能去部署我们的项目
我们执行下面这个
bash
docker compose up

bash
ubuntu@10-13-52-255:~/cpp-chatsystem/server/deployment$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7aac28b26f2c message_transmite_server:latest "/im/bin/entrypoint...." 2 minutes ago Up 20 seconds 0.0.0.0:10004->10004/tcp, [::]:10004->10004/tcp message_transmite-service
4a08af144d65 user_server:latest "/im/bin/entrypoint...." 2 minutes ago Up 20 seconds 0.0.0.0:10003->10003/tcp, [::]:10003->10003/tcp user-service
4460dba772c4 gateway_server:latest "/im/bin/entrypoint...." 2 minutes ago Up 20 seconds 0.0.0.0:8000-8001->8000-8001/tcp, [::]:8000-8001->8000-8001/tcp gateway-service
ee78f7d22386 speech_recognition_server:latest "/im/bin/entrypoint...." 2 minutes ago Up 20 seconds 0.0.0.0:10001->10001/tcp, [::]:10001->10001/tcp speech_recognition-service
5857e76536de message_storage_server:latest "/im/bin/entrypoint...." 2 minutes ago Up 20 seconds 0.0.0.0:10005->10005/tcp, [::]:10005->10005/tcp message_storage-service
ef1d5724ed0a friend_server:latest "/im/bin/entrypoint...." 2 minutes ago Up 20 seconds 0.0.0.0:10006->10006/tcp, [::]:10006->10006/tcp friend-service
480575e3dabf file_server:latest "/im/bin/entrypoint...." 2 minutes ago Up 20 seconds 0.0.0.0:10002->10002/tcp, [::]:10002->10002/tcp file-service
baf39f1c69c5 mysql:8.0.39 "docker-entrypoint.s..." 2 minutes ago Up 21 seconds mysql-service
2208c2742b20 elasticsearch:7.17.21 "/bin/tini -- /usr/l..." 2 minutes ago Up 21 seconds elasticsearch-service
f2490b4153f4 redis:6.0.16 "docker-entrypoint.s..." 2 minutes ago Up 21 seconds redis-service
b1fb29d71bbf rabbitmq:3.9.13 "docker-entrypoint.s..." 2 minutes ago Up 21 seconds rabbitmq-service
0bab2bc8f1b0 quay.io/coreos/etcd:v3.3.25 "/usr/local/bin/etcd" 2 minutes ago Up 21 seconds etcd-service
这个时候就可以运行我们的客户端来测试了,这个我们就先不说了。