写点什么

华为云短信服务教你用 C++ 实现 Smgp 协议

  • 2024-06-11
    广东
  • 本文字数:5527 字

    阅读完需:约 18 分钟

华为云短信服务教你用C++实现Smgp协议

本文分享自华为云社区《华为云短信服务教你用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;}
复制代码


相关开源项目


总结


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


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

发布于: 5 小时前阅读数: 10
用户头像

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

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

评论

发布
暂无评论
华为云短信服务教你用C++实现Smgp协议_c++_华为云开发者联盟_InfoQ写作社区