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

smgp · 浏览次数 : 11

小编点评

本文主要介绍了如何使用C++语言实现基于华为云短信服务的Smgp协议。通过C++实现Smgp协议栈,可以更好地理解Smgp协议的工作原理和应用场景。 1. 引言部分介绍了中国联合网络通信有限公司短消息网关系统接口协议(SGIP)的定义和特点,以及对Smgp协议的基本概念和工作方式的描述。 2. 协议概述部分详细介绍了SGIP协议的工作原理,包括客户端/服务端模型的构建、TCP长连接的建立、命令与响应的交换等。 3. 连接成功,发送短信部分展示了如何使用C++实现的Smgp客户端连接到短信网关并发送短信的示例代码。 4. 连接成功,从SMGW接收到短信部分详细介绍了如何在C++中处理从短信网关收到的PDU包。 5. 协议帧介绍部分总结了Smgp协议的各个组成部分,包括Header、Message Length、Command ID、Sequence Number等字段的意义和用途。 6. 实现Smgp协议栈里的建立连接部分描述了如何初始化数据结构、编码和解码Pdu以及建立连接的过程。 7. 实现客户端和提交方法部分实现了客户端的基本功能,包括连接管理、消息发送和接收处理。 总的来说,本文通过一系列的示例代码和头文件,为读者提供了一个实用的C++实现Smgp协议的框架。尽管实际商用应用中还需要考虑更多因素,如流控、运营商对接、传输层安全等,但这些示例代码仍然能帮助开发者入门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接收到短信

 
 
 

协议帧介绍

image.png

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;
}

image.png

相关开源项目

总结

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

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

 

与华为云短信服务教你用C++实现Smgp协议相似的内容:

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

本文简单对SGIP协议进行了介绍,并尝试用C++实现协议栈,但实际商用发送短信往往更加复杂,可以选择华为云消息&短信服务通过HTTP协议接入。

云图说|移动应用安全服务—App的体检中心,全面检测,安全上路!

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 摘要: 移动应用安全服务,面向App安全检测服务,为企业解决App(包括安卓/鸿蒙)安全漏洞、隐私合规等安全问题。 本文分享

云小课|使用SpringBoot快速构建FunctionGraph HTTP函数

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 摘要:本篇云小课主要指导使用Springboot应用的用户,快速部署业务到函数工作流FunctionGraph服务。 本文分

云小课|MRS基础原理之Oozie任务调度

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 摘要:Oozie是一个基于工作流引擎的开源框架,它能够提供对Hadoop作业的任务调度与协调。Oozie是一个工作流引擎服务

云小课|帮您高效快速上传组件至私有依赖库

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 摘要:软件开发平台中的发布服务类似于网盘,可以存储任何软件包和工具。用户可以使用发布服务中的私有依赖库(俗称私服),管理Ma

云小课|云小课带你玩转可视化分析ELB日志

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 云日志服务支持可视化查看日志,当ELB日志接入云日志服务后,可通过SQL语句查询分析日志,将日志结果保存为多种图表,并将图表

云小课|基于华为云WAF的日志运维分析,构筑设备安全的城墙

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 摘要:云日志服务用于收集来自主机和云服务的日志数据,通过海量日志数据的分析与处理,可以将云服务和应用程序的可用性和性能最大化

AI赋能音乐创作,人人都是音视频创作者

华为HMS Core音频编辑服务(Audio Editor Kit)依托自身AI技术的研发优势,上线全新的歌声合成音色及伴奏,给音视频创作者提供更多的创作可能。在短视频场景中,用户自定义歌词的歌声结合视频让用户感受到身临其境,自由表达自己的情绪;在虚拟偶像场景中,歌声合成功能赋予虚拟歌手们演唱风格各

云小课|GaussDB如何进行性能调优

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 摘要:GaussDB性能调优过程需要综合考虑多方面因素,因此,调优人员应对系统软件架构、软硬件配置、数据库配置参数、并发控制

云小课|GaussDB(DWS)数据存储尽在掌控,冷热数据切换自如

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 摘要: GaussDB(DWS)支持根据业务系统的不同使用需求,对膨胀的数据进行冷热分级管理,将数据按照时间分为热数据、冷数