当前位置: 首页 > news >正文

基于AIOHTTP、Websocket和Vue3一步步实现web部署平台,无延迟控制台输出,接近原生SSH连接

背景:笔者是一名Javaer,但是最近因为某些原因迷上了Python和它的Asyncio,至于什么原因?请往下看。在着迷”犯浑“的过程中,也接触到了一些高并发高性能的组件,通过简单的学习和了解,aiohttp这个组件引起了我极大的兴趣。

协程、异步非阻塞、”吓人“的性能,这些关键词让我不得不注意到它。

老样子,我们先看成品,再讲讲我曲折的过程。

读取实时日志

 构建+部署

技术痛点揭露

相信大家一定遇到过笔者这次的场景:

        疫情隔离,居家办公,这次我们做的是一个小程序,前端的小伙伴们要联调接口,可是不能用公司的资源,因为公司都断电了😭 ,于是乎我自己买(bai)了(piao)云服务器,自己搭建了一套环境,用自己的域名给他们架上了。本以为事情解决了,前后端可以愉快地调试接口了,但是想都别想,现实还是无情地用它宽大的手掌啪啪打我的三寸小脸。

        你看,后端的小伙伴写完代码,改完bug,提交了之后,一次又一次的让你部署,导致吃饭都想着部署,每次都是噼里啪啦一堆命令,脑瓜子嗡嗡的。(First Blood!)

        你再看,后端猿小帅和前端媛小美正在对接接口,小美说接口怎么一直报错?小帅眉头一皱,手一抖,微信窗口多出个小表情,一脸无辜:"我本地可以啊!"。对,就是这句话,“我本地可以啊,为啥线上不行”,成了接口对接中的口头禅。完了,肯定又是我的活,这不,咔咔一顿"艾特",让我帮忙看日志,啊啊啊啊啊啊,一天到晚登上服务器看了不下N次日志,我的头发在抗议。(Double Kill!)

        你再再再看,我们对接用的yapi,在接口未完成之前,前端调用的是mock,完成之后,得切换到真实接口。为了保证项目开发进度,让前后端的联调顺滑如丝,那付出的肯定是我了。一天下来,在忙上面事情的同时,我还在不断地调整Nginx反向代理配置,为他们放开一个个接口的代理。我内心只能说:mmp。(Triple Kill!)

        你再再再再看看,正常开发过程中总有些粗心捣蛋的人,提交的代码像一个炸弹。这不,刚刚这哥们还在小区楼底下蹦迪,下一秒回家晕乎乎地写了几个bug,潇洒提交,又蹦迪去了。这不提交不要紧,一提交之后,紧接着我习惯性的部署上去,一系列的连锁反应导致几个接口不能用了,兄弟们叫苦不迭,要不是居家,我真想上去抽那仁兄几个嘴巴子。这屁股还是得我擦,回退到上个版本,先凑合调试着。这种操作隔三差五在上演,也是麻烦的很。。。(Quadra kill!)

        最后脑补一个五杀(Penta kill!)🧠

尝试曲线救国

        上面列举了那么多痛点,是个人都被折磨的够呛吧,拜托🙏🏻,疫情即使在家办公也是要高效,更何况家人都在身边,不能焦躁,不能焦躁,不能焦躁!

        这个时候就有大佬说了,你搞这么多费力不讨好的事情,为啥不直接用CI/CD(持续集成)呢?我花费了5根头发想了想,我这1GB内存,1核CPU还能再战吗?再摸了摸我那比纸都薄的钱包,最后点了三炷香“祭奠”了一下我死去的5根头发,心里默默说了声,算了,忍忍,你可以的。

方案一   脚本大法 + 代理

        我开始尝试写脚本。我们的项目是微服务,正常部署都应该用docker-compose,或者直接上到k8s集群里,但是非常时期我们没有办法,只能人工部署。所以我写了一个又一个的脚本,然后写好备注,然后写了一个Low到爆的 HTML,写了超级烂的几行Java代码来调用这些脚本。最后通过Nginx给他们代理出去,把URL分发出去让他们自己点。

        我花费了几个小时完成了上述工作,就在我以为万事大吉的时候,我发现我服务器进不去了。。。WTF?登上控制台,看到CPU使用率125%?我就一个核怎么还超过100捏?虚机超频?呸呸呸,言归正传,排查了半天,我发现是因为多个人短时间内执行构建脚本和部署脚本,直接启动多个进程把机器“干”死了,我摇了摇头,方案1?去你的吧。

方案二  方案一的“进化”

        鉴于方案一存在的致命短板,我不得不针对这个问题进行优化,优化的手段嘛,不出大家所料,还是脚本,用low到爆的一个办法:每次运行构建,都通过 ps | grep | xargs kill -9 杀死之前的进程,再进行构建。运行结果也增加了反馈,用户执行结果会根据Sheel执行返回值进行判断,给出成功与否的响应。至于触发方法嘛,当然是老样子, 继续HTML点击,Java调用脚本。

        再次试验效果,我组织了一场视频会议,会议上我让小美和阿伟还有阿强分别点击部署,哇,效果嘎(chao)嘎(la)的(ji),小美先点击的居然部署成功了,阿伟和阿强后来的居然被杀了?awsl(阿伟死了)。后来排查了半天,发现小美家的wifi只有一格信号,请求发到后台慢了。总体来说方案二的可用度提高了,但是依然没什么卵用,小美提交的代码运行一会后报错了,原因是阿伟提交的一段代码引用了jdk中sun包内的东西,服务器openjdk没有相关类,服务压根没起来。还是很鸡肋。

方案三 另辟蹊径

        方案一和方案二都是短时间内拍脑门儿想出来的活,到现在为止我已经发现问题不能这么草率的解决了, 否则永远都是不断地返工。我深刻地分析了一下,作为一个完备的协同部署功能,至少需要满足以下几个条件:

1. 能够协同工作和实时交互。看了比较大的运维平台,基本上都具备实时的反馈,接近SSH会话级别的体验,能够确认当前的部署状态和部署进度,用户可以及时发现并避免和其他人的交叉使用。此外,如果可能的话,应该实现当前部署状态未完成,其他用户不可操作服务器。

2. 能够查看实时日志。系统运行的状况如何,应该具备日志查看的入口,这些入口开放给开发人员,才能够做到每个人都能及时处理自己的问题。此外,日志滚动频率过快,应该提供“暂停日志”和“恢复日志”的能力。

3. 实现用户身份标识。该功能也是必须的,因为通过HTML按钮点击部署出了问题,往往无法追溯是谁干的😭。后面,我设计为每个用户提供身份标识确认,通过线下发放key的方式提供服务的使用权限,每个key可以绑定到具体的用户,绑定key后,才可以正常使用运维能力。

4. 具有版本控制和一键回滚。一个合格的部署平台,必须具有防范风险的能力,体现在健壮性上来说,就是版本控制。利用shell脚本实现版本控制并不难,实现一键回滚也不难,难的是库表结构修改后产生的种种恩怨情仇。

        经过系统的分析之后,我们说干就干。

= 开始干活 =

        工欲善其事,必先利其器。干活前老样子,先做技术选型。为了一步到位,我直接选择了Vue3去写前端,后端压根没想着用Java去写,因为我写过很多pipleline的代码,java处理起来冗长又效率低下,果断选择了Python大法。事实证明,我的选择太明智了。

搭建Vue项目

技术栈

  • Vue 3
  • Ant Design Vue 3.1.1
  • Socket.io-client
  • CodeMirror Editor
  • Axios

我们使用最新的vue-cli搭建项目。

1. 环境准备

# 安装 Vue CLI
npm install -g @vue/cli# 创建项目
vue create web# 安装依赖
cd web
yarn add ant-design-vue @ant-design/icons-vue axios socket.io-client codemirror-editor-vue3

2. 项目配置

babel.config.js - 按需加载配置
module.exports = {presets: ['@vue/cli-plugin-babel/preset'],plugins: [["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }]]
}
vue.config.js - 开发服务器配置
module.exports = defineConfig({transpileDependencies: true,devServer: {port: 7777,proxy: {'/api': {target: 'http://localhost:8090',ws: false,changeOrigin: true,},}},
})

2. 核心功能实现

1. WebSocket 通信模块

项目使用 Socket.io 实现与后端的实时通信:

const wsConnect = (write, done) => {const handler = (data) => {// 处理管道数据const { success, end, content, msg = '' } = dataif (success) {content && write(content.replaceAll('\0', ' '))end && write('\n任务执行完毕', done)} else {write('\n管道读取失败!' + msg, done)}}return {io: null,async connect() {this.io = io(WS_URL, {transports: ['websocket'],query: { token }}).on('pipeline', this.handler.bind(this))},// 发送请求request(event, data = {}) {return new Promise((resolve, reject) => {const rid = Date.now()this.io.emit(event, { rid, ...data })this.pending[rid] = { resolve, reject }})}}
}
2. 部署控制台实现
<template><div class="deploy"><a-card :body-style="{padding: '10px 24px'}"><div class="opt-group">构建和部署:<a-button v-if="!deploying" :disabled="running" class="primary" type="primary" @click="deploy"><template #icon><build-filled /></template>构建并部署</a-button><a-button v-else :loading="stopping" class="primary" type="danger" @click="stop"><template #icon><close-circle-filled /></template>停止部署</a-button><a-button :disabled="deploying || running" class="primary" type="primary" @click="web"><template #icon><global-outlined /></template>构建部署前端</a-button><a-dropdown-button :disabled="deploying || running" type="danger" @click="restore" @visibleChange="loadHistories"><hourglass-filled />回滚版本<template #overlay><a-menu @click="editFile"><template v-if="histories.length"><a-menu-item :key="file" v-for="file in histories">{{file}}</a-menu-item></template><a-menu-item v-else disabled key="more">暂无可回滚版本</a-menu-item></a-menu></template></a-dropdown-button></div><a-divider class="divider" type="vertical" /><div class="opt-group">运行监控:<a-button v-if="!running" :disabled="deploying" class="primary" type="primary" @click="log"><template #icon><snippets-filled /></template>读取运行日志</a-button><a-button v-else :loading="stopping" class="primary" type="danger" @click="stop"><template #icon><close-circle-filled /></template>停止日志读取</a-button><a-button :disabled="deploying || stopping || running" :loading="restarting" class="primary" type="danger"@click="restart"><template #icon><appstore-filled /></template>重启项目</a-button><a-button v-if="!paused" :disabled="!running" @click="pause"><template #icon><pause-circle-filled /></template>暂停日志</a-button><a-button v-else :disabled="!running" type="primary" @click="play"><template #icon><play-circle-filled /></template>恢复日志</a-button></div><template v-if="admin"><a-divider class="divider" type="vertical" /><div class="opt-group">配置维护:<a-button class="primary" type="primary" @click="editFile"><template #icon><setting-filled /></template>修改配置文件</a-button><a-dropdown trigger="click"><template #overlay><a-menu @click="editFile"><a-menu-item :key="file" v-for="file in files">{{file}}</a-menu-item><a-menu-item key="more">创建脚本...</a-menu-item></a-menu></template><a-button @click="loadFiles">修改项目脚本<DownOutlined /></a-button></a-dropdown></div></template></a-card><a-card><template #extra><a href="#">当前版本v1.5.3</a></template><template #title><code-filled style="margin-right: 10px" />控制台<a-divider type="vertical" /><a v-if="current === 'deploy'">当前:部署日志</a><a v-else>当前:运行日志</a></template><code-mirror ref="editorRef" :height="350" :options="cmOptions" class="console" /></a-card><a-drawerwidth="1000":visible="!!editing.key"title="修改文件内容"placement="right"><code-mirror height="100%" :options="cmOptions" v-model:value="editing.content" class="console" /><template #footer><div style="text-align: center"><a-button style="margin-right: 8px" @click="editing = {content: ''}">取消</a-button><a-button type="primary" :loading="editing.loading" @click="saveFile">保存</a-button></div></template></a-drawer></div>
</template><script>
import { onMounted, ref } from 'vue';
import CodeMirror from 'codemirror-editor-vue3';
import { message, Modal } from 'ant-design-vue';
import {AppstoreFilled,BuildFilled,CloseCircleFilled,CodeFilled,DownOutlined,GlobalOutlined,HourglassFilled,PauseCircleFilled,PlayCircleFilled,SettingFilled,SnippetsFilled,
} from '@ant-design/icons-vue';
// import base style
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/material-darker.css'
// language
import 'codemirror/mode/javascript/javascript.js';
import createSocket from '@/api/pipeline';export default {name: 'DeployPage',components: {CodeMirror,CodeFilled,GlobalOutlined,BuildFilled,SettingFilled,HourglassFilled,SnippetsFilled,PauseCircleFilled,PlayCircleFilled,CloseCircleFilled,AppstoreFilled,DownOutlined},// 在我们的组件中setup() {// socketioconst socket = ref(null);// 编辑器实例const editorRef = ref(null);const editor = ref(null);// 活跃的回调const callback = ref(null);// 日志运行状态const running = ref(false);// 日志暂停状态const paused = ref(false);// 部署运行状态const deploying = ref(false);// 重启运行状态const restarting = ref(false);// 通用停止按钮状态const stopping = ref(false);// 配置文件列表const files = ref([]);// 历史版本列表const histories = ref([]);// 当前控制台视图,支持deploy部署、log日志const current = ref('deploy');// 运行管道信息const runningKey = ref('');// 缓存内容const cache = ref('');// 当前编辑文件const editing = ref({});// 目标对应字段const dicts = {set deploy(value) {deploying.value = value;},get deploy() {return deploying.value;},set web(value) {deploying.value = value;},get web() {return deploying.value;},set log(value) {running.value = value;},get log() {return running.value;},set restart(value) {restarting.value = value;running.value = value;if (!value) {appender('\n已完成重启,请读取日志查看')}},get restart() {return restarting.value;},set restore(value) {deploying.value = value;},get restore() {return deploying.value;},};// 日志追加器const appender = (text, end, clear) => {if (clear) {return editor.value?.setValue(text || '')}if (text) {// 如果暂停了,进缓存if (paused.value) {cache.value += text;} else {editor.value?.replaceRange(text, { line: Infinity });editor.value?.scrollTo(0, Infinity);}}// 具有回调,代表结束,做一些重置if (end) {end();cache.value = '';paused.value = false;restarting.value = false;runningKey.value = '';}};// 建立pipeline并读取const connector = async (target) => {current.value = target;callback.value = error => {if (error) appender('\n连接中断或异常,' + error);dicts[target] = false;};try {dicts[target] = true;runningKey.value = await socket.value.open(target);appender('\n管道建立成功!进程id:' + runningKey.value + '\n');} catch (e) {appender('\n无法建立管道连接,' + e.message, callback);}}// 挂载后获取实例onMounted(async () => {editor.value = editorRef.value?.cminstance;socket.value = await createSocket(appender, callback).connect();const instance = editor.value;if (instance) {instance.setValue('暂无运行日志\n\n\n\n\n\n\n\n\n\n\n\n\n');instance.focus();}});// 返回命名空间return {editorRef,running,stopping,current,deploying,restarting,paused,files,histories,editing,get admin() {return socket.value?.admin;},play: () => {paused.value = false;const cached = cache.value;cache.value = ''appender(cached);},pause: () => paused.value = true,deploy: async () => connector('deploy'),log: async () => connector('log'),restart: async () => connector('restart'),web: async () => connector('web'),// 停止pipeline并清理stop: async () => {try {stopping.value = true;await socket.value.kill(runningKey.value);appender('\n成功发送杀死指令')} catch (e) {appender('\n杀死作业失败!' + e.message)} finally {stopping.value = false;}},restore: () => {Modal.confirm({title: '请确认操作',content: '该操作会将上次运行的构建结果替换到当前环境运行,并且不可撤销,请确认操作',okText: '我确定',cancelText: '还是不了',onOk: async () => connector('restore'),})},loadFiles: async () => {files.value = await socket.value.listFile();},loadHistories: async visible => {try {histories.value = visible ? await socket.value.listHistory() : [];} catch (e) {message.error(e.message);}},editFile: async ({ key }) => {try {const body = { key };if (!key) {Object.assign(body, await socket.value.createFile('config.json'))} else if (key === 'more') {Object.assign(body, await socket.value.createFile())} else {body.content = await socket.value.getFile(key)}editing.value = body;} catch (e) {message.warn(e.message || e);}},saveFile: async () => {const close = message.loading('正在保存中...', 0);try {editing.value.loading = true;const { key, content } = editing.value;await socket.value.saveFile(key, content);editing.value = { content: '' };message.success('保存成功!')} catch (e) {message.error(e.message);} finally {close();editing.value.loading = false;}},cmOptions: {mode: "text/javascript", // Language modetheme: "material-darker", // ThemelineNumbers: true, // Show line numbersmartIndent: true, // Smart indentviewportMargin: 350,indentUnit: 2, // The smart indent unit is 2 spaces in lengthfoldGutter: true, // Code foldingstyleActiveLine: true, // Display the style of the selected row},}},
}
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {margin: 40px 0 0;
}ul {list-style-type: none;padding: 0;
}li {display: inline-block;margin: 0 10px;
}a {color: #42b983;
}.primary {margin-right: 20px
}.console {}.divider {margin: 0 15px;
}.opt-group {display: inline-block;line-height: 50px;
}@media screen and (max-width: 954px) {.opt-group {display: block;}.divider {display: none;}
}
</style>
3. 项目结构
web/
├── src/
│   ├── components/    # 组件
│   │   └── Deploy.vue # 部署控制台组件
│   ├── App.vue        # 根组件
│   └── main.js        # 入口文件
├── public/
│   └── banner.jpg     # 静态资源
├── babel.config.js    # babel配置
└── vue.config.js      # Vue CLI配置

至此,我们实现了:

  • 实时部署状态监控
  • 运行日志实时查看
  • 配置文件在线编辑
  • 版本回滚功能
  • 项目重启功能

总结一下,这波操作采用 Vue 3 + Ant Design Vue 的技术栈,实现了一个功能完整的智能部署控制台。通过 WebSocket 实现了与后端的实时通信,使用 CodeMirror 提供了良好的代码编辑体验。

Python异步部署服务端

经过技术的吸收,我实现了基于Python 3.9+的异步部署工具,主要特点:

  • 基于WebSocket的全双工实时通信
  • 支持自定义部署脚本
  • 支持配置热加载
  • 支持多用户管理
  • 支持部署历史版本管理

技术栈

  • Python 3.9+

  • aiohttp - 异步Web框架

  • python-socketio - WebSocket库

  • SQLite3 - 轻量级数据库

  • watchdog - 文件监控

核心实现

1. WebSocket服务器

使用python-socketio实现WebSocket服务器:

# 初始化socketio服务器
sio = socketio.AsyncServer(async_mode='aiohttp',cors_allowed_origins=['http://localhost:7777', 'http://deploy.flyfish.group'])
app = web.Application()
sio.attach(app)# 处理连接事件
@sio.event
async def connect(sid, environ):user = validate_token(environ['aiohttp.request'], sid)# 缓存客户端clients[sid] = {'process': None, 'killed': False, 'name': user['name']}await send(sid, {'success': True, 'user': {'name': user['name'], 'authority': user['authorities']}})# 处理断开事件
@sio.event 
async def disconnect(sid):if sid in clients:process = clients[sid]['process']if process:await kill_pipeline(sid, {'pid': process.pid})del clients[sid]
2. 异步管道实现

使用asyncio.create_subprocess_shell创建子进程,实现命令执行:

# 打开管道
@sio.event
async def open_pipeline(sid, message):# 取得类型和命令pipe_type = message['type']command = configs['scripts'][pipe_type]# 启动子进程proc = await asyncio.create_subprocess_shell(f'cd {configs["work_dir"]} && {command} {client["name"]}',stdout=asyncio.subprocess.PIPE,preexec_fn=os.setsid)# 返回成功await send(sid, {'success': True, 'pid': proc.pid, 'rid': message['rid']})# 等待提交await submit(sid, proc)# 异步读取输出
async def submit(sid, proc):item = clients[sid]item['process'] = procwhile True:# 异步读取输出line = await proc.stdout.read(BLOCK_SIZE)if not line:break# 实时推送到客户端    await send(sid, {'success': True, 'content': str(line, encoding='utf-8')})
3. 配置热加载

使用watchdog监控配置文件变化:

# 配置文件监听器
class ConfigFileHandler(FileSystemEventHandler):def on_modified(self, event):path = event.src_pathif path.endswith('config.json'):print("修改了配置文件,尝试加载...")if load_config():print('😊配置文件已经重载')# 初始化监听
async def init_app():observer = Observer()observer.schedule(ConfigFileHandler(), './')observer.start()load_config()return app
4. 数据库操作封装

使用上下文管理器封装SQLite操作:

class SqlSession:def execute(self, sql, param=()):with self.conn:cursor = self.conn.cursor()try:return cursor.execute(sql, param)except sqlite3.Error as e:cursor.close()raise eclass SqlOperation:# 插入操作def insert(self, data):if 'id' in data:del data['id']data['create_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')keys = data.keys()values = data.values()sql = f'insert into {self.table} ({",".join(keys)}) values ({",".join(["?"] * len(keys))})'self.session.execute(sql, tuple(values))
5. Webhook实现

实现Gitee的Webhook接收,能够自动部署,持续集成:

@app.route('/deploy', methods=['POST'])
def post_data():# 验证tokentoken = request.headers.get('X-Gitee-Token')if token != gitee_secret:return "token认证无效", 401# 获取推送信息data = json.loads(request.data)name = data['pusher']['name']# 执行部署脚本os.system(f'sh deploy.sh {name}')return jsonify({"status": 200})

项目结构

hooks/
├── bin/                # 核心代码
│   ├── app.py         # WebSocket服务器
│   ├── db.py          # 数据库操作
│   ├── hook.py        # Webhook接收器
│   └── post.py        # 消息推送
├── logs/              # 日志目录
└── requirements.txt   # 依赖配置

总结

到此,我们实现了以下所有能力

1. 全异步通信

  • 使用aiohttp和python-socketio实现全双工通信
  • 异步子进程管理,实时输出
  • 支持多用户并发操作
  • 实时配置
  • 配置文件热加载
  • 支持自定义部署脚本
  • 支持工作目录配置
  • 用户管理
  • 基于Token的认证
  • 会话管理
  • 权限控制

4. 部署管理

  • 支持部署历史
  • 支持版本回滚
  • 支持运行日志查看

通过以上努力,我采用Python异步编程实现了一个功能完整的部署工具,通过WebSocket实现了与前端的实时通信,支持多用户并发操作。

结束语 - 让部署不再是996的理由 🚀

写在最后

各位看官读到这里,相信你已经发现这不是一个普通的部署工具,而是一个能让你告别"部署恐惧症"的神器!

从此告别的场景 😂

  • 再也不用半夜被运维电话叫醒:"服务器挂了!"
  • 不用每次部署都像在玩俄罗斯轮盘赌
  • 告别"在我电脑上能运行"系列尴尬
  • 不用为搞错配置而痛哭流涕

你将收获的快乐 🎉

  • 一键部署,比订外卖还快
  • 实时日志,像看抖音一样上瘾
  • 版本回滚,时光机般的存在
  • 配置热加载,改完配置说走就走

写给犹豫的你 🤔

如果你还在为以下问题困扰:

  • 部署靠"祈祷"
  • 改配置要"跪求"
  • 看日志要"冥想"
  • 回滚要"许愿"

那么,来试试这个工具吧!它不仅能让你的部署工作变得轻松愉快,还能让你在同事面前装个小小的技术大佬。😎

彩蛋时间 🎮

知道为什么我们选择 WebSocket 吗?

  • 因为 HTTP 太慢了,慢得像极了周一的早晨
  • 因为实时通信,快得像极了发工资的瞬间
  • 因为全双工通信,比你谈恋爱还要双向奔赴

最后的最后 🌟

记住,这个工具的诞生不是为了让你加班,而是为了让你有更多时间:

  • 摸鱼 🐟
  • 追剧 📺
  • 打游戏 🎮
  • 谈恋爱 💑

如果这个项目帮你节省了时间,别忘了给我们点个星⭐️

如果没帮你节省时间...那一定是你还没用熟练 😅

愿你的每一次部署,都像喝可乐一样爽快!

愿你的每一次发版,都像春游一样愉快!

愿你的每一次回滚,都像退货一样简单!

结语中的结语 📝

记住我们的口号:

> 部署不再难,生活更自然!

>

> 配置不用愁,周末早回家!

>

> 日志一目了然,Bug无处遁藏!

最后送大家一句话:

> 工具再好,也补不了你的bug!

>

> 但至少...它能让你改bug的时候心情好一点!😊

好了,快去试试吧!让我们一起告别996,拥抱995.9!🎯


注:本项目副作用可能包括但不限于:让你对其他部署工具产生严重的依赖性鄙视,让你的同事对你投来羡慕的眼光,让你的老板觉得你太闲需要安排更多任务... 😜

代码下载

🎉 是的!我们开源啦!

💝 为什么要开源?

因为我们相信:

  • 好的代码应该像老婆的美貌一样,值得炫耀
  • 优秀的项目应该像奶茶一样,值得分享
  • 牛逼的工具应该像八卦一样,让更多人知道

下载地址奉上,希望大家支持!开发不易,请尊重博主的劳动成果!

https://download.csdn.net/download/wybaby168/90373568

相关文章:

基于AIOHTTP、Websocket和Vue3一步步实现web部署平台,无延迟控制台输出,接近原生SSH连接

背景&#xff1a;笔者是一名Javaer&#xff0c;但是最近因为某些原因迷上了Python和它的Asyncio&#xff0c;至于什么原因&#xff1f;请往下看。在着迷”犯浑“的过程中&#xff0c;也接触到了一些高并发高性能的组件&#xff0c;通过简单的学习和了解&#xff0c;aiohttp这个…...

CentOS 7.8 安装MongoDB 7教程

文章目录 CentOS 7.8 安装MongoDB 7教程一、准备工作1. 系统更新2. 权限 二、添加MongoDB软件源1. 创建MongoDB的yum源文件2. 添加以下内容3. 保存并退出编辑器 三、安装MongoDB1. 更新yum缓存2. 安装MongoDB 四、启动MongoDB服务1. 启动MongoDB2. 设置MongoDB开机自启动 五、配…...

瑞芯微开发板/主板Android调试串口配置为普通串口方法 深圳触觉智能科技分享

本文介绍瑞芯微开发板/主板Android调试串口配置为普通串口方法&#xff0c;不同板型找到对应文件修改&#xff0c;修改的方法相通。触觉智能RK3562开发板演示&#xff0c;搭载4核A53处理器&#xff0c;主频高达2.0GHz&#xff1b;内置独立1Tops算力NPU&#xff0c;可应用于物联…...

【HDFS】addInternalPBProtocol、setProtocolEngine和registerProtocolAndImpl

本文主要与Hadoop的RPC框架相关 DFSUtil#addInternalPBProtocol: /*** Add protobuf based protocol to the {@link org.apache.hadoop.ipc.RPC.Server}.* This method is for exclusive use by the hadoop libraries, as its signature* changes with the version of the sha…...

springboot239-springboot在线医疗问答平台(源码+论文+PPT+部署讲解等)

&#x1f495;&#x1f495;作者&#xff1a; 爱笑学姐 &#x1f495;&#x1f495;个人简介&#xff1a;十年Java&#xff0c;Python美女程序员一枚&#xff0c;精通计算机专业前后端各类框架。 &#x1f495;&#x1f495;各类成品Java毕设 。javaweb&#xff0c;ssm&#xf…...

web3是什么,最简单的介绍

Web3是指第三代互联网技术&#xff0c;也被称为分布式互联网。它是在传统互联网&#xff08;Web2.0&#xff09;基础上发展出来的一种新技术体系&#xff0c;旨在通过区块链技术来重新定义人们访问和使用网络服务的方式。以下是关于Web3的详细介绍&#xff1a; 一、核心特点 …...

机器学习 - 词袋模型(Bag of Words)实现文本情感分类的详细示例

为了简单直观的理解模型训练&#xff0c;我这里搜集了两个简单的实现文本情感分类的例子&#xff0c;第一个例子基于朴素贝叶斯分类器&#xff0c;第二个例子基于逻辑回归&#xff0c;通过这两个例子&#xff0c;掌握词袋模型&#xff08;Bag of Words&#xff09;实现文本情感…...

【注意】sql语句where条件中的数据类型不一致,不仅存在性能问题,还会有数据准确性方面的bug......

隐式类型转换规则 MySQL 在进行比较操作时&#xff0c;如果比较双方的数据类型不一致&#xff0c;通常会尝试将其中一个数据类型转换为另一个数据类型&#xff0c;以便进行比较。 对于 select * from t_order where order_no 1538808276987285507 &#xff0c;当 order_no 为 …...

w~大模型~合集30

我自己的原文哦~ https://blog.51cto.com/whaosoft/13284996 #VideoMamba 视频理解因大量时空冗余和复杂时空依赖&#xff0c;同时克服两个问题难度巨大&#xff0c;CNN 和 Transformer 及 Uniformer 都难以胜任&#xff0c;Mamba 是个好思路&#xff0c;让我们看看本文是…...

基于单片机的仓库安防系统(论文+源码)

2.1 需求分析 仓库由于存有大量物品&#xff0c;因此对仓库的监控非常重要&#xff0c;目前仓库已经普遍装有安防系统&#xff0c;以保证仓库的安全&#xff0c;本次基于单片机的仓库安防系统设计&#xff0c;在功能上设计如下&#xff1a; 用户可通过IC卡进入仓库&#xff1…...

AndroidStudio查看Sqlite和SharedPreference

1.查看Sqlite 使用App Inspection&#xff0c;这是个好东西 打开方式&#xff1a;View → Tool Windows → App Inspection 界面如图&#xff1a; App inspection不但可以看Sqlite还可以抓包network和background task连抓包工具都省了。 非常好使 2.查看sharedPreference 使…...

仿 RabbitMQ 实现的简易消息队列

文章目录 项目介绍开放环境第三⽅库介绍ProtobufMuduo库 需求分析核⼼概念实现内容 消息队列系统整体框架服务端模块数据管理模块虚拟机数据管理模块交换路由模块消费者管理模块信道&#xff08;通信通道&#xff09;管理模块连接管理模块 客户端模块 公共模块日志类其他工具类…...

JavaScript基础知识及高频用法

目录 一、语言基础&#xff1a;构建代码逻辑的积木 二、核心概念&#xff1a;理解JavaScript的灵魂 三、高频用法&#xff1a;现代开发必备技巧 四、避坑指南&#xff1a;常见错误与调试 五、学习路线与资源推荐 从入门到实战&#xff0c;掌握现代Web开发基石 作为全球使…...

VUE项目中实现权限控制,菜单权限,按钮权限,接口权限,路由权限,操作权限,数据权限实现

VUE项目中实现权限控制&#xff0c;菜单权限&#xff0c;按钮权限&#xff0c;接口权限&#xff0c;路由权限&#xff0c;操作权限&#xff0c;数据权限实现 权限系统分类&#xff08;RBAC&#xff09;引言菜单权限按钮权限接口权限路由权限 菜单权限方案方案一&#xff1a;菜单…...

多机器人系统的大语言模型:综述

25年2月来自 Drexel 大学的论文“Large Language Models for Multi-Robot Systems: A Survey”。 大语言模型 (LLM) 的快速发展为多机器人系统 (MRS) 开辟新的可能性&#xff0c;从而增强通信、任务规划和人机交互。与传统的单机器人和多智体系统不同&#xff0c;MRS 带来独特…...

如何在 Java 应用中实现数据库的主从复制(读写分离)?请简要描述架构和关键代码实现?

在Java应用中实现数据库主从复制&#xff08;读写分离&#xff09; 一、架构描述 &#xff08;一&#xff09;整体架构 主库&#xff08;Master&#xff09; 负责处理所有的写操作&#xff08;INSERT、UPDATE、DELETE等&#xff09;。它是数据的源头&#xff0c;所有的数据变…...

Redis 数据类型 Hash 哈希

在 Redis 中&#xff0c;哈希类型是指值本⾝⼜是⼀个键值对结构&#xff0c;形如 key "key"&#xff0c;value { { field1, value1 }, ..., {fieldN, valueN } }&#xff0c;Redis String 和 Hash 类型⼆者的关系可以⽤下图来表⽰。 Hash 数据类型的特点 键值对集合…...

17.推荐系统的在线学习与实时更新

接下来就讲解推荐系统的在线学习与实时更新。推荐系统的在线学习和实时更新是为了使推荐系统能够动态地适应用户行为的变化&#xff0c;保持推荐结果的实时性和相关性。以下是详细的介绍和实现方法。 推荐系统的在线学习与实时更新 在线学习的概念 在线学习&#xff08;Onli…...

网络安全检测思路

对于主机的安全检测&#xff0c;我们通常直接采用nmap或者类似软件进行扫描&#xff0c;然后针对主机操作系统及其 开放端口判断主机的安全程度&#xff0c;这当然是一种方法&#xff0c;但这种方法往往失之粗糙&#xff0c;我仔细考虑了一下&#xff0c;觉 得按下面的流程进行…...

老游戏回顾:SWRacer

竞速类游戏里&#xff0c;我很怀念它。 虽然已经25年过去了。 相比之下&#xff0c;别的游戏真的没法形容。 ---- 是LucasArts制作的一款赛车竞速游戏&#xff1b; 玩家要扮演一名银河旅行者参加各种赛车比赛&#xff0c;赢得奖金&#xff0c;在经历了八个不同星球上的24场…...

MySQL面试题合集

1.MySQL中的数据排序是怎么实现的? 回答重点 排序过程中,如果排序字段命中索引,则利用 索引排序。 反之,使用文件排序。 文件排序中,如果数据量少则在内存中排序, 具体是使用单路排序或者双路排序。 如果数据大则利用磁盘文件进行外部排序,一 般使用归并排序。 知识…...

如何在Java EE中使用标签库?

在Java EE&#xff08;现在称为Jakarta EE&#xff09;中使用标签库&#xff08;Tag Library&#xff09;&#xff0c;主要是通过JSP标准标签库&#xff08;JSTL&#xff09;或自定义标签库来实现的。标签库允许在JSP页面中使用自定义的标签&#xff0c;从而简化页面逻辑、增强…...

第 12 天:行为树(Behavior Tree),让 AI 更智能!

&#x1f3af; 目标&#xff1a; ✅ 理解 Unreal Engine 5 行为树&#xff08;Behavior Tree&#xff09; ✅ 创建行为树&#xff08;BT&#xff09;和黑板&#xff08;Blackboard&#xff09;管理 AI 状态 ✅ 使用任务&#xff08;Task&#xff09;让 AI 巡逻、追踪、攻击玩家…...

LabVIEW 用户界面设计基础原则

在设计LabVIEW VI的用户界面时&#xff0c;前面板的外观和布局至关重要。良好的设计不仅提升用户体验&#xff0c;还能提升界面的易用性和可操作性。以下是设计用户界面时的一些关键要点&#xff1a; 1. 前面板设计原则 交互性&#xff1a;组合相关的输入控件和显示控件&#x…...

自然语言处理NLP入门 -- 第三节词袋模型与 TF-IDF

目标 了解词袋模型&#xff08;BoW&#xff09;和 TF-IDF 的概念通过实际示例展示 BoW 和 TF-IDF 如何将文本转换为数值表示详细讲解 Scikit-learn 的实现方法通过代码示例加深理解归纳学习难点&#xff0c;并提供课后练习和讲解 3.1 词袋模型&#xff08;Bag of Words, BoW&a…...

Flappy Bird开发学习记录

概述 为了了解一下Unity的开发过程&#xff0c;或者说感受&#xff1f;先搞简单的练练手。 工具 Unity:2022.3.51f1c1 visual studio 2022 开发过程 项目基本设置 新建2d项目&#xff0c;游戏画面设置为1080*1920&#xff08;9&#xff1a;16&#xff09;。 图片素材设…...

Visual Studio 使用 “Ctrl + /”键设置注释和取消注释

问题&#xff1a;在默认的Visual Studio中&#xff0c;选择单行代码后&#xff0c;按下Ctrl /键会将代码注释掉&#xff0c;但再次按下Ctrl /键时&#xff0c;会进行双重注释&#xff0c;这不是我们想要的。 实现效果&#xff1a;当按下Ctrl /键会将代码注释掉&#xff0c;…...

CTF-WEB: 利用Web消息造成DOM XSS

如果索引中有类似如下代码 <!-- Ads to be inserted here --> <div idads> </div> <script>window.addEventListener(message, function(e) {document.getElementById(ads).innerHTML e.data;}); </script>这行代码的作用是将接收到的消息内容…...

2025 西湖论剑wp

web Rank-l 打开题目环境&#xff1a; 发现一个输入框&#xff0c;看一下他是用上面语言写的 发现是python&#xff0c;很容易想到ssti 密码随便输&#xff0c;发现没有回显 但是输入其他字符会报错 确定为ssti注入 开始构造payload&#xff0c; {{(lipsum|attr(‘global…...

常见的排序算法:插入排序、选择排序、冒泡排序、快速排序

1、插入排序 步骤&#xff1a; 1.从第一个元素开始&#xff0c;该元素可以认为已经被排序 2.取下一个元素tem&#xff0c;从已排序的元素序列从后往前扫描 3.如果该元素大于tem&#xff0c;则将该元素移到下一位 4.重复步骤3&#xff0c;直到找到已排序元素中小于等于tem的元素…...

LVDS接口总结--(5)IDELAY3仿真

仿真参考资料如下&#xff1a; https://zhuanlan.zhihu.com/p/386057087 timescale 1 ns/1 ps module tb_idelay3_ctrl();parameter REF_CLK 2.5 ; // 400MHzparameter DIN_CLK 3.3 ; // 300MHzreg ref_clk ;reg …...

数据库的基本概念

在当今的信息时代&#xff0c;数据已成为企业乃至整个社会的重要资产。如何有效地存储、管理和利用这些数据成为了技术发展的关键领域之一。数据库系统作为数据管理的核心工具&#xff0c;在软件开发、数据分析等多个方面扮演着不可或缺的角色。本文将带你了解数据库的一些基本…...

Redis性能优化

1.是否使用复杂度过高的命令 首先&#xff0c;第一步&#xff0c;你需要去查看一下 Redis 的慢日志&#xff08;slowlog&#xff09;。 Redis 提供了慢日志命令的统计功能&#xff0c;它记录了有哪些命令在执行时耗时比较久。 查看 Redis 慢日志之前&#xff0c;你需要设置慢…...

CCF-CSP第34次认证第二题——矩阵重塑(其二)【需反复思考学习!!!】

第34次认证第二题——矩阵重塑&#xff08;其二&#xff09; 官网题目链接 时间限制&#xff1a; 1.0 秒 空间限制&#xff1a; 512 MiB 相关文件&#xff1a; 题目目录&#xff08;样例文件&#xff09; 题目背景 矩阵转置操作是将矩阵的行和列交换的过程。在转置过程中&…...

大模型DeepSeek-R1学习

学习路线 机器学习-> 深度学习-> 强化学习-> 深度强化学习 大模型演进分支 微调&#xff1a; SFT 监督学习蒸馏&#xff1a;把大模型作为导师训练小模型RLHF&#xff1a;基于人类反馈的强化学习 PPO 近端策略优化 油门 - 重要性采样 权重 * 打分刹车 - clip 修剪…...

Spring Cloud — 深入了解Eureka、Ribbon及Feign

Eureka 负责服务注册与发现&#xff1b;Ribbon负责负载均衡&#xff1b;Feign简化了Web服务客户端调用方式。这三个组件可以协同工作&#xff0c;共同构建稳定、高效的微服务架构。 1 Eureka 分布式系统的CAP定理&#xff1a; 一致性&#xff08;Consistency&#xff09;&am…...

19.4.9 数据库方式操作Excel

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 本节所说的操作Excel操作是讲如何把Excel作为数据库来操作。 通过COM来操作Excel操作&#xff0c;请参看第21.2节 在第19.3.4节【…...

《深度LSTM vs 普通LSTM:训练与效果的深度剖析》

在深度学习领域&#xff0c;长短期记忆网络&#xff08;LSTM&#xff09;以其出色的处理序列数据能力而备受瞩目。而深度LSTM作为LSTM的扩展形式&#xff0c;与普通LSTM在训练和效果上存在着一些显著的不同。 训练方面 参数数量与计算量&#xff1a;普通LSTM通常只有一层或较少…...

认识一下redis的分布式锁

Redis的分布式锁是一种通过Redis实现的分布式锁机制&#xff0c;用于在分布式系统中确保同一时刻只有一个客户端可以访问某个资源。它通常用于防止多个应用实例在同一时间执行某些特定操作&#xff0c;避免数据的不一致性或竞争条件。 实现分布式锁的基本思路&#xff1a; 1. …...

untiy3D为游戏物体制作简单的动画

1.创建一个物体挂载动画组件Animator 2.创建一个动画控制器 3.动画控制器挂载到Animator组件 4.创建动画窗口>动画 入口默认执行left 执行效果 20250212_151707 脚本控制动画 鼠标点击是切换到动画t using System.Collections; using System.Collections.Generic; usi…...

微服务与网关

什么是网关 背景 单体项目中,前端只用访问指定的一个端口8080,就可以得到任何想要的数据 微服务项目中,ip是不断变化的,端口是多个的 解决方案:网关 网关:就是网络的关口,负责请求的路由、转发、身份校验。 前段还是访问之前的端口8080即可 后端对于前端来说是透明的 网…...

ArcGIS基础知识之ArcMap基础设置——ArcMap选项:常规选项卡设置及作用

作为一名 GIS 从业者,ArcMap 是我们日常工作中不可或缺的工具。对于初学者来说,掌握 ArcMap 的基础设置是迈向 GIS 分析与制图的第一步。今天,就让我们一起深入了解 ArcMap 选项中常规选项卡的各个设置,帮助大家更好地使用这款强大的软件。 在 ArcMap 中,常规选项卡是用户…...

Ubuntu轻松部署ToolJet低代码开发平台结合内网穿透远程访问

文章目录 前言1.关于ToolJet2.Docker部署3.简单使用演示4.安装cpolar内网穿透5. 配置公网地址6. 配置固定公网地址 前言 本文主要介绍如何在本地Linux服务器使用Docker部署轻量级开源文件分享系统ToolJet&#xff0c;并结合cpolar内网穿透工具轻松实现跨网络环境远程访问与使用…...

[MySQL]5-MySQL扩展(分片)

随着数据量和用户量增加&#xff0c;MySQL会有读写负载限制。以下是部分解决方案 目录 功能拆分 使用读池拓展读&#xff08;较复杂&#xff09; 排队机制 &#x1f31f;分片拓展写 按业务或职责划分节点或集群 大数据集切分 分片键的选择 多个分片键 跨分片查询 资料…...

如何使用 DeepSeek 帮助自己的工作

Hi&#xff0c;我是布兰妮甜 &#xff01;在当今快速发展的技术领域&#xff0c;人工智能&#xff08;AI&#xff09;工具已经成为提高工作效率、促进创新的重要助手。DeepSeek作为一款先进的AI解决方案&#xff0c;为用户提供了强大的数据处理、分析以及预测能力&#xff0c;可…...

Redis缓存穿透、击穿和雪崩面试相关问题整理

在互联网公司的面试中&#xff0c;Redis 的缓存穿透、击穿和雪崩是高频考点&#xff0c;尤其在北京的头部互联网公司&#xff08;如字节、阿里、美团、快手等&#xff09;。以下是针对这三个问题的详细解析及常见面试题方向&#xff1a; 一、缓存穿透&#xff08;Cache Penetra…...

Flink之Watermark

Apache Flink 是一个分布式流处理框架&#xff0c;它非常擅长处理实时数据流。流处理中的一个关键挑战是事件时间的处理&#xff0c;因为在流式数据中&#xff0c;事件到达系统的顺序可能并不代表它们的实际发生时间。为了解决这一问题&#xff0c;Flink 引入了**Watermark&…...

vs构建网络安全系统 网络安全和网络搭建

网站的组成和搭建 网站由服务器&#xff0c;容器&#xff0c;脚本&#xff0c;数据库组成。 服务器和家庭电脑一样。 容器又为环境或服务&#xff1a;apache&#xff0c;lls&#xff0c;tomcat&#xff0c;nginx等 脚本&#xff1a;php&#xff0c;aspx&#xff0c;asp&#x…...

缓存穿透问题及解决方案

一、什么是缓存穿透&#xff1f; 在分布式系统中&#xff0c;缓存常常用于提高系统的性能&#xff0c;减轻数据库的压力。缓存穿透问题指的是请求的数据在缓存和数据库中都不存在&#xff0c;导致请求每次都直接查询数据库&#xff0c;无法从缓存中获取数据&#xff0c;从而绕…...

ElementUI el-popover弹框背景色设置

1.el-popover样式由于使用了 absolute 属性&#xff0c;导致脱离了节点&#xff0c;所以在父级元素使用class无法进行权重处理来修改其样式&#xff0c;解决方式如下&#xff1a;通过popper-class实现样式处理&#xff0c;避免全局样式污染 // html <el-popoverplacement&q…...