feat:更新文档架构

This commit is contained in:
2026-05-20 11:26:38 +08:00
parent ad9a4d6806
commit 8c9b3adc57
46 changed files with 218 additions and 75 deletions

79
docs/Web/Others/AList.md Normal file
View File

@@ -0,0 +1,79 @@
---
title: AList简介和使用
date: 2025-12-17
---
# 一、简介
  [AList](https://alistgo.com/zh/)是一个支持多种存储,支持网页浏览和 WebDAV 的文件列表程序,由 gin 和 Solidjs 驱动。
# 二、安装
## 2.1 Docker安装
```yml
services:
alist:
image: xhofe/alist:v3.41.0
container_name: alist
restart: unless-stopped
volumes:
- ./data:/opt/alist/data
- /srv:/srv
ports:
- "5244:5244"
environment:
- PUID=0
- PGID=0
```
  这里的srv为宿主机的系统服务数据目录添加该映射可以让Alist能够访问宿主机/srv目录下的本地文件。
# 三、挂载
## 3.1 挂载本地存储
  打开控制台,选择存储->添加->本地存储,设置挂载路径(自定义,如`/local`),选择根文件夹路径为`/srv`,点击添加。
  这里的`/srv`文件夹为`docker`里面的文件夹路径,一般只需要更改`docker compose`文件里面的`/srv:/srv`前面的`srv`路径为宿主机路径即可,后面的`/srv`路径不要更改。
  可以在宿主机的`/srv`目录中添加文件查看AList的主页是否可以正确显示。
# 四、API
  在线调试地址:[Alist-Public](https://alist-public.apifox.cn/)
## 4.1 登录
```JavaScript
const username = ''
const password = ''
// 1. 登录获取token
const login = await axios.post(`${AListUrl}/api/auth/login`, {
username,
password
})
const token = login.data.data.token
```
## 4.2 上传文件
```JavaScript
const data = new FormData()
data.append('file', options.file)
const filePath = `${AListPath}/${options.file.name}`
await axios({
method: 'put',
url: `${AListUrl}/api/fs/form`,
headers: {
Authorization: token,
'File-Path': filePath
},
data
})
```
## 4.3 下载文件
  在AList的文件路径中加上前缀`/p/`。例如文件路径为`/local/Test/test.mp4`,则下载路径为`/p/local/Test/test.mp4`
::: tip
以上为相对路径使用时加上相应的ip和端口号。
:::
::: warning
如果出现无法访问的情况,需要在设置->全局里面,将`签名所有`关闭。
签名所有Alist是否将为这个存储下的所有文件请求生成带签名的临时链接如果开启则需要带上校验码签名否则不需要。
:::

View File

@@ -0,0 +1,100 @@
---
title: 浏览器渲染全流程解析
date: 2025-12-13
---
# 一、网络请求阶段
1. DNS 解析
  浏览器先解析输入的网址对应的 IP 地址,先检查本地 DNS 缓存,若没有则向本地 DNS 服务器发起查询。最终通过 DNS 系统获取服务器的 IP 地址。
2. 建立网络连接TCP/IP 握手)
  通过三次握手建立客户端(浏览器)与服务器的 TCP 连接。若网址使用 HTTPS还需完成 TLS/SSL 握手,确保数据加密传输。
3. 发起 HTTP/HTTPS 请求
  浏览器向服务器发送请求报文,包含:
- **请求行**方法GET、URL、协议版本如 HTTP/1.1)。
- **请求头**包含浏览器信息User-Agent、缓存策略Cache-Control等。
- **若为 POST 请求**:还包含请求体数据。
4. 服务器处理请求
  服务器接收到请求后,根据 URL 路由到对应的后端服务(如 Node.js、Java 等)。
- **若为 Vue3 项目**
* 若为单页应用SPA服务器通常返回一个通用的 `index.html` 页面(前端路由由 Vue Router 处理)。
* 若涉及 API 请求(如获取数据),服务器返回 JSON 格式的数据(如通过 Express、Spring Boot 等接口)。
5. 返回响应结果
  服务器返回响应报文,包含:
- **状态行**:状态码(如 200 OK、404 Not Found
- **响应头**包含内容类型Content-Type: text/html、缓存控制Cache-Control: max-age=3600等。
- **响应体**
* 若为 `index.html`:返回 HTML 文本(包含 Vue3 项目的入口脚本引用,如 `<script src="/static/js/app.js"></script>`)。
* 若为 API 数据:返回 JSON 格式的数据(如用户信息、列表数据等)。
# 二、解析与执行阶段
1. 解析 HTML 并构建 DOM 树
&emsp;&emsp;浏览器逐行解析 HTML 内容,构建 **DOM 树**Document Object Model
- 遇到 `<link rel="stylesheet">` 标签时,异步加载 CSS 文件并构建 **CSSOM 树**CSS Object Model
- 遇到 `<script>` 标签时:
* 若为 **同步脚本**`<script src="app.js">`):暂停 HTML 解析,下载并执行脚本。
* 若为 **异步脚本**`<script async src="app.js">`):异步下载脚本,不阻塞 HTML 解析。
2. 加载 Vue3 相关资源
&emsp;&emsp;浏览器下载并执行 Vue3 的核心库(`vue.runtime.global.js`)、项目打包后的 JS 文件(如 `app.js`)和 CSS 文件(如 `app.css`)。
::: tip
- `app.js`:包含 Vue3 应用的入口逻辑(如 `createApp(App).mount('#app')`)、组件定义、路由配置等。
- `app.css`:包含全局样式或组件样式(通过 `<style>` 标签在组件中定义的样式会被编译为内联样式)。
:::
3. 执行 Vue3 应用初始化
&emsp;&emsp;当浏览器执行到 Vue3 入口脚本时,会触发以下流程:
- **创建应用实例**:通过 `createApp(App)` 创建 Vue 应用实例。
- **配置插件和路由**
* 安装路由插件 `vue-router``app.use(router)`
* 安装状态管理插件(如 `pinia``app.use(pinia)`
- **挂载应用到 DOM**:通过 `app.mount('#app')` 将 Vue 组件渲染到 HTML 中的指定容器(如 `<div id="app"></div>`)。
4. Vue3 组件渲染流程
&emsp;&emsp;**初始化组件**:从根组件(`App.vue`)开始,递归解析组件树,初始化响应式数据。
- **模板编译**
* 若使用单文件组件(`.vue`),模板(`<template>`)会被编译为渲染函数(`render`)。
- **响应式依赖收集**Vue3 通过 `Proxy``Reflect` 实现响应式,当组件访问数据(如 `message`)时,自动建立依赖关系。
- **首次渲染(挂载阶段)**
* 根据渲染函数生成 **虚拟 DOMVNode**
* 通过 `patch` 函数将虚拟 DOM 渲染为真实 DOM并插入到页面中。
* 例如,根组件渲染后,`#app` 容器内会生成对应的 HTML 结构。
5. 处理异步操作(如数据请求)
&emsp;&emsp;在组件的 `setup` 函数或生命周期钩子(如 `onMounted`)中,通过 `axios` 或 Vue3 内置的 `useFetch` 发起 API 请求:
- **数据更新与重新渲染**:当响应数据返回后,更新响应式数据(如 `data.value`Vue3 检测到变化后,重新生成虚拟 DOM 并与真实 DOM 对比,执行 **差异化更新Diff 算法)**,只更新变化的部分。
# 三、页面渲染与交互阶段
1. 合成图层并绘制页面
&emsp;&emsp;浏览器将 DOM 树和 CSSOM 树合并为 **渲染树Render Tree**计算每个元素的布局Layout和样式Style。通过 **图层合成Composite** 将不同层级的元素绘制到页面上,最终显示完整的 UI。
2. 用户交互与组件更新
- 当用户触发事件(如点击按钮、输入表单)时:
* Vue3 通过 `v-on` 指令绑定事件处理函数(如 `@click="handleClick"`)。
* 事件处理函数中修改响应式数据,触发组件重新渲染。
- 点击按钮时,`count` 的值更新Vue3 自动重新渲染按钮文本。
# 四、总结
::: tip
输入网址 → DNS解析 → TCP连接 → 服务器返回index.html → 解析HTML加载Vue3脚本 →
执行createApp初始化 → 挂载组件到DOM → 首次渲染虚拟DOM转真实DOM
异步请求数据 → 数据更新触发重新渲染 → 页面最终呈现 → 用户交互驱动组件更新
:::
&emsp;&emsp;通过以上流程,浏览器从输入网址到完成 Vue3 应用的渲染,充分利用了前端框架的响应式机制和虚拟 DOM 技术,实现高效的页面更新与交互。

313
docs/Web/Others/ElUpload.md Normal file
View File

@@ -0,0 +1,313 @@
---
title: Element Plus 上传器
date: 2026-01-29
---
# 一、上传图片
```vue
<template>
<el-upload
v-model:file-list="imageFileList"
list-type="picture-card"
:on-preview="handlePreview"
:limit="3"
:before-upload="beforeUpload"
:http-request="httpRequest"
:on-exceed="handleExceed"
:before-remove="beforeRemove"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">
请上传小于10M的图片
</div>
</template>
<el-dialog v-model="dialogVisible">
<el-image :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</el-upload>
</template>
<script lang="ts" setup>
import { Plus } from '@element-plus/icons-vue'
import { onMounted, ref } from 'vue'
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
import type { UploadRequestOptions } from 'element-plus'
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
import { getFileExtension, getFileHashName, validateImage } from '@/utils/file.ts'
import { uploadFileApi } from '@/apis/file.ts'
const imageFileList = ref<UploadUserFile[]>([])
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const baseUrl = ''
const apiImageList = [] as string[]
onMounted(async () => {
imageFileList.value = []
// 根据后端传来的List数据将信息添加文件列表中
apiImageList.forEach((item, index) => {
imageFileList.value.push({
name: index.toString(),
url: `${baseUrl}${item}`
})
})
})
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const maxFileMax = 1024 * 1024 * 10
const fileType = rawFile.type
const fileName = rawFile.name
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
if (!validateImage(fileType, fileExtension)) {
ElMessage.error('请上传图片')
return false
}
if (rawFile.size > maxFileMax) {
ElMessage.error('请上传10M以内的文件!')
return false
}
return true
}
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
const loading = ElLoading.service({
lock: true,
text: '文件上传中 请等待',
background: 'rgba(0, 0, 0, 0.7)'
})
try {
// 重命名文件
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
const data = new FormData()
data.append('file', options.file)
data.append('filePath', filePath)
// 调用后端接口上传文件
await uploadFileApi(data)
apiImageList.push(filePath)
ElMessage.success('上传成功')
} catch (error) {
console.log(error)
ElMessage.error('上传失败 请联系管理员')
} finally {
loading.close()
}
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传3张图片')
}
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
const index = imageFileList.value.findIndex((item) => item.uid === uploadFile.uid)
apiImageList.splice(index, 1)
}
const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
dialogImageUrl.value = uploadFile.url!
dialogVisible.value = true
}
const beforeRemove: UploadProps['beforeRemove'] = () => {
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
}
</script>
```
# 二、上传视频
```vue
<template>
<el-upload
v-model:file-list="videoFileList"
:limit="1"
:before-upload="beforeUpload"
:http-request="httpRequest"
:on-exceed="handleExceed"
:before-remove="beforeRemove"
:on-remove="handleRemove"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">
请上传小于200M的视频
</div>
</template>
</el-upload>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
import type { UploadRequestOptions } from 'element-plus'
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
import { getFileExtension, getFileHashName, validateVideo } from '@/utils/file.ts'
import { uploadFileApi } from '@/apis/file.ts'
const videoFileList = ref<UploadUserFile[]>([])
let baseUrl = ''
const apiVideoUrl = ''
onMounted(async () => {
videoFileList.value = []
if (apiVideoUrl) {
videoFileList.value.push({
name: 'video',
url: `${baseUrl}p/${apiVideoUrl}`
})
}
})
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const maxFileMax = 1024 * 1024 * 200
const fileType = rawFile.type
const fileName = rawFile.name
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
if (!validateVideo(fileType, fileExtension)) {
ElMessage.error('请上传视频')
return false
}
if (rawFile.size > maxFileMax) {
ElMessage.error('请上传200M以内的文件!')
return false
}
return true
}
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
const loading = ElLoading.service({
lock: true,
text: '文件上传中 请等待',
background: 'rgba(0, 0, 0, 0.7)'
})
try {
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
const data = new FormData()
data.append('file', options.file)
data.append('filePath', filePath)
await uploadFileApi(data)
baseUrl = filePath
ElMessage.success('上传成功')
} catch (error) {
console.log(error)
ElMessage.error('上传失败 请联系管理员')
} finally {
loading.close()
}
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传1个视频')
}
const beforeRemove: UploadProps['beforeRemove'] = () => {
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
}
const handleRemove: UploadProps['onRemove'] = () => {
baseUrl = ''
}
</script>
```
# 三、工具类
```ts
import { hashSHA256 } from 'vue3-common/utils/cryptoUtil'
export const getFileHashName = (name: string) => {
return hashSHA256(name)
}
export const getFileExtension = (name: string) => {
return name.substring(name.lastIndexOf('.') + 1).toLowerCase()
}
export const validateExcel = (fileType: string, fileExtension: string): boolean => {
// 常见的 Excel MIME 类型
const excelMimeTypes = [
'application/vnd.ms-excel', // .xls
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel.sheet.macroEnabled.12', // .xlsm
'text/csv' // CSV
]
// 常见的 Excel 文件扩展名
const excelExtensions = ['xls', 'xlsx', 'xlsm', 'csv']
return excelMimeTypes.includes(fileType) || excelExtensions.includes(fileExtension.toLowerCase())
}
export const validateImage = (fileType: string, fileExtension: string): boolean => {
// 常见的图片 MIME 类型
const imageMimeTypes = [
'image/jpeg', // JPEG
'image/jpg', // JPG
'image/png', // PNG
'image/gif', // GIF
'image/bmp', // BMP
'image/webp', // WebP
'image/svg+xml' // SVG
]
// 常见的图片文件扩展名
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
return imageMimeTypes.includes(fileType) || imageExtensions.includes(fileExtension.toLowerCase())
}
export const validateVideo = (fileType: string, fileExtension: string): boolean => {
// 常见的视频 MIME 类型
const videoMimeTypes = [
'video/mp4', // MP4
'video/mpeg', // MPEG
'video/quicktime', // MOV
'video/x-msvideo', // AVI
'video/x-matroska', // MKV
'video/webm', // WebM
'video/3gpp', // 3GP
'video/3gpp2', // 3G2
'video/x-flv', // FLV
'video/mp2t', // TS (MPEG Transport Stream)
'application/x-mpegURL', // M3U8 (HLS)
'video/H264', // H264
'video/H265' // H265/HEVC
]
// 常见的视频文件扩展名
const videoExtensions = [
'mp4',
'mpeg',
'mpg',
'mov',
'avi',
'mkv',
'webm',
'3gp',
'3g2',
'flv',
'ts',
'm3u8',
'h264',
'h265',
'hevc'
]
return videoMimeTypes.includes(fileType) || videoExtensions.includes(fileExtension.toLowerCase())
}
```

View File

@@ -0,0 +1,82 @@
---
title: InfluxDB简介和使用
date: 2026-05-11
---
# 一、简介
&emsp;&emsp;InfluxDB 是一个开源的时序数据库Time Series Database, TSDB由 InfluxData 公司开发专门用于存储、查询和处理时间序列数据按时间顺序记录的数据点。它在监控、物联网IoT、实时分析等场景中广泛应用是时序数据领域的流行解决方案之一。
&emsp;&emsp;InfluxDB 3.0 是一次底层的彻底重构(内核代号 IOx核心代码从 Go 改写为 Rust并从自研的 TSM 引擎转向基于 Apache Arrow 生态的开放架构。
# 二、特点
1. Rust 全新重写:更快、更省内存、更稳
2. 支持无限标签基数:不再怕设备 ID、用户 ID 这类高基数场景
3. 原生支持标准 SQL不用只学 InfluxQL普通 SQL 直接查
4. 底层基于 Arrow + Parquet列式存储、压缩率高、查询快
5. 原生对接对象存储:支持 S3/OSS冷热数据自动分层可存海量历史数据
# 三、概念
1. 库 Database相当于 MySQL 的库,隔离业务数据
2. 测量 Measurement相当于 MySQL 的表(比如 cpu、温度传感器
3. 标签 Tag维度、索引字段字符串如设备 id、地区用来筛选分组
4. 字段 Field实际指标值数值如温度、CPU 使用率,不做索引)
5. 时间 Time自带时间戳时序数据唯一主键、按时间排序
# 四、安装
## 4.1 Docker安装
```yml
services:
influxdb3-core:
image: influxdb:3.9.1-core
container_name: influxdb3-core
restart: unless-stopped
networks:
- influx-network
ports:
- "8181:8181"
volumes:
- ./influxdb3/data:/var/lib/influxdb3/data
- ./influxdb3/plugins:/var/lib/influxdb3/plugins
user: root
environment:
- TZ=Asia/Shanghai
command: >
influxdb3 serve
--node-id=node0
--object-store=file
--data-dir=/var/lib/influxdb3/data
--plugin-dir=/var/lib/influxdb3/plugins
influxdb3-explorer:
image: influxdata/influxdb3-ui:1.6.3
container_name: influxdb3-explorer
restart: unless-stopped
networks:
- influx-network
ports:
- "8600:80"
- "8888:8888"
volumes:
- ./config:/app-root/config:ro
- ./db:/db:rw
- ./ssl:/etc/nginx/ssl:ro
environment:
- TZ=Asia/Shanghai
command: --mode=admin
networks:
influx-network:
driver: bridge
```
## 4.2 token生成
&emsp;&emsp;进入influxdb3-core容器输入命令获取token
```bash
influxdb3 create token --admin
```
## 4.3 可视化页面
&emsp;&emsp;打开ip:8600具体端口看docker的配置选择`Configure->Servers`,点击`Connect Your First Server`输入InfluxDB服务器信息进行配置。
&emsp;&emsp;`Query Data->Data Explorer`中查看数据。
# 五、使用
## 5.1 SpringBoot使用

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,943 @@
---
title: 网络编程简介
date: 2025-12-15
---
# 一、TCP
## 1.1 定义
&emsp;&emsp;TCPTransmission Control Protocol传输控制协议是互联网核心的**面向连接、可靠、字节流**的传输层协议,工作在 OSI 模型的传输层TCP/IP 模型的传输层),基于 IP 协议提供端到端的可靠数据传输服务。它是 HTTP、HTTPS、WebSocket、MQTT 等应用层协议的底层依赖,核心目标是解决 IP 协议 “无连接、不可靠、无顺序” 的缺陷,确保数据在不可靠的网络中准确、完整、有序地传输。
## 1.2 特性
### 1.2.1 面向连接
&emsp;&emsp;通信前必须完成「三次握手」建立连接,通信后通过「四次挥手」释放连接:
- 三次握手:客户端发 SYN → 服务器回 SYN+ACK → 客户端发 ACK确保双方收发能力正常
- 四次挥手:客户端发 FIN → 服务器回 ACK → 服务器发 FIN → 客户端回 ACK确保数据传输完毕
### 1.2.2 可靠传输
- 序号与确认号:每个字节都有序号,接收方收到后回复确认号,未收到则发送方重传;
- 超时重传:发送方未在规定时间收到确认,自动重传数据;
- 流量控制:通过滑动窗口机制,防止发送方发送过快导致接收方缓冲区溢出;
- 拥塞控制:通过慢启动、拥塞避免等算法,适应网络带宽变化。
### 1.2.3 面向字节流
&emsp;&emsp;TCP 将应用层数据视为连续的字节流,不保留应用层数据的边界(与 UDP 的 “数据报” 模式不同):
- 发送方:应用层数据被拆分为 TCP 报文段Segment发送拆分规则由 TCP 协议决定(如 MSS 限制)。
- 接收方:将收到的报文段按顺序重组为完整的字节流,再交给应用层,确保数据顺序与发送时一致。
### 1.2.4 有序传输
&emsp;&emsp;TCP 报文段头部包含 “序号Sequence Number” 和 “确认号Acknowledgment Number
- 序号SN标识发送方当前发送的字节流位置如序号为 100 表示当前报文段的第一个字节是整个字节流的第 100 字节)。
- 确认号ACK标识接收方期望下次接收的字节流位置如确认号为 200 表示已正确接收前 199 字节,下次需从 200 字节开始接收)。
- 接收方通过序号排序报文段,丢弃重复报文,确保按发送顺序交付数据。
### 1.2.5 全双工通信
&emsp;&emsp;TCP 连接是双向的,双方可同时发送和接收数据,无需等待对方结束发送:
- 每个方向都有独立的发送缓冲区和接收缓冲区,以及独立的滑动窗口用于流量控制。
- 示例:客户端发送数据的同时,服务器可同步向客户端返回响应,无需等待客户端发送完毕。
## 1.3 优缺点
&emsp;&emsp;优点:
1. 可靠、有序、无丢包
2. 支持流量 / 拥塞控制
3. 适用于大数据传输
&emsp;&emsp;缺点:
1. 连接建立 / 释放开销大
2. 头部开销大20-60 字节)
3. 不适合实时性要求极高的场景(如直播低延迟)
## 1.4 Python实现
&emsp;&emsp;TCP服务器
```python
import socket
import signal
import threading
from concurrent.futures import ThreadPoolExecutor
from dotenv import load_dotenv
import os
from model.mqtt import UNKNOWN_MESSAGE
from processor.tcp_processor import TcpMessageProcessor
from config.logger_config import logger
class TCPServer:
def __init__(self):
load_dotenv()
self.host = os.getenv("TCP_HOST", 'localhost')
self.port = int(os.getenv("TCP_PORT", 9100))
self.max_workers = 10
self.timeout = 300
self.server_socket = None
self.running = False
self.thread_pool = ThreadPoolExecutor(max_workers=self.max_workers)
self.message_processor = TcpMessageProcessor()
# 已经连接的客户端
self.clients = {}
self.client_lock = threading.Lock()
self.server_thread = None
signal.signal(signal.SIGTERM, self._handle_signal)
signal.signal(signal.SIGINT, self._handle_signal)
def _handle_signal(self, signum, frame):
"""处理终止信号,触发优雅关闭"""
logger.info(f"收到信号 {signum},准备关闭服务器...")
self.running = False
def _start_loop(self):
"""启动服务器"""
try:
# 创建 TCP/IP socket AF_INET: IPv4地址族 SOCK_STREAM: TCP协议面向连接
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置SO_REUSEADDR选项允许重用地址和端口避免服务端重启时出现 “地址已被占用” 的错误
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定到指定主机和端口 0.0.0.0 表示所有主机
self.server_socket.bind((self.host, self.port))
# 设置最大等待连接数
self.server_socket.listen(5)
# 设置socket超时时间1.0秒)
self.server_socket.settimeout(1.0)
self.running = True
logger.info(f"TCP服务器启动监听 {self.host}:{self.port} "
f"(最大线程: {self.max_workers}, 超时: {self.timeout}s")
while self.running:
try:
# accept() 会阻塞直到有客户端连接
# client_socket: 与客户端通信的新socket client_address: 客户端地址(ip, port)元组
client_socket, client_address = self.server_socket.accept()
# 获取客户端IP
client_ip = client_address[0]
# 设置客户端socket超时
# 防止客户端长时间不发送数据
client_socket.settimeout(self.timeout)
logger.info(f"新连接: {client_address}")
# 存储客户端
with self.client_lock:
self.clients[client_ip] = client_socket
# 提交到线程池处理
# 提交到线程池是为了让服务器能同时服务多个客户端,而不让一个慢客户端阻塞所有其他客户端
self.thread_pool.submit(self.handle_client, client_socket, client_ip)
except socket.timeout:
continue
except Exception as e:
if self.running:
logger.error(f"接受连接失败: {str(e)}")
except Exception as e:
logger.error(f"TCP服务器启动失败: {str(e)}")
finally:
self.stop()
def start(self):
self.server_thread = threading.Thread(target=self._start_loop, daemon=True)
self.server_thread.start()
def stop(self):
if not self.running:
return
self.running = False
logger.info("开始关闭服务器...")
# 移除客户端
with self.client_lock:
for client_socket in self.clients.values():
try:
client_socket.close()
except Exception as e:
logger.warning(f"关闭客户端连接失败: {str(e)}")
self.clients.clear()
# 关闭线程池
self.thread_pool.shutdown(wait=True)
logger.info("所有客户端处理线程已结束")
# 关闭连接
if self.server_socket:
self.server_socket.close()
logger.info(f"服务器已关闭({self.host}:{self.port}")
def handle_client(self, client_socket, client_ip):
"""处理客户端连接"""
try:
while True:
data = client_socket.recv(1024)
if not data:
logger.info(f"客户端 {client_ip} 主动断开连接")
break
message = data.decode('utf-8').strip()
logger.info(f"收到 {client_ip} 的消息: {message}")
# 放到消息处理器里面处理
response = self.message_processor.process(message)
# 回复消息
if response != UNKNOWN_MESSAGE:
client_socket.sendall(response.encode('utf-8'))
logger.info(f"回复 {client_ip}: {response}")
except socket.timeout:
logger.warning(f"客户端 {client_ip} 超时未活动")
except Exception as e:
logger.error(f"处理 {client_ip} 出错: {str(e)}")
finally:
# 异常情况下关闭连接
with self.client_lock:
if client_ip in self.clients:
del self.clients[client_ip]
try:
client_socket.close()
logger.info(f"客户端 {client_ip} 连接已关闭")
except Exception as e:
logger.warning(f"关闭 {client_ip} 连接失败: {str(e)}")
def send_to_client(self, client_ip, message):
# 先获取客户端连接(加锁保护)
with self.client_lock:
client_socket = self.clients.get(client_ip)
if not client_socket:
logger.warning(f"客户端 {client_ip} 不存在或已断开连接")
return False
# 发送消息
try:
client_socket.sendall(message.encode('utf-8'))
logger.info(f"主动发送消息给 {client_ip}: {message}")
return True
except Exception as e:
logger.error(f"向 {client_ip} 发送消息失败: {str(e)}")
# 发送失败时移除无效连接
with self.client_lock:
if client_ip in self.clients:
del self.clients[client_ip]
return False
tcp_server = TCPServer()
```
&emsp;&emsp;main.py
```python
from endpoint.tcp_server import tcp_server
from config.logger_config import logger
import time
if __name__ == "__main__":
try:
tcp_server.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("收到终止信号,开始关闭程序...")
finally:
tcp_server.stop()
logger.info("程序已退出")
except Exception as e:
logger.critical(f"程序启动失败: {str(e)}", exc_info=True)
exit(1)
```
&emsp;&emsp;启动tcp_server后通过while循环防止主线程退出从而让后台的TCP服务器线程能继续运行。
&emsp;&emsp;在TCPServer中通过while循环持续接收tcp客户端的连接每当有一个客户端连接时会提交到线程池中去处理通过自定义消息处理器将处理完的结果返回给客户端。
&emsp;&emsp;TCP消息处理器
::: code-group
```python [抽象消息处理器]
from abc import ABC, abstractmethod
class AbstractDeviceMessageParser(ABC):
"""设备消息解析器基类"""
@abstractmethod
def check(self, message):
"""判断当前解析器是否能处理该消息"""
pass
@abstractmethod
def parse(self, message):
"""解析消息内容"""
pass
@abstractmethod
def response(self, parsed_data):
"""根据解析后的数据生成响应"""
pass
```
```python [示例消息处理器]
from datetime import datetime
from model.band import BandData
from model.mqtt import MqttTopic, MqttData, UNKNOWN_MESSAGE
from parser.abstract_parsers import AbstractDeviceMessageParser
import re
from utils.index import str_length_to_4hex
from config.logger_config import logger
from endpoint.mqtt_client import mqtt_client
class JuweiBandParser(AbstractDeviceMessageParser):
"""聚伟手环消息解析器"""
def __init__(self):
# 聚伟手环消息格式: [MNYD*设备ID*内容长度*内容]
self.pattern = r'MNYD'
self.vendor = "聚伟手环"
self.tag = "MNYD"
self.device_id = ""
def check(self, message):
"""检查是否为聚伟手环的消息格式"""
return re.search(self.pattern, message) is not None
def parse(self, message):
"""解析聚伟手环消息"""
try:
parts = message.strip("[]").split("*")
self.device_id = parts[1]
content = parts[3]
return {
'vendor': self.vendor,
'device_id': self.device_id,
'content': content,
}
except Exception as e:
logger.error(f"解析{self.vendor}消息出错: {str(e)}")
return None
def publish_vital_data(self, item, value):
mqtt_client.publish(
topic=MqttTopic.JuWei_Band_Post.value,
payload=MqttData(
deviceIp="",
deviceId=self.device_id,
payload=BandData(item=item, value=value).model_dump_json(),
).model_dump_json())
def response(self, parsed_data):
"""生成聚伟手环的响应消息"""
if not parsed_data:
return UNKNOWN_MESSAGE
# 处理不同命令
content = parsed_data['content']
parts = content.split(",", 1)
content_tag = parts[0] if len(parts) > 0 else ""
content_value = parts[1] if len(parts) > 1 else ""
replay = UNKNOWN_MESSAGE
logger.info(f"解析{content_tag}消息")
match content_tag:
# PING消息 [MNYD*334588000000156*0004*PING]
case "PING":
replay = "PING,1"
# 日期,步数,翻滚次数,电量百分数,里程数(km)
# [MNYD*334588000000156*0014*KA,120414,50,100,100,100.12]
case "KA":
replay = content_tag
# 位置数据上报
# case "UD":
# replay = ""
# 报警数据上报
# [MNYD*334588000000156*00CD*AL,180916,064153,A,22.570512,N,113.8623267,
# E,0.00,154.8,0.0,11,100,100,0,0,00100018,7,0,460,1,9529,
# 21809,155,9529,21242,132,9529,21405,131,9529,63554,131,9529,
# 63555,130,9529,63556,118,9529,21869,116,0,12.4]
case "AL":
replay = content_tag
# 获取服务器端时间
# [MNYD*YYYYYYYYYYYYYYY*LEN*LGZONE]
case "LGZONE":
now = datetime.now()
current_date = now.date().strftime("%Y-%m-%d")
current_time = now.time().strftime("%H:%M:%S")
replay = f"{content_tag},+8,{current_time},{current_date}"
# 请求位置数据 TODO
case "WG":
replay = content_tag
# 请求电话本设置信息 TODO
case "PHLQ":
replay = "PHL"
# 请求SOS设置信息 TODO
case "SOS":
replay = content_tag
# 终端心率上传
case "heart":
self.publish_vital_data(content_tag, content_value)
replay = content_tag
# 上传体温数据 [MNYD*334588000000156*0009*temp,36.2]
case "temp":
self.publish_vital_data(content_tag, content_value)
replay = content_tag
# 上传血压数据 [MNYD*334588000000156*000C*blood,150,70]
case "blood":
self.publish_vital_data(content_tag, content_value)
replay = content_tag
# 上传血氧数据 [MNYD*334588000000156*0009*oxygen,97]
case "oxygen":
self.publish_vital_data(content_tag, content_value)
replay = content_tag
# 上传睡眠数据报告
case "SLEEPRPT":
replay = "SLEEP"
case _:
logger.info("不需要回复")
return UNKNOWN_MESSAGE
return f"[{self.tag}*{parsed_data['device_id']}*{str_length_to_4hex(replay)}*{replay}]"
```
```python [消息处理器]
from model.mqtt import UNKNOWN_MESSAGE
from parser.juwei_band_parser import JuweiBandParser
from config.logger_config import logger
class TcpMessageProcessor:
"""消息处理器,负责将消息路由到正确的设备解析器"""
def __init__(self):
# 注册所有支持的设备解析器
self.parsers = [JuweiBandParser()]
def process(self, message):
"""处理消息,返回响应"""
# 尝试找到能处理该消息的解析器
for parser in self.parsers:
if parser.check(message):
parsed_data = parser.parse(message)
if parsed_data:
logger.info(f"处理{parsed_data['vendor']}消息: {message}")
return parser.response(parsed_data)
# 没有找到合适的解析器
logger.warning(f"未识别的消息格式: {message}")
return UNKNOWN_MESSAGE
```
:::
# 二、HTTP
## 2.1 定义
&emsp;&emsp;HTTPHyperText Transfer Protocol超文本传输协议是互联网的核心协议之一用于**客户端如浏览器、App与服务器之间的通信**是万维网WWW数据交换的基础。它定义了请求 / 响应的格式、传输规则和状态码等核心机制,支持从简单文本到复杂多媒体(图片、视频、文件)的传输,也是现代 Web 应用的底层通信标准。
## 2.2 特性
### 2.2.1 请求 - 响应模式
&emsp;&emsp;通信由客户端主动发起请求服务器接收后处理并返回响应不存在服务器主动向客户端推送数据的情况HTTP/2 引入 Server Push 扩展,可主动推送关联资源)。
&emsp;&emsp;一次完整通信流程:**客户端建立连接 → 发送请求 → 服务器处理 → 返回响应 → 连接关闭**HTTP/1.1 默认开启长连接 Keep-Alive
### 2.2.2 无状态
&emsp;&emsp;**服务器不会保存客户端的会话状态**(如登录状态、浏览记录),每次请求都是独立的,服务器无法通过协议本身识别连续请求是否来自同一客户端。通过 Cookie、Session、Token如 JWT等机制补充状态管理。
## 2.3 版本
### 2.3.1 HTTP/1.01996 年)
- 基础版本,支持 GET、POST、HEAD 三种请求方法。
- 每次请求都需要建立新的 TCP 连接(短连接),连接建立和关闭的开销大,性能较低。
- 不支持长连接、管线化请求,仅支持简单的文本传输。
### 2.3.2 HTTP/1.11999 年)
- 默认开启 长连接Keep-Alive同一 TCP 连接可处理多个请求,减少连接开销。
- 支持 管线化请求:客户端可连续发送多个请求,无需等待前一个响应返回(部分浏览器未完全支持)。
- 新增请求方法PUT、DELETE、OPTIONS、TRACE、CONNECT。
- 支持 chunked 编码分块传输、缓存控制Cache-Control、内容协商等核心功能。
### 2.3.3 HTTP/22015 年)
&emsp;&emsp;基于 SPDY 协议优化,核心目标是提升性能:
- 二进制帧传输:将请求 / 响应数据拆分为二进制帧,而非 HTTP/1.x 的文本格式,解析效率更高。
- 多路复用:同一 TCP 连接中可并发处理多个请求(通过帧的 Stream ID 区分),解决 HTTP/1.1 的 “队头阻塞” 问题。
- 服务器推送Server Push服务器可主动向客户端推送关联资源如 HTML 引用的 CSS/JS减少客户端请求次数。
- 头部压缩HPACK对请求头和响应头进行压缩减少传输体积HTTP/1.x 头部重复传输开销大)。
### 2.3.4 HTTP/32022 年)
&emsp;&emsp;基于 SPDY 协议优化,核心目标是提升性能:
- 解决 TCP 队头阻塞UDP 无连接特性,单个流的阻塞不影响其他流。
- 更快的连接建立QUIC 集成 TLS 1.3减少握手次数1-RTT 甚至 0-RTT 建立连接)。
- 更好的移动网络支持:支持连接迁移(如手机切换 WiFi/4G 时,连接不中断)。
## 2.4 组成
### 2.4.1 请求消息Request
&emsp;&emsp;客户端向服务器发送的请求格式,由 请求行、请求头、空行、请求体 四部分组成:
```http
GET /api/courses/1 HTTP/1.1 # 请求行
Host: lms.example.com # 请求头(键值对形式)
Authorization: Bearer <token>
Accept: application/json
User-Agent: Mozilla/5.0 (Chrome/120.0.0.0)
Content-Type: application/json
{"username": "admin", "password": "123456"} # 请求体可选POST/PUT 等方法常用)
```
#### 请求行
- 请求方法:表示请求的操作类型(常用方法如下表)。
- 请求 URI指定服务器上的资源路径。
- 协议版本:如 HTTP/1.1、HTTP/2。
#### 请求头
&emsp;&emsp;描述请求的附加信息,常用字段:
- Host目标服务器域名如 lms.example.comHTTP/1.1 必选字段。
- User-Agent客户端身份标识如浏览器版本、App 名称)。
- Accept客户端可接收的响应数据格式如 application/json、text/html
- Content-Type请求体的数据格式如 application/json、multipart/form-data文件上传
- Authorization身份认证信息如 Token、Basic Auth
- Cookie客户端存储的会话信息如登录态 Cookie
- Cache-Control缓存控制策略如 no-cache 表示不使用缓存)。
#### 请求体
&emsp;&emsp;可选部分,仅在需要向服务器提交数据时使用(如 POST 提交表单、PUT 更新资源),数据格式由 Content-Type 指定:
- 表单数据application/x-www-form-urlencoded如 username=admin&password=123
- JSON 数据application/json如 {"key": "value"})。
- 文件上传multipart/form-data如 LMS 系统的作业文件上传)。
- 纯文本text/plain。
### 2.4.2 响应消息Response
&emsp;&emsp;服务器向客户端返回的响应格式,由 状态行、响应头、空行、响应体 四部分组成:
```http
HTTP/1.1 200 OK # 状态行
Server: Nginx
Content-Type: application/json
Content-Length: 128
Set-Cookie: sessionId=abc123; Path=/
{"code": 200, "message": "success", "data": {"id": 1, "name": "Vue3 实战课程"}} # 响应体
```
#### 状态行
- 协议版本:如 HTTP/1.1。
- 状态码:表示请求处理结果。
- 状态短语:状态码的文字描述(如 OK、Not Found
#### HTTP状态码
| 分类 | 状态码范围 | 含义 | 常用码 |
|------|------------|-----------------------|-------------------------|
| 1xx | 100-199 | 信息性响应(临时响应)| 100 Continue预检通过|
| 2xx | 200-299 | 成功响应 | 200 OK成功、201 Created资源创建成功、204 No Content成功无响应体|
| 3xx | 300-399 | 重定向 | 301 永久重定向、302 临时重定向、304 Not Modified缓存有效|
| 4xx | 400-499 | 客户端错误 | 400 Bad Request请求参数错误、401 Unauthorized未认证、403 Forbidden权限不足、404 Not Found资源不存在、405 Method Not Allowed请求方法不支持|
| 5xx | 500-599 | 服务器错误 | 500 Internal Server Error服务器内部错误、502 Bad Gateway网关错误、503 Service Unavailable服务不可用、504 Gateway Timeout网关超时|
#### 响应头
&emsp;&emsp;描述响应的附加信息,常用字段:
- Server服务器软件标识如 Nginx、Tomcat
- Content-Type响应体的数据格式如 application/json、text/html
- Content-Length响应体的字节大小。
- Set-Cookie服务器向客户端设置 Cookie如登录态、会话 ID
- Cache-Control缓存控制策略如 max-age=3600 表示缓存 1 小时)。
- Access-Control-Allow-Origin跨域资源共享CORS配置如 * 表示允许所有域名跨域)。
- Location重定向目标地址3xx 状态码必选)。
#### 响应体
&emsp;&emsp;服务器返回的核心数据,数据格式由 Content-Type 指定,常见格式:
- JSON前后端分离项目首选如 {"code": 200, "data": [...]})。
- HTML传统 Web 页面如静态网页、JSP 页面)。
- 图片 / 视频:二进制流(如 image/jpeg、video/mp4
- 纯文本text/plain。
# 三、WebSocket
## 3.1 定义
&emsp;&emsp;WebSocket 是一种**全双工、双向、持久化的网络通信协议**(属于应用层协议),由 HTML5 规范定义,专门解决 HTTP 协议无法实现服务器主动向客户端推送数据的问题。
&emsp;&emsp;它的核心特点是:**一旦客户端与服务器建立连接,双方就可以在这个连接上实时、双向地发送数据**,无需像 HTTP 那样每次通信都由客户端发起请求,非常适合实时通信场景(如聊天、直播弹幕、实时数据监控、在线协作等)。
| 特性 | HTTP | WebSocket |
|---------------------|-------------------------------|-------------------------------|
| 通信方向 | 单向(客户端请求→服务器响应)| 全双工(双方可同时发数据)|
| 连接类型 | 短连接 / 长连接(需重复请求) | 持久连接(一次建立,持续通信) |
| 数据传输效率 | 每次请求带大量头部信息,效率低 | 连接建立后仅传输数据,开销小 |
| 服务器主动推送 | 不支持HTTP/2 的 Server Push 仅能推送资源,非实时数据) | 原生支持,可主动向客户端发数据 |
| 协议标识 | http:// / https:// | ws:// / wss://(加密版)|
## 3.2 特性
### 3.2.1 握手过程
1. 客户端发送 HTTP 请求,请求头包含 Upgrade: websocket 和 Connection: Upgrade表示要升级为 WebSocket 协议);
2. 服务器响应 101 Switching Protocols握手成功连接转为 WebSocket 持久连接;
3. 后续通信不再使用 HTTP 格式,而是 WebSocket 帧格式(二进制 / 文本)。
&emsp;&emsp;请求头:
```http
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket # 核心请求升级为WebSocket协议
Connection: Upgrade # 核心:表示连接要升级
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # 客户端生成的随机密钥,用于验证
Sec-WebSocket-Version: 13 # 指定WebSocket版本主流为13
Origin: https://example.com # 跨域验证
```
&emsp;&emsp;响应头:
```http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # 服务器加密后的密钥,客户端验证
```
### 3.2.2 全双工通信
&emsp;&emsp;此时 HTTP 连接已升级为 WebSocket 连接双方可以通过这个连接实时、双向地发送数据。数据传输采用帧Frame 格式,支持文本数据和二进制数据(如图片、视频流)。
### 3.2.3 帧格式
&emsp;&emsp;WebSocket 数据以帧为单位传输,帧头包含操作码(文本帧 0x01、二进制帧 0x02、关闭帧 0x08 等)、掩码(客户端发送数据需掩码,服务器无需);
### 3.2.4 无同源限制
&emsp;&emsp;WebSocket 不遵循同源策略(但服务器可通过 Origin 头限制跨域);
### 3.2.5 心跳机制
&emsp;&emsp;通过 Ping/Pong 帧维持连接(避免网络设备断开空闲连接,如 LMS 系统需定期发送 Ping 帧,服务器回复 Pong 帧)。
# 四、MQTT
## 4.1 定义
&emsp;&emsp;MQTTMessage Queuing Telemetry Transport消息队列遥测传输是一种**轻量级、低带宽、低功耗的发布 / 订阅Publish/Subscribe模式物联网IoT通信协议**,由 IBM 于 1999 年设计,核心目标是解决受限设备(如传感器、嵌入式设备)和低带宽、不稳定网络环境下的高效数据传输问题。
## 4.2 架构
- 发布者Publisher发送消息的设备 / 服务;
- 订阅者Subscriber接收消息的设备 / 服务;
- broker代理服务器核心中间件接收发布者的消息根据「主题Topic」转发给订阅者如 EMQ X、Mosquitto、RabbitMQ 支持 MQTT 插件);
- 主题Topic消息的分类标识。
## 4.3 特性
### 4.3.1 QoS服务质量等级
- QoS 0最多一次消息发送一次不保证送达
- QoS 1至少一次消息至少送达一次可能重复
- QoS 2恰好一次消息仅送达一次最可靠。
### 4.3.2 轻量级
&emsp;&emsp;头部开销极小(固定头部仅 2 字节),消息体支持二进制 / 文本,适合低带宽场景
### 4.3.3 保留消息Retained Message
&emsp;&emsp;broker 保存某个主题的最后一条消息,新订阅者订阅后立即收到该消息
### 4.3.4 遗嘱消息Will Message
&emsp;&emsp;客户端异常断开时broker 自动发送预设消息
### 4.3.5 清洁会话Clean Session
&emsp;&emsp;客户端断开连接后broker 是否保留订阅信息和未送达消息
## 4.4 组成
&emsp;&emsp;MQTT 消息由 固定头部Fixed Header、可变头部Variable Header、负载Payload 三部分组成:
| 部分 | 作用 |
|--------------|----------------------------------------------------------------------|
| 固定头部 | 必选2 字节起包含消息类型如发布、订阅、QoS 等级、是否保留消息等标识。 |
| 可变头部 | 可选,仅部分消息类型(如发布、订阅)需要,包含主题名、消息 ID 等信息。 |
| 负载 | 可选,消息的实际内容(如 JSON 字符串、二进制数据),例如 {"temperature": 25}。 |
# 五、.Net Core实现
## 5.1 SuperSocket
&emsp;&emsp;[SuperSocket](https://www.supersocket.net/)是一个轻量级, 跨平台而且可扩展的 .Net/Mono Socket 服务器程序框架。可以轻松构建TCP、UDP、WebSocket服务器。
## 5.2 安装依赖
&emsp;&emsp;NuGut安装SuperSocket、SuperSocket.WebSocket和SuperSocket.WebSocket.Server 2.0及以上版本。
## 5.3 配置文件
&emsp;&emsp;appsettings.json
```json
{
"serverOptions": {
"TcpServer": {
"name": "TcpServer",
"listeners": [
{
"ip": "Any",
"port": 4040
}
]
},
"WebSocketServer": {
"name": "WebSocket",
"listeners": [
{
"ip": "Any",
"port": 5050
}
]
}
},
"Mqtt": {
"Host": "127.0.0.1",
"Port": 1883,
"ClientId": "MqttClient",
"Topics": [
"test/topic1",
"test/topic2"
]
}
}
```
## 5.3 主程序
&emsp;&emsp;program.cs
```cs
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddSingleton<IPackageHandler<ProtocolFrame>, TcpPackageHandler>();
services.AddSingleton<WebSocketMessageHandler>();
// 1. 注入 MQTT 配置
services.Configure<MqttSettings>(context.Configuration.GetSection("Mqtt"));
// 2. 注册 MQTT 客户端
services.AddSingleton<IMqttClient>(serviceProvider => new MqttClientFactory().CreateMqttClient());
// 3. 注册 MQTT 后台服务
services.AddHostedService<MqttService>();
})
.AsMultipleServerHostBuilder()
.AddServer<TcpService, ProtocolFrame, BinaryPipelineFilter>(builder =>
{
builder.ConfigureServerOptions((ctx, config) => config.GetSection("TcpServer"));
})
.AddWebSocketServer(builder =>
{
builder
.UseWebSocketMessageHandler(async (session, package) =>
{
using var scope = session.Server.ServiceProvider.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<WebSocketMessageHandler>();
await handler.HandleAsync(session, package);
})
.ConfigureServerOptions((ctx, config) => config.GetSection("WebSocketServer"));
})
.ConfigureLogging(logging => logging.ClearProviders())
.UseNLog()
.Build();
await host.RunAsync();
```
&emsp;&emsp;通过.AsMultipleServerHostBuilder()可以构造多服务器实例。
## 5.4 TCP服务器
### 5.4.1 协议数据
```cs
/// <summary>
/// 协议数据结构 示例65 6D 00 05 00 01 68 65 6C 6C 6F
/// </summary>
/// <param name="Magic">帧头标识</param>
/// <param name="Length">数据长度</param>
/// <param name="Type">数据类型</param>
/// <param name="Payload">数据内容</param>
///
record ProtocolFrame(
ushort Magic,
ushort Length,
ushort Type,
byte[] Payload
);
```
&emsp;&emsp;可以根据实际情况自定义消息格式。
### 5.4.2 协议解析
```cs
class BinaryPipelineFilter : FixedHeaderPipelineFilter<ProtocolFrame>
{
/// <summary>
/// 固定头长度
/// </summary>
public BinaryPipelineFilter() : base(6)
{
}
/// <summary>
/// 从包头中解析出 Body 长度
/// </summary>
/// <param name="buffer">字节流</param>
/// <returns>Body 长度</returns>
protected override int GetBodyLengthFromHeader(ref ReadOnlySequence<byte> buffer)
{
var reader = new SequenceReader<byte>(buffer);
// 读取前2字节 → magic
reader.TryReadBigEndian(out ushort magic);
// 再读2字节 → Length
reader.TryReadBigEndian(out ushort length);
// 再读2字节 → Type
reader.TryReadBigEndian(out ushort type);
// 校验帧头
if (magic != 0x656D)
{
throw new Exception("非法帧头");
}
// 限制长度
if (length == 0 || length > 8192)
{
throw new Exception("非法长度");
}
// 返回 Payload 长度
return length;
}
/// <summary>
/// 把完整字节包 → 转换成 ProtocolFrame
/// </summary>
/// <param name="buffer">字节流</param>
/// <returns>ProtocolFrame</returns>
protected override ProtocolFrame DecodePackage(ref ReadOnlySequence<byte> buffer)
{
var reader = new SequenceReader<byte>(buffer);
// 读取前2字节 → magic
reader.TryReadBigEndian(out ushort magic);
// 再读2字节 → Length
reader.TryReadBigEndian(out ushort length);
// 再读2字节 → Type
reader.TryReadBigEndian(out ushort type);
var payload = buffer.Slice(6, length).ToArray();
// 构造ProtocolFrame
return new ProtocolFrame(magic, length, type, payload);
}
}
```
&emsp;&emsp;根据协议数据ProtocolFrame来解包。
### 5.4.3 消息处理
```cs
class TcpPackageHandler(ILogger<TcpPackageHandler> logger) : IPackageHandler<ProtocolFrame>
{
private readonly ILogger<TcpPackageHandler> _logger = logger;
public async ValueTask Handle(IAppSession session, ProtocolFrame package, CancellationToken cancellationToken)
{
_logger.LogInformation($"Magic={package.Magic:X4}, Type={package.Type}, Len={package.Length}");
await session.SendAsync(package.Payload, cancellationToken);
}
}
```
### 5.4.4 TCPService
```cs
class TcpService(IServiceProvider serviceProvider, IOptions<ServerOptions> serverOptions) : SuperSocketService<ProtocolFrame>(serviceProvider, serverOptions)
{
}
```
## 5.5 Websocket服务器
### 5.5.1 消息处理
```cs
class WebSocketMessageHandler(ILogger<WebSocketMessageHandler> logger)
{
private readonly ILogger<WebSocketMessageHandler> _logger = logger;
public async ValueTask HandleAsync(WebSocketSession session, WebSocketPackage package)
{
_logger.LogInformation($"[WebSocket] {package.Message}");
await session.SendAsync("ok");
}
}
```
## 5.6 MQTT服务器
### 5.6 MqttService
```cs
class MqttService(IMqttClient mqttClient, IOptions<MqttSettings> settings, ILogger<TcpPackageHandler> logger) : BackgroundService
{
private readonly IMqttClient _mqttClient = mqttClient;
private readonly MqttSettings _settings = settings.Value;
private readonly ILogger<TcpPackageHandler> _logger = logger;
private MqttClientOptions? _mqttOptions;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// 构建连接配置
_mqttOptions = new MqttClientOptionsBuilder()
.WithClientId($"{_settings.ClientId}_{Guid.NewGuid():N}")
.WithTcpServer(_settings.Host, _settings.Port)
.WithCleanSession()
.Build();
_mqttClient.ConnectedAsync += OnConnectedAsync;
_mqttClient.DisconnectedAsync += OnDisconnectedAsync;
_mqttClient.ApplicationMessageReceivedAsync += HandleMessage;
_logger.LogInformation("[MQTT] 正在连接到服务器 {Host}:{Port}...", _settings.Host, _settings.Port);
await _mqttClient.ConnectAsync(_mqttOptions, stoppingToken);
// 等待程序停止
await Task.Delay(Timeout.Infinite, stoppingToken);
}
public async Task OnConnectedAsync(MqttClientConnectedEventArgs arg)
{
_logger.LogInformation("[MQTT] 已成功连接到服务器");
// 连接成功后订阅主题
foreach (var topic in _settings.Topics)
{
await _mqttClient.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce);
_logger.LogInformation($"[MQTT] 已订阅主题:{topic}");
}
await Task.CompletedTask;
}
public async Task OnDisconnectedAsync(MqttClientDisconnectedEventArgs arg)
{
_logger.LogError("[MQTT] 连接失败,原因:{Reason}", arg.Reason);
await Task.CompletedTask;
}
public async Task HandleMessage(MqttApplicationMessageReceivedEventArgs arg)
{
var topic = arg.ApplicationMessage.Topic;
var payload = Encoding.UTF8.GetString(arg.ApplicationMessage.Payload);
_logger.LogInformation($"\n[MQTT] 收到消息\n主题{topic}\n内容{payload}\n");
await Task.CompletedTask;
}
public async Task<bool> PublishAsync(string topic, string payload, CancellationToken cancellationToken = default)
{
try
{
var message = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(payload)
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build();
await _mqttClient.PublishAsync(message, cancellationToken);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "[MQTT] 发布消息到主题 {Topic} 失败", topic);
return false;
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
await _mqttClient.DisconnectAsync();
await base.StopAsync(stoppingToken);
}
}
```
&emsp;&emsp;需要实现后台服务接口一直运行。

View File

@@ -0,0 +1,95 @@
---
title: 前端包管理器
date: 2025-12-13
---
# npm
&emsp;&emsp;2010年随Node.js发布的npm首次为JavaScript引入了标准的包管理系统。其核心创新是**package.json文件**,通过语义化版本规范定义了依赖声明标准。这一设计解决了手动管理依赖时的版本混乱问题,为模块化开发提供了基础设施。
## 第一阶段:嵌套依赖架构
&emsp;&emsp;早期版本采用树状嵌套的依赖结构。每个包将其依赖安装在自身的node_modules目录中形成深层嵌套。这种设计保证了依赖隔离但导致了几个严重问题
- 路径深度问题依赖树深度可达十几层触发Windows 260字符路径限制
- 空间极度浪费:相同包在不同层级重复安装,项目体积指数级膨胀
- 安装性能低下大量递归操作导致I/O效率极低
## 第二阶段:扁平化依赖重构
&emsp;&emsp;2015年发布的npm v3引入扁平化依赖安装策略。该方案将**可共享的依赖提升到顶层node_modules**,减少嵌套深度。
&emsp;&emsp;主要解决方案为:
- 依赖提升算法:将兼容版本的依赖包提升至顶层
- 冲突处理:版本冲突时,低版本保持嵌套结构
- 确定性牺牲:提升顺序影响最终结构
&emsp;&emsp;但同时也引入了新问题:
- 幻影依赖:未声明但被提升的包意外可用
- 依赖不确定性相同package.json产生不同目录结构
- 模块解析复杂化Node.js模块解析算法与扁平结构不匹配
# yarn
&emsp;&emsp;2016年发布的Yarn针对npm核心痛点提出了系统性解决方案:
- 确定性版本锁定yarn.lock文件记录精确版本和依赖树
- 并行下载优化:多线程并发下载提升网络利用率
- 离线缓存机制:全局缓存支持离线安装
- 完整性校验checksum验证确保包完整性
&emsp;&emsp;Yarn的成功迫使npm进行重大改进。npm v5版本借鉴了Yarn的核心设计增加了package-lock.json和缓存优化形成了技术竞争的良性循环。
# pnpm
&emsp;&emsp;2017年发布的pnpm从存储层面重新设计了包管理模型。其核心是**基于内容寻址的全局存储与符号链接架构**。
&emsp;&emsp;实现原理:
- 内容可寻址存储:包按内容哈希存储,全局唯一
- 硬链接复用:相同包文件在所有项目间共享
- 符号链接树通过软链接构建符合Node.js解析规则的依赖树
- 严格依赖隔离:每个包只能访问其声明的依赖
&emsp;&emsp;npm/yarn的node_modules目录
```cmd
项目A: node_modules/lodash@4.17.21
项目B: node_modules/lodash@4.17.21 # 重复存储
项目C: node_modules/lodash@4.17.21 # 重复存储
```
&emsp;&emsp;pnpm的node_modules目录
```cmd
全局存储: .pnpm-store/lodash@4.17.21
项目A: 硬链接 → 全局存储
项目B: 硬链接 → 全局存储
项目C: 硬链接 → 全局存储
```
&emsp;&emsp;npm/Yarn采用扁平化结构包可访问非声明依赖pnpm采用符号链接嵌套结构包仅可访问声明依赖。
&emsp;&emsp;pnpm的符号链接架构在Monorepo场景中表现优异。跨包依赖通过本地文件系统链接实现避免了重复安装。Workspaces功能通过优化符号链接策略确保开发环境与生产环境一致性。
## 简单使用
```cmd
# 通过 npm 安装
npm install -g pnpm
# 检查版本
pnpm --version
# 安装所有依赖(根据 package.json
pnpm install
# 安装生产依赖
pnpm add <package-name>
# 安装开发依赖
pnpm add -D <package-name>
# 移除依赖
pnpm remove <package-name>
# 查看所有配置
pnpm config list
# 设置存储路径
pnpm config set store-dir ~/.pnpm-store
# 使用淘宝镜像
pnpm config set registry https://registry.npmmirror.com/
```
::: tip
npm确立了基本范式Yarn解决了确定性问题pnpm重构了存储模型
:::

220
docs/Web/Others/RustFS.md Normal file
View File

@@ -0,0 +1,220 @@
---
title: RustFS简介和使用
date: 2025-12-09
---
# 简介
&emsp;&emsp;[RustFS](https://rustfs.com.cn/) 是一个基于 Rust 构建的高性能分布式对象存储系统。
&emsp;&emsp;具体以下特点:
- 高性能:基于 Rust 构建,确保极致的速度和资源效率。
- 分布式架构:可扩展且容错的设计,适用于大规模部署。
- S3 兼容性:与现有的 S3 兼容应用和工具无缝集成。
- 数据湖支持:专为高吞吐量的大数据和 AI 工作负载优化。
- 完全开源:采用 Apache 2.0 许可证,鼓励社区贡献和商业使用。
- 简单易用:设计简洁,易于部署和管理。
&emsp;&emsp;S3 标准通常指 Amazon S3 的 API 接口规范,现已成为对象存储领域事实上的行业标准接口协议。它定义了对象存储的核心操作方式和数据模型。
- 对象:基本存储单元,包含数据、元数据和唯一标识符
- 桶:对象的逻辑容器,类似文件夹
- 扁平结构无传统目录层级通过键名Key定位
# 安装部署
&emsp;&emsp;通过docker部署
```yml
services:
rustfs:
image: rustfs/rustfs:1.0.0-alpha.65
container_name: rustfs
ports:
- "9000:9000"
- "9001:9001"
volumes:
- ./data:/data
- ./logs:/logs
environment:
- TZ=Asia/Shanghai
- RUSTFS_ACCESS_KEY=admin
- RUSTFS_SECRET_KEY=19940822Cxx
- RUSTFS_CONSOLE_ENABLE=true
restart: unless-stopped
```
&emsp;&emsp;其中9000是API端口9001是控制台端口。
&emsp;&emsp;默认的登录账号密码为rustfsadmin / rustfsadmin。
# API调用
&emsp;&emsp;登录控制台获取access_key和secret_access。
## Python
1. 安装AWS官方SDK依赖库
```cmd
pip install boto3
```
2. 配置与连接
```python
import boto3
from botocore.client import Config
from config.setting import settings
url = 'ip:port'
access_key = ''
secret_access = ''
s3 = boto3.client('s3',
endpoint_url=url,
aws_access_key_id=access_key,
aws_secret_access_key=secret_access,
config=Config(signature_version='s3v4'),
region_name='cn-east-1'
)
```
3. 上传文件
```python
async def upload_file(md5: str, file: UploadFile = File(...)) -> str:
file_ext = file.filename.split('.')[-1]
unique_filename = f"{md5}.{file_ext}"
file_content = await file.read()
# 上传到S3
s3.put_object(
Bucket=BUCKET,
Key=unique_filename,
Body=file_content,
ContentType=file.content_type
)
# 返回文件url
return f"{BUCKET}/{unique_filename}"
```
## Flutter
1. 安装依赖包
```yaml
dependencies:
minio: ^3.5.8
file_picker: ^10.3.3
```
2. 工具类
```dart
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:minio/io.dart';
import 'package:minio/minio.dart';
import 'file_utils.dart';
class MinIOHelper {
static final MinIOHelper _instance = MinIOHelper._internal();
factory MinIOHelper() => _instance;
final String ip = 'ip';
final int port = 9100;
final String fileUrl = 'http://ip:port';
final String accessKey = '';
final String secretKey = '';
MinIOHelper._internal() {
_minio = Minio(
endPoint: ip,
port: port,
accessKey: accessKey,
secretKey: secretKey,
useSSL: false,
);
}
late Minio _minio;
Future<String> uploadFile({
required PlatformFile file,
required String bucketName,
Function(double)? onProgress,
}) async {
try {
if (isImageFile(file.name)) {
// 压缩图片
final compressedFile = await compressImage(File(file.path!));
String hashName = await generateMD5HashName(compressedFile.path);
String fileName = '$hashName${getFileExtension(file.name)}';
await _minio.fPutObject(bucketName, fileName, compressedFile.path);
await compressedFile.delete();
return fileName;
} else {
String hashName = await generateMD5HashName(file.path!);
String fileName = '$hashName${getFileExtension(file.name)}';
await _minio.fPutObject(bucketName, fileName, file.path!);
return fileName;
}
} catch (e) {
throw Exception('文件上传失败: $e');
}
}
}
```
&emsp;&emsp;文件工具类:
```dart
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'log_utils.dart';
String getFileExtension(String fileName) {
if (fileName.contains('.')) {
return '.${fileName.split('.').last.toLowerCase()}';
}
return '';
}
Future<String> generateMD5HashName(String filePath) async {
final file = File(filePath);
final bytes = await file.readAsBytes();
final hash = md5.convert(bytes);
return hash.toString();
}
// 图片压缩方法
Future<File> compressImage(File file) async {
try {
// 获取压缩后的文件路径
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
'${file.parent.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
minWidth: 800,
minHeight: 600,
quality: 70,
format: CompressFormat.jpeg,
);
if (result == null) {
throw Exception('图片压缩失败');
}
return File(result.path);
} catch (e) {
logger.e('图片压缩失败,使用原文件: $e');
return file;
}
}
// 检查是否为图片文件
bool isImageFile(String fileName) {
final imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
final extension = fileName.toLowerCase().substring(fileName.lastIndexOf('.'));
return imageExtensions.contains(extension);
}
```

View File

@@ -0,0 +1,243 @@
---
title: 视频压缩
date: 2026-01-29
---
# 一、简介
&emsp;&emsp;视频码率Bit Rate指的是视频文件在单位时间内通常是每秒所包含的数据量。单位通常是 Mbps兆比特每秒或 Kbps千比特每秒
&emsp;&emsp;码率越高:视频中包含的细节信息越多,画面越清晰、色彩越丰富、动态画面(如快速运动、爆炸等)越流畅,不容易出现马赛克或模糊。但代价是文件体积越大,传输所需的网络带宽也越高。码率越低:视频文件更小,传输更快,但在复杂画面中容易产生压缩瑕疵(如马赛克、模糊、色带等)。
&emsp;&emsp;如果不压缩原始视频数据量极其庞大一部2小时的未压缩1080p电影可能占用数TB的存储空间普通硬盘无法承受家庭宽带或移动网络无法实时传输如此巨大的数据量视频播放会不断卡顿。
&emsp;&emsp;如果服务器的带宽为10M适合 720p勉强支持低码率 1080p。
# 二、引入依赖
```xml
<!-- Jave 2 视频处理库 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.5.0</version>
</dependency>
```
# 三、工具类
```java
import lombok.extern.slf4j.Slf4j;
import ws.schild.jave.Encoder;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.encode.AudioAttributes;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.encode.VideoAttributes;
import ws.schild.jave.info.VideoSize;
import java.io.File;
/**
* 视频压缩工具类
* 提供将视频压缩到720p的静态方法
*/
@Slf4j
public class VideoCompressor {
// 支持的视频格式
private static final String[] SUPPORTED_FORMATS = {
".mp4", ".avi", ".mov", ".mkv", ".flv",
".wmv", ".webm", ".mpeg", ".mpg", ".3gp"
};
// 压缩参数常量
private static final int TARGET_WIDTH = 1280;
private static final int TARGET_HEIGHT = 720;
private static final int VIDEO_BITRATE = 1500000; // 1.5 Mbps
private static final int AUDIO_BITRATE = 128000; // 128 kbps
private static final String VIDEO_CODEC = "libx264";
private static final String AUDIO_CODEC = "aac";
private static final String OUTPUT_FORMAT = "mp4";
private static final String OUTPUT_SUFFIX = "_720p.mp4";
private static final int MAX_BITRATE = 2500; // 2.5 Mbps
private static final int MIN_BITRATE = 1000; // 1 Mbps
/**
* 压缩视频到720p统一输出为MP4格式
*
* @param inputFile 输入视频文件
* @return 压缩后的视频文件,如果不需要压缩则返回原文件
* @throws Exception 压缩过程中可能出现的异常
*/
public static File compressTo720p(File inputFile) throws Exception {
if (!isSupportedVideoFile(inputFile)) {
log.info("文件格式不支持或文件不存在,直接返回源文件");
return inputFile;
}
// 判断是否需要压缩
if (!needCompression(inputFile)) {
return inputFile;
}
File outputFile = createOutputFile(inputFile);
Encoder encoder = new Encoder();
// 配置编码参数
EncodingAttributes encodingAttributes = createEncodingAttributes();
log.info("开始压缩视频: {} -> {}", inputFile.getName(), outputFile.getName());
encoder.encode(new MultimediaObject(inputFile), outputFile, encodingAttributes);
log.info("视频压缩完成: {}", outputFile.getName());
return outputFile;
}
/**
* 智能判断视频是否需要压缩
* 基于分辨率、码率、数据密度等多维度综合判断
*/
public static boolean needCompression(File videoFile) throws Exception {
MultimediaObject media = new MultimediaObject(videoFile);
// 获取视频基本信息
double fileSizeMB = videoFile.length() / 1048576.0; // 转换为MB
double duration = media.getInfo().getDuration() / 1000.0;
int bitRateKbps = media.getInfo().getVideo().getBitRate() / 1000; // 转换为kbps
// 获取视频分辨率
int width = media.getInfo().getVideo().getSize().getWidth();
int height = media.getInfo().getVideo().getSize().getHeight();
log.info("视频分析: 分辨率 {}x{}, 大小 {}MB, 时长 {}秒, 码率约 {}kbps", width, height, fileSizeMB, duration, bitRateKbps);
// 判断逻辑:基于分辨率和码率的智能判断
boolean needsCompression = false;
// 1. 如果分辨率已经<=720p检查码率是否过高
if (height <= TARGET_HEIGHT && width <= TARGET_WIDTH) {
if (bitRateKbps > MAX_BITRATE) {
log.info("视频分辨率达标但码率({}kbps)过高,需要压缩", bitRateKbps);
needsCompression = true;
} else {
log.info("视频分辨率已达720p或以下且码率合理无需压缩");
}
}
// 2. 如果分辨率>720p但码率很低说明已是优化过的视频无需压缩
else if (height > TARGET_HEIGHT && bitRateKbps < MIN_BITRATE) {
log.info("视频虽为{}p高分辨率但码率({}kbps)已优化,无需压缩", height, bitRateKbps);
}
// 3. 高分辨率+高码率,需要压缩
else if (height > TARGET_HEIGHT && bitRateKbps >= MAX_BITRATE) {
log.info("视频为{}p高分辨率且码率较高({}kbps)需要压缩至720p", height, bitRateKbps);
needsCompression = true;
}
// 4. 其他情况(如异常数据)默认不压缩
else {
log.info("视频参数异常,保守处理:不进行压缩");
}
return needsCompression;
}
/**
* 检查文件是否为支持的视频格式
*/
private static boolean isSupportedVideoFile(File file) {
if (file == null || !file.exists()) {
return false;
}
String filename = file.getName().toLowerCase();
for (String format : SUPPORTED_FORMATS) {
if (filename.endsWith(format)) {
return true;
}
}
return false;
}
/**
* 创建输出文件对象
*/
private static File createOutputFile(File inputFile) {
String baseName = getFileBaseName(inputFile.getName());
String outputFilename = baseName + OUTPUT_SUFFIX;
return new File(inputFile.getParent(), outputFilename);
}
/**
* 获取文件名(不含扩展名)
*/
private static String getFileBaseName(String filename) {
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
}
/**
* 创建编码配置参数
*/
private static EncodingAttributes createEncodingAttributes() {
// 音频配置
AudioAttributes audio = new AudioAttributes();
audio.setCodec(AUDIO_CODEC);
audio.setBitRate(AUDIO_BITRATE);
// 视频配置
VideoAttributes video = new VideoAttributes();
video.setCodec(VIDEO_CODEC);
video.setBitRate(VIDEO_BITRATE);
video.setSize(new VideoSize(TARGET_WIDTH, TARGET_HEIGHT));
// 编码设置
EncodingAttributes attributes = new EncodingAttributes();
attributes.setOutputFormat(OUTPUT_FORMAT);
attributes.setAudioAttributes(audio);
attributes.setVideoAttributes(video);
return attributes;
}
}
```
&emsp;&emsp;通过分辨率和码率的双重判断,确定是否需要压缩。
# 四、使用
```java
public void uploadFile(MultipartFile file, String filePath) {
try {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
File tempFile = convertMultipartFileToFile(file);
log.info("临时文件地址:{}", tempFile.toPath());
log.info("开始压缩文件:{}", tempFile.getName());
File compressFile = VideoCompressor.compressTo720p(tempFile);
log.info("完成压缩文件:{}", tempFile.getName());
log.info("压缩文件地址:{}", tempFile.toPath());
body.add("file", new FileSystemResource(compressFile));
aListClient.put()
.uri("api/fs/form")
.header(HttpHeaders.AUTHORIZATION, token)
.header("File-Path", filePath)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(body)
.retrieve()
.body(Map.class);
Files.deleteIfExists(compressFile.toPath());
Files.deleteIfExists(tempFile.toPath());
} catch (Exception e) {
log.error("文件上传失败{}", e.getMessage());
throw new RuntimeException("文件上传失败", e);
}
}
private File convertMultipartFileToFile(MultipartFile file) throws IOException {
String suffix = (file.getOriginalFilename() != null && file.getOriginalFilename().contains("."))
? file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))
: ".tmp";
Path tempFile = Files.createTempFile(System.currentTimeMillis() + "", suffix);
file.transferTo(tempFile);
return tempFile.toFile();
}
```

View File

@@ -0,0 +1,171 @@
---
title: 前端包管理器
date: 2025-12-13
---
# 一、Vite简介
&emsp;&emsp;Vite是新一代的前端构建工具在尤雨溪开发Vue3.0的时候诞生。类似于Webpack+ Webpack-dev-server。
&emsp;&emsp;其主要利用浏览器**ESModule特性**导入组织代码在服务器端按需编译返回完全跳过了打包这个概念服务器随起随用。生产中利用Rollup作为打包工具号称下一代的前端构建工具。
# 二、核心原理
## 2.1 ESModule
&emsp;&emsp;ESModule是JavaScript提出的官方标准化模块系统不同于之前的CJSAMDCMD等等ESModule提供了更原生以及更动态的模块加载方案最重要的就是它是浏览器原生支持的也就是说我们可以**直接在浏览器中去执行import**,动态引入我们需要的模块,而不是把所有模块打包在一起。
&emsp;&emsp;例如声明一个script标签`<script type="module" src="/src/main.js"></script>`当浏览器解析资源时会往当前域名发起一个GET请求`main.js`文件:
```javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
```
&emsp;&emsp;请求到了main.js文件会检测到内部含有import引入的包又会import引用发起HTTP请求获取模块的内容文件如App.vue文件。
## 2.2 基于ESModule的Dev Server
&emsp;&emsp;Vite其核心原理是利用浏览器现在已经支持ES6的import碰见import就会发送一个HTTP请求去加载文件**Vite启动一个koa服务器拦截这些请求并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合然后再以ESM格式返回返回给浏览器**。
&emsp;&emsp;Vite整个过程中没有对文件进行打包编译做到了真正的按需加载所以其运行速度比原始的webpack开发编译速度快出许多。
## 2.3 基于ESModule的热更新
&emsp;&emsp;热更新主要思路是通过WebSocket创建浏览器和服务器的通信监听文件的改变当文件被修改时服务端发送消息通知客户端修改相应的代码客户端对应不同的文件进行不同的操作的更新。
&emsp;&emsp;**Vite 通过 chokidar 来监听文件系统的变更,只用对发生变更的模块重新加载**,只需要精确的使相关模块与其临近的 HMR边界连接失效即可这样HMR 更新速度就不会因为应用体积的增加而变慢而 Webpack 还要经历一次打包构建。所以 HMR 场景下Vite 表现也要好于 Webpack。
## 2.4 基于Esbuild的开发环境编译
&emsp;&emsp;Esbuild是一个JavaScript Bundler 打包和压缩工具它提供了与Webpack、Rollup等工具相似的资源打包能力。可以将JavaScript 和TypeScript代码打包分发在网页上运行。但其打包速度却是其他工具的10100倍。
&emsp;&emsp;在开发环境中Vite 使用 esbuild 进行快速的热重载和模块编译。
### 2.4.1 Esbuild高性能原理
1. Go 语言实现:避开 JavaScript 运行时的性能瓶颈。
2. 并行编译:利用多核 CPU 同时处理多个文件。
3. 内存优化:在单个进程中完成解析、转换和生成,减少内存开销。
### 2.4.2 Esbuild关键特性
1. 语法转换TypeScript → JS保留类型信息或移除、现代 JS 语法降级(如 ?.、?? 转换)等。
2. 压缩优化:变量重命名、死代码消除、支持 CSS 压缩(通过 esbuild-plugin-css-minify等。
## 2.5 基于Rollup的生产环境打包
&emsp;&emsp;Rollup是基于ESM的JavaScript打包工具。相比于其他打包工具如Webpack他总是能打出更小、更快的包。因为 Rollup 基于 ESM 模块,比 Webpack 和 Browserify 使用的 CommonJS模块机制更高效。Rollup的亮点在于同一个地方一次性加载。能针对源码进行 Tree Shaking(去除那些已被定义但没被使用的代码),以及 Scope Hoisting 以减小输出文件大小提升运行性能。
&emsp;&emsp;在生产环境中Vite 使用 Rollup 进行最终的打包和优化。
### 2.5.1 Rollup构建流程
1. 解析入口文件:从 index.js 开始,递归分析所有 import 语句。
2. 生成抽象语法树AST使用 acorn 解析 JS 文件为 AST。
3. 应用插件转换:如 @rollup/plugin-typescript 处理 TypeScript。
4. 生成依赖图:分析模块间的依赖关系。
5. 输出优化:
Tree-shaking基于 ESM 静态分析,移除未使用的代码
代码分割识别动态导入import())并拆分包。。
# 三、Mini-Vite
## 3.1 初始化工程
&emsp;&emsp;初始化NodeJs工程并添加Vue3依赖
## 3.2 Node Server
```javascript
const http = require('http');
const fs = require('fs');
const path = require('path');
const miniVueParse = require('./mini-vue-parser')
const server = http.createServer((req, res) => {
// 获取请求的路径
const reqPath = req.url;
// 构建本地文件路径
const filePath = path.join(__dirname, reqPath);
console.log('filePath: ', filePath)
// 检查文件是否存在
fs.access(filePath, fs.constants.R_OK, (err) => {
if (err) {
console.error('文件不存在:', err);
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('文件不存在');
} else {
// 读取文件并发送给客户端
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('读取文件出错:', err);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('内部服务器错误');
} else {
// 根据文件类型设置合适的 Content-Type
const extname = path.extname(filePath);
let contentType = 'text/plain';
switch (extname) {
case '.html':
contentType = 'text/html';
break;
case '.js':
contentType = 'text/javascript';
break;
case '.vue':
contentType = 'text/javascript';
break;
case '.css':
contentType = 'text/css';
break;
// 添加其他需要支持的文件类型
}
res.writeHead(200, { 'Content-Type': contentType });
if (extname === '.js') {
data = data.replace(/from ['"]vue['"]/g, "from './node_modules/vue/dist/vue.esm-browser.js'");
}
if (extname === '.vue') {
const fileName = path.basename(filePath);
data = miniVueParse.parse(data, fileName)
}
res.end(data);
}
});
}
});
});
const port = 3000;
server.listen(port, () => {
console.log(`代理服务器正在监听端口 ${port}`);
});
```
&emsp;&emsp;在浏览器输入`http://localhost:3000/index.html`,会首先解析`<script type="module" src="main.js"></script>`,获取`main.js`,然后会把`import { createApp } from 'vue'`中的`Vue`换成替换成:`./node_modules/vue/dist/vue.esm-browser.js'`,在遇到`Vue`文件时,会利用`Vue`的模板编译器转成Javascript文件
&emsp;&emsp;mini-vue-parser.js
```JavaScript
const compilerSfc = require("@vue/compiler-sfc");
const path = require('path');
function parse(vueFileString,fileName) {
const sfc = compilerSfc.parse(
vueFileString
);
const sfcTemplate = compilerSfc.compileTemplate({
id: 'sfc',
filename: fileName + '.template.vue',
source: sfc.descriptor.template.content
})
const sfcScript = compilerSfc.compileScript(sfc.descriptor, {
filename: 'sfc.script.vue'
})
let result = '';
result += sfcTemplate.code.replace("export", "") + '\n';
result += sfcScript.content.replace("export default ", "const __script = ") + '\n';
result += `__script.render=render;\n`
result += `export default __script;`
result = result.replace(/from ['"]vue['"]/g, "from './node_modules/vue/dist/vue.esm-browser.js'");
return result;
}
module.exports = {
parse
}
```
# 四、参考资料
1. [深入浅出vite核心原理 + 手撕mini-vite](https://juejin.cn/post/7026285200766140453#heading-37)
2. [深入理解Vite核心原理](https://juejin.cn/post/7064853960636989454)
3. [step-vite](https://github.com/Llyonss/step-vite)