本文分享自華爲雲社區《華爲雲短信服務教你用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或使用羣發助手,即可使用驗證碼、通知短信服務。