• 华为云短信服务教你用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接收到短信

     
     
     

    协议帧介绍

    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 
    #include 
    
    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 
    #include 
    
    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 encodePdu(const Pdu& pdu);
    Pdu decodePdu(const std::vector& buffer);
    
    #endif //PROTOCOL_H
    复制代码

    内容

    复制代码
    #include "sgipcpp/Protocol.h"
    #include 
    #include 
    #include 
    #include 
    
    size_t lengthBind(const Bind& bind) {
        return 1 + 16 + 16 + 8;
    }
    
    void encodeBind(const Bind& bind, std::vector& 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& 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 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 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& 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& data);
        std::vector receive(size_t length);
    };
    
    #endif //CLIENT_H
    复制代码

    内容

    复制代码
    #include "sgipcpp/Client.h"
    #include 
    
    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(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& data) {
        asio::write(socket_, asio::buffer(data));
    }
    
    std::vector Client::receive(size_t length) {
        std::vector buffer(length);
        asio::read(socket_, asio::buffer(buffer));
        return buffer;
    }
    复制代码

    运行example,验证连接成功

    复制代码
    #include "sgipcpp/Client.h"
    #include 
    
    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语言 统计汉字的个数
    ctfshow-ssti
    自动化测试面试题解析,半小时通透
    微服务保护--Sentinel
    ssm+jsp黄梅戏曲艺剧团管理系统
    会话跟踪cookie、session、token
    雇佣收银员(差分约束)
    笔记01:第一行Python
    MySQL8访问限制用户的创建
    嵌入式开发:使用快速应用程序开发 (RAD) 工具的3个技巧
  • 原文地址:https://www.cnblogs.com/huaweiyun/p/18241540