Refactor the message distribution process

To solve a major problem: when there are many devices connected,
the broker will blocked in processing messages.

By the way, I've rewritten and reorganized the entire code architecture,
improved performance.

Some new features has been added:
* support device grouping.
* support show device's IP address.

Change-Id: I250e18091be7fd42028c82767b6edef50b3f6d8f
Signed-off-by: Jianhui Zhao <zhaojh329@gmail.com>
This commit is contained in:
Jianhui Zhao
2025-07-04 11:54:19 +08:00
parent ff6cbda736
commit a0bfc21d11
28 changed files with 1762 additions and 1421 deletions
+1 -1
View File
@@ -148,7 +148,7 @@ export default {
params: this.cmdData.params
}
this.axios.post(`/cmd/${item.id}?wait=${this.cmdData.wait}`, data).then((response) => {
this.axios.post(`/cmd/${item.id}?group=${item.group}&wait=${this.cmdData.wait}`, data).then((response) => {
if (this.cmdData.wait === 0) {
this.cmdStatus.responses.push({
err: 0,
+7 -1
View File
@@ -76,12 +76,14 @@ export default {
callback()
}}]
},
group: '',
devid: '',
devProto: null
}
},
methods: {
show(dev) {
this.group = dev.group
this.devid = dev.id
this.devProto = dev.proto
this.formData.proto = 'http'
@@ -122,7 +124,11 @@ export default {
path = '/'
const addr = encodeURIComponent(`${ipaddr}:${port}${path}`)
window.open(`/web/${this.devid}/${proto}/${addr}`)
if (this.group)
window.open(`/web2/${this.group}/${this.devid}/${proto}/${addr}`)
else
window.open(`/web/${this.devid}/${proto}/${addr}`)
}, 100)
})
}
+2 -1
View File
@@ -10,6 +10,7 @@
"Reset": "Reset",
"Signin Fail! password wrong.": "Signin Fail! password wrong.",
"Refresh List": "Refresh List",
"ungrouped": "Ungrouped",
"Please enter the filter key...": "Please enter the filter key...",
"Execute command": "Execute command",
"device-count": "Device Count: {count}",
@@ -68,4 +69,4 @@
"Already copied to clipboard": "Already copied to clipboard",
"Please use shortcut \"Shift+Insert\"": "Please use shortcut \"Shift+Insert\"",
"Your device's rtty does not support https proxy, please upgrade it.": "Your device's rtty does not support https proxy, please upgrade it."
}
}
+3 -2
View File
@@ -10,6 +10,7 @@
"Reset": "复位",
"Signin Fail! password wrong.": "登录失败,密码错误",
"Refresh List": "刷新列表",
"ungrouped": "未分组",
"Please enter the filter key...": "请输入关键字进行过滤...",
"Execute command": "执行命令",
"device-count": "设备数: {count}",
@@ -24,6 +25,7 @@
"Uptime": "运行时长",
"Description": "描述",
"Please select the devices you want to operate": "请选择您要操作的设备",
"Access your devices's Web": "访问您的设备的 Web",
"Command": "命令",
"Cancel": "取消",
"OK": "确定",
@@ -54,7 +56,6 @@
"Upload or download file": "上传或者下载文件",
"About": "关于",
"Please execute command \"rtty -R\" or \"rtty -S\" in current terminal!": "请在当前终端中执行命令 \"rtty -R\" 或者 \"rtty -S\"",
"Access your devices's Web": "访问您的设备的 Web",
"Proto": "协议",
"ipaddr": "IP 地址",
"port": "端口",
@@ -69,4 +70,4 @@
"Already copied to clipboard": "已复制到剪切板",
"Please use shortcut \"Shift+Insert\"": "请使用快捷键 \"Shift+Insert\"",
"Your device's rtty does not support https proxy, please upgrade it.": "你的设备的 rtty 不支持 https 代理,请升级"
}
}
+60 -38
View File
@@ -1,44 +1,54 @@
<template>
<div style="padding:5px;">
<div style="display: flex; justify-content: space-between;">
<el-container>
<el-header class="header">
<el-space>
<el-button type="primary" round icon="Refresh" @click="handleRefresh" :disabled="loading">{{ $t('Refresh List') }}</el-button>
<el-select v-model="group" filterable :placeholder="$t('ungrouped')" @change="getDevices">
<el-option v-for="item in groups" :key="item" :label="item == '' ? $t('ungrouped'): item " :value="item"/>
</el-select>
<el-input style="width:200px" v-model="filterString" search @input="handleSearch" :placeholder="$t('Please enter the filter key...')"/>
<el-button @click="showCmdForm" type="primary">{{ $t('Execute command') }}</el-button>
</el-space>
<el-space style="float: right;">
<el-space>
<span style="color: var(--el-color-primary); font-size: 24px">{{ $t('device-count', {count: devlists.length}) }}</span>
<el-divider direction="vertical" />
<el-button type="primary" @click="handleLogout">{{ $t('Sign out') }}</el-button>
</el-space>
</div>
<el-table :loading="loading" :data="pagedevlists" style="margin-top: 10px; margin-bottom: 10px; width: 100%" :empty-text="$t('No devices connected')" @selection-change='handleSelection'>
<el-table-column type="selection" width="60" />
<el-table-column prop="id" :label="$t('Device ID')" width="200" />
<el-table-column :label="$t('Connected time')" width="200">
<template #default="{ row }">
<span>{{ formatTime(row.connected) }}</span>
</el-header>
<el-main>
<el-card>
<el-table height="calc(100vh - 215px)" :loading="loading" :data="pagedevlists" :empty-text="$t('No devices connected')" @selection-change='handleSelection'>
<el-table-column type="selection" width="40" />
<el-table-column prop="id" :label="$t('Device ID')" width="300" />
<el-table-column :label="$t('Connected time')" width="150">
<template #default="{ row }">
<span>{{ formatTime(row.connected) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('Uptime')" width="150">
<template #default="{ row }">
<span>{{ formatTime(row.uptime) }}</span>
</template>
</el-table-column>
<el-table-column prop="ipaddr" :label="$t('ipaddr')" width="150" />
<el-table-column prop="description" :label="$t('Description')" show-overflow-tooltip width="150" />
<el-table-column width="100">
<template #default="{ row }">
<el-space size="large">
<el-icon size="25" color="black" style="cursor:pointer;" @click="connectDevice(row.id)"><TerminalIcon /></el-icon>
<el-icon size="25" color="#409EFF" style="cursor:pointer;" @click="connectDeviceWeb(row)"><IEIcon /></el-icon>
</el-space>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-pagination background layout="prev, pager, next, total,sizes" :total="filteredDevices.length" @change="handlePageChange" class="pagination"/>
</template>
</el-table-column>
<el-table-column :label="$t('Uptime')" width="200">
<template #default="{ row }">
<span>{{ formatTime(row.uptime) }}</span>
</template>
</el-table-column>
<el-table-column prop="description" :label="$t('Description')" show-overflow-tooltip />
<el-table-column width="200">
<template #default="{ row }">
<el-space size="large">
<el-icon size="25" color="black" style="cursor:pointer;" @click="connectDevice(row.id)"><TerminalIcon /></el-icon>
<el-icon size="25" color="#409EFF" style="cursor:pointer;" @click="connectDeviceWeb(row)"><IEIcon /></el-icon>
</el-space>
</template>
</el-table-column>
</el-table>
<el-pagination background layout="prev, pager, next, total,sizes" :total="filteredDevices.length" @change="handlePageChange"/>
</el-card>
</el-main>
<RttyCmd ref="rttyCmd" :selection="selection"/>
<RttyWeb ref="rttyWeb"/>
</div>
</el-container>
</template>
<script>
@@ -57,6 +67,8 @@ export default {
},
data() {
return {
group: '',
groups: [],
filterString: '',
loading: true,
devlists: [],
@@ -115,8 +127,16 @@ export default {
return d.id.toLowerCase().indexOf(filterString) > -1 || d.description.toLowerCase().indexOf(filterString) > -1
})
},
getGroups() {
this.axios.get('/groups').then(res => {
this.groups = res.data
if (this.groups.indexOf(this.group) === -1)
this.group = this.groups[0]
this.getDevices()
})
},
getDevices() {
this.axios.get('/devs').then(res => {
this.axios.get(`/devs?group=${this.group}`).then(res => {
this.loading = false
this.devlists = res.data
this.selection = []
@@ -128,14 +148,14 @@ export default {
handleRefresh() {
this.loading = true
setTimeout(() => {
this.getDevices()
this.getGroups()
}, 500)
},
handleSelection(selection) {
this.selection = selection
},
connectDevice(devid) {
window.open('/rtty/' + devid)
window.open(`/rtty/${devid}?group=${this.group}`)
},
connectDeviceWeb(dev) {
this.$refs.rttyWeb.show(dev)
@@ -145,17 +165,19 @@ export default {
}
},
mounted() {
this.getDevices()
this.getGroups()
}
}
</script>
<style scoped>
:deep(.el-pagination) .el-pagination__total {
color: white;
}
.header {
display: flex;
justify-content: space-between;
}
:deep(.el-pagination) {
justify-content: center;
}
.pagination {
display: flex;
justify-content: center;
}
</style>
+38 -17
View File
@@ -1,6 +1,6 @@
<template>
<div>
<div ref="terminal" :style="{height: termHeight + 'px', margin: '5px'}" @contextmenu.prevent="showContextmenu"/>
<div ref="terminal" :style="{height: termHeight + 'px', margin: '5px'}" @contextmenu.prevent="showContextmenu"></div>
<el-dialog v-model="file.modal" :title="$t('Upload file to device')" @close="onUploadDialogClosed" :width="400">
<el-upload :before-upload="beforeUpload" action="#">
<el-button type="primary">{{ $t("Select file") }}</el-button>
@@ -62,7 +62,9 @@ export default {
accepted: false,
file: null,
offset: 0,
fr: new FileReader()
fr: new FileReader(),
name: '',
chunks: []
},
disposables: [],
resizeDelay: null,
@@ -70,7 +72,6 @@ export default {
socket: null,
term: null,
fitAddon: null,
sid: '',
unack: 0
}
},
@@ -232,9 +233,11 @@ export default {
}
},
mounted() {
const group = this.$route.query.group ?? ''
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'
const socket = new WebSocket(protocol + location.host + `/connect/${this.devid}`)
const socket = new WebSocket(protocol + location.host + `/connect/${this.devid}?group=${group}`)
socket.binaryType = 'arraybuffer'
this.socket = socket
@@ -252,18 +255,14 @@ export default {
return
}
this.sid = msg.sid
this.openTerm()
socket.addEventListener('close', () => this.closed())
socket.addEventListener('error', () => this.closed())
} else if (msg.type === 'sendfile') {
const el = document.createElement('a')
el.style.display = 'none'
el.href = '/file/' + this.sid
el.download = msg.name
el.click()
this.file.name = msg.name
this.file.chunks = []
socket.send(JSON.stringify({type: 'fileAck'}))
} else if (msg.type === 'recvfile') {
this.file.modal = true
this.file.file = null
@@ -275,13 +274,35 @@ export default {
}
} else {
const data = new Uint8Array(ev.data)
this.unack += data.length
this.term.write(data)
if (this.unack > AckBlkSize) {
const msg = {type: 'ack', ack: this.unack}
socket.send(JSON.stringify(msg))
this.unack = 0
if (data[0] === 0) {
this.unack += data.length - 1
this.term.write(data.slice(1))
if (this.unack > AckBlkSize) {
const msg = {type: 'ack', ack: this.unack}
socket.send(JSON.stringify(msg))
this.unack = 0
}
} else {
if (data.length === 1) {
const blob = new Blob(this.file.chunks)
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = this.file.name
document.body.appendChild(a)
a.click()
setTimeout(() => {
this.file.chunks = []
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
}, 100)
} else {
this.file.chunks.push(data.slice(1))
socket.send(JSON.stringify({type: 'fileAck'}))
}
}
}
})
+3 -3
View File
@@ -18,6 +18,9 @@ export default defineConfig({
],
server: {
proxy: {
'/groups': {
target: 'http://127.0.0.1:5913'
},
'/devs': {
target: 'http://127.0.0.1:5913'
},
@@ -39,9 +42,6 @@ export default defineConfig({
},
'^/web/*': {
target: 'http://127.0.0.1:5913'
},
'^/file/.*': {
target: 'http://127.0.0.1:5913'
}
}
}