基于jenkins+docker实现CI/CD实践

项目简介

利用 Jenkins、Docker、SonarQube 和 Harbor 技术,搭建一个完整的 CI/CD 管道,实现持续集成、持续交付和持续部署的流程。通过自动化构建、测试、代码质量检查和容器化部署,将开发人员从繁琐的手动操作中解放出来,提高团队的开发效率、软件质量和安全性,实现持续更新迭代和持续部署交付。

CICD流程图

jenkins.drawio.png

流程说明

  1. 开发人员将代码提交到Gitlab代码仓库时,gitlab请求jenkins的webhook地址,触发持续构建和持续部署流程。
  2. Jenkins从Gitlab中拉取项目源码,编译并打成jar包,然后调用SonarQube完成代码扫描。
  3. 扫描完成调用docker打包成容器镜像,并推送至Harbor镜像仓库。
  4. Jenkins发送SSH远程命令,让生产部署服务器到Harbor私有仓库拉取镜像到本地,然后创建容器。
  5. Jenkins完成CICD流程后,将结果邮件通知给开发和运维人员。
  6. 用户访问项目服务器。

服务器列表

服务器名称主机名IP部署服务
代码托管服务器gitlab192.168.10.72Gitlab
持续集成服务器jenkins192.168.10.73Jenkins、Maven、Docker
代码审查服务器sonarqube192.168.10.71SonarQube
镜像仓库服务器harbor192.168.10.100Docker、harbor
服务部署服务器springboot192.168.10.74Docker

项目代码仓库地址

gitee:https://gitee.com/cuiliang0302/sprint_boot_demo
github:https://github.com/cuiliang0302/sprint-boot-demo

服务部署(rpm方式)

gitlab部署

参考文档:https://www.cuiliangblog.cn/detail/section/92727905

jenkins部署

参考文档:https://www.cuiliangblog.cn/detail/section/15130009

docker部署

参考文档:https://www.cuiliangblog.cn/detail/section/26447182

harbor部署

参考文档:https://www.cuiliangblog.cn/detail/section/15189547

SonarQube部署

参考文档:https://www.cuiliangblog.cn/detail/section/131467837

harbor项目权限配置

创建项目

Harbor的项目分为公开和私有的:
公开项目:所有用户都可以访问,通常存放公共的镜像,默认有一个library公开项目。
私有项目:只有授权用户才可以访问,通常存放项目本身的镜像。 我们可以为微服务项目创建一个新的项目
image.png

创建用户

创建一个普通用户cuiliang。
image.png

配置项目用户权限

在spring_boot_demo项目中添加普通用户cuiliang,并设置角色为开发者。
image.png
权限说明

角色权限
访客对项目有只读权限
开发人员对项目有读写权限
维护人员对项目有读写权限、创建webhook权限
项目管理员出上述外,还有用户管理等权限

上传下载镜像测试

可参考文章https://www.cuiliangblog.cn/detail/section/15189547,此处不再赘述。

gitlab项目权限配置

具体gitlab权限配置参考文档:https://www.cuiliangblog.cn/detail/section/131513569
创建开发组develop,用户cuiliang,项目springboot demo

创建组

管理员用户登录,创建群组,组名称为develop,组权限为私有
image.png

创建项目

创建sprint boot demo项目,并指定develop,项目类型为私有
image.png

创建用户

创建一个普通用户cuiliang
image.png

用户添加到组中

将cuiliang添加到群组develop中,cuiliang角色为Developer
image.png

开启分支推送权限

image.png

用户权限验证

使用任意一台机器模拟开发人员拉取代码,完成开发后推送至代码仓库。
拉取仓库代码

[root@tiaoban opt]# git clone https://gitee.com/cuiliang0302/sprint_boot_demo.git
正克隆到 'sprint_boot_demo'...
remote: Enumerating objects: 69, done.
remote: Counting objects: 100% (69/69), done.
remote: Compressing objects: 100% (54/54), done.
remote: Total 69 (delta 15), reused 0 (delta 0), pack-reused 0
接收对象中: 100% (69/69), 73.15 KiB | 1.49 MiB/s, 完成.
处理 delta 中: 100% (15/15), 完成.
[root@tiaoban opt]# cd sprint_boot_demo/
[root@tiaoban sprint_boot_demo]# ls
email.html  Jenkinsfile  LICENSE  mvnw  mvnw.cmd  pom.xml  readme.md  sonar-project.properties  src  test

推送至gitlab仓库

# 修改远程仓库地址
[root@tiaoban sprint_boot_demo]# git remote set-url origin http://192.168.10.72/develop/sprint-boot-demo.git
[root@tiaoban sprint_boot_demo]# git remote -v
origin  http://192.168.10.72/develop/sprint-boot-demo.git (fetch)
origin  http://192.168.10.72/develop/sprint-boot-demo.git (push)
# 推送代码至gitlab
[root@tiaoban sprint_boot_demo]# git push --set-upstream origin --all
Username for 'http://192.168.10.72': cuiliang
Password for 'http://cuiliang@192.168.10.72': 
枚举对象中: 55, 完成.
对象计数中: 100% (55/55), 完成.
使用 4 个线程进行压缩
压缩对象中: 100% (34/34), 完成.
写入对象中: 100% (55/55), 71.51 KiB | 71.51 MiB/s, 完成.
总共 55(差异 10),复用 52(差异 9),包复用 0
To http://192.168.10.72/develop/sprint-boot-demo.git* [new branch]      main -> main
分支 'main' 设置为跟踪 'origin/main'

查看验证
image.png

jenkins流水线配置

拉取gitlab仓库代码

具体步骤可参考文档:https://www.cuiliangblog.cn/detail/section/127410630,此处以账号密码验证为例,并启用webhook配置。
jenkins流水线配置如下
image.png
拉取代码部分的jenkinsfile如下

pipeline {agent anystages {stage('拉取代码') {environment {// gitlab仓库信息GITLAB_CRED = "gitlab-cuiliang-password"GITLAB_URL = "http://192.168.10.72/develop/sprint-boot-demo.git"}steps {echo '开始拉取代码'checkout scmGit(branches: [[name: '*/main']], extensions: [], userRemoteConfigs: [[credentialsId: "${GITLAB_CRED}", url: "${GITLAB_URL}"]])echo '拉取代码完成'}}}
}

当git仓库提交代码后,Gitlab会自动请求Jenkins的webhook地址,自动触发流水线,执行结果如下:
image.png

Maven打包编译

具体步骤可参考文档:https://www.cuiliangblog.cn/detail/section/131898197
打包编译部分的jenkinsfile如下

pipeline {agent anystages {stage('拉取代码') {……}stage('打包编译') {steps {echo '开始打包编译'sh 'mvn clean package'echo '打包编译完成'}}}
}

触发流水线结果如下
image.png

SonarQube代码审查

具体步骤可参考文档:https://www.cuiliangblog.cn/detail/section/165534414
代码审查阶段的jenkinsfile如下

pipeline {agent anystages {stage('拉取代码') {……}stage('打包编译') {……}stage('代码审查') {environment {// SonarQube信息SONARQUBE_SCANNER = "SonarQubeScanner"SONARQUBE_SERVER = "SonarQubeServer"}steps{echo '开始代码审查'script {def scannerHome = tool "${SONARQUBE_SCANNER}"withSonarQubeEnv("${SONARQUBE_SERVER}") {sh "${scannerHome}/bin/sonar-scanner"}}echo '代码审查完成'}}}
}

触发流水线结果如下
image.png
代码审查结果如下
image.png

构建并推送镜像至仓库

具体步骤可参考文档:https://www.cuiliangblog.cn/detail/section/166573065
构建并推送镜像的jenkinsfile如下

pipeline {agent anystages {stage('拉取代码') {……}stage('打包编译') {……}stage('代码审查') {……}stage('构建镜像') {environment {// harbor信息HARBOR_CRED = "harbor-cuiliang-password"HARBOR_URL = "harbor.local.com"HARBOR_PROJECT = "spring_boot_demo"// 镜像信息IMAGE_APP = "demo"IMAGE_TAG = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()IMAGE_NAME = "${HARBOR_URL}/${HARBOR_PROJECT}/${IMAGE_APP}:${IMAGE_TAG}"}steps {echo '开始构建镜像'script {docker.build "${IMAGE_NAME}"}echo '构建镜像完成'echo '开始推送镜像'script {docker.withRegistry("https://${HARBOR_URL}", "${HARBOR_CRED}") {docker.image("${IMAGE_NAME}").push()}}echo '推送镜像完成'echo '开始删除镜像'script {sh "docker rmi -f ${IMAGE_NAME}"}echo '删除镜像完成'}}}
}

触发流水线结果如下
image.png
查看harbor镜像仓库,已上传镜像
image.png

docker运行服务

远程执行命令具体内容可参考文档:https://www.cuiliangblog.cn/detail/section/166296541
部署运行阶段的jenkinsfile如下

pipeline {agent anyenvironment {// 全局变量HARBOR_CRED = "harbor-cuiliang-password"IMAGE_NAME = ""IMAGE_APP = "demo"} stages {stage('拉取代码') {……}stage('打包编译') {……}stage('代码审查') {……}stage('构建镜像') {……}stage('项目部署') {environment {// 目标主机信息HOST_NAME = "springboot1"}steps {echo '开始部署项目'// 获取harbor账号密码withCredentials([usernamePassword(credentialsId: "${HARBOR_CRED}", passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {// 执行远程命令sshPublisher(publishers: [sshPublisherDesc(configName: "${HOST_NAME}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "sh -x /opt/jenkins/springboot/deployment.sh ${HARBOR_USERNAME} ${HARBOR_PASSWORD} ${IMAGE_NAME} ${IMAGE_APP}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/opt/jenkins/springboot', remoteDirectorySDF: false, removePrefix: '', sourceFiles: 'deployment.sh')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])}echo '部署项目完成'}}}
}    

触发流水线后运行结果如下
image.png
登录springboot服务器验证

[root@springboot ~]# docker ps
CONTAINER ID   IMAGE                                            COMMAND                CREATED              STATUS                          PORTS                                       NAMES
e80896487125   harbor.local.com/spring_boot_demo/demo:8880a30   "java -jar /app.jar"   About a minute ago   Up About a minute (unhealthy)   0.0.0.0:8888->8888/tcp, :::8888->8888/tcp   demo
[root@springboot ~]# curl 127.0.0.1:8888/
<h1>Hello SpringBoot</h1><p>Version:v2 Env:test</p>[root@springboot1 ~]# 
[root@springboot ~]# ls /opt/jenkins/springboot/
deployment.sh  Dockerfile  email.html  Jenkinsfile  LICENSE  mvnw  mvnw.cmd  pom.xml  readme.md  sonar-project.properties  src  target  test

添加邮件通知推送

发送邮件配置具体内容可参考文档:https://www.cuiliangblog.cn/detail/section/133029974
在项目根路径下新增email.html文件,内容如下

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title><style>body {font-family: Arial, sans-serif;line-height: 1.6;margin: 0;padding: 0;}.container {max-width: 1000px;margin: 20px auto;padding: 20px;border: 1px solid #ccc;border-radius: 5px;}.container h2 {text-align: center;}.logo img {max-width: 150px;height: auto;}.content {padding: 20px;background-color: #f9f9f9;border-radius: 5px;}.footer {margin-top: 20px;text-align: center;}</style></head><body><div class="container"><div class="content"><h2>Jenkins ${PROJECT_NAME}项目构建结果</h2><p>尊敬的用户:</p><p>${PROJECT_NAME}项目构建结果为<span style="color:red;font-weight: bold;">${BUILD_STATUS}</span>,以下是详细信息:</p><h4>构建信息</h4><hr/><ul><li>项目名称:${PROJECT_NAME}</li><li>构建编号:第${BUILD_NUMBER}次构建</li><li>触发原因:${CAUSE}</li><li>构建状态:${BUILD_STATUS}</li><li>构建日志:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li><li>构建Url:<a href="${BUILD_URL}">${BUILD_URL}</a></li><li>工作目录:<a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li><li>项目Url:<a href="${PROJECT_URL}">${PROJECT_URL}</a></li></ul><h4>失败用例</h4><hr/><p>$FAILED_TESTS</p><h4>最近提交</h4><hr/><ul>${CHANGES_SINCE_LAST_SUCCESS, reverse=true, format="%c", changesFormat="<li>%d [%a] %m</li>"}</ul><h4>提交详情</h4><hr/><p><a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></p><p style="margin-top:50px">如有任何疑问或需要帮助,请随时联系我们。</p></div><div class="footer"><p>此为系统自动发送邮件,请勿回复。</p></div></div></body>
</html>

完整的jenkinsfile如下

pipeline {agent anyenvironment {// 全局变量HARBOR_CRED = "harbor-cuiliang-password"IMAGE_NAME = ""IMAGE_APP = "demo"} stages {stage('拉取代码') {environment {// gitlab仓库信息GITLAB_CRED = "gitlab-cuiliang-password"GITLAB_URL = "http://192.168.10.72/develop/sprint-boot-demo.git"}steps {echo '开始拉取代码'checkout scmGit(branches: [[name: '*/main']], extensions: [], userRemoteConfigs: [[credentialsId: "${GITLAB_CRED}", url: "${GITLAB_URL}"]])echo '拉取代码完成'}}stage('打包编译') {steps {echo '开始打包编译'sh 'mvn clean package'echo '打包编译完成'}}stage('代码审查') {environment {// SonarQube信息SONARQUBE_SCANNER = "SonarQubeScanner"SONARQUBE_SERVER = "SonarQubeServer"}steps{echo '开始代码审查'script {def scannerHome = tool "${SONARQUBE_SCANNER}"withSonarQubeEnv("${SONARQUBE_SERVER}") {sh "${scannerHome}/bin/sonar-scanner"}}echo '代码审查完成'}}stage('构建镜像') {environment {// harbor仓库信息
HARBOR_URL = "harbor.local.com"HARBOR_PROJECT = "spring_boot_demo"// 镜像名称IMAGE_TAG = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()}steps {echo '开始构建镜像'script {IMAGE_NAME = "${HARBOR_URL}/${HARBOR_PROJECT}/${IMAGE_APP}:${IMAGE_TAG}" docker.build "${IMAGE_NAME}"}echo '构建镜像完成'echo '开始推送镜像'script {docker.withRegistry("https://${HARBOR_URL}", "${HARBOR_CRED}") {docker.image("${IMAGE_NAME}").push()}}echo '推送镜像完成'echo '开始删除镜像'script {sh "docker rmi -f ${IMAGE_NAME}"}echo '删除镜像完成'}}stage('项目部署') {environment {// 目标主机信息HOST_NAME = "springboot1"}steps {echo '开始部署项目'// 获取harbor账号密码withCredentials([usernamePassword(credentialsId: "${HARBOR_CRED}", passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {// 执行远程命令sshPublisher(publishers: [sshPublisherDesc(configName: "${HOST_NAME}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "sh -x /opt/jenkins/springboot/deployment.sh ${HARBOR_USERNAME} ${HARBOR_PASSWORD} ${IMAGE_NAME} ${IMAGE_APP}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/opt/jenkins/springboot', remoteDirectorySDF: false, removePrefix: '', sourceFiles: 'deployment.sh')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])}echo '部署项目完成'}}}post {always {echo '开始发送邮件通知'// 构建后发送邮件emailext(subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',body: '${FILE,path="email.html"}',to: 'cuiliang0302@qq.com')echo '邮件通知发送完成'}}
}    

触发流水线后运行结果如下
image.png
邮件通知内容如下
image.png
至此,整个CICD流程完成。

查看更多

微信公众号

微信公众号同步更新,欢迎关注微信公众号《崔亮的博客》第一时间获取最近文章。

博客网站

崔亮的博客-专注devops自动化运维,传播优秀it运维技术文章。更多原创运维开发相关文章,欢迎访问https://www.cuiliangblog.cn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/314710.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQLite运行时可加载扩展(三十五)

返回&#xff1a;SQLite—系列文章目录 上一篇:SQLite轻量级会话扩展&#xff08;三十四&#xff09; 下一篇:SQLite的DBSTAT 虚拟表&#xff08;三十六) 1. 概述 SQLite 能够在运行时加载扩展&#xff08;包括新的应用程序定义的 SQL 函数、整理序列、虚拟表和 VFS&…

【深度学习】烟雾和火焰数据集,野外数据集,超大量数据集,目标检测,YOLOv5

标注了2w张数据集&#xff0c;是目标检测yolo格式的&#xff0c;有火焰、烟雾两个目标&#xff0c;下图是训练时候的样子&#xff1a; 训练方法看这里&#xff1a; https://qq742971636.blog.csdn.net/article/details/138097481 数据集介绍 都是博主辛苦整理和标注的&…

C++必修:类与对象(一)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C学习 贝蒂的主页&#xff1a;Betty’s blog 1. 面向过程与面向对象 1.1. 面向过程 我们之前学习的C语言就是一种面向过程的语…

SVN小乌龟汉化问题

1.首先确认中文语言包和SVN版本需要一致&#xff08;点击右键 选择最后一个选项即可查看&#xff09; 官网链接 点击这个官网链接可以下载对应版本的中文包 2.下载好之后直接无脑下一步安装即可 3.如果还是没有中文&#xff0c;找到这个文件夹&#xff0c;把里面的内容全部删…

【css】select实现placeholder效果

场景&#xff1a;使用select下拉选择框的时候&#xff0c;需要像其他控件一样提示默认信息。 问题&#xff1a;表单控件select没有placeholder属性。 解决方案&#xff1a;通过css实现&#xff0c;不需要js <style>select > option[disabled]{ color:#999;cursor: n…

VUE3 ref,props,生命周期

1.--ref属性 1.1代码 1.1.1子表 <template><div class"person"><h1>中国</h1><h2 ref"title2">北京</h2><h3>尚硅谷</h3><button click"showLog">点我输出h2这个元素</button>&l…

【软测学习笔记】Day01

&#x1f31f;博主主页&#xff1a;我是一只海绵派大星 &#x1f4da;专栏分类&#xff1a;前端 &#x1f4da;参考教程&#xff1a;黑马教程❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、测试介绍 1、什么是软件测试&#xff1f; 2、测试主流技能 二、测试常用分…

unity cinemachine相机 (案例 跟随角色移动)

安装相机包 打开包管理工具 在 unity registry 搜索cinemachine 会在maincamera中生成一个组件cinemachineBrain 只能通过虚拟相机操控 主相机 虚拟相机的参数 案例 1.固定相机效果 位置 在固定的地方 默认的模式 2.相机跟随人物效果 焦距设置 20 跟随设置 把playere…

Django框架之python后端框架介绍

一、网络框架及MVC、MTV模型 1、网络框架 网络框架&#xff08;Web framework&#xff09;是一种软件框架&#xff0c;用于帮助开发人员构建Web应用程序和Web服务。它提供了一系列预先编写好的代码和工具&#xff0c;以简化开发过程并提高开发效率。网络框架通常包括以下功能…

Web前端框架/库/工具

前言 俗话说&#xff1a;前端从步枪&#xff08;原生js&#xff09;到了半自动武器&#xff08;jQuery&#xff09;并进化为全自动武器&#xff08;三大框架&#xff08;angular&#xff0c;react&#xff0c;vue及其生态链&#xff09;&#xff09;。 常说工欲善其事必先利其…

python作业 切片逆转

题目&#xff1a; &#xff08;反转显示一个整数&#xff09;编写下面的函数&#xff0c;反向显示一个整数。 列如&#xff1a;reserse(3456)。编写一个测试程序&#xff0c;提示用户输入一个整数&#xff0c;然后显示它的反向数。 第一步定义一个函数&#xff1a; def rev…

AUTOSAR-SD篇

1 概述 服务发现模块的主要任务是管理在车内通信中被称为服务的功能实体的可用性&#xff0c;以及控制事件消息的发送行为。只允许向需要这些事件消息的接收器发送事件消息&#xff08;发布/订阅&#xff09;。 这里描述的解决方案也被称为SOME/IP-SD&#xff08;基于IP -服务发…

前端css中keyframes(关键帧)的简单使用

前端css中keyframes的使用 一、前言二、例子&#xff08;一&#xff09;、例子源码1&#xff08;二&#xff09;、源码1运行效果1.视频效果2.截图效果 三、结语四、定位日期 一、前言 关键帧keyframes是css动画的一种&#xff0c;主要用于定义动画过程中某一阶段的样式变化&am…

【STM32+HAL】读取电池电量

一、准备工作 有关CUBEMX的初始化配置&#xff0c;参见我的另一篇blog&#xff1a;【STM32HAL】CUBEMX初始化配置 有关定时器触发ADC模式配置&#xff0c;详见【STM32HAL】ADC采集波形实现 有关软件触发ADC模式配置&#xff0c;详见【STM32HAL】三轴按键PS2摇杆 二、所用工具…

Bytebase 2.16.0 - 支持 Oracle 和 SQL Server DML 变更的事前备份

&#x1f680; 新功能 支持 Oracle 和 SQL Server DML 变更的事前备份。 支持在 SQL 编辑器中显示存储过程和函数。 支持兼容 TDSQL 的 MySQL 和 PostgreSQL 版本。 支持把数据库密码存储在 AWS Secrets Manager 和 GCP Secret Manager。 支持通过 IAM 连接到 Google Clou…

VirtualBox7.0.16的蓝屏大坑与ssh登陆ubuntu虚拟机的办法

背景&#xff1a; 安装了最新版的VirtualBox&#xff0c;装了ubuntu系统&#xff0c;在win10下通过ssh死活无法与ubuntu进行正常登陆控制。 然后开始了踩坑。 问题1&#xff1a;ssh登陆失败&#xff0c;但是主机能ping通ubuntu&#xff0c;反过来也能ping通&#xff0c;网络…

线性代数基础1向量

1、向量是什么 1.1、向量的定义 在数学中&#xff0c;向量&#xff08;也称为欧几里得向量、几何向量、矢量&#xff09;&#xff0c;指具有大小和方向的量。它可以形象化地表示为带箭头的线段。箭头所指&#xff1a;代表向量的方向&#xff1b;线段长度&#xff1a;代表向量的…

增加PyQt5界面的交通流量预测(模型为CNN_GRU,CNN_BiGRU_ATTENTION,LSTM,Python代码)

1.效果视频&#xff1a;增加PyQt5界面的交通流量预测&#xff08;模型为CNN_GRU&#xff0c;CNN_BiGRU_ATTENTION&#xff0c;LSTM&#xff09;_哔哩哔哩_bilibili&#xff09; 2.三个模型和数据集的介绍 交通流量预测(python代码&#xff0c;压缩包中带有数据&#xff0c;CN…

NAT的知识点和实现

1.NAT的作用&#xff1a; &#xff08;1&#xff09;、把内网私网IP转换公网IP&#xff1b; &#xff08;2&#xff09;、隐藏内网&#xff0c;起到保护内网作用&#xff1b; &#xff08;3&#xff09;、适当的缓解的IPv4地址空间枯竭&#xff1b; &#xff08;4&#xff…

自己搭建的大疆无人机RTMP流媒体服务延迟太大

流程&#xff1a;无人机摄像头->图传->遥控器->流媒体服务器->取流播放&#xff0c;延迟有10秒来的&#xff0c;大家有没有什么好的方案。