写点什么

教你用 Rust 实现 Smpp 协议

  • 2024-02-18
    广东
  • 本文字数:4655 字

    阅读完需:约 15 分钟

教你用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(())}
复制代码


相关开源项目


总结


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


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

发布于: 刚刚阅读数: 2
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
教你用Rust实现Smpp协议_rust_华为云开发者联盟_InfoQ写作社区