本文分享自華為雲社群《華為雲簡訊服務教你用C++實現Smgp協議》,作者:張儉。
引言&協議概述
中國聯合網路通訊有限公司短訊息閘道器係統介面協議(SGIP)是中國網通為實現簡訊業務而制定的一種通訊協議,全稱叫做Short Message Gateway Interface Protocol,用於在短訊息閘道器(SMG)和服務提供商(SP)之間、短訊息閘道器(SMG)和短訊息閘道器(SMG)之間通訊。
Perl的IO::Async模組提供了一套簡潔的非同步IO程式設計模型。
SGIP 協議基於客戶端/服務端模型工作。由客戶端(簡訊應用,如手機,應用程式等)先和簡訊閘道器(SMG Short Message Gateway)建立起 TCP 長連線,並使用 SGIP 命令與SMG進行互動,實現簡訊的傳送和接收。在SGIP協議中,無需同步等待響應就可以傳送下一個指令,實現者可以根據自己的需要,實現同步、非同步兩種訊息傳輸模式,滿足不同場景下的效能要求。
時序圖
連線成功,傳送簡訊
連線成功,從SMGW接收到簡訊
協議幀介紹
SGIP Header
- Message Length:長度為4位元組,整個PDU的長度,包括Header和Body。
- Command ID:長度為4位元組,用於標識PDU的型別(例如,Login、Submit等)。
- Sequence Number:長度為8位元組,序列號,用來匹配請求和響應。
使用C++實現SMGP協議棧裡的建立連線
├── CMakeLists.txt
├── examples
│ └── smgp_client_login_example.cpp
└── include
└── sgipcpp
├── BoundAtomic.h
├── Client.h
├── Protocol.h
└── impl
├── BoundAtomic.cpp
├── Client.cpp
└── Protocol.cpp
CMakeLists.txt:用來生成Makefile和編譯專案
examples:存放示例程式碼- smgp_client_login_example.cpp:存放Smgp的login樣例
- BoundAtomic.h:遞增工具類,用來生成SequenceId
- Client.h:Smgp定義,負責與Smgp服務進行通訊,例如建立連線、傳送簡訊等
- Protocol.h:存放PDU,編解碼等
- impl/BoundAtomic.cpp:BoundAtomic類的實現
- impl/Client.cpp:Client類的實現
- impl/Protocol.cpp:Protocol中相關函式的實現
實現SequenceId遞增
SequenceId是從1到0x7FFFFFFF的值,使用**BoundAtomic
**類實現遞增:
標頭檔案
#ifndef BOUNDATOMIC_H #define BOUNDATOMIC_H #include <atomic> #include <cassert> class BoundAtomic { public: BoundAtomic(int min, int max); int next_val(); private: int min_; int max_; std::atomic<int> integer_; }; #endif //BOUNDATOMIC_H
內容
#include "sgipcpp/BoundAtomic.h" BoundAtomic::BoundAtomic(int min, int max) : min_(min), max_(max), integer_(min) { assert(min <= max); } int BoundAtomic::next_val() { int current = integer_.load(); int next; do { next = current >= max_ ? min_ : current + 1; } while (!integer_.compare_exchange_strong(current, next)); return next; }
實現SMGP PDU以及編解碼函式
在**Protocol.h
**中定義SMGP PDU以及編解碼函式:
標頭檔案
#ifndef PROTOCOL_H #define PROTOCOL_H #include <cstdint> #include <vector> constexpr uint32_t SGIP_BIND = 0x00000001; constexpr uint32_t SGIP_BIND_RESP = 0x80000001; constexpr uint32_t SGIP_UNBIND = 0x00000002; constexpr uint32_t SGIP_UNBIND_RESP = 0x80000002; constexpr uint32_t SGIP_SUBMIT = 0x00000003; constexpr uint32_t SGIP_SUBMIT_RESP = 0x80000003; constexpr uint32_t SGIP_DELIVER = 0x00000004; constexpr uint32_t SGIP_DELIVER_RESP = 0x80000004; constexpr uint32_t SGIP_REPORT = 0x00000005; constexpr uint32_t SGIP_REPORT_RESP = 0x80000005; constexpr uint32_t SGIP_ADDSP = 0x00000006; constexpr uint32_t SGIP_ADDSP_RESP = 0x80000006; constexpr uint32_t SGIP_MODIFYSP = 0x00000007; constexpr uint32_t SGIP_MODIFYSP_RESP = 0x80000007; constexpr uint32_t SGIP_DELETESP = 0x00000008; constexpr uint32_t SGIP_DELETESP_RESP = 0x80000008; constexpr uint32_t SGIP_QUERYROUTE = 0x00000009; constexpr uint32_t SGIP_QUERYROUTE_RESP = 0x80000009; constexpr uint32_t SGIP_ADDTELESEG = 0x0000000A; constexpr uint32_t SGIP_ADDTELESEG_RESP = 0x8000000A; constexpr uint32_t SGIP_MODIFYTELESEG = 0x0000000B; constexpr uint32_t SGIP_MODIFYTELESEG_RESP = 0x8000000B; constexpr uint32_t SGIP_DELETETELESEG = 0x0000000C; constexpr uint32_t SGIP_DELETETELESEG_RESP = 0x8000000C; constexpr uint32_t SGIP_ADDSMG = 0x0000000D; constexpr uint32_t SGIP_ADDSMG_RESP = 0x8000000D; constexpr uint32_t SGIP_MODIFYSMG = 0x0000000E; constexpr uint32_t SGIP_MODIFYSMG_RESP = 0x8000000E; constexpr uint32_t SGIP_DELETESMG = 0x0000000F; constexpr uint32_t SGIP_DELETESMG_RESP = 0x8000000F; constexpr uint32_t SGIP_CHECKUSER = 0x00000010; constexpr uint32_t SGIP_CHECKUSER_RESP = 0x80000010; constexpr uint32_t SGIP_USERRPT = 0x00000011; constexpr uint32_t SGIP_USERRPT_RESP = 0x80000011; constexpr uint32_t SGIP_TRACE = 0x00001000; constexpr uint32_t SGIP_TRACE_RESP = 0x80001000; struct Header { uint32_t total_length; uint32_t command_id; uint64_t sequence_number; }; struct Bind { char login_type; char login_name[16]; char login_passwd[16]; char reserve[8]; }; struct BindResp { char result; char reserve[8]; }; struct Pdu { Header header; union { Bind bind; BindResp bind_resp; }; }; size_t lengthBind(); std::vector<uint8_t> encodePdu(const Pdu& pdu); Pdu decodePdu(const std::vector<uint8_t>& buffer); #endif //PROTOCOL_H
內容
#include "sgipcpp/Protocol.h" #include <cstring> #include <ostream> #include <stdexcept> #include <sys/_endian.h> size_t lengthBind(const Bind& bind) { return 1 + 16 + 16 + 8; } void encodeBind(const Bind& bind, std::vector<uint8_t>& buffer) { size_t offset = 16; buffer[offset++] = bind.login_type; std::memcpy(buffer.data() + offset, bind.login_name, 16); offset += 16; std::memcpy(buffer.data() + offset, bind.login_passwd, 16); offset += 16; std::memcpy(buffer.data() + offset, bind.reserve, 8); } BindResp decodeBindResp(const std::vector<uint8_t>& buffer) { BindResp bindResp; size_t offset = 0; offset += sizeof(uint32_t); offset += sizeof(uint32_t); bindResp.result = buffer[offset++]; std::memcpy(bindResp.reserve, buffer.data() + offset, sizeof(bindResp.reserve)); return bindResp; } std::vector<uint8_t> encodePdu(const Pdu& pdu) { size_t body_length; switch (pdu.header.command_id) { case SGIP_BIND: body_length = lengthBind(pdu.bind); break; default: throw std::runtime_error("Unsupported command ID for encoding"); } std::vector<uint8_t> buffer(body_length + 16); uint32_t total_length = htonl(body_length + 16); std::memcpy(buffer.data(), &total_length, 4); uint32_t command_id = htonl(pdu.header.command_id); std::memcpy(buffer.data() + 4, &command_id, 4); uint32_t sequence_number = htonl(pdu.header.sequence_number); std::memcpy(buffer.data() + 8, &sequence_number, 8); switch (pdu.header.command_id) { case SGIP_BIND: encodeBind(pdu.bind, buffer); break; default: throw std::runtime_error("Unsupported command ID for encoding"); } return buffer; } Pdu decodePdu(const std::vector<uint8_t>& buffer) { Pdu pdu; uint32_t command_id; std::memcpy(&command_id, buffer.data(), 4); pdu.header.command_id = ntohl(command_id); uint64_t sequence_number; std::memcpy(&sequence_number, buffer.data() + 8, 8); pdu.header.sequence_number = ntohl(sequence_number); switch (pdu.header.command_id) { case SGIP_BIND_RESP: pdu.bind_resp = decodeBindResp(buffer); break; default: throw std::runtime_error("Unsupported command ID for decoding"); } return pdu; }
實現客戶端和登入方法
在**Client
**中實現客戶端和登入方法:
標頭檔案
#ifndef CLIENT_H #define CLIENT_H #include "BoundAtomic.h" #include "Protocol.h" #include "asio.hpp" #include <string> class Client { public: Client(const std::string& host, uint16_t port); ~Client(); void connect(); BindResp bind(const Bind& bind_request); void close(); private: std::string host_; uint16_t port_; asio::io_context io_context_; asio::ip::tcp::socket socket_; BoundAtomic* sequence_number_; void send(const std::vector<uint8_t>& data); std::vector<uint8_t> receive(size_t length); }; #endif //CLIENT_H
內容
#include "sgipcpp/Client.h" #include <iostream> Client::Client(const std::string& host, uint16_t port) : host_(host), port_(port), socket_(io_context_) { sequence_number_ = new BoundAtomic(1, 0x7FFFFFFF); } Client::~Client() { close(); delete sequence_number_; } void Client::connect() { asio::ip::tcp::resolver resolver(io_context_); asio::connect(socket_, resolver.resolve(host_, std::to_string(port_))); } BindResp Client::bind(const Bind& bind_request) { Pdu pdu; pdu.header.total_length = sizeof(Bind) + sizeof(Header); pdu.header.command_id = SGIP_BIND; pdu.header.sequence_number = sequence_number_->next_val(); pdu.bind = bind_request; send(encodePdu(pdu)); auto length_data = receive(4); uint32_t total_length = ntohl(*reinterpret_cast<uint32_t*>(length_data.data())); auto resp_data = receive(total_length - 4); Pdu resp_pdu = decodePdu(resp_data); return resp_pdu.bind_resp; } void Client::close() { socket_.close(); } void Client::send(const std::vector<uint8_t>& data) { asio::write(socket_, asio::buffer(data)); } std::vector<uint8_t> Client::receive(size_t length) { std::vector<uint8_t> buffer(length); asio::read(socket_, asio::buffer(buffer)); return buffer; }
執行example,驗證連線成功
#include "sgipcpp/Client.h" #include <iostream> int main() { try { Client client("127.0.0.1", 8801); client.connect(); std::cout << "Connected to the server." << std::endl; Bind bindRequest; bindRequest.login_type = 1; std::string login_name = "1234567890123456"; std::string login_password = "1234567890123456"; std::string reserve = "12345678"; std::copy(login_name.begin(), login_name.end(), bindRequest.login_name); std::copy(login_password.begin(), login_password.end(), bindRequest.login_passwd); std::copy(reserve.begin(), reserve.end(), bindRequest.reserve); BindResp response = client.bind(bindRequest); if (response.result == 0) { std::cout << "Login successful." << std::endl; } else { std::cout << "Login failed with result code: " << static_cast<int>(response.result) << std::endl; } client.close(); std::cout << "Connection closed." << std::endl; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; } return 0; }
相關開源專案
- netty-codec-sms 存放各種SMS協議(如cmpp、sgip、smpp)的netty編解碼器
- sms-client-java 存放各種SMS協議的Java客戶端
- sms-server-java 存放各種SMS協議的Java服務端
- cmpp-python cmpp協議的python實現
- cngp-zig cmpp協議的python實現
- sgip-cpp sgip協議的cpp實現
- smgp-perl smgp協議的perl實現
- smpp-rust smpp協議的rust實現
總結
本文簡單對SGIP協議進行了介紹,並嘗試用C++實現協議棧,但實際商用傳送簡訊往往更加複雜,面臨諸如流控、運營商對接、傳輸層安全等問題,可以選擇華為雲訊息&簡訊(Message & SMS)服務透過HTTP協議接入,華為雲簡訊服務是華為雲攜手全球多家優質運營商和渠道,為企業使用者提供的通訊服務。企業呼叫API或使用群發助手,即可使用驗證碼、通知簡訊服務。
點選關注,第一時間瞭解華為雲新鮮技術~