本文分享自华为云社区《华为云短信服务教你用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样例
include/sgipcpp:包含所有的c++头文件和实现文件
- 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或使用群发助手,即可使用验证码、通知短信服务。
发表评论