教你用Rust实现Smpp协议
本文分享自华为云社区《华为云短信服务教你用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(())
}

相关开源项目
- netty-codec-sms 存放各种SMS协议(如cmpp、sgip、smpp)的netty编解码器
 - sms-client-java 存放各种SMS协议的Java客户端
 - sms-server-java 存放各种SMS协议的Java服务端
 - smpp-rust smpp协议的rust实现
 
总结
本文简单对SMPP协议进行了介绍,并尝试用rust实现协议栈,但实际商用发送短信往往更加复杂,面临诸如流控、运营商对接、传输层安全等问题,可以选择华为云消息&短信(Message & SMS)服务,华为云短信服务是华为云携手全球多家优质运营商和渠道,为企业用户提供的通信服务。企业调用API或使用群发助手,即可使用验证码、通知短信服务。
教你用Rust实现Smpp协议的更多相关文章
- [Micropython][ESP8266] TPYBoard V202 之MQTT协议接入OneNET云平台
		
随着移动互联网的发展,MQTT由于开放源代码,耗电量小等特点,将会在移动消息推送领域会有更多的贡献,在物联网领域,传感器与服务器的通信,信息的收集,MQTT都可以作为考虑的方案之一.在未来MQTT会进 ...
 - Lua编写wireshark插件初探——解析Websocket上的MQTT协议
		
一.背景 最近在做物联网流量分析时发现, App在使用MQTT协议时往往通过SSL+WebSocket+MQTT这种方式与服务器通信,在使用SSL中间人截获数据后,Wireshark不能自动解析出MQ ...
 - 早产的《HelloGitHub》第 65 期
		
兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 分享 GitHub 上有趣.入门级的开源项目. 这里有实战项目.入门教程.黑科技.开源书籍.大厂开源项目等,涵盖多种编程语言 Pyt ...
 - 【商业源码】生日大放送-Newlife商业源码分享
		
今天是农历六月二十三,是@大石头的生日,记得每年生日都会有很劲爆的重量级源码送出,今天Newlife群和论坛又一次疯狂了,吃水不忘挖井人,好的东西肯定要拿到博客园分享.Newlife组件信息: 论坛: ...
 - CMPP错误码说明
		
与中国移动代码的对应关系. MI::zzzzSMSC返回状态报告的状态值为EXPIREDMJ:zzzzSMSC返回状态报告的状态值为DELETEDMK:zzzzSMSC返回状态报告的状态值为UNDEL ...
 - CMPP3.0 长短信实现方案
		
长短信息:是指超过70个汉字,140个字节的信息内容 一.CMPP协议相关字段分析 CMPP协议具体部分请参考<中国移动互联网短信网关接口协议(V3.0.0).doc> CMPP_SUBM ...
 - 一篇文章,读懂 Netty 的高性能架构之道
		
原文 Netty是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机 ...
 - Wireshark命令行工具tshark
		
Wireshark命令行工具tshark 1.目的 写这篇博客的目的主要是为了方便查阅,使用wireshark可以分析数据包,可以通过编辑过滤表达式来达到对数据的分析:但我的需求是,怎么样把Data部 ...
 - FreeSWITCH第三方库(其他)的简单介绍(三)
		
FreeSWITCH使用了大量的第三方库,本文档主要介绍关联相关库的信息: 音频相关库的信息介绍参考:http://www.cnblogs.com/yoyotl/p/5486753.html 视频相关 ...
 - Netty系列之Netty可靠性分析
		
作者 李林锋 发布于 2014年6月19日 | 29 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件分享 稍后阅读 我的阅读清单 1. 背景 1.1. 宕机的代价 1.1. ...
 
随机推荐
- 【CubeMX】使用 CubeMX 生成对应的配置代码需要设置 “User Label”
			
如要生成 SPI 的管脚配置代码,需要设置 User Label,这样工具才能知道应该配置什么,否则不会生成
 - 百度网盘(百度云)SVIP超级会员共享账号每日更新(2023.12.2)
			
一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...
 - 海思Hi35xx 通过uboot 读取U盘文件进行固件升级
			
前言 基本过程为:uboot 启动后,通过命令将U盘的的文件读取到内存中,再通过uboot 的flash 写入命令将读取到内存中的升级文件写入到flash的固定位置. (一)usb常用命令 uboot ...
 - 【scikit-learn基础】--『回归模型评估』之偏差分析
			
模型评估在统计学和机器学习中具有至关重要,它帮助我们主要目标是量化模型预测新数据的能力. 本篇主要介绍模型评估时,如何利用scikit-learn帮助我们快速进行各种偏差的分析. 1. **R² ** ...
 - [转帖]TiDB Lightning 在数据迁移中的应用与错误处理实践
			
TiDB Lightning 在数据迁移中的应用与错误处理实践 作者简介:DBA,会点 MySQL,懂点 TiDB,Python. 个人主页:https://tidb.net/u/seiang/ans ...
 - [转帖]jmeter正则表达式提取器获取数组数据-02篇
			
接上篇,当我们正则表达式匹配到多个值以后,入下图所示,匹配到21个结果,如果我们想一次拿到这一组数据怎么办呢 打开正则表达式提取器页面,匹配数字填入-1即可 通过调试取样器就可以看到匹配到已经匹配到多 ...
 - [转帖]py_innodb_page_info.py工具使用
			
目录 一.Linux安装Python3 1. 解压包 2. 安装环境 3. 生成编译脚本 4. 检查python3.10的编译器 5. 建立Python3和pip3的软链 6. 添加到PATH 7. ...
 - [转帖]360孵化奇安信科创板上市,IPO前清空股权赚37亿元分手费
			
https://baijiahao.baidu.com/s?id=1666485645739027654&wfr=spider&for=pc 来源:IPO头条 来源:IPO头条原创 ...
 - [转帖]漏洞预警|Apache Tomcat 信息泄露漏洞
			
http://www.hackdig.com/03/hack-953615.htm 棱镜七彩安全预警 近日网上有关于开源项目Apache Tomcat 信息泄露漏洞,棱镜七彩威胁情报团队第一时间探测到 ...
 - CentOS7 上面升级git 2.24的方法
			
本来想使用tar包进行安装 但是发现tar包安装时总是报错如下: [root@centos76 git-2.25.0]# make LINK git-imap-send imap-send.o: In ...