本文分享自华为云社区《华为云短信服务教你用Rust实现Smpp协议》,作者: 张俭。

协议概述

SMPP(Short Message Peer-to-Peer)协议起源于90年代,最初由Aldiscon公司开发,后来由SMPP开发者论坛维护和推广。SMPP常用于在SMSC(Short Message Service Center,短信中心)和短信应用之间传输短消息,支持高效的短信息发送、接收和查询功能,是电信运营商和短信服务提供商之间互通短信的主要协议之一。

SMPP协议基于客户端/服务端模型工作。由客户端(短信应用,如手机,应用程序等)先和SMSC建立起TCP长连接,并使用SMPP命令与SMSC进行交互,实现短信的发送和接收。在SMPP协议中,无需同步等待响应就可以发送下一个指令,实现者可以根据自己的需要,实现同步、异步两种消息传输模式,满足不同场景下的性能要求。

时序图

绑定transmitter模式,发送短信并查询短信发送成功

绑定receiver模式,从SMSC接收到短信

协议帧介绍

在SMPP协议中,每个PDU都包含两个部分:SMPP Header和SMPP Body。

SMPP Header

Header包含以下字段,大小长度都是4字节:

  • Command Length:整个PDU的长度,包括Header和Body。
  • Command ID:用于标识PDU的类型(例如,BindReceiver、QuerySM等)。
  • Command Status:响应状态码,表示处理的结果。
  • Sequence Number:序列号,用来匹配请求和响应。

用Rust实现SMPP协议栈里的BindTransmitter

本文的代码均已上传到smpp-rust

选用Tokio作为基础的异步运行时环境,tokio有非常强大的异步IO支持,也是rust库的事实标准。

代码结构组织如下:

├── lib.rs
├── const.rs
├── protocol.rs
├── smpp_client.rs
└── smpp_server.rs
  • lib.rs Rust项目的入口点
  • const.rs 包含常量定义,如commandId、状态码等
  • protocol.rs 包含PDU定义,编解码处理等
  • smpp_client.rs 实现smpp客户端逻辑
  • smpp_server.rs 实现

利用rust原子类实现sequence_number

sequence_number是从1到0x7FFFFFFF的值,利用Rust的AtomicI32来生成这个值。

use std::sync::atomic::{AtomicI32, Ordering};
use std::num::TryFromIntError; struct BoundAtomicInt {
min: i32,
max: i32,
integer: AtomicI32,
} impl BoundAtomicInt {
pub fn new(min: i32, max: i32) -> Self {
assert!(min <= max, "min must be less than or equal to max");
Self {
min,
max,
integer: AtomicI32::new(min),
}
} pub fn next_val(&self) -> Result<i32, TryFromIntError> {
let next = self.integer.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| {
Some(if x >= self.max { self.min } else { x + 1 })
})?;
Ok(next)
}
}

在Rust中定义SMPP PDU

pub struct SmppPdu {
pub header: SmppHeader,
pub body: SmppBody,
} pub struct SmppHeader {
pub command_length: i32,
pub command_id: i32,
pub command_status: i32,
pub sequence_number: i32,
} pub enum SmppBody {
BindReceiver(BindReceiver),
BindReceiverResp(BindReceiverResp),
BindTransmitter(BindTransmitter),
BindTransmitterResp(BindTransmitterResp),
QuerySm(QuerySm),
QuerySmResp(QuerySmResp),
SubmitSm(SubmitSm),
SubmitSmResp(SubmitSmResp),
DeliverSm(DeliverSm),
DeliverSmResp(DeliverSmResp),
Unbind(Unbind),
UnbindResp(UnbindResp),
ReplaceSm(ReplaceSm),
ReplaceSmResp(ReplaceSmResp),
CancelSm(CancelSm),
CancelSmResp(CancelSmResp),
BindTransceiver(BindTransceiver),
BindTransceiverResp(BindTransceiverResp),
Outbind(Outbind),
EnquireLink(EnquireLink),
EnquireLinkResp(EnquireLinkResp),
SubmitMulti(SubmitMulti),
SubmitMultiResp(SubmitMultiResp),
}

实现编解码方法

impl SmppPdu {
pub fn encode(&self) -> Vec<u8> {
let mut body_buf = match &self.body {
SmppBody::BindTransmitter(bind_transmitter) => bind_transmitter.encode(),
_ => unimplemented!(),
}; let command_length = (body_buf.len() + 16) as i32;
let header = SmppHeader {
command_length,
command_id: self.header.command_id,
command_status: self.header.command_status,
sequence_number: self.header.sequence_number,
}; let mut buf = header.encode();
buf.append(&mut body_buf);
buf
} pub fn decode(buf: &[u8]) -> io::Result<Self> {
let header = SmppHeader::decode(&buf[0..16])?;
let body = match header.command_id {
constant::BIND_TRANSMITTER_RESP_ID => SmppBody::BindTransmitterResp(BindTransmitterResp::decode(&buf[16..])?),
_ => unimplemented!(),
};
Ok(SmppPdu { header, body })
}
} impl SmppHeader {
pub(crate) fn encode(&self) -> Vec<u8> {
let mut buf = vec![];
buf.extend_from_slice(&self.command_length.to_be_bytes());
buf.extend_from_slice(&self.command_id.to_be_bytes());
buf.extend_from_slice(&self.command_status.to_be_bytes());
buf.extend_from_slice(&self.sequence_number.to_be_bytes());
buf
} pub(crate) fn decode(buf: &[u8]) -> io::Result<Self> {
if buf.len() < 16 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Buffer too short for SmppHeader"));
}
let command_id = u32::from_be_bytes(buf[0..4].try_into().unwrap());
let command_status = i32::from_be_bytes(buf[4..8].try_into().unwrap());
let sequence_number = i32::from_be_bytes(buf[8..12].try_into().unwrap());
Ok(SmppHeader {
command_length: 0,
command_id,
command_status,
sequence_number,
})
}
} impl BindTransmitter {
pub(crate) fn encode(&self) -> Vec<u8> {
let mut buf = vec![];
write_cstring(&mut buf, &self.system_id);
write_cstring(&mut buf, &self.password);
write_cstring(&mut buf, &self.system_type);
buf.push(self.interface_version);
buf.push(self.addr_ton);
buf.push(self.addr_npi);
write_cstring(&mut buf, &self.address_range);
buf
} pub(crate) fn decode(buf: &[u8]) -> io::Result<Self> {
let mut offset = 0;
let system_id = read_cstring(buf, &mut offset)?;
let password = read_cstring(buf, &mut offset)?;
let system_type = read_cstring(buf, &mut offset)?;
let interface_version = buf[offset];
offset += 1;
let addr_ton = buf[offset];
offset += 1;
let addr_npi = buf[offset];
offset += 1;
let address_range = read_cstring(buf, &mut offset)?; Ok(BindTransmitter {
system_id,
password,
system_type,
interface_version,
addr_ton,
addr_npi,
address_range,
})
}
}

实现同步的bind_transmitter方法

pub async fn bind_transmitter(
&mut self,
bind_transmitter: BindTransmitter,
) -> io::Result<BindTransmitterResp> {
if let Some(stream) = &mut self.stream {
let sequence_number = self.sequence_number.next_val();
let pdu = SmppPdu {
header: SmppHeader {
command_length: 0,
command_id: constant::BIND_TRANSMITTER_ID,
command_status: 0,
sequence_number,
},
body: SmppBody::BindTransmitter(bind_transmitter),
};
let encoded_request = pdu.encode();
stream.write_all(&encoded_request).await?; let mut length_buf = [0u8; 4];
stream.read_exact(&mut length_buf).await?;
let msg_length = u32::from_be_bytes(length_buf) as usize - 4; let mut msg_buf = vec![0u8; msg_length];
stream.read_exact(&mut msg_buf).await?; let response = SmppPdu::decode(&msg_buf)?;
if response.header.command_status != 0 {
Err(io::Error::new(
io::ErrorKind::Other,
format!("Error response: {:?}", response.header.command_status),
))
} else {
// Assuming response.body is of type BindTransmitterResp
match response.body {
SmppBody::BindTransmitterResp(resp) => Ok(resp),
_ => Err(io::Error::new(io::ErrorKind::InvalidData, "Unexpected response body")),
}
}
} else {
Err(io::Error::new(io::ErrorKind::NotConnected, "Not connected"))
}
}

运行example,验证连接成功

use smpp_rust::protocol::BindTransmitter;
use smpp_rust::smpp_client::SmppClient; #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = SmppClient::new("127.0.0.1", 2775);
client.connect().await?;
let bind_transmitter = BindTransmitter{
system_id: "system_id".to_string(),
password: "password".to_string(),
system_type: "system_type".to_string(),
interface_version: 0x34,
addr_ton: 0,
addr_npi: 0,
address_range: "".to_string(),
};
client.bind_transmitter(bind_transmitter).await?;
client.close().await?;
Ok(())
}

相关开源项目

总结

本文简单对SMPP协议进行了介绍,并尝试用rust实现协议栈,但实际商用发送短信往往更加复杂,面临诸如流控、运营商对接、传输层安全等问题,可以选择华为云消息&短信(Message & SMS)服务,华为云短信服务是华为云携手全球多家优质运营商和渠道,为企业用户提供的通信服务。企业调用API或使用群发助手,即可使用验证码、通知短信服务。

点击关注,第一时间了解华为云新鲜技术~

教你用Rust实现Smpp协议的更多相关文章

  1. [Micropython][ESP8266] TPYBoard V202 之MQTT协议接入OneNET云平台

    随着移动互联网的发展,MQTT由于开放源代码,耗电量小等特点,将会在移动消息推送领域会有更多的贡献,在物联网领域,传感器与服务器的通信,信息的收集,MQTT都可以作为考虑的方案之一.在未来MQTT会进 ...

  2. Lua编写wireshark插件初探——解析Websocket上的MQTT协议

    一.背景 最近在做物联网流量分析时发现, App在使用MQTT协议时往往通过SSL+WebSocket+MQTT这种方式与服务器通信,在使用SSL中间人截获数据后,Wireshark不能自动解析出MQ ...

  3. 早产的《HelloGitHub》第 65 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 分享 GitHub 上有趣.入门级的开源项目. 这里有实战项目.入门教程.黑科技.开源书籍.大厂开源项目等,涵盖多种编程语言 Pyt ...

  4. 【商业源码】生日大放送-Newlife商业源码分享

    今天是农历六月二十三,是@大石头的生日,记得每年生日都会有很劲爆的重量级源码送出,今天Newlife群和论坛又一次疯狂了,吃水不忘挖井人,好的东西肯定要拿到博客园分享.Newlife组件信息: 论坛: ...

  5. CMPP错误码说明

    与中国移动代码的对应关系. MI::zzzzSMSC返回状态报告的状态值为EXPIREDMJ:zzzzSMSC返回状态报告的状态值为DELETEDMK:zzzzSMSC返回状态报告的状态值为UNDEL ...

  6. CMPP3.0 长短信实现方案

    长短信息:是指超过70个汉字,140个字节的信息内容 一.CMPP协议相关字段分析 CMPP协议具体部分请参考<中国移动互联网短信网关接口协议(V3.0.0).doc> CMPP_SUBM ...

  7. 一篇文章,读懂 Netty 的高性能架构之道

    原文 Netty是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机 ...

  8. Wireshark命令行工具tshark

    Wireshark命令行工具tshark 1.目的 写这篇博客的目的主要是为了方便查阅,使用wireshark可以分析数据包,可以通过编辑过滤表达式来达到对数据的分析:但我的需求是,怎么样把Data部 ...

  9. FreeSWITCH第三方库(其他)的简单介绍(三)

    FreeSWITCH使用了大量的第三方库,本文档主要介绍关联相关库的信息: 音频相关库的信息介绍参考:http://www.cnblogs.com/yoyotl/p/5486753.html 视频相关 ...

  10. Netty系列之Netty可靠性分析

      作者 李林锋 发布于 2014年6月19日 | 29 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件分享 稍后阅读 我的阅读清单   1. 背景 1.1. 宕机的代价 1.1. ...

随机推荐

  1. 【CubeMX】使用 CubeMX 生成对应的配置代码需要设置 “User Label”

    如要生成 SPI 的管脚配置代码,需要设置 User Label,这样工具才能知道应该配置什么,否则不会生成

  2. 百度网盘(百度云)SVIP超级会员共享账号每日更新(2023.12.2)

    一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...

  3. 海思Hi35xx 通过uboot 读取U盘文件进行固件升级

    前言 基本过程为:uboot 启动后,通过命令将U盘的的文件读取到内存中,再通过uboot 的flash 写入命令将读取到内存中的升级文件写入到flash的固定位置. (一)usb常用命令 uboot ...

  4. 【scikit-learn基础】--『回归模型评估』之偏差分析

    模型评估在统计学和机器学习中具有至关重要,它帮助我们主要目标是量化模型预测新数据的能力. 本篇主要介绍模型评估时,如何利用scikit-learn帮助我们快速进行各种偏差的分析. 1. **R² ** ...

  5. [转帖]TiDB Lightning 在数据迁移中的应用与错误处理实践

    TiDB Lightning 在数据迁移中的应用与错误处理实践 作者简介:DBA,会点 MySQL,懂点 TiDB,Python. 个人主页:https://tidb.net/u/seiang/ans ...

  6. [转帖]jmeter正则表达式提取器获取数组数据-02篇

    接上篇,当我们正则表达式匹配到多个值以后,入下图所示,匹配到21个结果,如果我们想一次拿到这一组数据怎么办呢 打开正则表达式提取器页面,匹配数字填入-1即可 通过调试取样器就可以看到匹配到已经匹配到多 ...

  7. [转帖]py_innodb_page_info.py工具使用

    目录 一.Linux安装Python3 1. 解压包 2. 安装环境 3. 生成编译脚本 4. 检查python3.10的编译器 5. 建立Python3和pip3的软链 6. 添加到PATH 7.  ...

  8. [转帖]360孵化奇安信科创板上市,IPO前清空股权赚37亿元分手费

      https://baijiahao.baidu.com/s?id=1666485645739027654&wfr=spider&for=pc 来源:IPO头条 来源:IPO头条原创 ...

  9. [转帖]漏洞预警|Apache Tomcat 信息泄露漏洞

    http://www.hackdig.com/03/hack-953615.htm 棱镜七彩安全预警 近日网上有关于开源项目Apache Tomcat 信息泄露漏洞,棱镜七彩威胁情报团队第一时间探测到 ...

  10. CentOS7 上面升级git 2.24的方法

    本来想使用tar包进行安装 但是发现tar包安装时总是报错如下: [root@centos76 git-2.25.0]# make LINK git-imap-send imap-send.o: In ...