跳到主要内容

SMSc - 短信服务中心应用程序

基于 Phoenix/Elixir 构建的短信中心应用程序,提供集中式消息队列和用于短信路由和交付的 REST API。

文档

核心文档

合规性文档

性能

架构概述

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 为 null
  • deliver_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"
}

重要: PUTPATCH 的行为不同:

  • PUT → 增加交付尝试并使用退避(不需要请求体)
  • PATCH → 执行部分字段更新(需要请求体)

前端健康跟踪

SMS_C 核心通过注册系统跟踪外部前端的健康和可用性。这允许监控前端的正常运行时间,检测故障,并维护历史可用性数据。

注意: 前端注册不用于消息交付或路由。消息根据 dest_smsc 字段进行路由。注册系统仅用于健康监控和操作可见性。

前端注册工作原理

每个外部前端(SMPP、IMS、SS7/MAP 网关)定期向 SMS_C 核心发送心跳注册:

  1. 心跳间隔:前端应每 30-60 秒注册一次
  2. 过期时间:注册在 90 秒内未更新后过期
  3. 自动状态管理
    • 新前端创建新的注册记录
    • 现有活动前端更新其注册(延长过期时间)
    • 过期的前端重新上线后创建新的注册周期

前端注册端点

注册/更新前端(心跳)
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"
# 启动前端应用程序

消息未被交付

症状: 消息保持未交付,重试尝试增加

检查:

  1. 前端日志中的发送错误
  2. 外部网络连接性
  3. 前端配置(凭据、地址)
  4. 消息格式兼容性

查看失败的消息:

# 获取重试尝试的消息
curl https://localhost:8443/api/messages | jq '.data[] | select(.delivery_attempts > 0)'

高消息延迟

症状: 消息花费时间比预期长,队列积压

检查:

  1. 前端轮询间隔(可能需要减少以更频繁地轮询)
  2. 数据库性能
  3. 与外部系统的网络延迟

监控队列深度:

watch -n 5 'curl -s https://localhost:8443/api/messages | jq ".data | length"'