嵌入式网络服务器设计

前言

  • 本文目标:设计一款针对低性能嵌入式arm设备的网络服务器,包含Web服务,Tcp命令解析,用户登录,会话管理等功能

整体概览

  1. 整体程序架构泳道图

  2. 整体逻辑解析

    • 程序总共划分为五个泳道,分别是主线程,时间同步线程,传感器数据接收线程,TcpServer线程以及WebServer线程
    • 主线程启动后,初始化日志以及配置类,用于日志记录和配置项的获取.
      • 随后初始化用户管理功能,用于监听登录以及记录操作日志
      • 启动时间同步任务SNTP
      • 启动传感器数据采集线程
      • 向IOC提交Tcp和Web的服务监听程序
      • 创建线程池提供给IOC使用
      • 等待SIGINT和SIGTEAM信号以终止程序
    • 时间同步线程使用SNTP进行时间同步,SNTP是NTP的子集,提供了轻量的毫秒级的时间精度同步
      • 时间同步通过主线程获取到的配置,向指定的SNTP服务器发起SNTP对时
      • 对时完成后设置本地时间
    • 传感器数据接收线程阻塞性的等待CAN总线的数据,接收数据后更新内存副本以及数据库
    • TcpServer主要提供字符串命令的方式与服务器交互的通道
    • WebServer则提供了Web功能的同时,将Http升级为WebSocket,并通过长连接实现Web服务器的交互功能

核心模块设计

会话模块设计

  1. 登录会话设计流图

  2. 设计理念

    • 项目要求支持使用纯字符串命令与服务器进行交互,同时需要具有登录和一定的权限管理功能
    • 项目要求具备网络页面的登录,操作相关功能
    • 尽管网络页面的前端逻辑可以携带Cookie,考虑到资源受限,保持业务逻辑的一致性,设计字符串服务器和网页功能使用同一套的会话逻辑
  3. 设计解析

    • 前端页面使用长连接WebSocket与后端进行连接,连接建立成功后即可传输字符串命令
    • TcpServer连接建立后,也可以传输字符串命令
    • 传输字符串命令进行登录逻辑,登录逻辑读取SQLite中的记录进行验证后,使用SessionManager进行内存管理
    • SessionManager内部包含三个字典:
      • 字典一:存放生成的随机字符串SessionId和实际的用户信息UserSession之间的映射
      • 字典二:存放生成的SessionId和建立连接的连接Id之间的映射
      • 字典三:存放生成的SessionId和建立连接的连接Id之间的反向映射
    • 实际业务命令处理时,需要从当前的物理连接的ID获取对应的连接ID,再通过连接ID获取对应的UserInfo以获取当前操作用户的相关信息
    • 当连接断开或者超时,通过物理ID获取映射关系清空内存中的映射
  4. 这样设计的好处

    • 统一的业务逻辑:无论是TCP命令行客户端还是Web浏览器,都使用相同的会话管理和权限验证逻辑,减少了代码重复和维护成本,特别适合嵌入式环境下的资源约束
    • 内存高效的会话管理:通过三个映射表实现O(1)时间复杂度的用户信息查询和连接管理,会话数据存储在内存中避免频繁数据库访问,适合资源受限的嵌入式设备
    • 安全的连接隔离:使用随机UUID作为sessionId替代可预测的连接ID,避免了ID枚举攻击风险,同时实现了网络层和业务层的解耦,提高了系统安全性

文件下载功能实现

  1. 下载数据流程图

  2. 设计理念

    文件下载功能的设计将安全性用户体验置于首位。为防止未经授权的数据访问和URL猜测攻击,我们摒弃了简单的静态链接下载方式。同时,为了避免因数据准备耗时过长而导致的请求超时,设计上将“下载授权与准备”和“数据传输”两个阶段完全分离,确保了系统的健壮性和前端的流畅响应。

  3. 设计解析

    系统采用了一种基于Token的、两阶段异步下载模型,将下载流程解耦为通过WebSocket的指令交互和通过HTTP的数据传输。

  4. 这样设计的好处

    • 极高的安全性: 每次下载都必须先通过已认证的WebSocket连接发起,有效杜绝了未授权访问。基于Token的一次性URL机制,从根本上防止了链接的泄露、重放和恶意爬取。
    • 卓越的用户体验: 将耗时的数据准备过程与实际下载分离。用户点击后,UI可立即获得“正在准备”的反馈,并在数据就绪后才开始下载,避免了长时间的等待和请求超时。
    • 健壮高效的后端架构: 职责清晰,WebSocket服务负责业务逻辑,HTTP服务负责数据传输。通过std::move实现了高效的零拷贝数据传递,并利用std::lock_guard保证了DownloadManager在并发访问下的线程安全

前后端交互

  1. 页面切换流程图

  2. 设计理念

    为在资源有限的嵌入式设备上提供流畅、媲美原生应用的交互体验,前端采用了**单页应用(Single Page Application - SPA)**架构。该设计的核心是避免传统的页面刷新和跳转,所有UI更新都在同一个HTML页面内通过JavaScript动态完成。这不仅消除了页面加载时的“白屏”现象,也极大地降低了对服务器的HTTP请求压力,所有交互都优先通过已建立的WebSocket连接进行。

  3. 设计解析

    页面的切换和渲染由一套事件驱动的轻量级路由机制来管理,其工作流程高度解耦,主要分为以下四个阶段:

    • 模板化设计与预加载: 所有的UI片段都被预先定义为HTML字符串模板,并存储在全局的window.PageTemplates对象中。应用启动时,这些模板即被加载到内存,为后续的快速切换做好准备。
    • 事件驱动的路由 (PageRouter): PageRouter是页面导航的核心。当用户点击导航链接触发navigateTo或navigateToSubPage方法时,它执行两个核心操作:
      • 从PageTemplates中获取对应的HTML模板。
      • 通过innerHTML将这段HTML注入到指定的DOM容器中(如#pageContainer或#subPageContainer)。
        完成内容注入后,PageRouter会广播一个名为pageLoaded的全局自定义事件,然后它的任务就结束了。这种设计让PageRouter的职责非常单一,只负责“切换内容”和“发信号”。
    • 中央事件分发 (onPageLoaded): App的主控制器(AppController)始终在监听pageLoaded事件。一旦捕获到该事件,它就会调用onPageLoaded(pageName)函数。这个函数扮演着中央调度器的角色,其内部的switch语句会根据传入的页面名称,精确地分派给该页面专属的初始化函数(例如 initNetworkSettingsPage())。
    • 动态事件绑定: 每个页面的专属init…函数负责执行“激活”页面的最后一步。它会在刚刚注入的、原本是静态的HTML内容中,通过getElementById或querySelectorAll找到需要交互的元素(如按钮、输入框),并使用addEventListener为其动态绑定事件监听器。只有在这一步完成后,新“页面”才真正变得完整且可交互
  4. 这样设计的好处

    • 极致的性能与响应速度: 由于所有HTML模板都已在内存中,页面切换无需任何网络请求,响应几乎是瞬时的,为用户提供了极其流畅的体验。

    • 高度解耦的架构: 路由(PageRouter)、逻辑调度(AppController)和页面具体逻辑(init…函数)三者职责分明、互不干扰。新增一个页面只需增加一个模板、一个case分支和一个init函数,对现有代码的侵入性极低。

    • 流畅的用户体验: 配合CSS过渡动画,内容的动态注入可以实现平滑的淡入淡出效果,避免了传统页面跳转带来的生硬感。

    • 模板复用与高效开发: 对于UI结构相似的页面(如所有文件下载列表),可以共用同一个fileList模板,仅在初始化时加载不同的数据,遵循了DRY(Don’t Repeat Yourself)原则,提高了开发效率

总结

本文档详述了一款针对资源受限嵌入式设备的高性能网络服务器的完整实现。

该设计的核心优势在于其后端架构:通过统一的内存会话管理,为TCP和WebSocket客户端提供了高效、安全的认证机制;并采用基于Token的两阶段异步模型,实现了安全与体验俱佳的文件下载功能。

前端则采用轻量级单页应用(SPA)架构,通过动态内容注入和事件驱动逻辑,在极低的资源占用下提供了媲美原生应用的流畅交互。

综上,本项目提供了一套完整、健壮的嵌入式网络服务解决方案,在性能、安全、资源效率和用户体验之间取得了精妙的平衡。