包含关键字 typecho 的文章

在消费升级与数字化浪潮的双重推动下,便利店行业正从传统 “线下单一场景” 向 “线上线下融合” 的新零售模式转型。传统收银系统功能单一、数据割裂、用户粘性弱等问题,已难以满足便利店高效运营与用户多元需求。OctShop 便利店收银 + 点单 + 商城系统源码,凭借 “三位一体” 的功能架构、轻量化部署与高适配性,为便利店提供了从线下收银到线上经营的全场景解决方案。它不仅打破了传统便利店的经营边界,更通过数字化工具帮助商家降本增效、提升用户复购,成为便利店实现新零售转型的核心技术支撑。

图片

OctShop便利店收银+点单+商城系统源码: https://pc.opencodetiger.com/Cashier

一、核心优势在于收银、点单、商城三大模块的深度融合

OctShop 系统源码的核心优势在于收银、点单、商城三大模块的深度融合,形成闭环式经营体系,覆盖便利店全业务场景。收银模块作为线下经营的核心,具备 “高效便捷 + 精准安全” 的双重特性。系统支持多种支付方式,包括现金、银行卡、微信支付、支付宝、银联云闪付等,满足不同用户的支付习惯;同时集成条码扫描枪、收银小票机、钱箱等硬件设备,无需额外适配,即插即用,新店员上手培训时间可缩短至 1 小时内。在功能细节上,系统支持 “商品快速录入”,通过扫码枪扫描商品条码即可自动填充名称、价格、库存等信息,避免手动输入误差;针对促销场景,可一键设置 “满减、折扣、第二件半价” 等活动规则,收银时自动计算优惠金额,无需人工干预;此外,系统具备 “断网应急模式”,断网状态下仍可正常收银,联网后自动同步数据,避免因网络问题导致的营业中断,保障门店经营连续性。

二、多渠道接单 + 高效履约”,助力便利店拓展服务半径

点单模块则聚焦 “多渠道接单 + 高效履约”,助力便利店拓展服务半径。该模块支持线下柜台点单与线上点单双模式:线下场景中,店员可通过收银台终端或移动 Pad 快速记录用户订单,尤其适合早餐、简餐等即时性消费需求;线上场景则覆盖微信小程序、美团、饿了么等主流平台,系统可自动同步各平台订单至后台,无需店员切换多个平台操作,避免订单遗漏或错单。在订单处理环节,系统支持 “智能分单”,根据商品库存、制作进度自动分配订单优先级,同时通过语音播报提醒店员接单,大幅提升出单效率;针对外卖订单,可对接第三方配送平台(如蜂鸟即配、美团专送),实时同步配送员位置与订单状态,方便店员与用户追踪;此外,点单模块还支持 “预订单” 功能,用户可提前预约取餐或配送时间,便利店可根据预订单量提前备货,减少高峰时段压力,提升用户体验。

图片

三、商城模块是便利店突破物理边界、实现 “线上增收” 的关键

商城模块是便利店突破物理边界、实现 “线上增收” 的关键。基于 OctShop 系统源码搭建的线上商城,可与线下门店库存实时同步,用户通过微信小程序即可浏览商品、在线下单,支持 “到店自提” 与 “配送到家” 两种履约方式。在商品运营上,商城模块支持 “分类管理”,可将商品划分为零食饮料、日用百货、生鲜速食、烟酒特产等类别,方便用户快速查找;同时支持 “个性化推荐”,根据用户历史下单记录、浏览轨迹推送相关商品,提升转化率。针对便利店的 “即时性消费” 特点,系统可设置 “满额免配送费”“限时秒杀”“新人优惠券” 等营销活动,吸引用户线上下单;此外,商城模块还具备 “会员积分体系”,用户线上线下消费均可累积积分,积分可用于兑换商品或抵扣现金,增强用户粘性。值得注意的是,商城模块与收银、点单模块数据互通,商家可通过后台查看全渠道销售数据,精准分析商品热销榜、用户消费偏好,为采购与营销决策提供数据支撑。

四、技术层面高适配性与易扩展性

在技术层面,OctShop便利店系统源码展现出高适配性与易扩展性,满足不同规模便利店的需求。对于小型社区便利店,系统提供 “轻量化部署方案”,无需搭建独立服务器,通过云服务器即可快速上线,初期投入成本低;对于连锁便利店,系统支持 “多门店统一管理”,总部可通过后台实时查看各门店的销售数据、库存情况、员工绩效,统一制定促销活动与商品定价,实现标准化运营。在数据安全方面,系统采用 SSL 加密传输技术,对支付数据、用户信息、销售数据进行全程加密,防止信息泄露;同时支持 “自动备份” 功能,每日自动备份数据至云端,即使本地设备故障,也可通过备份快速恢复,保障数据安全。此外,系统预留丰富的 API 接口,可根据便利店需求对接会员管理系统、供应链管理系统、财务统计系统等第三方工具,实现功能扩展,避免后期系统重构成本。

图片

五、实际应用价值多维度的经营提升

从实际应用价值来看,OctShop 系统源码能为便利店带来多维度的经营提升。在成本控制上,通过自动化收银与订单处理,可减少 1-2 名人工成本,同时降低因人工操作导致的收银误差与订单错漏;在库存管理上,系统实时同步线上线下库存,自动预警低库存商品,避免缺货或积压,库存周转效率可提升 30% 以上;在用户运营上,线上商城与会员体系的搭建,可将线下客流转化为线上私域用户,通过精准营销提升复购率,据实测数据,接入系统的便利店线上订单占比可提升至 20%-30%,整体营业额增长 15% 以上。以社区便利店为例,接入 OctShop 系统后,用户可通过小程序线上下单、到店自提,便利店通过 “生鲜预售”“日用品团购” 等活动,进一步提升客单价与用户粘性,实现从 “传统夫妻店” 到 “社区新零售服务站” 的转型。

六、怎么选择OctShop

对于计划接入 OctShop 系统源码的便利店商家,可根据自身规模与需求选择合适的部署方案。单店商家建议选择 “基础版”,包含核心收银、点单与简易商城功能,投入成本低、上线速度快;连锁商家可选择 “企业版”,享受多门店管理、供应链对接、数据看板等高级功能,实现规模化运营;若有个性化需求(如定制会员等级、开发专属营销工具),可借助系统源码的二次开发能力,联合技术团队进行功能定制。同时,系统提供详细的部署手册与操作教程,商家无需专业技术团队,通过客服指导即可完成搭建;后期运营中,官方提供免费的系统升级服务,确保功能始终适配行业最新需求,如新增 “刷脸支付”“电子发票” 等功能,帮助便利店持续提升服务能力。综上,OctShop 便利店收银 + 点单 + 商城系统源码,以 “全场景覆盖、高性价比、易部署扩展” 的核心优势,为便利店新零售转型提供了切实可行的技术方案。它不仅解决了传统便利店经营中的效率低、数据散、用户粘性弱等痛点,更通过线上线下融合的模式,帮助商家挖掘新的营收增长点。在消费需求日益多元的今天,OctShop 系统源码将持续助力便利店行业数字化升级,推动便利店从 “商品销售渠道” 向 “社区综合服务平台” 转型,实现长期可持续发展。

昨天想把贴了好几年的膜撕掉,结果贴的太紧,大力出奇迹,屏幕直接被拉起来了。

如图,因为换屏很贵,可以单独换这个排线吗。有木有懂维修的前辈帮忙指导一下。

现在是开机状态,手机还在连接,就是屏幕不显示。花大钱就不如直接换新来的好。

watch5

企业网站的安全性已成为用户信任的“第一道门槛”。许多企业在选择SSL证书时,常常在DV、OV和EV三种类型之间犹豫不决。OV(组织验证)SSL证书凭借其“安全与成本”的完美平衡,逐渐成为绝大多数企业的首选。本文将深入解析为什么企业建议申请OV证书,并为您推荐一个优秀的国产SSL证书品牌——JoySSL。

一、为什么OV证书是企业的“黄金选择”?

1. 比DV证书:多一层身份验证,彻底告别“匿名”

DV证书仅验证域名所有权,虽然快速且便宜,但无法确认网站背后的运营主体是谁。这意味着钓鱼网站可以轻松伪造DV证书来冒充正规企业。而OV证书的申请流程严格审核企业真实身份(如营业执照、对公账户等)。一旦部署,用户的浏览器地址栏不仅能看到小锁,还能点击查看该企业的正式名称。这种“身份可视化”能有效降低跳出率,提升用户咨询转化率,让访客第一时间确认“这不是一个钓鱼网站”。

2. 比EV证书:节省50%以上成本,功能实用不浪费

EV证书曾以其地址栏绿色名称显示而备受推崇,但近年来浏览器UI的调整使得EV与OV的视觉差异逐渐缩小,且EV证书的价格通常是OV的2-3倍,审核周期也更长。对于绝大多数企业官网、电商平台及SaaS服务而言,OV证书提供的“企业名称显示+数据加密”已完全满足商业信任需求,无需为溢价的EV买单。

3. 满足合规刚需:等保与密评的“标配”

随着《网络安全法》及“等保2.0”的普及,仅验证域名的DV证书已无法满足合规要求。特别是在等保二级及以上系统中,明确建议或要求采用OV及以上级别的证书。OV证书能提供经过严格核验的组织身份信息,确保通信主体可信,帮助企业顺利通过测评。

二、为什么选择JoySSL部署OV证书?

OV证书申请入口

在明确了OV证书的优势后,选择一个可靠的证书颁发机构(CA)至关重要。JoySSL作为中国自主品牌的SSL证书提供商,正在成为越来越多国内企业的信赖之选。

  1. 自主可控,安全合规:JoySSL携手全球可信顶级根,基于国内服务器验证签发。针对等保和密评需求,JoySSL提供支持国密算法(SM2/SM3)  的OV证书,并可实现“国密+国际”双算法自适应,既满足合规监管,又兼容所有主流浏览器。
  2. 高性价比与服务保障:相比国际品牌动辄数千元的OV证书,JoySSL提供了极高的性价比。其OV证书不仅价格亲民,还提供一对一全程技术指导,从CSR生成到服务器部署,帮助运维人员快速上手。
  3. 产品线丰富:无论是保护主域名的单域名OV证书,还是能同时保护多个子域名的通配符OV证书,JoySSL都能提供,完美适配集团官网、电商平台及多分支企业门户。

三、行动指南:如何快速获取JoySSL OV证书?

如果您已决定为企业官网或关键业务系统部署OV证书,可以通过以下步骤轻松申请:

  1. 访问官网:前往JoySSL官方网站。
  2. 注册账号:在注册过程中,为了获取专属优惠和技术支持,建议填写推荐邀请码:230970。这将帮助您对接专业客服,获取最适合您企业架构的证书选型建议。
  3. 选择证书:在“企业OV SSL”类别中,根据您的域名数量选择“单域名”、“多域名”或“通配符”证书。
  4. 提交验证:提交企业营业执照和域名所有权验证,CA审核团队通常在1-3个工作日内完成严格的人工审核。
  5. 部署上线:验证通过后下载证书,在技术客服的协助下安装到服务器,开启安全可信的企业之旅。

总结:  对于追求品牌信誉、注重用户信任且有合规需求的企业而言,OV证书是性价比最高的“务实之选”。选择像JoySSL这样兼具合规资质、本土服务与技术创新的国产品牌,不仅能保障数据传输的绝对安全,更能向您的每一位访客展示企业的正规形象与责任担当。

愿元宵的甜蜜,能冲淡改 bug 的苦涩;愿团圆的温暖,能驱散加班的疲惫。元宵节快乐!

更新日志:

1、设置界面增加 WSL2 全局设置入口(~/.wslconfig)。
2、发行版配置 /etc/wsl.conf 。
3、优化已安装的发新版列表刷新策略(侧边栏选中首页,且非关闭至系统栏托盘,才会定时刷新)。
4、USB 设备管理(基于 usbipd-win 提供 Windows 系统插入的 USB 给 WSL 中的 Linux 使用;刷新策略与发新版列表一致)。
5、设置界面下拉菜单交互优化。
6、启动流程优化,去除冗余逻辑。

项目地址: https://github.com/owu/wsl-dashboard

若该项目对您有用,请帮我点一个星标; 若有任何建议,可以在帖子下方留言。

下一个版本初步计划 : 网络 或 Docker

image

image

点赞 + 关注 + 收藏 = 学会了

整理了一个n8n小专栏,有兴趣的工友可以关注一下 👉 《n8n修炼手册》

可能有些工友看了旧版的教程,发现工作流顶部导航栏里有一个“活跃(Activate)”按钮。

后台问我,为什么自己的却是这样的。

“活跃(Activate)”按钮旷工了吗?

那定时任务怎么办?n8n 还能不能用了?

别方,新版的(本文使用的版本是 2.6.3)“活跃(Activate)”被取消掉了,如果你的工作流有定时任务,直接点击“Publish”按钮一样会定时定候自动工作。

我搞个简单的定时任务证明一下。

每10秒中生成一个名字和内容都是「年月日时分秒」的txt文件。

这个工作流的 JSON 文件放在文末,有需要的工友可以拿去测试一下。

点击工作流右上角的“Publish”按钮,它就会变成“Published”,并且左侧还有个小绿点。

此时定时任务就已经开始工作了。

打开 n8n 文件存储的文件夹可以看到每隔10秒就会生成要给 .txt 文件。

就算你关掉 n8n 页面它也会继续运行。

如果想关掉定时器,就要找到这个工作流,然后点击它的“Unpublish”按钮才能停下来。

本文示例的工作流。

{
  "name": "测试定时任务",
  "nodes": [
    {
      "parameters": {
        "operation": "toText",
        "sourceProperty": "title",
        "options": {}
      },
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        512,
        0
      ],
      "id": "7292c851-10c2-4a44-a708-dd58bc3f0f89",
      "name": "Convert to File"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "876bfd58-b014-43d4-9990-2826d028e562",
              "name": "title",
              "value": "={{ $now.toFormat('yyyyMMddHHmmss') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        288,
        0
      ],
      "id": "90a6b894-6564-4c95-b49e-39b685be009c",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "={{ `/home/node/.n8n-files/${$now.toFormat('yyyyMMddHHmmss')}.txt` }}",
        "options": {}
      },
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        736,
        0
      ],
      "id": "282036a8-f1d6-4d51-8e88-61c238d94878",
      "name": "Read/Write Files from Disk1"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "seconds",
              "secondsInterval": 10
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        64,
        0
      ],
      "id": "c3bb6706-1f39-4161-a2e5-7b3a8253a7b8",
      "name": "Schedule Trigger"
    }
  ],
  "pinData": {},
  "connections": {
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Convert to File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to File": {
      "main": [
        [
          {
            "node": "Read/Write Files from Disk1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "4f3e7f88-c69c-4a4d-a6c8-9eda94fea91d",
  "meta": {
    "instanceId": "8b30a8ba058f5126b4f4e9373018ce0596139a4d2028982510061e844b858b71"
  },
  "id": "POOpHifJ6XWEsOVdjYblV",
  "tags": []
}

以上就是本文的全部内容啦,想了解更多n8n玩法欢迎关注《n8n修炼手册》👏

如果你有 NAS,我非常建议你在 NAS 上部署一套 n8n,搞搞副业也好,帮你完成工作任务也好 《『NAS』不止娱乐,NAS也是生产力,在绿联部署AI工作流工具-n8n》

点赞 + 关注 + 收藏 = 学会了

点赞 + 关注 + 收藏 = 学会了

💡整理了一个 NAS 专属玩法专栏,感兴趣的工友可以戳这里关注 👉 《NAS邪修》

在一个雷雨交加的夜晚,我打开绿联NAS用Docker装了个飞牛OS。同样是NAS系统为什么要这样套娃?这到底是绿联的沦丧还是飞牛的扭曲?背后究竟隐藏着哪些不为人知的玄机?

错综复杂、真假难辨、蛛丝马迹。敬请收看本期在绿联NAS用Docker装飞牛OS。

部署前准备

  • 设备:绿联 NAS(本文以 DXP4800 Plus 为例)
  • 硬盘空间:不小于 8G
  • 内存建议:3G 以上(推荐分配 4G)

打开绿联的「文件管理」应用,在 docker 文件夹下新建 fnOS 文件夹。

打开「Docker」应用,新增一个项目。

  • 项目名称:fnos
  • 存放路径:/docker/fnos

粘贴以下 Compose 配置

services:
  fnos:
    image: ghcr.io/qemus/qemu:latest
    container_name: fnos
    environment:
      BOOT: "https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-1.1.20-1596.iso?sign=b666841bf8e67f5666c2ed3e2a686882&t=1772379402"
      RAM_SIZE: "4G"
      CPU_CORES: "4"
      DISK_SIZE: "128G"
    devices:
      - /dev/kvm
      - /dev/net/tun
    cap_add:
      - NET_ADMIN
    ports:
      - 8006:8006
      - 5666:5666
    volumes:
      - ./dir1:/storage
    restart: unless-stopped
    stop_grace_period: 2m

这里有几个参数需要说明一下。

BOOT 填入飞牛系统(fnOS)的下载地址,这个地址可能会变,实际以你部署时飞牛官网给的地址为准。

fnOS 的下载地址可以在飞牛官网找:https://www.fnnas.com/download

x86/ARM 按自己 CPU 架构选择。

我的 NAS 是绿联 DXP4800 Plus,所以我下载 x86 这个。

点击“下载 fnOS(x86)”按钮,之后再点击“直接下载”按钮(不要选“迅雷下载”)。

然后在浏览器的“下载记录”里就能看到 fnOS 的下载记录,点击它的链接按钮就能复制它的地址,填入 BOOT 这项里。

environment 里还有几个参数要了解的:

  • RAM_SIZE: 分配给 fnOS 的内存(建议≥4G)
  • CPU_CORES: 分配 CPU 核心数,比如"4"
  • DISK_SIZE: 分配给 fnOS 的虚拟硬盘,比如"128G"

ports 这里配置了2个端口,分别是 80065666,冒号左边的数字可以自定义,右边的数字不能改。

  • 8006:8006 是飞牛后台端口,装系统时要访问这个端口。
  • 5666:5666 是 WebUI 端口,装完系统后,我们要用图形界面就要访问这个端口。

配置好 Compose 代码后,点击“立即部署”,等待项目构建成功后切换到“容器面板”,找到 fnos 这项,点击它旁边的箭头按钮,打开 8006 这项(或者在浏览器访问 NAS_IP:8006)。

首次打开会看到它在下载飞牛系统(fnOS)。

系统下载完成后,就进入安装系统环节。

飞牛支持 Graphical 和 Rescuing 两种安装方式,首选 Graphical,如果安装失败再试试 Rescuing。

我选了 Graphical 方式安装,然后就进入“选择安装位置”这步。由于我在 Compose 配置里只设置了1个硬盘(128G),所以当前可以选的就只有1个盘。

选中它,点击“下一步”。

系统分区建议大于8G,我给了64G。

分配的内存也建议大于3G,我给了4G。

分配完系统资源,点击“下一步”,等待它把系统装完。

装完后,进度条就是100%,点击“下一步”。

这里要给 fnOS 配置网络,但没什么可以选的了,所以直接默认,点击“保存”。

装好系统后就可以开机了。

选择“FNOS GNU/Linux”这项。

然后就是等待开机的过程,界面会展示黑底白LOGO的牛头。

开机成功后,在浏览器访问 NAS_IP:5666 ,这是 fnOS 图形化界面的入口。

进入后会进入一个五彩斑斓的界面,点击“开始 NAS 之旅”。

首次使用需要设置设备名字以及管理员账号,设置完成后点击“进入 fnOS”。

登录后就能进入 fnOS 桌面了,长这样子。

进入桌面后第一件事是把硬盘分配好(系统设置 - 存储空间管理 - 创建存储空间)。

创建成功后,存储空间就会看到新增了一项。

之后就可以正常使用 fnOS 的各项功能了。

我试了一下开启“远程访问”,打开“远程访问 - FN Connect”,配置一个 FN ID。

配置完成后,通过手机App也能远程访问到了。


以上就是本文的全部内容啦,你有好玩的镜像推荐吗?欢迎在评论区留言讨论!

想了解更多NAS玩法记得关注《NAS邪修》👏

往期推荐:

点赞 + 关注 + 收藏 = 学会了

本文由网易云音乐技术团队入云分享,有修订和排版优化。

1、引言

说起 IM,大家应该都或多或少了解过一些,一般被熟知是在一些聊天场景里应用的比较多;而一般情况下我们常接触的业务中大多是做一些接口的查询提交之类的操作,用正常的 Ajax 请求就足以满足需求,比较难接触到 IM 这种方案。

但如果涉及到一些需要频繁更新数据的业务场景,使用常规接口查询难免会给服务端造成比较大的性能开销,并且数据更新的延迟也会很大;尝试使用 IM 则可以让我们在业务开发中更好地应对频繁的数据更新场景,以提升用户体验和业务价值。近期在做一个多人实时打怪兽的场景,即多名玩家同时攻击一个怪兽,任意一个玩家攻击怪兽,其它玩家需要实时感知到怪兽的状态更新,比如怪兽血量和玩家伤害排行等信息。
图片
本文将从H5游戏场景下的实时信息更新需求切入,探讨下在类似这种高并发、低延迟的业务需求中,如何使用 IM 方案来解决频繁的数据更新问题,也顺便介绍下 WebSocket 的基本运作流程等。
图片

2、数据更新方案选型

在谈论 IM 之前,对于数据的实时更新,除了使用 IM ,还有哪些可选用的方案,可能包括但不限于下面几种。

2.1 接口轮询

接口轮询这种方式相信大家都很熟悉,主要是使用通过定期发送 HTTP 请求来达到数据更新的方式,实现起来也比较简单。例如一些榜单数据的定时更新:// 请求榜单接口const refreshRank = (familyId) => {    getMonsterDamageRank({ familyId }).then((res) => {        setRank(res);    }).catch((err) => {        Toast.warn(err.message || '服务器繁忙');    });}; // 每3秒刷新一次接口setInterval(() => {    refreshRank(currentFamily.familyId);}, 3000);这里使用 setInterval 每隔3秒请求一次榜单数据,用来更新排行榜信息,通常用于实现一些要求数据更新相对频繁,但又允许有一定延迟的场景;同时轮询也是一种实现起来最简单的方案。但轮询也有几个比较大的缺点,比如:1)带宽浪费:轮询需要定期向服务器发送请求,即使服务端没有新数据可用,这将会造成大量的带宽和服务器资源浪费。2)延迟高:数据的更新频率受轮询间隔影响,如果轮询间隔时间过长,会导致数据更新的延迟较高。3)负载过高:要降低数据的延迟,就必须提高接口轮询的频率,但轮询的频率过高,将会导致服务器负载过高,从而影响其他用户的体验。
图片

2.2 接口长轮询

长轮询(Long Polling)是一种改进的轮询技术,它的主要思想是在客户端发送请求后,服务端保持连接打开,但并不立即响应,而是在有新数据可用时才响应给客户端。当客户端接收到响应后,再次发起请求,以保持连接打开。相比于传统的轮询:长轮询可以降低网络延迟和服务器压力;因为长轮询的响应是异步的,服务器不需要在每个固定时间间隔内返回响应,这样可以减少不必要的请求。同时,当服务器有新数据可用时,也可以立即返回响应,从而提高数据的实时性。
图片
 如上图,长轮询的实现通常分为下面几个阶段:1)客户端向服务器发起请求。2)服务器接收到请求后,如果没有新数据可用,则保持连接打开。3)服务器有新数据可用时,响应给客户端。4)客户端接收到响应后,再次向服务器发起请求。 ......下面是使用 Node.js 实现的一个简单的长轮询服务端示例:const http = require('http');const messages = []; // 开始每隔1秒检查下messages中是否有信息function waitForNewMessages(response) {  const intervalId = setInterval(() => {    if (messages.length > 0) { // message 中有消息之后返回响应      response.writeHead(200, { 'Content-Type': 'application/json' });      response.end(JSON.stringify(messages));      clearInterval(intervalId);    }  }, 1000);   setTimeout(() => { // 30秒无数据,返回一个空数组    clearInterval(intervalId);    response.writeHead(200, { 'Content-Type': 'application/json' });    response.end(JSON.stringify([]));  }, 30000);}function handleRequest(request, response) {  if (request.url === '/messages') {    /* 请求到“/messages”时,如果有新消息,则立即向客户端发送响应       否则,等待一段时间后再次检查是否有新消息      /    waitForNewMessages(response);  } else {    response.writeHead(404);    response.end();  }}const server = http.createServer(handleRequest);server.listen(3000);在上面的代码中:我们使用  setInterval  函数每秒检查一次是否有新消息。如果有新消息,我们立即向客户端发送响应,并清除定时器。为了防止一直 pending,如果在30秒内没有新的消息,我们会向客户端发送一个空数组作为响应。这样,客户端就可以在收到新消息时立即更新页面。对于长轮询的实现仍有许多细节需要注意,如连接保持、连接断开重连等问题。此外,长轮询仍然需要消耗大量的带宽和服务器资源,因为每个连接都需要保持打开状态,可以想象有很多个请求到达服务端,服务端需要开启多个异步来保持链接在 pending 的状态。

2.3 SSE(Server-Sent Events)

SSE 也是一种浏览器与服务器之间实现实时通信的技术。它允许服务器向浏览器发送数据。在 SSE 中,浏览器可通过 EventSource API 来建立与服务器的连接,并监听来自服务器的事件。服务器通过向客户端发送特定格式的数据(包括事件名称和数据),来触发浏览器的事件监听器。下面同样使用 Nodejs 来实现一个 Demo:// Server 端const http = require('http'); const server = http.createServer((req, res) => {  // 设置头部信息  res.writeHead(200, {    'Content-Type': 'text/event-stream', // 设置响应类型为SSE    'Cache-Control': 'no-cache',    'Connection': 'keep-alive',    'Access-Control-Allow-Origin': '*' // 允许跨域请求  });   // 发送数据到客户端  setInterval(() => {    res.write('data: ' + new Date().toISOString() + '\n\n'); // 发送SSE消息  }, 1000);}); server.listen(3000, () => {  console.log('Server started on port 3000');}); // Client端const sse = new EventSource('http://localhost:3000'); // 监听SSE消息sse.addEventListener('message', (event) => {  console.log(event.data);});在客户端使用 new EventSource 访问 “h t t p : // localhost:3000” 时,服务器会返回一个 SSE 流,这里注意需要将响应头中的 Content-Type 设置为 text/event-stream,表示该响应是 SSE 流;将  Cache-Control  设置为 no-cache,表示浏览器不缓存该响应, Connection  设置为 keep-alive,表示服务器与客户端之间的连接应该保持打开状态。在 SSE 流中,每一条消息都需要以 data: 开头,并以两个换行符(\n\n)结尾。在本例中,使用 setInterval() 函数每秒发送一条消息。但对于SSE而言,也具有下面几个缺点:1)单向传输:只能从服务器向客户端推送数据,无法实现双向通信;2)只支持纯文本:事件流只能传输一个简单的文本数据流, 并且文本只能使用 UTF-8 格式编码;3)SSE对于一些浏览器的支持不够完善:比如在 Safari 和 iOS 中,可能会对 SSE 连接的数量和连接时间等方面进行限制,从而影响 SSE 的稳定性和可靠性。

2.4 HTTP/2 Server Push

相对于 HTTP/1.1 而言,HTTP/2 其实也是支持了服务端主动推送的,不过目前 HTTP/2 的主动推送,主要是用于提升页面加载性能的,它允许服务器在响应请求时向客户端推送预先缓存的资源(例如,CSS、JavaScript 和图像),以减少请求次数和延迟,是一种页面加载的优化手段。但考虑到其相对于 WebSocket 而言,目前的安全性和稳定性还有待进一步提升,用于实现即时通信还不是特别成熟,所以这里就不再赘述了。对与如何实现提前推送静态文件,具体可以参考下《快速理解HTTP/2的服务器推送(Server Push)》。

2.5 WebSocket

上面介绍的几种方式,都是基于HTTP协议的,而 WebSocket 则是一种新的协议。WebSocket诞生于 2008 年 6 月,在 2011 年 12 月成为 RFC6455 国际标准,并且WebSocket协议是一种专门为实时通信而设计的协议。所以对于实现即时通信而言,WebSocket 可以说是最佳选择。相对于上面几种方式,它具有下面几个优点:1)低延迟:WebSocket 通过保持持久连接,避免了HTTP短连接频繁地建立和关闭连接的开销,从而降低了延迟。2)双向通信:WebSocket 协议支持双向通信,客户端和服务器都可以向对方发送数据,从而实现更加灵活的通信方式。3)跨域支持:WebSocket 协议支持跨域通信,可以在不同源之间传输数据,从而支持更多种场景下的应用。4)更少的数据传输:WebSocket 协议支持二进制数据传输和数据压缩,可以减少数据传输的延迟和带宽消耗。说到底,上面提到了好几种方案,其实都可以在不同程度上实现数据的实时更新,但是它们跟本次需求中使用到的 IM 方案有什么关系呢?或者说 IM 究竟是个什么样的方案呢?下面的章节我们将先明确下 IM 的概念。

2.6 延伸阅读

因篇幅有限,本章节涉及到的技术无法为你深入介绍,如有兴趣可进一步阅读以下资料,夯实基础:新手入门贴:史上最全Web端即时通讯技术原理详解Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE详解Web端通信方式的演进:从Ajax、JSONP 到 SSE、Websocket网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket搞懂现代Web端即时通讯技术一文就够:WebSocket、socket.io、SSE

3、认识IM

3.1 IM 具体是指什么

即时通信(Instant Messaging,简称IM)是一种透过网络进行实时通信的系统,允许两人或多人使用网络即时的传递文字消息、文件、语音与视频交流。通常以网站、电脑软件或移动应用程序的方式提供服务(来自百科)。换句话说:我们只要采用某种方式,能实现两人或多人之间可以通过网络实时的交换信息,就可以称之为是一种 IM 方案。那么上面所提到的几种实现数据更新方式,都可以用做实现 IM 方案的底层实现方案。PS:如您还有疑问,可进一步阅读以下资料:知识科普:IM聊天应用是如何将消息发送给对方的?(非技术篇)零基础IM开发入门(一):什么是IM系统?

3.2 Web 端 IM 的发展历程

对于 Web 端 IM 的发展历程,其实大致都囊括了上面提到的几种实现方式;这些技术经过不断优化,持续提升了用户体验。其演变过程可以大致概括为从早期的轮询技术到长轮询,再发展到现代的 WebSocket、Server Push 的实现方式。而 WebSocket 的出现,则实现了更高效、更实时的即时通信。

图片

本次要实现多人打怪兽同步信息的场景,对数据更新的实时性要求非常高,所以本次需求所依赖的 IM 方案,就是基于更稳定的 WebSocket 实现的。所以下面就详细介绍下 WebSocket 和HTTP的区别,以及 WebSocket 的运作流程。

4、WebSocket 与 HTTP 到底是什么关系?

WebSocket 虽然是一种新的协议,但同 HTTP 协议一样,WebSocket 协议也是运行在 TCP 协议之上的,与 HTTP 协议同属于应用层网络数据传输协议。那 WebSocket 和 HTTP 究竟有哪些不一样呢?HTTP 属于短连接,每发起一次请求都需要建立一次连接,请求结束后立即关闭连接,属于“请求-响应模式”,即客户端需要主动发送请求才能获取到服务器返回的数据。即便是我们上面介绍的“长轮询”,也是需要依赖服务端来“hold”住请求。HTTP 是一种无状态协议,每个请求都是独立的,服务器不会保存客户端的状态信息。所以每次客户端发送请求,都会在请求头里塞一些类似于 Cookie 这种信息用来标识当前请求属于哪个用户。不同于 HTTP,WebSocket 协议中客户端和服务端只需要完成一次握手,两者之间就可以建立持久性的连接,并可以进行双向的数据传输。

图片

 PS:关于HTTP和WebSocket的区别可进一步阅读《WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)》)。

5、快速入门WebSocket

5.1 建立连接

Demo 跑起来看着是挺简单的,但 WebSocket 长链接到底是怎么建立的呢?在介绍连接建立之前,我们先来了解下 HTTP 协议请求头中 Upgrade 这么一个字段。HTTP 协议是一种文本协议,虽然其灵活性很高,但在处理大量数据和多媒体内容时效率较低。将协议升级为 WebSocket 或 HTTP/2 可以支持更多数据格式的传输;所以为了支持将协议升级,在 HTTP/1.1 中新增了 Upgrade 请求头,它允许客户端请求将其连接升级到另一个协议:Upgrade: <protocol>其中,protocol 表示希望升级到的协议名称,例如 WebSocket、HTTP/2 等。另外,Upgrade 头部还可以与 Connection 头部一起使用,以指示客户端希望使用持久连接。这可以减少每个请求的开销,从而提高网络性能和效率,要将协议升级为 WebSocket,就需要将这两个字段结合起来:Connection: UpgradeUpgrade: WebSocket这里我们了解到 WebSocket 协议是通过 HTTP 协议升级而来的,那么具体的长链接的生命周期是怎样的呢?下面是一个大致的 WebSocket 流程图:
图片
如上图,可以简单的将 WebSocket 的生命周期大致分为三个阶段:1)通过一次HTTP握手建立 WebSocket 长链接(也就是协议升级的过程);2)使用 WebSocket 协议进行数据传输;3)任意一方发送关闭帧,对方响应关闭帧后,长链接关闭。

5.2 握手请求

WebSocket 的建立是通过一次 HTTP 请求握手来实现的,客服端通过发送一个 GET 请求,并在 Request Header 里携带一些协议升级所需的参数,告诉服务器对本次 HTTP 请求进行升级。GET ws://localhost:3000/ HTTP/1.1Host: localhost:3000Connection: UpgradePragma: no-cacheCache-Control: no-cacheUpgrade: websocketSec-WebSocket-Version: 13Sec-WebSocket-Key: nFPKUyeo5Ul58tbe7Dg5lA==上面是一个 WebSocket 的请求快照,对于 Upgrade 字段上面已经介绍过,这里看下剩下的几个关键的参数:Sec-WebSocket-Key:是由客户端生成的一次性随机值,该值与服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如防止恶意或者无意的连接。Sec-WebSocket-Version:这里表明 WebSocket 协议的唯一可接受版本是13。

5.3 握手响应

一旦客户端发送了打开 WebSocket 连接的初始请求,它就会等待服务器的回复。该回复必须有一个 HTTP 101 切换协议的响应代码。HTTP 101 切换协议响应表明,服务器正在切换到客户端在其升级请求头中所指定的协议。同样的,在响应头里也会包括 Upgrade 字段,标识协议已被升级。HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: 89D1tEKizEJHFrVDhswIIpAf4ww=此外,响应头中的 Sec-WebSocket-Accept 是一个处理后的 base64 编码,是通过客户端请求头中的 Sec-WebSocket-Key 和 RFC6455 中定义的静态值 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接来生成的。计算步骤为:将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接通过 SHA1 计算出摘要,并转成 base64 字符串:
图片
通过这样一个HTTP的请求和响应,就表明长链接的握手过程已经完成,并将协议升级成了 WebSocket 协议,后续双方就可以通过这个长链接通道传输数据了。那数据具体又是怎样传输的呢?

5.4 数据传输

WebSocket 在传输数据的过程中,实际上会将大块数据(消息)分成若干帧进行传输,RFC6455 中给出了帧的概述。这里大致介绍下一些重要字段:
图片
FIN (Final):表示当前帧是否为最后一个片段;1 表示是消息的最后一个片段,0 表示不是消息的最后一个片段RSV1,  RSV2,  RSV3 (Reserved):扩展字段,各占 1 比特,一般情况全为 0opcode:每个帧都有一个操作码,这个操作码决定如何来解释这个帧的有效载荷数据。opcode操作码具体分为以下几个类型:
图片

5.5 链接关闭

要关闭 WebSocket 连接,发送端需要发送一个关闭帧(opcode 0x8)。如果连接的任何一方收到一个关闭帧,它必须发送一个关闭帧作为响应,一旦双方都收到了关闭帧,WebSocket 连接将会断开。以上就是WebSocket连接从建立到断开的全过程,如果你还想深入学习,可以继续阅读以下资料:WebSocket从入门到精通,半小时就够!WebSocket详解(六):刨根问底WebSocket与Socket的关系Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?

6、WebSocket协议升级代码实践

根据上面的 WebSocket 的流程描述,我们可以使用 Nodejs 实现一个简单版的协议升级逻辑,并使用浏览器 Api 实现对应的客户端逻辑。

6.1 服务端逻辑

以下是一个使用Node.js原生模块实现 WebSocket 服务端的例子:// 导入所需的Node.js原生模块const http = require('http');const crypto = require('crypto');// 创建HTTP服务器const server = http.createServer(); // 解析WebSocket帧function parseFrame(buffer) {    // 这里获取操作码(opcode),表示数据帧的类型    const opcode = buffer[0] & 0x0f;    // 这行代码获取负载长度(payload length)。它表示数据帧的实际数据长度。这里仅考虑了较短的数据长度,实际上可能需要处理更长的数据长度    const payloadLength = buffer[1] & 0x7f;    // 数据帧的数据部分(payload)是以掩码的形式发送的,需要使用掩码来解码。    const mask = buffer.slice(2, 6);    // 这里获取帧中的实际数据    const payload = buffer.slice(6);    let decodedPayload = '';    if (opcode === 1) { // 文本数据帧        for (let i = 0; i < payloadLength; i++) {            // 这里使用异或操作对数据字节与相应的掩码字节进行解码:payload[i] ^ mask[i % 4]。这里使用模运算(i % 4)确保在掩码的 4 个字节之间循环。            // 使用 String.fromCharCode() 将解码后的字节转换为字符,并将解码后的字符添加到 decodedPayload 字符串中。            decodedPayload += String.fromCharCode(payload[i] ^ mask[i % 4]);        }    } else if (opcode === 8) { // 关闭帧        return { type: 'close' };    }     return { type: 'text', data: decodedPayload };} // 根据给定的文本消息创建一个文本数据帧function createTextFrame(message) {    // 根据消息长度分配一个缓冲区。这里仅处理较短的消息,因此分配 2 个额外字节用于帧头    const buffer = Buffer.alloc(2 + message.length);    // 设置帧头的第一个字节。0x81 表示一个最终帧(FIN = 1)且操作码为文本(opcode = 1)    buffer[0] = 0x81;    // 设置帧头的第二个字节。这里仅处理较短的消息,所以直接将消息长度设置为负载长度。这意味着没有掩码(mask = 0)    buffer[1] = message.length;    // 将消息写入缓冲区。对于每个字符,获取其字符编码(Unicode 编码)并将其添加到缓冲区。    for (let i = 0; i < message.length; i++) {        buffer[i + 2] = message.charCodeAt(i);    }    return buffer;} // 向客户端发送消息function sendTextMessage(socket, message) {    const frame = createTextFrame(message);    socket.write(frame);} // 监听服务器的upgrade事件server.on('upgrade', (req, socket, head) => {    // 检查WebSocket协议和版本    if (req.headers['upgrade'] !== 'websocket' || req.headers['sec-websocket-version'] !== '13') {        socket.destroy();        return;    }     // 获取客户端发送的Sec-WebSocket-Key    const key = req.headers['sec-websocket-key'];     // 计算Sec-WebSocket-Accept    const sha1 = crypto.createHash('sha1');    sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');    const accept = sha1.digest('base64');     // 构建响应头    const headers = [        'HTTP/1.1 101 Switching Protocols',        'Upgrade: websocket',        'Connection: Upgrade',        'Sec-WebSocket-Accept: ' + accept,        '\r\n'    ];     // 发送响应头    socket.write(headers.join('\r\n'));     // 监听数据    socket.on('data', (buffer) => {        const frame = parseFrame(buffer);         if (frame.type === 'text') {            console.log('Received message:', frame.data);            // 在此处实现处理收到的文本消息            // 向客户端发送消息            sendTextMessage(socket, 'Hello, client!');        } else if (frame.type === 'close') {            console.log('Client closed the connection.');            socket.destroy();        }    });     // 监听关闭    socket.on('close', () => {        console.log('Socket has been closed.');    });}); server.listen(3000, () => {    console.log('WebSocket server listening on port 3000');});这里创建了一个基于 HTTP 的 WebSocket 服务,并监听 3000 端口,其中细节部分已经在代码里注释。当客户端发起升级请求时,服务器将在握手过程中验证客户端的请求,并在成功升级到WebSocket连接后监听来自客户端的数据同时发送一个 'Hello, client!' 作为回复。6.2 客户端逻辑下面是对应客户端逻辑,使用浏览器原生 Api WebSocket 来实现长链接的建立:<!DOCTYPE html><html><head>  <meta charset="UTF-8">  <title>WebSocket demo</title></head><body>  <h1>WebSocket demo</h1>  <input type="text" id="message">  <button>Send</button>   <button>End</button>  <div id="output"></div>  <script>    // 创建 WebSocket 连接    const ws = new WebSocket('ws://localhost:3000/');     // 监听消息    ws.onmessage = (event) => {      const output = document.getElementById('output');      output.innerHTML += <p>${event.data}</p>;    };     // 长链接断开    ws.onclose = () => {        const output = document.getElementById('output');        output.innerHTML += <p>WebSocket closed</p>;    }     // 发送消息    function sendMessage() {      const message = document.getElementById('message').value;      ws.send(message);    }     // 关闭长链接    function handleEnd() {      ws.close();    }  </script></body></html>在客户端创建一个 WebSocket 并连接到ws://localhost:3000服务器,这里注册了长链接的 onmessage 和 onclose 事件;在输入框里输入信息点击发送,会向服务端发送一个消息,服务端在接收到客户端消息时,紧接着会向客户端发送一个文本消息,同时在页面中点击 end 可以将长链接关闭。客户端:
图片
服务端:
图片
对于浏览器 WebSocket Api 除了常用回调 onclose、onmessage、 onerror、onopen,WebSocket 实例本身还有一些属性可以判断长链接当前的状态(如下图),详细参数可参考 MDN 中的详细介绍「WebSocket」。
图片

7、基于IM即时通信方案的最终落地

7.1 概述

上面我们使用 Nodejs 和浏览器 WebSocket Api 实现了一个简单的即时通信,但这还远远达不到可以在生产环境中使用的标准,比如涉及到网络异常、掉线重连、较大数据量处理、兼容性处理等问题。当然 WebSocket 有很多成熟的库可以直接使用,比如 Socket.IO、Ws,这些库都是经过广泛使用和测试的开源项目,具有良好的稳定性和可靠性。对于生产环境使用这些成熟的库可以减少很多不必要的麻烦。作为本次需求的 IM 方案,我们则选择使用的是由 云信 提供的 Web IM 即时通讯能力;改方案提供了包括服务端和 Web 端一套完整的方案,可以快速集成到我们的工程中,实现即时通信的能力。云信 Web SDK 提供了多种常见聊天场景,例如单聊、群聊、聊天室等。本次需求主要涉及多人场景,并且战斗不要求持久性,一场战斗由多人同时在线参与,并且在战斗结束后就解散,所以这里选择使用的是聊天室场景。

对于消息的流转如下:
图片

7.2 客户端集成

SDK整体方案的集成可参考云信官方网站,这里仅介绍下客户端的集成过程,和所需要注意的问题。对于客户端这里选择通过 npm 集成 SDK:npm install @yxim/nim-web-sdk@latestSDK 所包含的三个文件的说明如下:dist/SDK├── NIM_Web_Chatroom.js       提供聊天室功能,浏览器适配版(UMD 格式)├── NIM_Web_NIM.js       提供 IM 功能,包括单聊、会话和群聊等,但不包含聊天室。浏览器适配版(UMD 格式)├── NIM_Web_SDK.js       提供 IM 功能和聊天室功能的集成包,浏览器适配版(UMD 格式)这里使用的是聊天室能力,可通过单例模式初始化登陆聊天室,如下:import Chatroom from '@yxim/nim-web-sdk/dist/SDK/NIM_Web_Chatroom';export class InitChatRoom {  static async getRoomInstance({ onChatMsg = () => {} }) {    if (!InitChatRoom.instance) {        InitChatRoom.instance = Chatroom.getInstance({            appKey: 'appKey', // 在云信管理后台查看应用的 appKey            account: 'account', // 帐号, 应用内唯一            token: 'token', // 帐号的 token, 用于建立连接            chatroomId: 'chatroomId', // 聊天室 id            chatroomAddresses: [ // 聊天室地址列表              'address1',              'address2'            ],            onconnect: () => {}, // 长链接建立成功回调            onmsgs: (data) => {}, // 消息触达回调            ondisconnect: () => {}, // 长链接断开回调            onwillreconnect: () => {} // 长链接即将重连        });    }    return InitChatRoom.instance;  }}

7.3 消息的过滤

在本次需求中,怪兽血量和状态的更新主要是依赖消息的推送。另外活动是每天定点开放,所以活动开始时会有大量用户同时涌入参与攻击怪兽,在这种情况可能会造成消息在服务端的堆积,导致消息触达到客户端时不能保证消息是按产生的先后时间到达的。举个例子,比如 20:01 产生的消息,由于消息堆积可能会在 20:02 产生的消息后面到达,这样就可能会导致怪兽的血量忽大忽小的跳动;或者是怪兽已经死了,而怪兽掉血的消息才刚刚到达,此时就需要将这些过时的消息抛弃掉。处理方式也比较简单,就是针对每一个消息体都会添加一个消息产生的时间戳,通过这个时间戳可以将延迟触达的消息过滤掉。onmsgs: (data) => {       // 延迟消息的过滤,判断掉血消息的时间是否大于之前的消息时间    const { msgTime } = data; // 当前消息产生时间    const preTime = monster?.msgTime || 0; // 上一条消息时间    if (msgTime > preTime) {      monster.remainingHp = remainHp; // 更新怪兽剩余血量      monster.damage = damage;      monster.msgTime = msgTime;    }}

8、本文小结

本次需求也是首次在日常活动需求中使用 IM 方案,整体看起来也没有预期的那么复杂,总的来讲相对于之前常用的接口轮询的方式,会减少很多对服务端的压力,同时 IM 方案更新数据的及时性,也大幅提升了用户体验。项目稳定运行一年多,也验证了 IM 方案在日常需求中的可行性。

9、参考资料

[1] RFC6455 协议文档、WebSocket API文档、SSE API文档
[2] 新手入门贴:史上最全Web端即时通讯技术原理详解
[3] Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE
[4] 详解Web端通信方式的演进:从Ajax、JSONP 到 SSE、Websocket
[5] 网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket
[6] 搞懂现代Web端即时通讯技术一文就够:WebSocket、socket.io、SSE
[7] AI大模型爆火的SSE技术到底是什么?万字长文,一篇读懂SSE!
[8] WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)
[9] WebSocket详解(六):刨根问底WebSocket与Socket的关系
[10] Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?
[11] WebSocket从入门到精通,半小时就够!
[12] 理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性
[13] 微信团队分享:来看看微信十年前的IM消息收发架构,你做到了吗
[14] 零基础IM开发入门(一):什么是IM系统?
[15] 转转客服IM系统的WebSocket集群架构设计和部署方案
[16] 转转客服IM聊天系统背后的技术挑战和实践分享
[17] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等
[18] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等
[19] 转转平台IM系统架构设计与实践(一):整体架构设计
[20] 如何保障分布式IM聊天系统的消息有序性(即消息不乱)
[21] 脑残式网络编程入门(四):快速理解HTTP/2的服务器推送(Server Push)

即时通讯技术学习:

(本文已同步发布于:http://www.52im.net/thread-4896-1-1.html

📘论文标题:
Thermodynamic Simulation-assisted Random Forest: Towards explainable fault diagnosis of combustion chamber components of marine diesel engines

🔗论文链接ScienceDirect

💡原作者Blog(包含代码、数据集)

一、高可靠性系统对透明决策的需求

在轨道交通、航行安全等关键工业领域,现代系统正向复杂化与集成化演进,技术人员难以采信缺乏物理依据的诊断结果。船用柴油机作为核心动力设备,其设计具有极高的可靠性,导致实际运行中重大故障数据极度匮乏。此外,传统诊断方法在融合样本数据与物理模型,并合理解释特征决策过程方面存在显著局限。2025年《Measurement》期刊发表的研究提出了一种热力学仿真辅助随机森林(ThermoRF)方法。该方法在获取故障样本与实现结果可解释性之间寻求平衡,为复杂系统的智能运维提供了备选路径。

二、ThermoRF 架构:物理模型与数据驱动的融合

该研究构建了一个具备“模型数据协同”特点的诊断框架,主要包含以下核心阶段:

1.热力学建模与校准

构建柴油机一维热力学模型,用于模拟故障状态并生成训练数据。通过与真实航行数据校准,该模型参数偏差小于 5%,确保了仿真环境的可靠性。下图为柴油机的一维热力学模型。

图1 柴油机的一维热力学模型

2.高保真故障数据集生成

通过微调物理参数,模拟出包括气缸盖开裂(F1)、活塞烧蚀(F2)、缸套磨损(F3)、活塞环磨损(F4)及活塞环粘着(F5)在内的五种典型故障,解决了实际运维中故障数据匮乏的难题。下表为各故障状态细节。

3.基于 SHAP 理论的特征筛选

利用 SHAP(Shapley Additive Explanations)值量化 14 个热力学参数的重要性。最终精选出涡轮后排气温度(P14)、缸套壁热流(P05)等 8 个核心参数,组成优化特征子集以提升诊断效率。下图为基于SHAP的参数选择过程。

4.智能故障识别

将优化后的子集输入随机森林(RF)分类器。实验证明,RF 在该数据集上的平均准确率高达 99.07%,其表现优于 KNN 与 SVM 等分类算法。下图为 KNN、SVM 和 RF 的精度-召回率曲线图。

三、双尺度可解释故障诊断分析

该研究的核心价值在于使故障判定的逻辑透明化,实现了两个层面的可解释性分析:

1.局部尺度解释

采用瀑布图直观展示模型对单个样本的决策逻辑。例如,通过捕捉窜气热流(P06)与质量流量(P07)的特定异常,揭示模型如何根据物理规律判定特定故障。

2.全局尺度解释

利用蜂群图、交互图及依赖图从整体层面提炼核心指标。分析揭示了“窜气导致涡轮前排气压力(P11)降低”等深层物理联系,证明了数据特征与内燃机热力学机制的一致性。

下图为基于 SHAP 值的双尺度故障分析:(a)瀑布图;(b)蜂群图;(c)交互图;(d)依赖图。

四、结论与展望

ThermoRF 方法成功发挥了物理建模与数据驱动的互补优势,不仅提升了故障诊断的精度,更通过双尺度分析为诊断结果提供了物理支撑。这种可解释故障诊断模式,对于提升重大装备运维的安全感与科学性具有重要意义。

原始文献
C. Luo, M. Zhao, X. Fu, S. Zhong, S. Fu, K. Zhang, X. Yu. Thermodynamic simulation-assisted random forest: Towards explainable fault diagnosis of combustion chamber components of marine diesel engines[J]. Measurement, 2025, 251: 117252.

零、前言

各位 V 友,分享一款本人开发的 24 点解谜 APP——《 24 点 - 解得出的谜题》,它在 IOS 某付费榜中排第 9 名。
该 app 体验极佳,核心亮点很突出,适合碎片时间消遣、锻炼逻辑,附上 20 枚免费兑换码,先到先得~

一、全平台,全兼容

目前可在 App Store 、鸿蒙(后续将上架至安卓、MacOs 、Win 等)等平台上下载、使用。欢迎各位大佬支持&交流、提出建议或意见~

二、整体概览图

整体概览图

三、核心功能(不冗余,直击重点)

  1. 题库丰富:数千道题目,覆盖不同难度,从新手到高手都能找到适配的题目,无需额外付费解锁题库。
    题库数千:主打 [乐在其中,无法自拔]

  2. 模式多样:包含竞速、随机、收藏三种核心模式,可按需选择,兼顾趣味性和挑战性。
    多种模式,趣味无穷:竞速、随机、收藏,应有尽有

  3. 功能灵活:支持高度自定义,可根据个人习惯调整游戏设置,无强制引导,体验流畅。
    功能丰富:你的游戏,你做主

  4. 求解便捷:内置智能求解模式,遇到卡壳的题目可快速获取灵感,不影响解题体验,无卡顿。
    求解模式:灵感,不再卡顿

四、适合场景

通勤、摸鱼、睡前碎片化时间,无需复杂操作,打开就能玩,既能解压,也能悄悄锻炼数学逻辑。

五、兑换码

兑完请在评论区告知,方便其他 V 友:

LHRAPWRJRA6X

K7WH9E4MH7RW

WA63YP637PTA

N7HE76XYMRFR

AFT9LL3FYM4W

YJ7TM7PKJ9YW

HHF4HPNRF4XK

APAWWL4FFNR3

34Y64WMXR6XP

RP4YFFTJ4FWE

1. 前言

当前端到端智能驾驶技术发展迅速,SparseDrive 作为代表性模型受行业关注。工程化落地时,其模型导出与性能评测环节存在普遍技术挑战,涉及架构与环境兼容性、算子适配等多维度。为推动端到端智驾技术社区化发展,本文梳理 SparseDrive 从 ONNX 导出到硬件部署的技术链路,剖析算子替换、编译报错修复、量化策略优化等案例,构建含环境配置、数据集处理、权重管理、配置工程化的全流程技术指南,为社区提供可复用的端到端模型工程化方案,加速智驾模型从研究到车规级部署转化。

代码库:https://github.com/swc-17/SparseDrive

2. 环境部署

解压公版代码包,然后创建 python 虚拟环境:

conda create -n sparsedrive python=3.8 -y
conda activate sparsedrive
pip3 install --upgrade pip
#whl包获取:
curl -O -u 'openexplorer:c5R,2!pG' ftp://vrftp.horizon.ai/misc_j5/torch/torch-1.13.0+cu116-cp38-cp38-linux_x86_64.whl
curl -O -u 'openexplorer:c5R,2!pG' ftp://vrftp.horizon.ai/misc_j5/torch/torchvision-0.14.0+cu116-cp38-cp38-linux_x86_64.whl
pip3 install torch-1.13.0+cu116-cp38-cp38-linux_x86_64.whl
pip3 install torchvision-0.14.0+cu116-cp38-cp38-linux_x86_64.whl 
pip3 install torchaudio==0.13.0
cd ~/SparseDrive-main

直接 pip3 install -r requirement.txt 会报错,这里打算逐个安装 whl 包。

2.1 升级 gcc(for 安装 mmcv-full)

步骤 1:安装新版 GCC/G++

使用 conda 安装,不会破坏系统自带的 GCC 4.8.5:

安装 GCC 10

conda install  -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main  -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge  gcc_linux-64=10 gxx_linux-64=10

安装完成后,你会在 Conda 环境里有新 GCC,例如:

$ which gcc

/home/users/yue01.chen/anaconda3/envs/sparsedrive/bin/x86\_64-conda-linux-gnu-gcc

步骤 2:指定编译器环境变量

为了确保 pip 编译 mmcv-full 时使用 Conda 的新版 GCC,而不是系统 4.8.5,需要设置环境变量:

export CC=$(which x86\_64-conda-linux-gnu-gcc) export CXX=$(which x86\_64-conda-linux-gnu-g++)

可以把这两行添加到 。bashrc 或 。zshrc 中,保证每次激活环境自动生效。

步骤 3:卸载旧的 mmcv/mmcv-full

pip uninstall mmcv mmcv-full -y

步骤 4:从源码编译 mmcv-full

使用 --no-binary 强制从源码编译:

pip install mmcv-full==1.7.1 --force-reinstall --no-cache-dir --no-binary mmcv-full

说明:

  • --no-binary mmcv-full 表示不使用预编译 wheel,直接编译 C++/CUDA 扩展。
  • --force-reinstall + --no-cache-dir 可以避免 pip 缓存的旧版本干扰。

步骤 5:验证安装

Python 中验证 mmcv-full GPU 扩展是否可用:

import mmcv from mmcv.ops import nms\_match print("mmcv-full GPU extensions are ready!")

  1. 如果报错 ModuleNotFoundError: No module named 'mmcv。\_ext',说明编译仍有问题,需要检查:
  • GCC 版本 ≥ 7
  • CUDA 环境变量 CUDA\_HOME 是否指向 /home/users/yue01.chen/cuda-11.8
  • nvcc 可用 (nvcc --version)

后续在运行中缺乏什么库就直接 pip3 install 即可。

3. 创建数据集与权重下载

3.1 生成 pkl

  1. 从官网下载 nuscenes 数据集,解压后把 expansion 文件夹放到 maps 下,
  2. 然后运行:
sh scripts/create_data.sh

代码运行完成会在 data/info 目录下生成:

├── data
│   ├── infos
│   │   ├── mini
│   │   ├── nuscenes_infos_train.pkl
│   │   └── nuscenes_infos_val.pkl

报错的时候把这个注释了:

报错的时候把这个注释了:

img

3.2 生成 kmeans.py

sh scripts/kmeans.sh

3.3 权重下载

https://github.com/swc-17/SparseDrive/releases/download/v1.0/sparsedrive\_stage1.pth https://github.com/swc-17/SparseDrive/releases/download/v1.0/sparsedrive\_stage2.pth

https://download.pytorch.org/models/resnet50-19c8e357.pth

下载完成后放在 ckpt 文件夹。

4. config 文件修改

#单卡单batch
total_batch_size = 1
num_gpus = 1
#使用pytorch实现的dfa
use_deformable_func = False  # mmdet3d_plugin/ops/setup.py needs to be executed
#导出的onnx不要with_motion_plan,因为跑验证集的时候发现这部分跑不通
task_config = dict(
    with_det=True,
    with_map=True,
    with_motion_plan=False,

另外,还有非常重要的一点,config 文件中的 MultiheadFlashAttention 都替换为普通的 MultiheadAttention。

5. 导出脚本和适配修改

导出思路:为了不大幅侵入源码,在导出脚本里重写了 forward,并增加环境变量进行控制

5.1 去除后处理

使用环境变量 my\_var=="export\_to\_onnx"进行控制:

img

5.2 重写 forward

在 tools 文件夹下构建 forward\_export.py,重写 sparsedrive、det\_head 和 map\_head 的 orward 函数,如下所示:

from typing import List, Optional, Tuple, Union
import warnings
​
import numpy as np
import torch
import torch.nn as nn
#为了适配输入的形式和时序输入,重写了"SparseDrive" 类的forward
def simple_test_onnx_wrapper(self, img, T_global, T_global_inv, timestamp, projection_mat, image_wh, ego_status,cached_anchor,cached_feature,mask,cached_confidence,cached_map_anchor,cached_map_feature,cached_map_confidence):
    data = {
        "img_metas": [{

5.3 self.instance\_bank.get\_for\_export\_det\_onnx()函数

路径:SR/12yuanrong/SparseDrive-main/projects/mmdet3d\_plugin/models/instance\_bank.py

def get_for_export_det_onnx(self, batch_size, metas=None, dn_metas=None):
        instance_feature = self.instance_feature.unsqueeze(0).repeat(batch_size, 1, 1)
        anchor = self.anchor.unsqueeze(0).repeat(batch_size, 1, 1)  
        #从上一帧的时序输出中获取输入 
        cached_anchor = metas["cached_anchor"]
        cached_feature = metas["cached_feature"]
        self.mask = metas["mask"]
        self.confidence = metas["cached_confidence"]
        time_interval=metas["img_metas"][0]["timestamp"]

5.4 self.instance\_bank.get\_for\_export\_map\_onnx()函数

路径:SparseDrive-main/projects/mmdet3d\_plugin/models/instance\_bank.py

def get_for_export_map_onnx(self, batch_size, metas=None, dn_metas=None):
        instance_feature = self.instance_feature.unsqueeze(0).repeat(batch_size, 1, 1)
        anchor = self.anchor.unsqueeze(0).repeat(batch_size, 1, 1)   
        cached_anchor = metas["cached_map_anchor"]
        cached_feature = metas["cached_map_feature"]
        self.mask = metas["mask"]
        self.confidence = metas["cached_map_confidence"]
        time_interval=metas["img_metas"][0]["timestamp"]
​
        return (

5.5 修改导出会报错的代码

报错 1

img

将 instance\_inds 修改为 np.int32 类型。

报错 2

报错:

traceback : Traceback (most recent call last):
    File "/home/users/yue01.chen/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/distributed/elastic/multiprocessing/errors/__init__.py", line 346, in wrapper
      return f(*args, **kwargs)
    File "./tools/export_onnx.py", line 314, in main
      torch.onnx.export(
    File "/home/users/yue01.chen/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 504, in export
      _export(
    File "/home/users/yue01.chen/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1529, in _export
      graph, params_dict, torch_out = _model_to_graph(
    File "/home/users/yue01.chen/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1115, in _model_to_graph

报错原因:

PyTorch aten::tile 运算符在 ONNX opset 17 中没有对应的实现,所以导出失败。

解决办法

把 torch.tile 替换成等价的 repeat

在 PyTorch 里,torch.tile 其实就是 repeat 的一个封装,功能等价。 而 repeat ONNX 里是受支持的(映射到 Repeat 节点)

解决办法:

把 self.instance\_bank.get\_for\_export\_det\_onnx()和 self.instance\_bank.get\_for\_export\_map\_onnx()函数中的

instance_feature = torch.tile(
            self.instance_feature[None], (batch_size, 1, 1)
        )
anchor = torch.tile(self.anchor[None], (batch_size, 1, 1))

修改成 repeat 实现,如下:

instance_feature = self.instance_feature.unsqueeze(0).repeat(batch_size, 1, 1)
anchor = self.anchor.unsqueeze(0).repeat(batch_size, 1, 1)

报错 3(重要)

报错截图:

File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 504, in export     _export(   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1529, in _export     graph, params_dict, torch_out = _model_to_graph(   File "/home/users/naconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1115, in _model_to_graph     graph = _optimize_graph(   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 663, in _optimize_graph     graph = _C._jit_pass_onnx(graph, operator_export_type)   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1867, in _run_symbolic_function     return symbolic_fn(graph_context, *inputs, **attrs)   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/symbolic_opset9.py", line 6664, in onnx_placeholder     return torch._C._jit_onnx_convert_pattern_from_subblock(block, node, env)   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1867, in _run_symbolic_function     return symbolic_fn(graph_context, *inputs, **attrs)   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/symbolic_opset11.py", line 230, in index_put     if symbolic_helper._is_bool(indices_list[idx_]):   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/symbolic_helper.py", line 736, in _is_bool     return _is_in_type_group(value, {_type_utils.JitScalarType.BOOL})   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/symbolic_helper.py", line 708, in _is_in_type_group     scalar_type = value.type().scalarType() RuntimeError: r INTERNAL ASSERT FAILED at "../aten/src/ATen/core/jit_type_base.h":547, please report a bug to PyTorch.

报错原因:

ONNX 导出失败的根因是图里某处会把一个标量常量以没有 dtype(即 None) 的形式传给了 ONNX 导出器,导致 torch.onnx。\_type\_utils.JitScalarType.from\_name 收到 None 并抛出 ValueError: Scalar type name cannot be None。这类情况常在用高级索引/原地赋值(tensor[index] = other、index\_put、masked\_scatter 等)时出现,导出器有时会把标量常量漏掉 dtype。

优先级修复建议(按顺序尝试)

  1. 定位问题代码:查找模型中类似 x[:, idx] = y、x[index] = y、index\_put、masked\_scatter、masked\_fill 的用法。也可在 torch.onnx.export(..., verbose=True) 打印的导出图里查找 aten::index\_put、index\_put、prim::ListConstruct 等节点位置。
  2. 把原地/索引赋值改写为 ONNX 友好的算子:常用替代方法:

    1. 用 scatter:
    2. x = x.clone() x = x.scatter(dim, indices.unsqueeze(-1).expand(...), y)
    3. 用布尔 mask + torch.where:
    4. mask = torch.zeros\_like(x, dtype=torch.bool) mask[:, indices] = True x = torch.where(mask, y\_broadcasted, x)
  3. 这两种通常能被 ONNX 导出器更好地支持。
  4. 确保传入 torch.onnx.export 的示例输入都有明确 dtype(不要传 None 或 Python 原始标量),例如 tensor.float(). cuda()、indices.long().cuda()。
  5. 尝试不同的 opset 或更新 PyTorch:有些导出器 bug 在较新 opset 或 PyTorch 版本里被修复。可试 opset\_version=12、14 等;若可行,升级 PyTorch 往往能解决这类问题。
  6. 临时回退方案:如果短时间无法改模型,可使用 ATen fallback(operator\_export\_type=OperatorExportTypes.ONNX\_ATEN\_FALLBACK)导出,得到包含 ATen 节点的 ONNX(不适合生产但便于调试)。
  7. 不要修改 site-packages(除非非常了解风险):虽然可以在 \_type\_utils.from\_name 做防守性修改防止报错,但这不是长期或推荐的做法。

通过二分法定位到是 refine 模块的报错(即在 refine 模块前 return 导出 onnx 不报错,经过 refine 层以后 return 会报错),然后逐渐定位到其中的这个部分触发了上述 1 中的错误,如下:

output[..., self.refine_state] = (
#     output[..., self.refine_state] + anchor[..., self.refine_state]
# )
# if self.normalize_yaw:
#     output[..., [SIN_YAW, COS_YAW]] = torch.nn.functional.normalize(
#         output[..., [SIN_YAW, COS_YAW]], dim=-1
#     )
# if self.output_dim > 8:
#     if not isinstance(time_interval, torch.Tensor):
#         time_interval = instance_feature.new_tensor(time_interval)

修改后的代码:

@PLUGIN_LAYERS.register_module()
class SparseBox3DRefinementModule(BaseModule):
    def __init__(
        self,
        embed_dims=256,
        output_dim=11,
        num_cls=10,
        normalize_yaw=False,
        refine_yaw=False,
        with_cls_branch=True,

5.6 scatternd 消除

由于征程 6 工具链目前只支持 CPU 实现的 scatternd,所以在导出 onnx 的时候把这部分替换成 slice+concat 的实现。

路径:SparseDrive-main/projects/mmdet3d\_plugin/models/detection3d/detection3d\_blocks.py

def forward(
        self,
        anchor,
        instance_feature=None,
        T_cur2temp_list=None,
        cur_timestamp=None,
        temp_timestamps=None,
    ):
        bs, num_anchor = anchor.shape[:2]
        size = anchor[..., None, [W, L, H]].exp()

5.7 导出代码

导出脚本 export\_onnx.py 基于 SparseDrive-main/tools/test.py 进行编写,其具体实现如下:

# Copyright (c) OpenMMLab. All rights reserved.
import argparse
import mmcv
import os
from os import path as osp
​
import torch
import warnings
from mmcv import Config, DictAction
from mmcv.cnn import fuse_conv_bn

另外,需要对 tools/dist\_test.sh 进行修改如下;

img

导出脚本运行:

bash scripts/test.sh

5.9 cache 过程的 scatternd 和 Cast 算子消除(如果模型中存在 cache 过程的话)

如果想要在模型中增加输出 cache 的功能,即在 forward\_export.py 的函数中增加以下代码:

img

但是公版的 self.instance\_bank.cache()函数的写法会引入工具链只能在 CPU 上支持的 ScatterND 算子和 Cast 算子,所以这里需要对代码做两处适配。

5.9.1 消除 scatternd 算子:

路径:SparseDrive-main/projects/mmdet3d\_plugin/models/instance\_bank.py 中的 cache 函数:

if self.confidence is not None:
    # confidence[:, : self.num_temp_instances] = torch.maximum(
    #     self.confidence[0] * self.confidence_decay,
    #     confidence[:, : self.num_temp_instances],
    # )
    left = torch.maximum(
        self.confidence[0] * self.confidence_decay,
        confidence[:, :self.num_temp_instances],)
    right = confidence[:, self.num_temp_instances:]
    confidence = torch.cat([left, right], dim=1)

5.9.2 消除 cast 算子:

路径:SparseDrive-main/projects/mmdet3d\_plugin/models/instance\_bank.py 中的 topk 函数:

def topk(confidence, k, *inputs):
    # bs, N = confidence.shape[:2]
    # confidence, indices = torch.topk(confidence, k, dim=1)
    # indices = (
    #     indices + torch.arange(bs, device=indices.device)[:, None] * N
    # ).reshape(-1)
    # outputs = []
    # for input in inputs:
    #     outputs.append(input.flatten(end_dim=1)[indices].reshape(bs, k, -1))
    bs, N = confidence.shape[:2]

6. 性能评测

6.1 算子支持情况

  1. nash-p 下可以编译成功
  2. 修改模型后,所有算子支持 BPU 实现
b30.binary_eltwise  : 2071
 b30.conv2d          : 503
 b30.gather2d        : 10
 b30.lut             : 314
 b30.pool2d          : 1
 b30.reduce          : 528
 b30.resize2d        : 3
 b30.warp            : 48
 b30vpu.dequantize   : 9
 b30vpu.quantize     : 8

6.2 静态 per 性能分析

6.2.1 确定性能瓶颈

获取到 perf.html 和 perf.json 后,使用【新版 perf 文件解读与性能分析】附录中的脚本对性能进行分析,输入为 perf.json,输出如下所示:

按照算子类型统计的耗时:

img

耗时排名 TOP20 的算子:

img

根据以上信息,可以得出优化目标:

  1. Mul 和 ReduceSum 算子的耗时最久,而且 mul 算子 ddr 耗时超过计算耗时的 65%,引发了带宽问题;
  2. ToP12 耗时的算子就是 Mul 和 ReduceSum,所以重点是优化 Mul 和 ReduceSum 算子。

6.2.2 性能优化策略

查看模型结构发现,模型中耗时的 Mul 和 ReduceSum 都处于这样的子结构中,所以我们主要是对这个结构进行性能优化。

img

此结构主要由 Mul、ReduceSum 和数据搬运算子组成,一方面 MulReduceSum 是运行在专门做向量计算的 VAE,加速效果不如张量,另一方面输入的 shape 非常大,也就解释了为何会引发带宽问题。、

所以这里考虑将 Mul+ReduceSum 计算替换为等价的 Mamtmul,从而使得这部分计算在 VAE 上加速。

性能优化效果验证

这里主要有以下步骤:

  1. 替换为 Matmul 计算:根据上述子图结构将其替换为 Matmul 计算,并导出 optimized.onnx;
  2. 替换等价性验证:在原始 onnx 中提取上述子图,和 optimized.onnx 进行输出一致性验证;
  3. 性能评测:同时对原始 onnx 子图和 optimized.onnx 进行 fast-perf,验证性能收益。

上述步骤可以参考:https://developer.horizon.auto/blog/13065

1. 前言

当前端到端智能驾驶技术发展迅速,SparseDrive 作为代表性模型受行业关注。工程化落地时,其模型导出与性能评测环节存在普遍技术挑战,涉及架构与环境兼容性、算子适配等多维度。为推动端到端智驾技术社区化发展,本文梳理 SparseDrive 从 ONNX 导出到硬件部署的技术链路,剖析算子替换、编译报错修复、量化策略优化等案例,构建含环境配置、数据集处理、权重管理、配置工程化的全流程技术指南,为社区提供可复用的端到端模型工程化方案,加速智驾模型从研究到车规级部署转化。

代码库:https://github.com/swc-17/SparseDrive

2. 环境部署

解压公版代码包,然后创建 python 虚拟环境:

conda create -n sparsedrive python=3.8 -y
conda activate sparsedrive
pip3 install --upgrade pip
#whl包获取:
curl -O -u 'openexplorer:c5R,2!pG' ftp://vrftp.horizon.ai/misc_j5/torch/torch-1.13.0+cu116-cp38-cp38-linux_x86_64.whl
curl -O -u 'openexplorer:c5R,2!pG' ftp://vrftp.horizon.ai/misc_j5/torch/torchvision-0.14.0+cu116-cp38-cp38-linux_x86_64.whl
pip3 install torch-1.13.0+cu116-cp38-cp38-linux_x86_64.whl
pip3 install torchvision-0.14.0+cu116-cp38-cp38-linux_x86_64.whl 
pip3 install torchaudio==0.13.0
cd ~/SparseDrive-main

直接 pip3 install -r requirement.txt 会报错,这里打算逐个安装 whl 包。

2.1 升级 gcc(for 安装 mmcv-full)

步骤 1:安装新版 GCC/G++

使用 conda 安装,不会破坏系统自带的 GCC 4.8.5:

安装 GCC 10

conda install  -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main  -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge  gcc_linux-64=10 gxx_linux-64=10

安装完成后,你会在 Conda 环境里有新 GCC,例如:

$ which gcc

/home/users/yue01.chen/anaconda3/envs/sparsedrive/bin/x86\_64-conda-linux-gnu-gcc

步骤 2:指定编译器环境变量

为了确保 pip 编译 mmcv-full 时使用 Conda 的新版 GCC,而不是系统 4.8.5,需要设置环境变量:

export CC=$(which x86\_64-conda-linux-gnu-gcc) export CXX=$(which x86\_64-conda-linux-gnu-g++)

可以把这两行添加到 。bashrc 或 。zshrc 中,保证每次激活环境自动生效。

步骤 3:卸载旧的 mmcv/mmcv-full

pip uninstall mmcv mmcv-full -y

步骤 4:从源码编译 mmcv-full

使用 --no-binary 强制从源码编译:

pip install mmcv-full==1.7.1 --force-reinstall --no-cache-dir --no-binary mmcv-full

说明:

  • --no-binary mmcv-full 表示不使用预编译 wheel,直接编译 C++/CUDA 扩展。
  • --force-reinstall + --no-cache-dir 可以避免 pip 缓存的旧版本干扰。

步骤 5:验证安装

Python 中验证 mmcv-full GPU 扩展是否可用:

import mmcv from mmcv.ops import nms\_match print("mmcv-full GPU extensions are ready!")

  1. 如果报错 ModuleNotFoundError: No module named 'mmcv。\_ext',说明编译仍有问题,需要检查:
  • GCC 版本 ≥ 7
  • CUDA 环境变量 CUDA\_HOME 是否指向 /home/users/yue01.chen/cuda-11.8
  • nvcc 可用 (nvcc --version)

后续在运行中缺乏什么库就直接 pip3 install 即可。

3. 创建数据集与权重下载

3.1 生成 pkl

  1. 从官网下载 nuscenes 数据集,解压后把 expansion 文件夹放到 maps 下,
  2. 然后运行:
sh scripts/create_data.sh

代码运行完成会在 data/info 目录下生成:

├── data
│   ├── infos
│   │   ├── mini
│   │   ├── nuscenes_infos_train.pkl
│   │   └── nuscenes_infos_val.pkl

报错的时候把这个注释了:

报错的时候把这个注释了:

img

3.2 生成 kmeans.py

sh scripts/kmeans.sh

3.3 权重下载

https://github.com/swc-17/SparseDrive/releases/download/v1.0/sparsedrive\_stage1.pth https://github.com/swc-17/SparseDrive/releases/download/v1.0/sparsedrive\_stage2.pth

https://download.pytorch.org/models/resnet50-19c8e357.pth

下载完成后放在 ckpt 文件夹。

4. config 文件修改

#单卡单batch
total_batch_size = 1
num_gpus = 1
#使用pytorch实现的dfa
use_deformable_func = False  # mmdet3d_plugin/ops/setup.py needs to be executed
#导出的onnx不要with_motion_plan,因为跑验证集的时候发现这部分跑不通
task_config = dict(
    with_det=True,
    with_map=True,
    with_motion_plan=False,

另外,还有非常重要的一点,config 文件中的 MultiheadFlashAttention 都替换为普通的 MultiheadAttention。

5. 导出脚本和适配修改

导出思路:为了不大幅侵入源码,在导出脚本里重写了 forward,并增加环境变量进行控制

5.1 去除后处理

使用环境变量 my\_var=="export\_to\_onnx"进行控制:

img

5.2 重写 forward

在 tools 文件夹下构建 forward\_export.py,重写 sparsedrive、det\_head 和 map\_head 的 orward 函数,如下所示:

from typing import List, Optional, Tuple, Union
import warnings
​
import numpy as np
import torch
import torch.nn as nn
#为了适配输入的形式和时序输入,重写了"SparseDrive" 类的forward
def simple_test_onnx_wrapper(self, img, T_global, T_global_inv, timestamp, projection_mat, image_wh, ego_status,cached_anchor,cached_feature,mask,cached_confidence,cached_map_anchor,cached_map_feature,cached_map_confidence):
    data = {
        "img_metas": [{

5.3 self.instance\_bank.get\_for\_export\_det\_onnx()函数

路径:SR/12yuanrong/SparseDrive-main/projects/mmdet3d\_plugin/models/instance\_bank.py

def get_for_export_det_onnx(self, batch_size, metas=None, dn_metas=None):
        instance_feature = self.instance_feature.unsqueeze(0).repeat(batch_size, 1, 1)
        anchor = self.anchor.unsqueeze(0).repeat(batch_size, 1, 1)  
        #从上一帧的时序输出中获取输入 
        cached_anchor = metas["cached_anchor"]
        cached_feature = metas["cached_feature"]
        self.mask = metas["mask"]
        self.confidence = metas["cached_confidence"]
        time_interval=metas["img_metas"][0]["timestamp"]

5.4 self.instance\_bank.get\_for\_export\_map\_onnx()函数

路径:SparseDrive-main/projects/mmdet3d\_plugin/models/instance\_bank.py

def get_for_export_map_onnx(self, batch_size, metas=None, dn_metas=None):
        instance_feature = self.instance_feature.unsqueeze(0).repeat(batch_size, 1, 1)
        anchor = self.anchor.unsqueeze(0).repeat(batch_size, 1, 1)   
        cached_anchor = metas["cached_map_anchor"]
        cached_feature = metas["cached_map_feature"]
        self.mask = metas["mask"]
        self.confidence = metas["cached_map_confidence"]
        time_interval=metas["img_metas"][0]["timestamp"]
​
        return (

5.5 修改导出会报错的代码

报错 1

img

将 instance\_inds 修改为 np.int32 类型。

报错 2

报错:

traceback : Traceback (most recent call last):
    File "/home/users/yue01.chen/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/distributed/elastic/multiprocessing/errors/__init__.py", line 346, in wrapper
      return f(*args, **kwargs)
    File "./tools/export_onnx.py", line 314, in main
      torch.onnx.export(
    File "/home/users/yue01.chen/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 504, in export
      _export(
    File "/home/users/yue01.chen/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1529, in _export
      graph, params_dict, torch_out = _model_to_graph(
    File "/home/users/yue01.chen/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1115, in _model_to_graph

报错原因:

PyTorch aten::tile 运算符在 ONNX opset 17 中没有对应的实现,所以导出失败。

解决办法

把 torch.tile 替换成等价的 repeat

在 PyTorch 里,torch.tile 其实就是 repeat 的一个封装,功能等价。 而 repeat ONNX 里是受支持的(映射到 Repeat 节点)

解决办法:

把 self.instance\_bank.get\_for\_export\_det\_onnx()和 self.instance\_bank.get\_for\_export\_map\_onnx()函数中的

instance_feature = torch.tile(
            self.instance_feature[None], (batch_size, 1, 1)
        )
anchor = torch.tile(self.anchor[None], (batch_size, 1, 1))

修改成 repeat 实现,如下:

instance_feature = self.instance_feature.unsqueeze(0).repeat(batch_size, 1, 1)
anchor = self.anchor.unsqueeze(0).repeat(batch_size, 1, 1)

报错 3(重要)

报错截图:

File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 504, in export     _export(   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1529, in _export     graph, params_dict, torch_out = _model_to_graph(   File "/home/users/naconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1115, in _model_to_graph     graph = _optimize_graph(   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 663, in _optimize_graph     graph = _C._jit_pass_onnx(graph, operator_export_type)   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1867, in _run_symbolic_function     return symbolic_fn(graph_context, *inputs, **attrs)   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/symbolic_opset9.py", line 6664, in onnx_placeholder     return torch._C._jit_onnx_convert_pattern_from_subblock(block, node, env)   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/utils.py", line 1867, in _run_symbolic_function     return symbolic_fn(graph_context, *inputs, **attrs)   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/symbolic_opset11.py", line 230, in index_put     if symbolic_helper._is_bool(indices_list[idx_]):   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/symbolic_helper.py", line 736, in _is_bool     return _is_in_type_group(value, {_type_utils.JitScalarType.BOOL})   File "/home/users/anaconda3/envs/sparsedrive/lib/python3.8/site-packages/torch/onnx/symbolic_helper.py", line 708, in _is_in_type_group     scalar_type = value.type().scalarType() RuntimeError: r INTERNAL ASSERT FAILED at "../aten/src/ATen/core/jit_type_base.h":547, please report a bug to PyTorch.

报错原因:

ONNX 导出失败的根因是图里某处会把一个标量常量以没有 dtype(即 None) 的形式传给了 ONNX 导出器,导致 torch.onnx。\_type\_utils.JitScalarType.from\_name 收到 None 并抛出 ValueError: Scalar type name cannot be None。这类情况常在用高级索引/原地赋值(tensor[index] = other、index\_put、masked\_scatter 等)时出现,导出器有时会把标量常量漏掉 dtype。

优先级修复建议(按顺序尝试)

  1. 定位问题代码:查找模型中类似 x[:, idx] = y、x[index] = y、index\_put、masked\_scatter、masked\_fill 的用法。也可在 torch.onnx.export(..., verbose=True) 打印的导出图里查找 aten::index\_put、index\_put、prim::ListConstruct 等节点位置。
  2. 把原地/索引赋值改写为 ONNX 友好的算子:常用替代方法:

    1. 用 scatter:
    2. x = x.clone() x = x.scatter(dim, indices.unsqueeze(-1).expand(...), y)
    3. 用布尔 mask + torch.where:
    4. mask = torch.zeros\_like(x, dtype=torch.bool) mask[:, indices] = True x = torch.where(mask, y\_broadcasted, x)
  3. 这两种通常能被 ONNX 导出器更好地支持。
  4. 确保传入 torch.onnx.export 的示例输入都有明确 dtype(不要传 None 或 Python 原始标量),例如 tensor.float(). cuda()、indices.long().cuda()。
  5. 尝试不同的 opset 或更新 PyTorch:有些导出器 bug 在较新 opset 或 PyTorch 版本里被修复。可试 opset\_version=12、14 等;若可行,升级 PyTorch 往往能解决这类问题。
  6. 临时回退方案:如果短时间无法改模型,可使用 ATen fallback(operator\_export\_type=OperatorExportTypes.ONNX\_ATEN\_FALLBACK)导出,得到包含 ATen 节点的 ONNX(不适合生产但便于调试)。
  7. 不要修改 site-packages(除非非常了解风险):虽然可以在 \_type\_utils.from\_name 做防守性修改防止报错,但这不是长期或推荐的做法。

通过二分法定位到是 refine 模块的报错(即在 refine 模块前 return 导出 onnx 不报错,经过 refine 层以后 return 会报错),然后逐渐定位到其中的这个部分触发了上述 1 中的错误,如下:

output[..., self.refine_state] = (
#     output[..., self.refine_state] + anchor[..., self.refine_state]
# )
# if self.normalize_yaw:
#     output[..., [SIN_YAW, COS_YAW]] = torch.nn.functional.normalize(
#         output[..., [SIN_YAW, COS_YAW]], dim=-1
#     )
# if self.output_dim > 8:
#     if not isinstance(time_interval, torch.Tensor):
#         time_interval = instance_feature.new_tensor(time_interval)

修改后的代码:

@PLUGIN_LAYERS.register_module()
class SparseBox3DRefinementModule(BaseModule):
    def __init__(
        self,
        embed_dims=256,
        output_dim=11,
        num_cls=10,
        normalize_yaw=False,
        refine_yaw=False,
        with_cls_branch=True,

5.6 scatternd 消除

由于征程 6 工具链目前只支持 CPU 实现的 scatternd,所以在导出 onnx 的时候把这部分替换成 slice+concat 的实现。

路径:SparseDrive-main/projects/mmdet3d\_plugin/models/detection3d/detection3d\_blocks.py

def forward(
        self,
        anchor,
        instance_feature=None,
        T_cur2temp_list=None,
        cur_timestamp=None,
        temp_timestamps=None,
    ):
        bs, num_anchor = anchor.shape[:2]
        size = anchor[..., None, [W, L, H]].exp()

5.7 导出代码

导出脚本 export\_onnx.py 基于 SparseDrive-main/tools/test.py 进行编写,其具体实现如下:

# Copyright (c) OpenMMLab. All rights reserved.
import argparse
import mmcv
import os
from os import path as osp
​
import torch
import warnings
from mmcv import Config, DictAction
from mmcv.cnn import fuse_conv_bn

另外,需要对 tools/dist\_test.sh 进行修改如下;

img

导出脚本运行:

bash scripts/test.sh

5.9 cache 过程的 scatternd 和 Cast 算子消除(如果模型中存在 cache 过程的话)

如果想要在模型中增加输出 cache 的功能,即在 forward\_export.py 的函数中增加以下代码:

img

但是公版的 self.instance\_bank.cache()函数的写法会引入工具链只能在 CPU 上支持的 ScatterND 算子和 Cast 算子,所以这里需要对代码做两处适配。

5.9.1 消除 scatternd 算子:

路径:SparseDrive-main/projects/mmdet3d\_plugin/models/instance\_bank.py 中的 cache 函数:

if self.confidence is not None:
    # confidence[:, : self.num_temp_instances] = torch.maximum(
    #     self.confidence[0] * self.confidence_decay,
    #     confidence[:, : self.num_temp_instances],
    # )
    left = torch.maximum(
        self.confidence[0] * self.confidence_decay,
        confidence[:, :self.num_temp_instances],)
    right = confidence[:, self.num_temp_instances:]
    confidence = torch.cat([left, right], dim=1)

5.9.2 消除 cast 算子:

路径:SparseDrive-main/projects/mmdet3d\_plugin/models/instance\_bank.py 中的 topk 函数:

def topk(confidence, k, *inputs):
    # bs, N = confidence.shape[:2]
    # confidence, indices = torch.topk(confidence, k, dim=1)
    # indices = (
    #     indices + torch.arange(bs, device=indices.device)[:, None] * N
    # ).reshape(-1)
    # outputs = []
    # for input in inputs:
    #     outputs.append(input.flatten(end_dim=1)[indices].reshape(bs, k, -1))
    bs, N = confidence.shape[:2]

6. 性能评测

6.1 算子支持情况

  1. nash-p 下可以编译成功
  2. 修改模型后,所有算子支持 BPU 实现
b30.binary_eltwise  : 2071
 b30.conv2d          : 503
 b30.gather2d        : 10
 b30.lut             : 314
 b30.pool2d          : 1
 b30.reduce          : 528
 b30.resize2d        : 3
 b30.warp            : 48
 b30vpu.dequantize   : 9
 b30vpu.quantize     : 8

6.2 静态 per 性能分析

6.2.1 确定性能瓶颈

获取到 perf.html 和 perf.json 后,使用【新版 perf 文件解读与性能分析】附录中的脚本对性能进行分析,输入为 perf.json,输出如下所示:

按照算子类型统计的耗时:

img

耗时排名 TOP20 的算子:

img

根据以上信息,可以得出优化目标:

  1. Mul 和 ReduceSum 算子的耗时最久,而且 mul 算子 ddr 耗时超过计算耗时的 65%,引发了带宽问题;
  2. ToP12 耗时的算子就是 Mul 和 ReduceSum,所以重点是优化 Mul 和 ReduceSum 算子。

6.2.2 性能优化策略

查看模型结构发现,模型中耗时的 Mul 和 ReduceSum 都处于这样的子结构中,所以我们主要是对这个结构进行性能优化。

img

此结构主要由 Mul、ReduceSum 和数据搬运算子组成,一方面 MulReduceSum 是运行在专门做向量计算的 VAE,加速效果不如张量,另一方面输入的 shape 非常大,也就解释了为何会引发带宽问题。、

所以这里考虑将 Mul+ReduceSum 计算替换为等价的 Mamtmul,从而使得这部分计算在 VAE 上加速。

性能优化效果验证

这里主要有以下步骤:

  1. 替换为 Matmul 计算:根据上述子图结构将其替换为 Matmul 计算,并导出 optimized.onnx;
  2. 替换等价性验证:在原始 onnx 中提取上述子图,和 optimized.onnx 进行输出一致性验证;
  3. 性能评测:同时对原始 onnx 子图和 optimized.onnx 进行 fast-perf,验证性能收益。

上述步骤可以参考:https://developer.horizon.auto/blog/13065

这几天遇到一件比较糟心的事情, 想请各位大佬分析一下

是这样的, 因为本身是 IT 行业, 加上喜欢折腾一些软硬件, 所以家里有搭建一套完整的 HomeLab, Server 方面有几个 ProxmoxVE 节点组成的集群, 因为每个月电费/网费的成本确实比较高, 再加上 PVE Node 确实有不少闲置资源, 在几年前有开始挂小黄鱼上对外出租一部分网路/计算资源, 用来 cover 一部分 infra 的开销, 也没刻意推广过, 属于有一单做一单, 几年下来每月大概能有十几个用户这样子

在上个月 20 号的时候, 有一个新的用户购买了一台虚拟机资源, 从购买到交付一切正常, 使用了 4 天的时间, 期间每天都有流量进出, 24 号当天下午, 突然疯狂给我发消息, 大概意思就是服务器连不上, 要退款, 我检查了网路没有问题, server 也正常再跑, 虚拟机状态也正常, 也没有其他人反应出现问题, 当时没多想询问是否可以重启, 得到确定的答复后给他重启了一下 vm, 那天后面他也没再找我, 结果后面三四天, 每天都来找我, 各种各样的问题, 包括但不限于 RDP 远程桌面连不上, 网路不通连不上...

后面我让他测试 ping 到我这边的 domian, 看到 dns 能解析到正确的 ip, 但是 icmp 不可达, 我又让他测试 ping 到我的上级网关, 这个点就已经是中国联通的 ISP 了, 他还是不通, 最后我索性就让他 ping 测试到 221.6.4.66, 这个地址是江苏联通在南京的省级 DNS 伺服器, 绝对不可能不通的, 结果他截图还是不通, 这时候我就开始怀疑这家伙在骗我





后面我用 ITDog/BOCE/IPIP.net/ping.pe 这些测试站测试了到我这边连接全部正常的截图发给他, 表示这是他自己的问题, 我这边网路一切正常, 结果这人下面的操作给我看傻了, 他测出来的结果全是红的, 我到现在都不知道他是如何做到的, 但我可以肯定他一定在撒谎, 除非我俩在平行宇宙




这是我的测试图



这是他提供的测试图




按我的理解, 他肯定不可能控制对方的测试服务器让他们到我不通, 只有可能在前端做手脚, 但看对方的截屏感觉是手机/平板的截屏, 能用 F12 这种操作改前端页面吗? 真的想不明白他到底怎么做到的, 特意请教一下各位大佬们, 我实在是想不到还有什么阴招了

这几天遇到一件比较糟心的事情, 想请各位大佬分析一下

是这样的, 因为本身是 IT 行业, 加上喜欢折腾一些软硬件, 所以家里有搭建一套完整的 HomeLab, Server 方面有几个 ProxmoxVE 节点组成的集群, 因为每个月电费/网费的成本确实比较高, 再加上 PVE Node 确实有不少闲置资源, 在几年前有开始挂小黄鱼上对外出租一部分网路/计算资源, 用来 cover 一部分 infra 的开销, 也没刻意推广过, 属于有一单做一单, 几年下来每月大概能有十几个用户这样子

在上个月 20 号的时候, 有一个新的用户购买了一台虚拟机资源, 从购买到交付一切正常, 使用了 4 天的时间, 期间每天都有流量进出, 24 号当天下午, 突然疯狂给我发消息, 大概意思就是服务器连不上, 要退款, 我检查了网路没有问题, server 也正常再跑, 虚拟机状态也正常, 也没有其他人反应出现问题, 当时没多想询问是否可以重启, 得到确定的答复后给他重启了一下 vm, 那天后面他也没再找我, 结果后面三四天, 每天都来找我, 各种各样的问题, 包括但不限于 RDP 远程桌面连不上, 网路不通连不上...

后面我让他测试 ping 到我这边的 domian, 看到 dns 能解析到正确的 ip, 但是 icmp 不可达, 我又让他测试 ping 到我的上级网关, 这个点就已经是中国联通的 ISP 了, 他还是不通, 最后我索性就让他 ping 测试到 221.6.4.66, 这个地址是江苏联通在南京的省级 DNS 伺服器, 绝对不可能不通的, 结果他截图还是不通, 这时候我就开始怀疑这家伙在骗我





后面我用 ITDog/BOCE/IPIP.net/ping.pe 这些测试站测试了到我这边连接全部正常的截图发给他, 表示这是他自己的问题, 我这边网路一切正常, 结果这人下面的操作给我看傻了, 他测出来的结果全是红的, 我到现在都不知道他是如何做到的, 但我可以肯定他一定在撒谎, 除非我俩在平行宇宙




这是我的测试图



这是他提供的测试图




按我的理解, 他肯定不可能控制对方的测试服务器让他们到我不通, 只有可能在前端做手脚, 但看对方的截屏感觉是手机/平板的截屏, 能用 F12 这种操作改前端页面吗? 真的想不明白他到底怎么做到的, 特意请教一下各位大佬们, 我实在是想不到还有什么阴招了

🚀 OTFS/OCDM/AFDM 高机动 NTN 对比仿真平台

统一信道建模 · 三体制公平对比 · 双检测器性能评估 支持 OTFS / OCDM / AFDM 一键仿真,输出 BER 曲线与结构化结果文件

MATLAB Waveform Channel Detector

📌 为什么需要这个项目?

高机动场景下(高速终端、卫星链路)时延-多普勒扩散显著,传统单波形脚本很难回答“到底哪种体制在当前参数下更稳”。常见问题包括:

常见痛点(零散脚本)本项目解决方式
🔴 参数不统一,体制对比不公平✅ 统一 K/L/M/SNR/NTN 参数链,三体制同口径
🔴 信道模型拆散在多处,难验证正确性✅ 通用 NTN_channels+ 兼容封装 NTN_channels1/2
🔴 只有一种检测器,无法比较检测增益✅ 同时支持 LMMSEMMSE-SD
🔴 输出文件命名混乱、结果难复现✅ 固定文件名输出,单目录清晰落盘
🔴 演示与正式仿真混在一起demofull 双模式分离,场景明确

🎯 核心价值

🔬 研究价值

  • 公平比较框架:同一 NTN 参数下评估 OTFS/OCDM/AFDM。
  • 统一检测口径:在每个体制内同时给出 LMMSE 与 MMSE-SD。
  • 可解释链路:信道构造、域变换、等效信道、检测与 BER 统计全链路闭环。
  • 可复现实验:固定随机种子与固定输出行为,便于重复实验和论文复核。

💼 工程价值

  • 模块化结构清晰:入口脚本薄、算法模块独立、工具函数复用。
  • 输出可直接集成:图像 + MAT 结构化结果适合后处理与报告自动化。
  • 扩展路径明确:新增体制、新增检测器可低耦合接入。
  • 维护门槛低:全中文注释与文档,利于团队协作交接。

⚡ 技术亮点

🏗️ 完整工程架构

OTFS-OCDM-AFDM/
 ├── run_unified_comparison.m        # 正式仿真入口(full)
 ├── demo_waveform_showcase.m        # 演示入口(demo)
 ├── src/
 │   ├── default_params.m            # 参数工厂:full/demo 模式
 │   ├── prepare_output_dir.m        # 输出目录准备 + 旧结果清理
 │   ├── run_ocdm_simulation.m       # OCDM 仿真主循环
 │   ├── run_afdm_simulation.m       # AFDM 仿真主循环
 │   ├── run_otfs_simulation.m       # OTFS 仿真主循环
 │   ├── NTN_channels.m              # 通用 NTN 信道矩阵构造
 │   ├── NTN_channels1.m             # AFDM 兼容信道封装
 │   ├── NTN_channels2.m             # OCDM(含CP)兼容信道封装
 │   ├── mmse_sd_detector_unified.m  # MMSE-SD 统一检测器
 │   ├── normalize_channel_matrix.m  # 信道归一化
 │   ├── symbols_to_bits.m           # 符号转比特工具
 │   ├── plot_comparison_results.m   # 曲线绘图与保存
 │   └── print_performance_summary.m # 终端摘要与结构化统计
 └── docs/
     ├── 算法文档.md
     ├── 代码文档.md

🧠 统一仿真内核(关键设计)

  1. 统一参数入口default_params(mode) 生成全局参数,避免体制参数漂移。
  2. 统一信道构造思路NTN_channels 输出时变矩阵,AFDM/OCDM 通过兼容层适配。
  3. 统一检测对比:三体制均执行 LMMSE 与 MMSE-SD,结果结构一致。
  4. 统一输出策略:固定命名 + 单目录覆盖,利于快速比对“当前版本结果”。

📊 实测性能(full 模式结果)

数据来源:outputs/unified_comparison_results.mat(当前项目实测输出)

LMMSE BER
SNR (dB)OCDMAFDMOTFS最优体制
03.150e-013.073e-013.071e-01OTFS
42.299e-012.165e-012.171e-01AFDM
81.469e-011.254e-011.268e-01AFDM
127.530e-024.730e-024.740e-02AFDM
163.080e-028.200e-037.900e-03OTFS
201.370e-029.000e-049.000e-04AFDM/OTFS 并列
MMSE-SD BER
SNR (dB)OCDMAFDMOTFS最优体制
03.154e-013.076e-013.073e-01OTFS
42.312e-012.173e-012.179e-01AFDM
81.500e-011.272e-011.285e-01AFDM
127.880e-024.750e-024.770e-02AFDM
163.340e-025.900e-035.800e-03OTFS
201.800e-022.000e-043.000e-04AFDM
结论:在当前配置下,AFDM 与 OTFS 在中高 SNR 区间显著优于 OCDM;MMSE-SD 相对 LMMSE 的增益在高 SNR 更明显。

💻 核心实现展示

1) OCDM:Fresnel 域有效信道构造

% 发射端:QAM符号 -> OCDM调制 -> 加循环前缀
 s_qam = qammod(x, params.M, 'UnitAveragePower', true);
 s_ocdm = IFSnT * s_qam;
 s_cp = CP_mtx * s_ocdm;
 ​
 % 有效信道构造与归一化
 Heff = R_mtx * H * CP_mtx;
 D = normalize_channel_matrix(FSnT * Heff * IFSnT);

2) AFDM:DAFT 域等效信道

% DAFT 变换矩阵
 A = D2 * DFT * D1;
 AH = A';
 ​
 % 仿射域信道与检测输入
 Y = A * r_time;
 H_daft = normalize_channel_matrix(A * HT * AH);

3) OTFS:DD 域等效化

% OTFS 变换算子
 OP = kron(WH, eye(M));
 ​
 % 时域信道 -> DD域等效信道
 H_eff = normalize_channel_matrix(OP' * HT * OP);
 r_dd = H_eff * x + w;

4) MMSE-SD:增广系统 + QR 回代

A = [H; reg * eye(n_tx)];
 b = [y; zeros(n_tx, 1)];
 [Q, R] = qr(A, 0);
 z = Q' * b;
 ​
 for i = n_tx:-1:1
     inter = R(i, i+1:end) * x_hat(i+1:end);
     rhs = z(i) - inter;
     soft_val = rhs / R(i, i);
     x_hat(i) = nearest_constellation(soft_val, constellation);
 end

🔄 端到端调用链

run_unified_comparison / demo_waveform_showcase
         ↓
 default_params(mode)
         ↓
 prepare_output_dir(...)
         ↓
 run_ocdm_simulation
 run_afdm_simulation
 run_otfs_simulation
         ↓
 plot_comparison_results
 print_performance_summary
         ↓
 save(...results, params, summary, plot_paths)

🎬 一键运行

% 进入工程目录
 cd('D:/03_代码库/04.code4sale/OTFS-OCDM-AFDM')
 ​
 % 快速演示
 run('demo_waveform_showcase.m')
 ​
 % 正式仿真
 run('run_unified_comparison.m')

📂 输出预览

当前输出目录:outputs/

典型结果文件:

  • unified_comparison_results.mat
  • ber_comparison_full.png
  • ber_comparison_full.fig
  • demo_results.mat
  • ber_comparison_demo.png
  • detector_gain_demo.png

图像示例:
ber_comparison_full.png

🖥️ 运行环境

  • MATLAB:R2020a 或更高版本(建议 R2022a+)
  • 依赖函数:

    • qammod / qamdemod(通信相关工具箱)
    • physconst(物理常量函数)
  • 操作系统:Windows / Linux / macOS 均可(路径按系统调整)

🛒 获取方式

本文代码仅为核心片段,完整版工程已整理好。 关注公众号 【3GPP 仿真实验室】进行获取。

文件合并拆分 在线工具分享

平时整理资料、处理日志、拆分大文本时,很多人都会遇到一个问题:文件太大不方便传,或者内容分散不好汇总。为了解决这个高频小麻烦,我用 Vue 3(Nuxt 3) 做了一个「文件合并拆分」在线工具,打开网页就能直接用。

在线工具网址:https://see-tool.com/file-merge-split
工具截图:

这个工具主要有两种模式:

  1. 文件拆分:把一个大文件拆成多个小文件。支持按“行数”拆分,也支持按“文件大小(KB/MB)”拆分。
  2. 文件合并:把多个文本文件按顺序合并成一个文件,还可以设置分隔符,方便区分每段内容。

对普通用户来说,最实用的场景包括:

  • 大日志按大小拆分后再上传或发送
  • 长名单按行拆分给不同同事处理
  • 多个零散 TXT/CSV 合并成一个总文件
  • 批量整理导出的文本结果

使用也很简单。

如果你要拆分文件:选择“拆分”模式 -> 上传文件 -> 选择按行数或按大小 -> 点击开始拆分 -> 下载全部结果。

如果你要合并文件:选择“合并”模式 -> 一次上传多个文件 -> 选择分隔方式(可自定义) -> 点击开始合并 -> 下载合并后的文件。

我在交互上尽量做得直观:上传后会显示文件行数、体积和预计拆分数量,处理完成后可单独下载,也可一键下载全部,减少重复操作。

另外,这个工具更偏向日常实用,不需要安装软件,电脑和手机浏览器都能使用。对于经常处理文本文件的人来说,它可以明显节省整理时间。

如果你也经常碰到“文件太大不好发”或“文件太多不好管”的情况,可以试试这个工具。

终于可以在微信跟飞书上控制 AI Agent 们了。10 分钟就能配好。而且支持记忆跟定时任务。不用再去折腾 openclaw 。claude code 可以接国产模型,随时切换,性价比很高。而且对 skill 支持非常好,调度和 agent 能力也很强。微信需要公网 IP ,但通过企业微信可以很容易接入,并且可以直接在个人微信就可以聊。

控制他们干任何活,聊天均可。真正实现随时随地大小码。
睡觉前交代几个任务,醒来就可以看结果。

主要是 agent 能力很强,落地效果比 openclaw 更好。

使用下面的项目,10 分钟就可以接入啦。目前是在 beta 版本。安装非常简单。

github 地址:https://github.com/chenhg5/cc-connect

参考截图:

3e86f8becfb6d09bfc486f986bd75a74.JPG

15aeef6888984fa0eac05dddc83799c3.PNG

可以加用户群:

用户群