港美股与期货交易App的设计架构

iOS 工程师

  • Swift(纯代码 + SnapKit)
  • MVVM 变体
  • Alamofire(HTTP)
  • Starscream(行情 WebSocket)
  • Socket.IO-Client-Swift(交易网关,自维护的分支版本)
  • SwiftProtobuf(行情与交易二进制协议)
  • WCDB.swift(合约码表)
  • LocalAuthentication + Keychain
  • CocoaPods / SwiftLint / Jenkins

概述

这是一款港股、美股与期货的一站式交易 App:行情(市场、自选、K 线、排行)、交易(证券与期货下单、持仓、委托、资金)、开户与出入金(H5)、资讯、新股和员工期权。客户端为纯 Swift,界面全部纯代码。

网络上是一条 HTTP 加两条长连接:HTTP 承接账户与业务接口;行情走原生 WebSocket,交易走 Socket.IO 连到交易网关;两条长连接的负载都是 Protobuf。账户上是两级会话:先登录 App 账户,下单前还要再登录交易账户。

下面依次是业务结构、技术架构、关键流程和几处值得展开的设计,涉及三条网络通道的首版骨架、交易网关协议对接与事件分发、交易登录与二次认证、交易会话保活与被踢、交易首页与资金数据中心、资金查询的请求频率控制,以及行情推送、K 线、凭证续期、H5 桥和合约码表。

业务全景

交易
交易登录交易首页与资金下单 / 改单 / 撤单持仓 / 委托 / 成交
行情
市场与排行K 线与分时自选
账户与开户
App 登录开户与尽职审查出入金
内容与运营
资讯与公告新股中心消息 / 卡券 / 员工期权

同一套客户端也可以按业务模块来看。上面按用户路径串起来,下面按业务模块切开:行情、交易、开户与资金、资讯与运营。

资讯与运营开户与资金交易行情
市场列表 港股 / 美股 / 期货自选K 线 / 分时 / 指标盘口 / 逐笔证券期货交易会话资金 / 持仓 / 盈亏开户 H5入金 / 出金资讯 / 公告新股 / 暗盘活动 / 卡券 / 消息

1. 业务结构

1.1 架构演进

起步 HTTP、行情、交易三条通道 的骨架 Protobuf 生成代码进工程接通交易 期货与证券的请求和应答模 型 交易登录与二次认证加固 长连接重连节流与鉴权冷却 交易连接操作进一条串行队 列 凭证预续期与续期单飞收敛 交易数据的查询时机集中到 一处 资金查询的合并窗口与强制 通道 网关状态在页面基类统一监 听

项目是从零开始的新 App。起步阶段先把HTTP 客户端、行情 WebSocket、交易 Socket.IO 三条通道的首版骨架搭起来,之后再逐步加固长连接、接通交易协议、补齐交易业务。

1.2 模块一览

模块 职责(一句话) 主要技术点
交易登录(证券 / 期货) 下单前登录交易账户 应答驱动、二次认证、改密、请求序号区分静默登录
面容 / 指纹交易登录流程 用生物识别代替输交易密码 生物特征变更即解绑、有效期、密码错误自动解绑
交易会话保活与被踢 保持在线,被挤下线时提示 10 秒心跳、会话号与设备标识比对、按页面提示
交易首页与资金数据中心 多账户资金、总资产、浮动盈亏总览 推送 × 持仓 × 汇率现算、数据齐全才刷新
交易数据请求管理 统一决定「什么时候查什么」 按页面场景组合查询、合并窗口与强制通道
交易网关协议 请求、应答与主推 信封与事件;请求与应答模型、十几个应答事件的分发、平台与设备字段
行情推送与列表 市场、自选、排行实时刷新 只订阅可见行、价格聚合
K 线 自绘 K 线、分时、十余种指标 CAShapeLayer 局部绘制、TA-Lib
App 账户与凭证续期 登录、续期、登出 access / refresh、预刷新、单飞
H5 混合 开户、入金、活动 约 30 种桥接消息
合约码表 本地合约数据库 WCDB、预置库、增量更新
基础设施 网络、主题、多语言、组件库、监控 见第 2 节

模块之间的依赖是单向的:业务模块调用基础层;长连接层不理解业务,只把应答投递出去。

交易首页交易数据请求管理资金数据中心下单交易登录K 线交易长连接行情到持仓中间层行情长连接行情列表H5 容器HTTP 客户端账户

2. 技术架构

2.1 分层

从上到下是四层:页面壳、业务层、网络层、数据层。页面基类统一了导航栏、主题、生命周期、取消请求和网关状态提示。

数据层
用户模型归档合约码表钥匙串
网络层
HTTP 客户端行情长连接交易长连接协议生成代码
业务层
ViewModel交易登录资金数据中心行情中间层H5 桥
页面壳
页面基类组件库

2.2 一条 HTTP,两条长连接

服务端(概念)客户端交易网关JSONProtobuf 信封
HTTP 客户端行情长连接 原生 WebSocket交易长连接 Socket.IO业务网关 按路径前缀分服务行情服务器 按市场下发节点鉴权命名空间期货命名空间证券命名空间
通道 对端 负载 鉴权 保活与重连
HTTP 业务网关,按路径前缀分服务 JSON 请求头带用户凭证 15 秒超时;长任务 300 秒
行情长连接 行情服务器,按市场下发 Protobuf 信封 用行情专用凭证登录 10 秒心跳;节流重连;后台断开
交易长连接 交易网关的三个命名空间 Protobuf 事件负载 握手头带用户凭证 库内加应用层重连;10 秒账户心跳

为什么是两条。 后端是两套服务,凭证不同,流量模型也不同:行情是高频单向推送,交易是请求-应答加主推,还要按期货、证券两套柜台隔离。代价是重连和心跳各写一遍。选型、信封、发送策略和分发的细节见两条长连接。

交易连接的门控。 先连鉴权命名空间,成功之后才初始化期货和证券命名空间;鉴权失败就强制续期,并在冷却期内忽略新的连接请求,避免拿旧凭证反复鉴权。

用户凭证变化且不为空鉴权成功,初始化期货与证 券命名空间鉴权失败,强制续期新凭证到达15 秒内的连接请求被忽略断开或凭证清空回到前台或网络变化后检查
未连接鉴权中业务就绪冷却

信封。 两条长连接都是「类型 + 序号 + 二进制负载」。行情信封约 70 种类型值,覆盖心跳、登录、查询、订阅与退订和各类主推。交易信封额外带平台和设备标识,应答与主推用两个不同的事件名区分;请求序号由客户端生成、应答原样带回。

行情信封消息类型序号二进制负载版本交易信封请求序号消息类型二进制负载返回码平台设备标识行情负载登录 / 心跳订阅 / 退订快照 / 盘口 / 成交 / K 线主推交易负载登录 / 账户心跳资金 / 持仓 / 委托查询下单 / 改单 / 撤单成交 / 资金 / 掉线主推

协议源文件以子模块跟踪后端的协议仓库,一个脚本拉取并生成 Swift 代码,后端用同一份协议生成自己的代码。交易网关的请求与应答模型、十几个应答事件的分发、新版交易协议的适配,以及发送时附带平台与设备标识都在这一层。字段、请求序号和应答的对号方式见交易消息协议。

2.3 数据怎么分发给页面

项目里没有 Rx,也没有通用的事件总线。分发靠三种很朴素的手段:

  1. 单例连接 + 一个 weak 代理。 每条连接只认一个当前代理,页面在出现时把自己切成代理,全仓约 42 处这样的切换。同一时刻只有一个页面处理推送。
  2. 二次扇出。 市场容器拿到数据后,再转给当前子页和即将出现的子页;交易侧有一个中间层同时代理行情和交易两条连接,再派生出持仓、委托、成交、快照几类回调。
  3. 通知广播状态。 期货与证券网关的连接状态、凭证变化等用 NotificationCenter 广播。网络与网关状态的监听收进页面基类,子页面不再各自注册。交易登录的结果则直接写进登录管理器的属性,由属性观察器驱动后续处理。
当前代理当前代理
当前代理
状态通知写入属性
长连接单例当前页面市场容器当前子页即将出现的子页行情中间层持仓回调委托 / 成交回调快照回调页面基类统一监听交易登录管理

单代理加扇出的代价是同一时刻只有一个消费者、切换时机要靠约定守住;属性观察器让长连接层不必懂业务,但处理逻辑是隐式的。几种投递方式的对比见两条长连接第 4.5 节。

2.4 三级凭证

账户信息接口交易长连接鉴权后再登录用户凭证access 与 refresh本地记过期时间剩余不足 20 分钟就续期行情凭证随行情服务器地址下发同样 20 分钟阈值交易会话交易登录后获得期货与证券各一个没有本地过期时间
凭证 从哪来 用在哪 失效时
用户凭证 App 登录 HTTP 请求头、交易长连接建连、给 H5 续期凭证失效才登出,弱网不登出
行情凭证 账户信息接口,随服务器地址下发 行情长连接登录 过期就重拉账户信息
交易会话 交易网关登录应答 交易请求、账户心跳 掉线、被踢、登出时置空,只下线交易,不登出 App

续期的触发点是请求前、页面出现、启动与回前台、网络恢复,剩余有效期不足就续,续期请求同一时刻只发一次;服务端认证失败时强制续期,但不重放原请求。用户凭证一变化,交易长连接就用新凭证重连。详见 凭证续期。

2.5 启动到出数据

行情长连接交易长连接HTTP启动与市场页行情长连接交易长连接HTTP启动与市场页凭证快过期则续期账户信息行情节点与行情凭证码表、错误文案、H5 地址监听网络可达性连接鉴权命名空间鉴权成功,再连两个柜台市场页用下发节点建连登录成功,开始心跳订阅可见行
  • 地址从哪来。 业务接口是一个网关域名,按路径前缀分到各服务;交易网关也是域名;行情服务器是服务端按市场下发的节点,客户端直连,并把节点信息落盘缓存。
  • 客户端不选线。 没有测速、HTTPDNS、内地与香港线路切换,也没有失败切备用地址。如果有就近调度,是服务端下发节点时做的。

2.6 缓存与持久化

方式 用途
对象归档 用户模型,包括两种用户凭证
WCDB 合约码表:随包预置,启动后按版本与时间戳增量更新
钥匙串 设备标识、生物识别绑定的交易密码(加密后保存)
偏好设置 开关与引导状态
磁盘缓存 行情节点等配置

2.7 UI

  • MVVM 变体。页面基类统一导航栏、主题、生命周期:页面析构时取消它发起的 HTTP 请求,回调前持有者已经释放就丢弃结果。
  • 组件库:按钮、自动翻页列表、页面容器、手势冲突处理、空态、弹窗、下拉刷新。
  • 双维度主题:明暗 × 红涨绿跌;简体、繁体、英文三套文案。

2.8 H5 桥与码表

  • H5 桥:约 30 种消息,覆盖取凭证、App 信息、人脸识别、证件拍照、唤起原生登录、新股下单、进入行情或下单、自选读写、分享、日历等。H5 把回调函数名传过来,原生处理完再调用它。凭证不是提前注入,而是 H5 通过桥来取。
用户凭证桥H5 页面用户凭证桥H5 页面消息类型、回调名、参数取凭证前先检查有效期当前凭证执行回调并带回结果
  • 码表与发布:全量合约随包发布,首次启动不必等下载。生产码表的更新是一条半手工流程;打包按分支分开发、预发布、生产和商店几种包。
产品给出合约数据专用分支替换真机运行生成本地库导出数据库替换进工程 更新版本号与时间戳按分支打包

2.9 依赖与工程

  • 主要依赖:Alamofire、Starscream、Socket.IO-Client-Swift、SwiftProtobuf、WCDB.swift、SnapKit、Kingfisher、HandyJSON / SwiftyJSON、TA-Lib。
  • 工程:CocoaPods;SwiftLint 强约束(禁强解包、禁强转、代理必须 weak、禁 IB);Socket.IO 与下拉刷新用的是自维护的分支。Jenkins 按分支定时打包。另有自研的内存泄漏检测(页面退出后延迟检查是否释放)和卡顿监控。

2.10 安全(概念级)

  • 两级会话:App 账户之外,交易还要单独登录,可能还要短信或邮箱二次认证,密码过期时强制修改。
  • 生物识别:系统生物特征一变就解绑;有有效期;用本地保存的密码登录失败时自动解绑。
  • 交易请求带平台与设备标识,掉线和登出类应答只处理本设备的。

3. 关键流程

3.1 交易登录

交易网关钥匙串交易登录管理用户交易网关钥匙串交易登录管理用户alt[已绑定生物识别且特征-未变]alt[需要二次认证][需要改密][成功]进入交易,需要登录生物识别通过取该账号的交易密码交易密码弹窗输入密码登录,带新的请求序号应答短信或邮箱验证改密页保存会话号,启动账户心跳

登录管理器把期货和证券两套柜台的十几种应答收到一处处理:二次认证、未绑定认证方式、密码错误、必须改密、客户状态不允许、请求过于频繁等。开启生物识别时会先用交易密码做一次静默登录,它的应答靠请求序号认出来,只更新会话号,不走界面分支。登录入口还有 1 秒的防重入闸门。

3.2 被其他设备挤下线

设备 B交易网关设备 A设备 B交易网关设备 Aalt[相同][不同]用同一交易账户登录主推「重复登录」,带最新会话号与本地会话号比对本机重复登录,忽略只下线交易会话,停心跳按当前页面决定是否弹框

单点登录由网关保证,客户端要做的是认出「是不是自己」并决定怎么提示。证券侧靠比对会话号;期货侧的掉线主推带平台与设备标识,只处理本设备的。应答里出现「未登录」或「会话校验失败」也按掉线处理。只有在交易 Tab,或在行情 Tab 且导航栈里有下单页时才弹框;其他页面不打扰,下次进交易再要求登录。

3.3 交易首页的一次刷新

资金数据中心交易网关请求管理交易首页资金数据中心交易网关请求管理交易首页下拉、出现或重连后刷新证券资金与资金明细,走强制通道期货免密资金应答持仓 × 行情 × 汇率算浮动盈亏港美股盈亏到齐,或等待超时刷新总资产与盈亏

4. 几处值得展开的设计

下面挑七处按技术深度展开,每处一句问题、一句方案,完整过程见各篇案例。

案例 1:两条长连接、Protobuf 与数据分发

  • 问题:行情和交易是两套后端、两种凭证、两种流量模型,还要在没有 Rx 和事件总线的情况下把数据送到正确的页面。
  • 方案:行情走原生 WebSocket,交易走 Socket.IO 的三个命名空间;两边都是「类型 + 序号 + 二进制负载」的信封。分发用单例连接加单个 weak 代理,容器和中间层二次扇出,状态走通知。三条通道的首版骨架,以及交易网关的请求应答模型与事件分发
  • 详细案例:/ios/cases/trade-dual-long-connection

案例 2:交易消息协议:请求与应答的结构和请求序号

  • 问题:长连接上没有「一个请求对应一个响应」的边界:回来的可能是这次的应答、服务端的主推,也可能是另一台设备那次请求的结果。
  • 方案:信封加 Protobuf 负载两层;请求序号由客户端生成、应答原样带回,只在静默绑定登录等少数场景比对;其余应答和全部主推按类型分发,交易侧用两个事件名区分应答与主推,多设备时先按平台和设备标识过滤。超时、迟到和乱序应答的处理单独列为建议。
  • 详细案例:/ios/cases/trade-message-protocol

案例 3:交易会话:二次登录、保活与被踢

  • 问题:App 登录之外还有一级交易会话。两套柜台的应答、二次认证和改密各不相同;同一交易账户只允许一台设备在线,被挤下线时不能把用户踢出 App。
  • 方案:应答驱动的登录处理,请求序号区分静默登录与用户登录;登录后 10 秒账户心跳;靠会话号和设备标识判断是否被挤;掉线只下线交易会话,并按当前页面决定是否提示。交易登录、二次认证、保活与被踢
  • 详细案例:/ios/cases/trade-session-login-kickout

案例 4:交易资金查询的请求频率控制

  • 问题:交易首页的资金数据有定时器、推送、页面出现、下拉刷新、重连多种触发源,同一个查询会在短时间内被重复发出。
  • 方案:先给查询加计数,再按来源分层:轮询先降频再取消;被动触发走 1 秒合并窗口,用户动作走强制通道;盈亏推送引起的刷新和用户刷新分开;盈亏等齐再画,最多等一个上限。计数、强制通道、按场景的请求管理和推送分流
  • 详细案例:/ios/cases/trade-request-frequency-control

案例 5:网络优化:从现有网络层出发

  • 问题:交易 App 在地铁、电梯、Wi-Fi 与蜂窝切换时最容易出问题:连接看起来还在、其实已经死了;请求叠在一起;弱网下超时和重试没有统一口径。
  • 方案:先写清项目里已经有的(网络可达性监听、回前台与网络变化后的连接检查、重连节流、请求单飞与合并窗口、长连接压缩、二进制协议),再把还没做的单独列为建议(网络类型切换的主动重连、请求头瘦身、响应压缩、分级超时与退避重试)。
  • 详细案例:/ios/cases/trade-network-optimization

案例 6:access / refresh 凭证续期

  • 问题:用户凭证有效期短,多个页面会同时触发续期;弱网下不能把人踢出去。
  • 方案:剩余不足 20 分钟就提前续期,同一时刻只发一次;服务端认证失败时兜底强制续期;只有续期凭证本身失效才登出。和另一个项目「不续期、过期就登出」的做法放在一起对比。
  • 详细案例:/ios/cases/trade-token-refresh

案例 7:实时 K 线与行情性能

  • 问题:推送频繁时既不能打断用户拖动看历史,也要保持滑动流畅;列表行数多,全量订阅浪费带宽和 CPU。
  • 方案:解码离开主线程;时间戳相同就替换最后一根,否则追加,分时图按交易时间表补空分钟;只画可见区域;滚动时推送先缓存,停下再回放;列表只订阅可见行。
  • 详细案例:/ios/cases/trade-kline-quote-performance

5. 已经落地的优化

  • 交易首页资金查询:轮询从 3 秒改为 10 秒,之后改为事件驱动;页面出现时距上次刷新不足 10 秒就不再查。
  • 合并窗口与强制通道:被动触发的资金查询 1 秒内只发一次,用户动作绕过窗口。
  • 盈亏推送与用户刷新分流:修复了下拉刷新被盈亏推送「吃掉」的问题。
  • 网关状态统一监听:交易首页、证券主页、期货主页、期货资金详情共用页面基类的状态提示。
  • 长连接:重连节流、鉴权失败冷却、所有交易连接操作进一条串行队列。
  • 行情列表:只订阅可见行;市场页不再逐行查码表。
  • HTTP:请求跟随页面生命周期取消。
  • 续期:去掉失败后的自动重试,改为等下一个自然触发点。

6. 还可以做的

  • 凭证:认证失败时把请求挂起,续期成功后重放;续期用 actor 保证唯一在途;凭证从归档文件迁到钥匙串。
  • 生物识别:改用带生物识别访问控制的钥匙串项,「特征变化即失效」交给系统判断。
  • 长连接:两条连接抽一个公共的连接状态机;单代理改成按主题的多播订阅表。
  • 消息协议:加以请求序号为键的在途请求表,给下单类请求挂超时,迟到应答丢弃,乱序只认最新。
  • 交易状态:把属性观察器里的处理改成显式状态枚举加纯函数,补单元测试。
  • 金额:资金与盈亏改用 Decimal,计算移出主线程。
  • 网络切换:区分 Wi-Fi 与蜂窝的切换,切换后主动重建长连接,而不是只检查「是否已连接」。
  • 工程:码表生成脚本化进持续集成,商店包自动上传。

7. 重点模块与设计

每一条都是「要解决什么、设计是什么、为什么这样」。细节在对应案例里。

  • 行情和交易分成两条长连接。 问题是两套后端、两种凭证、两种流量。设计是行情原生 WebSocket,交易 Socket.IO 按柜台分命名空间,信封统一。首版骨架与交易协议对接。见 两条长连接。
  • 应答要能回到发起方。 问题是长连接上应答、主推和别的设备的结果混在一起。设计是信封带请求序号与设备标识,按类型分发为主、少数场景比对序号。见 交易消息协议。
  • 交易登录收成一个入口。 问题是两套柜台、十几种应答、二次认证和生物识别叠在一起。设计是应答驱动的处理加请求序号。交易登录与二次认证。见 交易会话。
  • 被挤下线只下线交易。 问题是交易会话和 App 账户是两级,交易掉线不能把人踢出 App。设计是会话号与设备标识比对,按页面决定是否提示。保活与被踢。见 交易会话。
  • 同一个查询不要被重复发。 问题是触发源太多。设计是被动触发合并、用户动作强制、推送与用户刷新分流。资金查询的频率控制。见 请求频率控制。
  • 浮动盈亏要客户端现算。 问题是柜台只给静态资金和持仓。设计是行情 × 持仓 × 汇率,港美股都到齐才刷新,等不到就用现有数据。资金数据中心。
  • 凭证提前续期。 问题是有效期短、并发多。设计是 20 分钟阈值、单飞、兜底强刷。见 凭证续期。
  • K 线不打断用户。 问题是推送频繁而用户在看历史。设计是局部绘制、滚动期缓存推送。见 实时 K 线。