SMSc - 短信服务中心应用程序
基于 Phoenix/Elixir 构建的短信中心应用程序,提供集中式消息队列和用于短信路由和交付的 REST API。
文档
核心文档
- 完整文档索引 - 从这里开始查看所有文档
- 配置参考 - 完整的配置选项
- API 参考 - REST API 文档
- 操作指南 - 日常操作和监控
- 短信路由指南 - 路由管理和配置
- 号码翻译指南 - 号码标准化和重写
- 性能调优 - 针对不同工作负载的优化
- 指标指南 - Prometheus 指标和监控
- 故障排除指南 - 常见问题及解决方案
- CDR 架构 - 通话详细记录格式
合规性文档
- ANSSI R226 拦截合规性 - 法国合法拦截技术规范
性能
- 基准测试 - 性能测试
架构概述
SMS_C 核心提供协议无关的消息队列和 REST API。外部 SMSC 前端(SMPP、IMS、SS7/MAP)作为独立网关连接,通过 REST API 与核心进行通信。
消息流
出站消息流 (MT - 移动终止)
入站消息流 (MO - 移动发起)
主要特性
1. 协议无关设计
- SMS_C 核心处理消息持久化、路由和 API
- 外部前端(SMPP、IMS、SS7/MAP)处理协议特定的通信
- 所有前端通过统一的 REST API 进行通信
- 添加新协议无需更改核心
- 每个前端可以独立扩展
2. 消息路由
- 动态路由引擎,具有运行时配置
- 基于前缀的路由(呼叫/被叫号码)
- SMSC 和源类型过滤(IMS/电路交换/SMPP)
- 基于优先级的路由和加权负载均衡
- 自动回复路由和消息丢弃能力
- 每条路由的计费控制
- 用于路由管理的 Web UI
- 实时路由更新,无需服务中断
📖 请参见 短信路由指南 获取全面文档
3. 带有指数退避的重试逻辑
- 交付失败时自动重试
- 指数退避:1分钟、2分钟、4分钟、8分钟等
- 可配置的最大重试次数
- 消息过期处理
- 每条消息的重试跟踪
操作指南
访问点:
- REST API:
https://localhost:8443(或http://localhost:8080,不使用 TLS) - 控制面板:
https://localhost:8086 - API 文档(Swagger UI):
https://localhost:8443/api/docs
启动外部前端: 每个协议前端都是一个独立的应用程序。请参阅各个前端文档以获取启动说明。
配置
所有配置直接在 config/runtime.exs 中管理。未使用环境变量。
核心配置
当前未使用核心应用程序配置环境变量。服务器端口和主机名在 config/runtime.exs 中配置:
- API 服务器:端口 8443(HTTPS),监听 0.0.0.0
- 控制面板:端口 80(HTTP),监听 0.0.0.0
数据库配置
数据库设置在 config/runtime.exs 中配置:
- 用户名:
omnitouch - 密码:
omnitouch2024 - 主机名:
localhost - 端口:
3306 - 数据库名称:
smsc_new - 池大小:
1
集群配置
集群设置在 config/runtime.exs 中配置:
- 集群节点:
""(空 - 默认不集群) - DNS 集群查询:
nil
消息队列配置
消息队列设置在 config/runtime.exs 中配��:
- 死信时间:
1440分钟(消息过期前 24 小时)
计费集成
计费设置在 config/runtime.exs 中配置:
- URL:
http://localhost:2080/jsonrpc - 租户:
mnc057.mcc505.3gppnetwork.org - 目标:
55512341234 - 源:
00101900000257 - 主题:
00101900000257 - 账户:
00101900000257
短信路由配置
短信路由系统使用动态、基于数据库的路由,可以通过 Web UI 或配置文件进行管理。路由在首次启动时从 config/runtime.exs 加载。
配置示例:
config :sms_c, :sms_routes, [
%{
called_prefix: "+44",
dest_smsc: "InternationalGW",
weight: 100,
priority: 100,
description: "英国国际短信",
enabled: true
},
%{
called_prefix: "1900",
dest_smsc: "PremiumGW",
charged: :yes,
priority: 50,
description: "美国高档号码",
enabled: true
}
]
特性:
- 基于前缀的匹配(呼叫/被叫号码)
- 源 SMSC 和类型过滤
- 基于优先级和权重的路由
- 自动回复和丢弃能力
- 每条路由的计费控制
- 通过 Web UI 在
/routing进行运行时管理
📖 请参见 短信路由指南 获取完整文档、示例和 API 参考。
REST API 端点
消息队列操作
提交短信(创建消息)
POST /api/messages
Content-Type: application/json
{
"source_msisdn": "+1234567890",
"destination_msisdn": "+0987654321",
"message_body": "Hello, World!",
"source_smsc": "web-app",
"dest_smsc": "smpp-provider", # 可选 - 路由引擎在为空时分配
"tp_dcs_character_set": "gsm7", # 可选: gsm7, 8bit, latin1, ucs2
"tp_dcs_coding_group": "general_data_coding",
"expires": "2025-10-17T10:30:00Z" # 可选 - 默认为 24 小时后
}
必填字段:
destination_msisdn- 目标电话号码message_body- 消息文本内容source_msisdn- 源电话号码source_smsc- 源系统标识符
可选字段:
dest_smsc- 目标网关(如果未提供,则由路由引擎分配)source_imsi,dest_imsi- IMSI 标识符tp_dcs_character_set- 字符编码(gsm7, 8bit, latin1, ucs2)tp_dcs_coding_group- DCS 编码组tp_dcs_compressed- 压缩标志(布尔值)tp_dcs_has_message_class- 消息类标志(布尔值)tp_dcs_message_class- 消息类值tp_user_data_header- 用户数据头(映射)message_part_number,message_parts- 多部分消息字段expires- 过期时间戳(默认为 24 小时)deliver_after- 延��交付时间戳deadletter,raw_data_flag,raw_sip_flag- 布尔标志
响应:
{
"status": "success",
"data": {
"id": 123,
"source_msisdn": "+1234567890",
"destination_msisdn": "+0987654321",
"dest_smsc": "smpp-provider",
"message_body": "Hello, World!",
"deliver_time": null,
"delivery_attempts": 0,
"expires": "2025-10-17T10:30:00Z",
"inserted_at": "2025-10-16T10:30:00Z"
}
}
获取 SMSC 的消息
GET /api/messages/get_by_smsc?smsc=my-smsc-name
返回所有未送达的消息,其中:
destination_smsc为 null 或与提供的 SMSC 名称匹配- 消息未过期
- 准备发送(deliver_after 为 null 或在过去)
响应:
{
"status": "success",
"data": [
{
"id": 123,
"source_msisdn": "+1234567890",
"destination_msisdn": "+0987654321",
"message_body": "Hello",
"destination_smsc": "my-smsc-name",
"delivery_attempts": 0
}
]
}
列出带可选 SMSC 过滤的消息
# 列出队列中的所有消息
GET /api/messages
# 列出特定 SMSC 的消息(带头部过滤)
GET /api/messages
SMSc: my-smsc-name
没有 SMSc 头部: 返回队列中所有消��,无论交付状态或过期情况。
有 SMSc 头部: 返回未送达的消息,其中:
dest_smsc匹配头部值或dest_smsc为 nulldeliver_time为 null(尚未交付)deliver_after为 null 或在当前时间之前/等于(准备交付)expires在当前时间之后(未过期)- 按插入时间排序(最旧的在前)
注意: SMSc 头部方法允许外部前端使用相同的端点模式轮询其消息,头部控制过滤行为。
响应:
[
{
"id": 123,
"source_msisdn": "+1234567890",
"destination_msisdn": "+0987654321",
"message_body": "Hello, World!",
"dest_smsc": "my-smsc-name",
"deliver_time": null,
"delivery_attempts": 0,
"expires": "2025-10-17T10:30:00Z",
"inserted_at": "2025-10-16T10:30:00Z"
}
]
获取单条消息
GET /api/messages/{id}
更新消息
PATCH /api/messages/{id}
Content-Type: application/json
{
"status": "delivered",
"delivered_at": "2025-10-16T10:30:00Z"
}
删除短信
DELETE /api/messages/{id}
处理交付失败(增加重试计数器)
当消息交付暂时失败时,增加交付尝试计数器并安排重试,使用指数退避。
方法 1:使�� PUT(推荐)
# 简单且语义明确 - PUT 表示更新交付状态
PUT /api/messages/{id}
方法 2:使用显式端点
# 替代显式端点
POST /api/messages/{id}/increment_delivery_attempt
这两种方法都会增加 delivery_attempts,并通过 deliver_after 设置指数退避延迟:
| 尝试 | 退避公式 | 延迟 | 总时间 |
|---|---|---|---|
| 第 1 次 | 2^1 分钟 | 2 分钟 | 2 分钟 |
| 第 2 次 | 2^2 分钟 | 4 分钟 | 6 分钟 |
| 第 3 次 | 2^3 分钟 | 8 分钟 | 14 分钟 |
| 第 4 次 | 2^4 分钟 | 16 分钟 | 30 分钟 |
| 第 5 次 | 2^5 分钟 | 32 分钟 | 1小时2分钟 |
| 第 6 次 | 2^6 分钟 | 64 分钟 | 2小时6分钟 |
响应:
{
"id": 123,
"delivery_attempts": 1,
"deliver_after": "2025-10-20T19:05:00Z",
"deliver_time": null,
"expires": "2025-10-21T19:03:00Z",
...
}
注意: 未来的 deliver_after 消息会在 GET 请求中自动过滤,直到退避期结束。
更新消息(部分更新)
用于更新特定消息字段(未更改行为):
PATCH /api/messages/{id}
Content-Type: application/json
{
"dest_smsc": "updated-gateway",
"status": "delivered"
}
重要: PUT 和 PATCH 的行为不同:
- PUT → 增加交付尝试并使用退避(不需要请求体)
- PATCH → 执行部分字段更新(需要请求体)
前端健康跟踪
SMS_C 核心通过注册系统跟踪外部前端的健康和可用性。这允许监控前端的正常运行时间,检测故障,并维护历史可用性数据。
注意: 前端注册不用于消息交付或路由。消息根据 dest_smsc 字段进行路由。注册系统仅用于健康监控和操作可见性。
前端注册工作原理
每个外部前端(SMPP、IMS、SS7/MAP 网关)定期向 SMS_C 核心发送心跳注册:
- 心跳间隔:前端应每 30-60 秒注册一次
- 过期时间:注册在 90 秒内未更新后过期
- 自动状态管理:
- 新前端创建新的注册记录
- 现有活动前端更新其注册(延长过期时间)
- 过期的前端重新上线后创建新的注册周期
前端注册端点
注册/更新前端(心跳)
POST /api/frontends
Content-Type: application/json
{
"frontend_name": "smpp-gateway-1",
"frontend_type": "SMPP",
"ip_address": "10.0.1.5",
"hostname": "smpp-gw-01",
"uptime_seconds": 3600,
"configuration": "{\"port\": 2775, \"system_id\": \"smpp_user\"}"
}
必填字段:
frontend_name- 前端实例的唯一标识符frontend_type- 前端类型(SMPP、IMS、MAP 等)
可选字段:
ip_address- 前端的 IP 地址(如果未提供,则从请求源自动检测)hostname- 前端服务器的主机名uptime_seconds- 前端启动以来的秒数configuration- 包含前端特定配置的 JSON 字符串
注意: 如果未提供 ip_address,SMS_C 核心将自动使用 HTTP 请求的源 IP。这适用于直接连接和代理请求(通过 X-Forwarded-For 头)。
响应:
{
"id": 42,
"frontend_name": "smpp-gateway-1",
"frontend_type": "SMPP",
"ip_address": "10.0.1.5",
"hostname": "smpp-gw-01",
"uptime_seconds": 3600,
"status": "active",
"last_seen_at": "2025-10-20T10:30:00Z",
"expires_at": "2025-10-20T10:31:30Z",
"inserted_at": "2025-10-20T10:00:00Z"
}
列出所有前端注册
GET /api/frontends
返回所有前端注册(活动和过期),按最近活动排序。
仅列出活动前端
GET /api/frontends/active
仅返回当前活动(未过期)前端。
获取前端统计信息
GET /api/frontends/stats
返回汇总统计信息:
{
"active": 5,
"expired": 12,
"unique_frontends": 8
}
获取前端历史
GET /api/frontends/history/{frontend_name}
返回特定前端的所有历史注册,便于分析正常运行时间/停机模式。
示例:
GET /api/frontends/history/smpp-gateway-1
获取特定注册
GET /api/frontends/{id}
外部前端的实现
外部前端应实现一个后台任务,发送心跳:
示例(伪代码):
import time
import requests
def send_heartbeat():
"""每 30 秒发送一次心跳"""
while True:
try:
data = {
"frontend_name": "my-smpp-gateway",
"frontend_type": "SMPP",
"ip_address": get_local_ip(),
"hostname": get_hostname(),
"uptime_seconds": get_uptime()
}
response = requests.post(
"https://smsc-core:8443/api/frontends",
json=data,
timeout=5
)
if response.status_code in [200, 201]:
logger.debug("心跳发送成功")
else:
logger.error(f"心跳失败: {response.status_code}")
except Exception as e:
logger.error(f"心跳错误: {e}")
time.sleep(30) # 每 30 秒发送一次
# 在后台线程中启动心跳
threading.Thread(target=send_heartbeat, daemon=True).start()
监控前端健康
控制面板 - Web UI 在 https://localhost:8086 显示:
- 当前活动的前端
- 每个前端的最后一次看到时间戳
- 正常运行时间跟踪
- 历史可用性
API 查询:
# 获取所有活动前端
curl https://localhost:8443/api/frontends/active
# 检查特定前端是否正常
curl https://localhost:8443/api/frontends/history/smpp-gateway-1 | jq '.[0].status'
# 获取健康统计信息
curl https://localhost:8443/api/frontends/stats
其��端点
状态
GET /api/status
位置
GET /api/locations
POST /api/locations
GET /api/locations/{id}
PATCH /api/locations/{id}
DELETE /api/locations/{id}
SS7 事件
GET /api/ss7_events
POST /api/ss7_events
GET /api/ss7_events/{id}
PATCH /api/ss7_events/{id}
DELETE /api/ss7_events/{id}
MMS 消息队列
GET /api/mms_message_queues
POST /api/mms_message_queues
GET /api/mms_message_queues/{id}
PATCH /api/mms_message_queues/{id}
DELETE /api/mms_message_queues/{id}
性能
SMS_C 核心使用 Mnesia 进行内存消息存储,并自动归档到 SQL,以实现出色的吞吐量和长期 CDR 保留。
基准测试结果
在 Intel i7-8650U @ 1.90GHz(8 核心)上测量:
消息插入性能:
insert_message(带路由):1,750 条消息/秒(平均延迟 0.58 毫秒)insert_message(简单):1,750 条消息/秒(平均延迟 0.57 毫秒)- ~1.5 亿条消息/天 的容量
查询性能:
get_messages_for_smsc: 800 条消息/秒(平均 1.25 毫秒)list_message_queues: 快速的内存访问- 内存使用:每次插入操作 62 KB
架构
存储策略:
- 活动消息:存储在 Mnesia(内存 + 磁盘)中,以实现超快访问
- 消息归档:自动归档到 SQL 以进行长期 CDR 存储
- 保留:可配置的保留期(默认:24 小时)
- 无 SQL 瓶颈:所有活动消息操作绕过 SQL
配置
消息存储和保留在 config/runtime.exs 中配置:
config :sms_c,
message_retention_hours: 24, # 归档超过 24 小时的消息
batch_insert_batch_size: 100, # SQL 归档的 CDR 批大小
batch_insert_flush_interval_ms: 100 # CDR 刷新间隔
有关详细调优指导,请参见: docs/PERFORMANCE_TUNING.md
监控
控制面板 - Web UI 在 https://localhost:8086
- 查看消息队列
- 提交测试消息
- 管理短信路由(请参见 路由指南)
- 模拟路由决策
- 查看系统资源
- 跟踪批处理工作者统计信息
批处理工作者统计信息:
# 获取当前批处理工作者统计信息
SmsC.Messaging.BatchInsertWorker.stats()
返回:
%{
total_enqueued: 10000,
total_flushed: 9900,
current_queue_size: 100,
last_flush_duration_ms: 45
}
日志 - 应用程序日志写入 stdout
# 实时查看日志
tail -f log/dev.log
故障排除
端口已被使用
# 查找使用端口的进程
lsof -i :4000
# 杀死该进程
kill -9 <PID>
外部前端未连接
症状: 消息卡在队列中,前端日志显示连接错误
检查:
- 验证
API_BASE_URL在前端中设置正确 - 检查 SMS_C 核心是否正在运行并可访问
- 查看网络/防火墙规则
- 验证前端配置
解决方案:
# 测试前端的 API 连接性
curl http://localhost:4000/api/status
# 重启前端
export API_BASE_URL="http://localhost:4000"
# 启动前端应用程序
消息未被交付
症状: 消息保持未交付,重试尝试增加
检查:
- 前端日志中的发送错误
- 外部网络连接性
- 前端配置(凭据、地址)
- 消息格式兼容性
查看失败的消息:
# 获取重试尝试的消息
curl https://localhost:8443/api/messages | jq '.data[] | select(.delivery_attempts > 0)'
高消息延迟
症状: 消息花费时间比预期长,队列积压
检查:
- 前端轮询间隔(可能需要减少以更频繁地轮询)
- 数据库性能
- 与外部系统的网络延迟
监控队列深度:
watch -n 5 'curl -s https://localhost:8443/api/messages | jq ".data | length"'