本文分享自华为云社区《华为云短信服务教你用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或使用群发助手,即可使用验证码、通知短信服务。

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

华为云短信服务教你用C++实现Smgp协议的更多相关文章

  1. php 阿里云短信服务及阿里大鱼实现短信验证码的发送

    一:使用阿里云的短信服务 ① 申请短信签名 ②申请短信模板 ③创建Access Key,获取AccessKeyId 与 AccessKeySecret.(为了安全起见,这里建议使用子用户的Access ...

  2. 阿里云短信服务bug

    接入阿里云短信服务,在springboot中写测试方法,执行到 IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou ...

  3. 阿里云短信服务调用例子-Python

    阿里云短信服务调用例子 阿里云官方文档https://helpcdn.aliyun.com/document_detail/101893.html 首先需要安装阿里云PythonSDK(下面是pyth ...

  4. ThinkPHP3.2.3框架下接入阿里云短信服务接口实现:注册登录

    首先介绍下短信注册登录流程: 注册页面点击获取手机号验证码按钮,用jquery的click事件POST或GET方法把手机号发送到后台控制器: 后台控制器创建函数,收到手机号后生成随机码,例如:6位的随 ...

  5. 移动端获取短信验证码java实现——阿里云短信服务

    需求:移动端输入手机号,获取验证码.点击登录,验证验证码是否输入错误.是否超时等情况,一旦校验通过,将用户数据保存到数据中(业务逻辑). 前提:注册阿里用户,开通短信服务,申请key.秘钥.签名.短信 ...

  6. 浏览器端获取短信验证码java实现——阿里云短信服务

    需求:浏览器端输入手机号,获取验证码.点击登录,验证验证码是否输入错误.是否超时等情况,一旦校验通过,将用户数据保存到数据中(业务逻辑). 前提:注册阿里用户,开通短信服务,申请key.秘钥.签名.短 ...

  7. flask+阿里云短信服务实现注册发送手机验证码

    效果图: 该效果主要讲解实现通过调用阿里云的SDK实现发送注册验证码短信(阿里云短信付费使用) 购买阿里云短信服务 购买链接:https://www.aliyun.com/product/sms 1. ...

  8. springboot 使用阿里云短信服务发送验证码

    一.申请阿里云短信服务 1.申请签名 2.申请模板 3.创建accesskey(鼠标悬停在右上角头像) 二.代码实现 1.springboot引入maven依赖 <dependency> ...

  9. 使用springboot集成腾讯云短信服务,解决配置文件读取乱码问题

    springboot集成腾讯云短信服务: (1)导入依赖 <dependency> <groupId>org.springframework.boot</groupId& ...

  10. Zabbix 3.4.3 使用阿里云短信服务进行报警

    目录 一.阿里云短信服务 1.1.首先开通阿里云短信服务 1.2 创建签名 1.3 创建短信模板 1.4 创建发送脚本 二.Zabbix Web 配置 2.1 增加 Media types 2.2 给 ...

随机推荐

  1. 力扣1668(java&python)-最大重复子字符串(简单)

    题目: 给你一个字符串 sequence ,如果字符串 word 连续重复 k 次形成的字符串是 sequence 的一个子字符串,那么单词 word 的 重复值为 k .单词 word 的 最大重复 ...

  2. 数百万台车联网设备同时在线0故障,中瑞集团的云原生探索之路 | 云原生Talk

    简介: 在保持对业界趋势调度关注的同时,始终选用最适合自身的技术,这可能是中瑞能在车联网领域引领行业的重要原因之一,正如中瑞CTO所说"阿里云云原生产品体系带给我们的,不是单纯的IT工具,而 ...

  3. 微信不再提供小程序打开App?借助H5为App引流的方式你必须知道!

    简介: 2021年5月14日App开发者领域发布了一条重要消息:微信开放平台为了提升用户体验,将于2021年5月20日(后来延期到2021年5月27日)起不再提供"小程序打开App技术服务& ...

  4. Alibaba FFI -- 跨语言编程的探索

    ​简介: 跨语言编程时现代程序语言中非常重要的一个方向,也被广泛应用于复杂的设计与实现中. 跨语言编程是现代程序语言中非常重要的一个方向,也被广泛应用于复杂系统的设计与实现中.本文是 GIAC 202 ...

  5. [FE] nvm-windows: Microsoft/NPM/Google 推荐 Windows 的 Node.js 版本管理器, posix 的 nvm-sh/nvm

    1. 到 github 下载 nvm-setup.zip 并安装. Releases · coreybutler/nvm-windows (github.com) 2. 安装一个版本的 nodejs. ...

  6. [Trading] 股票日内交易者能赚多少钱 - 看到日内交易的潜力并学习如何实现它

    无论是为了生活方式.寻求刺激还是为了挑战,股票市场短线交易者能赚多少钱的问题不可避免地出现了. 股票日内交易者的交易量变化很大,一些当日交易者损失了他们的资金,而另一些人利用他们的资金创造了一个高的月 ...

  7. HarmonyOS 应用生命周期有哪些? 按返回键会调用哪些生命周期?

    UIAbility 生命周期: onCreate :页面初始化,变量定义,资源加载. onWindowStageCreate:设置 UI 界面加载.设置 WindowStage 的事件订阅. onFo ...

  8. Gem离线包安装

    Gem离线包安装 项目环境 以 rest-client 为例 本地如果是rails项目环境: ruby '2.7.0' gem 'rails', '~> 6.0.3', '>= 6.0.3 ...

  9. Linux中的touch命令

    Linux中一个文件有3种时间属性,分别是mtime,ctime,atime: modification time (mtime) 当该文件的『内容数据』变更时,就会升级这个时间!内容数据指的是文件的 ...

  10. Ubuntu-kali配置动态ip(简单)

    使用gedit文本编辑器打开网络接口配置文件 gedit /etc/network/interfaces 新增两行内容如下: auto eth0 iface eth0 inet dhcp 其意思为:网 ...