简介:这是一份聚焦短信发送流程的PPT教案,面向通信工程专业学生、核心网运维人员及网络优化工程师,用图解方式讲解短信从发送方到接收方所经过的完整信令链路。内容按场景拆解为漫游用户MO流程、省内互通短信MO流程、省内用户MT流程、漫游用户MT流程、省外用户MT流程,并清晰标注了BSC、MSC、LSTP、HSTP、SMSC、ISMG、HLR等网元在每一步中的职责与应答关系,覆盖号段鉴权、位置查询、短信转发、计费话单生成等关键环节,便于读者快速建立端到端流程的全局视图。压缩包共1个pptx文件,大小158KB,内容结构紧凑,可直接用于课堂教学、自学入门或团队内部分享。目前已有67人学习下载,适合作为理解短信网络架构的基础材料,也能为后续排查短信发送失败、延迟等问题提供流程参考。
1. 短信发送流程到底在送什么:一次验证码的完整旅程
运营群里有人喊“验证码 10 分钟没到”,你查数据库看到状态是“提交成功,等待回执”,找通道方对线,对方甩来一句“已提交运营商”。这条短信到底卡在哪一跳?短信发送流程学习教案.pptx 这个标题,讲的就是这条从业务系统到用户手机的完整链路:验证码怎么被组装、怎么提交给网关、网关怎么转协议、运营商怎么下发、回执怎么回来。适合刚接手短信服务的后端开发、测试和运维,也适合要跟通道方掰扯的运营。学完不是为了背字段,而是能立刻回答三个问题:消息停在哪、该不该重发、找谁要回执。
2. 把短信发送流程拆成三段:业务侧、网关侧、运营商侧
2.1 短信发送流程里的三个角色与责任边界
我见过太多人一上来就啃 SMPP 报文格式,结果线上出故障时连“该找谁”都分不清。带这类教案,我习惯先借用企业数据架构设计方法里的思路:先画数据在哪产生、在哪流转、在哪消费,再填细节。短信发送流程也一样,三段边界必须在一开始就立住。
业务侧是你的服务,负责生成短信内容、校验签名模板、分配唯一消息 ID、记录发送状态。网关侧是通道方提供的接入服务,负责协议转换、按号段路由、重发控制、回执回调。运营商侧是移动、联通、电信的短信中心,负责真正的下行、计费、生成最终状态报告。故障发生时,第一条判断准则就是:这个状态是业务侧生成的,还是网关回传的,还是运营商回执里的。
| 环节 | 产生状态 | 谁负责写库 | 谁负责解释 |
|---|---|---|---|
| 业务侧提交 | SUBMITTED | 业务系统 | 本服务 |
| 网关确认 | ACK / NACK | 网关 | 通道方 |
| 运营商下发 | SENT | 网关回传 | 通道方 |
| 手机侧结果 | DELIVRD / UNDELIV / EXPIRED | 运营商回执 | 通道方 |
这张表是整份教案的地基。确认和回执是两个不同的信号,确认只代表“网关收了”,不代表“手机收到了”。很多线上误判,就是把 ACK 当成了成功。
2.2 SMPP、CMPP 与 HTTP API:三种接入方式怎么选
教案里绕不开协议选型。实际从业方案里,短信发送流程对接方式就三种:SMPP、CMPP、HTTP API。不是越底层越好,要看你的业务发往哪里。
SMPP 是国际通用协议,长连接、面向报文,适合跨国业务和自建网关,能承载大批量提交,但接入成本高,回执关联要自己处理。CMPP 是国内三大运营商行业网关常用的协议,很多老系统还在跑,字段里有 PK_TOTAL、PK_NUMBER 这类分包参数,调试比 SMPP 绕。HTTP API 是聚合服务商最常见的接入方式,提交一条 POST 请求,同步返回消息 ID,异步回调状态报告,接入成本最低,但排查问题时依赖对方日志。
| 接入方式 | 适用场景 | 接入成本 | 回执方式 | 常见坑 |
|---|---|---|---|---|
| SMPP | 国际业务、批量高并发 | 高 | 长连接 deliver_sm | 需自己维护 session 和窗口 |
| CMPP | 国内运营商行业网关 | 中 | 网关主动推送 | 分包、mt/ms 消息类型易混 |
| HTTP API | 验证码、通知、营销 | 低 | 回调或拉取 | 回调丢失、签名校验 |
如果只发国内验证码,我建议先走 HTTP API;如果业务线多、量级大,再考虑 SMPP 自建接入。教案里不要让学生陷入“哪个协议更高级”的争论,要看业务边界。
3. 用本地仿真脚本跑通最小短信发送流程:生产者、路由、回执
3.1 零依赖的 Python 仿真:从业务侧提交到手机侧回执
只看协议不跑代码,短信发送流程永远是黑匣子。我建议教案里加一个最小仿真脚本,不依赖任何第三方库,装完 Python 3.10 就能跑。它的意义不是模拟真实网关,而是把状态变化顺序印在脑子里:SUBMITTED → ACK → DELIVRD。
# mock_sms_flow.py # 最小短信发送流程仿真:业务侧提交、网关路由、运营商回执 import time import uuid from datetime import datetime def submit_sms(phone: str, content: str): """业务侧提交消息,分配 msgid 并写入初始状态""" msgid = uuid.uuid4().hex[:16] print(f"[SUBMIT] msgid={msgid} phone={phone} time={datetime.now()}") return { "msgid": msgid, "phone": phone, "content": content, "status": "SUBMITTED", "channel": "", } def route_sms(msg): """网关侧按号段路由,这里用 mock 通道代替真实 CMPP/SMPP 连接""" if msg["phone"].startswith("138"): msg["channel"] = "cmpp-mock" else: msg["channel"] = "smpp-mock" msg["status"] = "ACK" print(f"[ROUTE] msgid={msg['msgid']} channel={msg['channel']} status={msg['status']}") return msg def deliver_report(msg, delay: float = 1): """模拟运营商回执,默认投递成功 DELIVRD""" time.sleep(delay) msg["status"] = "DELIVRD" print(f"[REPORT] msgid={msg['msgid']} stat=DELIVRD done_time={datetime.now()}") return msg if __name__ == "__main__": msg = submit_sms("13800009999", "验证码:123456,5分钟内有效。") msg = route_sms(msg) msg = deliver_report(msg) print(f"[FINAL] phone={msg['phone']} status={msg['status']}")保存为 mock_sms_flow.py,在终端执行python3 mock_sms_flow.py。你会看到三条带时间戳的日志,顺序与真实发送流程一致。逻辑上,submit_sms 负责创造消息并分配 msgid,这一步对应真实系统里写发送记录;route_sms 根据号段把消息扔给不同通道,对应网关侧的路由表;deliver_report 延迟一秒后把状态改成 DELIVRD,对应运营商回执。
参数说明里最值得关注的是 delay。它模拟的是“运营商下发给手机”这段网络时间,真实环境里通常 3 到 30 秒。如果 delay 超过你的超时阈值,教案里就该引出一个问题:这条消息算失败吗?答案是不算,只能继续等回执,超时重发要按幂等规则来。
3.2 状态机表格与异步化改造建议
仿真脚本把发送流程压缩成了同步函数,真实系统里每一步都是异步的。教案里对应放一张状态机表,比堆协议字段有用得多。
| 状态 | 产生方 | 含义 | 下一步动作 |
|---|---|---|---|
| SUBMITTED | 业务系统 | 已生成消息,尚未确认 | 等待网关 ACK |
| ACK | 网关 | 网关已接收,不代表送达 | 等待状态报告 |
| SENT | 网关 | 已送运营商短信中心 | 继续等待最终回执 |
| DELIVRD | 运营商 | 手机已收到 | 流程结束 |
| UNDELIV | 运营商 | 投递失败 | 查原因,决定是否重发 |
| EXPIRED | 运营商 | 超过有效期未下发 | 终止,不重发 |
| REJECTD | 网关 | 被拒绝,如签名或模板不合法 | 修正后重发 |
真实系统里,SUBMITTED 之后的消息通常要进消息队列,用 Kafka 或 Redis 做削峰。网关侧提交后,回调线程按 msgid 找到对应记录更新状态;回执线程再按 msgid 更新最终结果。教案里抛一个问题给学员:如果 ACK 还没回来,业务服务重启了,这条消息应该怎么处理?从业方案是落一张发送流水表,以 msgid 为唯一键,重启后从未确认状态捞起来重新查询网关,而不是直接重发。
4. 状态报告与重试:把回执字段读对,短信发送流程才算闭环
4.1 回执字段到底怎么解析:DELIVRD 不是唯一答案
短信发送流程中最容易翻车的环节是回执解析。很多新手看到 stat 字段等于 DELIVRD 就认为成功,看到 UNDELIV 就重发,完全忽略 done_time、err_code 和 submit_date。回执要和原始提交记录按 msgid 关联,先查流水再改状态,否则回执晚到几分钟就会把成功记录覆盖成失败。
# parse_report.py # 回执解析:按 msgid 关联发送记录,区分最终状态和中间状态 REPORT_FINAL_STATES = {"DELIVRD", "UNDELIV", "EXPIRED", "REJECTD"} def handle_report(report: dict, send_records: dict): """report 包含 msgid/stat/done_time,send_records 是发送流水表""" record = send_records.get(report["msgid"]) if record is None: print(f"[ORPHAN] msgid={report['msgid']} 找不到对应提交记录") return stat = report.get("stat", "") print(f"[REPORT] msgid={report['msgid']} old={record['status']} new={stat} done={report.get('done_time')}") # 只做最终状态覆盖,避免 ACK/SENT 中间状态把结果改脏 if stat in REPORT_FINAL_STATES: record["status"] = stat if stat != "DELIVRD": print(f"[ALERT] msgid={report['msgid']} 失败原因 err_code={report.get('err_code')}")这里的关键是按 msgid 找到原始记录这一行。真实通道回执有两个最常见的坑:一是回执里的 msgid 是你提交时返回的,不是你自己生成的 UUID,需要建立映射;二是回执可能乱序,DELIVRD 先到、SENT 后到,所以只允许最终状态覆盖最终状态。代码里的 REPORT_FINAL_STATES 就是用来拦截中间状态的过滤器,没有它,晚到的 SENT 会把 DELIVRD 覆盖成“已发送”,线上就会出“明明成功却显示发送中”的幻觉。
4.2 重试退避与幂等:三个必调参数
短信重发是最容易引发客诉的操作。用户收不到验证码,业务侧第一反应是重发,结果网关回执只是延迟,用户最后收到两三条相同验证码。教案里要明确:重试只针对“明确知道网关没收到的场景”,例如 ACK 超时或 NACK,收到 UNDELIV、EXPIRED 这种运营商回执后不要自动重发,转人工或者走语音验证码兜底。
# retry_policy.py # 短信重试策略:指数退避 + 幂等保护 RETRY_CONFIG = { "max_retry": 3, "base_interval_sec": 5, "timeout_sec": 30, "power": 2, } def should_retry(status: str, retry_count: int) -> bool: """只有 ACK 超时才重试,明确的运营商失败状态不重试""" if status in ("UNDELIV", "EXPIRED", "REJECTD"): return False if retry_count >= RETRY_CONFIG["max_retry"]: return False return True def next_interval(retry_count: int) -> int: """退避间隔:5s、10s、20s,指数递增""" return RETRY_CONFIG["base_interval_sec"] * (RETRY_CONFIG["power"] ** retry_count)参数上最值得调整的是 timeout_sec。国内验证码通道业内常见阈值是 30 到 60 秒,如果设成 5 秒,基本每次都会误判超时然后重发。base_interval_sec 建议从 5 秒起,不要用固定 1 秒去轰炸网关。max_retry 超过 3 次意义不大,因为第 4 次重发时用户已经去点“获取语音验证码”了。
幂等部分必须落到数据库唯一索引上。从业方案里,发送流水表以 request_id 为唯一键,同一业务请求重复提交时直接返回已有 msgid,网关侧再配合业务侧的流水号去重,才能扛住双重重试。只靠 Redis 分布式锁不够,锁过期后第二次请求还是会进来。
5. 短信发送流程的避坑清单:从乱码到到达率玄学的 5 个真实场景
5.1 短信内容乱码,签名被截断
现象:用户收到的短信出现火星文,或者签名“【XX银行】”消失。原因:网关侧按 GSM-7 编码解析你传的 UTF-8 字符串,中文字符被拆错;另一种情况是短信超过 70 个字符被按长短信拆分,签名拼接位置不对被截断。解决:统一在提交参数里指定编码为 UCS-2,中文内容按 70 字一计费单位;长短信用网关的拼接能力,签名放在内容开头而不是结尾,并且不要把签名算进变量长度里。
5.2 状态显示 DELIVRD,用户硬说没收到
现象:发送记录里 stat=DELIVRD,用户投诉没收到。原因:DELIVRD 只代表运营商短信中心下发给手机成功,不代表用户看到了。手机关机后开机补发、短信被手机系统拦截、伪基站干扰,都会造成这种“假成功”。解决:别急着反驳用户,先按手机号找通道方要运营商侧的下发详情,确认短信中心下发时间和终端状态;同时让用户检查拦截列表。教案里要强调,回执是最终参考,不是唯一真相。
5.3 ACK 超时重发,用户收到两条验证码
现象:同一验证码短时间内收到两条短信,时间间隔 1 分钟左右。原因:业务侧设置的超时时间太短,网关实际已接收但 ACK 回传延迟,业务侧判断超时后重发。解决:把幂等键落到 request_id,重发前先查同一 request_id 的提交记录;超时后先调网关查询接口确认消息状态,不要直接重发。这条在教案里应该单独作为反面案例,比讲十页协议格式都管用。
5.4 测试环境一切正常,生产环境全部拒收
现象:同一套代码,测试通道跑得通,切到生产通道后短信全部被拦截或回执 REJECTD。原因:生产签名或模板没有报备,报备的签名与提交的签名不一致,变量内容与模板不匹配。解决:把签名、模板 ID、变量内容拆成三个字段管理,提交前做本地校验;测试环境也要用生产同款签名做“影子报备”,不要让开发裸奔到上线才暴露。很多公司把这条归为配置问题,实际是流程缺失。
5.5 回执永远不来,全部卡在 ACK
现象:发送后网关 ACK 正常,但状态报告一直不回调,流水全停在 ACK。原因:提交时没有开启回执开关,或者回调地址配错、回调验签失败被网关丢弃。解决:提交参数里把 registered_delivery 置为 1;检查回调接口是否返回 200,验签失败时要能看到日志而不是静默丢弃。这个坑我见过多次,多数是网关文档里叫回执标志,开发漏传了。
| 现象 | 首选排查位置 | 误判代价 |
|---|---|---|
| 乱码截断 | 编码参数与签名位置 | 用户看不懂短信 |
| DELIVRD 但没收到 | 运营商下发日志 | 客诉升级 |
| 重复验证码 | 幂等键与超时配置 | 资金安全风险 |
| 生产拒收 | 签名模板报备 | 上线失败 |
| 卡在 ACK | 回执开关与回调日志 | 功能不可用 |
6. 用教案做验收:从链路图到消息追踪练习
6.1 第一课先画链路图,再谈协议
我带人学短信发送流程时,第一个动作不是看 pptx 里的报文格式,而是让他在白板上画出六个节点:业务系统 → 发送流水 → 网关接口 → 运营商短信中心 → 用户手机 → 状态报告回流。每个节点写清楚状态字段和超时阈值,画完这张图,协议字段才有位置安放。教案里应该把这张链路图作为课前作业,而不是教学内容。
6.2 必考题:三分钟定位一条丢了的短信
最后一个练习,我会出一道必考题:用户在晚上 23:00 说没收到验证码,给你 msgid,三分钟内说出定位步骤。正确顺序是:第一步查流水,确认状态停在 SUBMITTED 还是 ACK;第二步查网关日志,确认 ACK 有没有回来;第三步调通道方查询接口拿到运营商回执;第四步看手机是否正常。每一步都有明确的负责人和后手,而不是一句“帮我查一下”。这条链路走一遍,教案的内容才算真正闭环。我现在的习惯是,任何短信接入项目,先备好语音验证码兜底,再把回执解析和幂等重试写成单元测试,最后才调 UI。希望帮到你。
本文还有配套的精品资源,点击获取