• 教你用Perl实现Smgp协议


    本文分享自华为云社区《华为云短信服务教你用Perl实现Smgp协议》,作者:张俭。

    引言&协议概述

    中国电信短消息网关协议(SMGP)是中国网通为实现短信业务而制定的一种通信协议,全称叫做Short Message Gateway Protocol,用于在短消息网关(SMGW)和服务提供商(SP)之间、短消息网关(SMGW)和短消息网关(SMGW)之间通信。

    Perl是一个老牌脚本语言,在众多Linux系统上都会默认安装,比如在ubuntu的22.04版本的基础镜像中,甚至没有Python,但是依然安装了Perl,Perl的普及度可见一斑。Perl的IO::Async模块提供了一套简洁的异步IO编程模型。

    SMGP 协议基于客户端/服务端模型工作。由客户端(短信应用,如手机,应用程序等)先和短信网关(SMGW Short Message Gateway)建立起 TCP 长连接,并使用 CNGP 命令与SMGW进行交互,实现短信的发送和接收。在CNGP协议中,无需同步等待响应就可以发送下一个指令,实现者可以根据自己的需要,实现同步、异步两种消息传输模式,满足不同场景下的性能要求。

    时序图

    连接成功,发送短信

     
     
     

    连接成功,从SMGW接收到短信

     
     
     

    协议帧介绍

    image.png

    SMGP Header

    Header包含以下字段,大小长度都是4字节

    • Packet Length:整个PDU的长度,包括Header和Body。
    • Request ID:用于标识PDU的类型(例如,Login、Submit等)。
    • Sequence Id:序列号,用来匹配请求和响应。

    使用perl实现SMGP协议栈里的建立连接

    复制代码
    ├── Makefile.PL
    ├── examples
    │   └── smgp_client_login_example.pl
    └── lib
        └── Smgp
            ├── BoundAtomic.pm
            ├── Client.pm
            ├── Constant.pm
            └── Protocol.pm
    复制代码
    Makefile.PL:用来生成Makefile

    examples:存放示例代码

    • smgp_client_login_example.pl:存放Smgp的login样例
    lib/Smgp:包含所有的Perl模块文件
    • BoundAtomic.pm:递增工具类,用来生成SequenceId
    • Client.pm:Smgp定义,负责与Smgp服务进行通信,例如建立连接、发送短信等
    • Protocol.pm:存放PDU,编解码等

    实现sequence_id递增

    sequence_id是从1到0x7FFFFFFF的值

    复制代码
    package Smgp::BoundAtomic;
    
    use strict;
    use warnings FATAL => 'all';
    
    sub new {
        my ($class, %args) = @_;
        my $self = {
            min     => $args{min},
            max     => $args{max},
            value   => $args{min},
        };
        bless $self, $class;
        return $self;
    }
    
    sub increment {
        my ($self) = @_;
        if ($self->{value} >= $self->{max}) {
            $self->{value} = $self->{min};
        } else {
            $self->{value}++;
        }
        return $self->{value};
    }
    
    sub get {
        my ($self) = @_;
        return $self->{value};
    }
    
    1;
    复制代码

    在Perl中定义SMGP PDU以及编解码函数

    复制代码
    package Smgp::Protocol;
    
    use strict;
    use warnings FATAL => 'all';
    
    use Smgp::Constant;
    
    sub new_login {
        my ($class, %args) = @_;
        my $self = {
            clientId            => $args{clientId},
            authenticatorClient => $args{authenticatorClient},
            loginMode           => $args{loginMode},
            timeStamp           => $args{timeStamp},
            version             => $args{version},
        };
        return bless $self, $class;
    }
    
    sub encode_login {
        my ($self) = @_;
        return pack("A8A16CNC", @{$self}{qw(clientId authenticatorClient loginMode timeStamp version)});
    }
    
    sub decode_login_resp {
        my ($class, $buffer) = @_;
        my ($status, $authenticatorServer, $version) = unpack("N4A16C", $buffer);
        return bless {
            status              => $status,
            authenticatorServer => $authenticatorServer,
            version             => $version,
        }, $class;
    }
    
    sub new_header {
        my ($class, %args) = @_;
        my $self = {
            total_length   => $args{total_length},
            request_id     => $args{request_id},
            command_status => $args{command_status},
            sequence_id    => $args{sequence_id},
        };
        return bless $self, $class;
    }
    
    sub encode_header {
        my ($self, $total_length) = @_;
        return pack("N3", $total_length, @{$self}{qw(request_id sequence_id)});
    }
    
    sub new_pdu {
        my ($class, %args) = @_;
        my $self = {
            header => $args{header},
            body   => $args{body},
        };
        return bless $self, $class;
    }
    
    sub encode_login_pdu {
        my ($self) = @_;
        my $encoded_body = $self->{body}->encode_login();
        return $self->{header}->encode_header(length($encoded_body) + 12) . $encoded_body;
    }
    
    sub decode_pdu {
        my ($class, $buffer) = @_;
        my ($request_id, $sequence_id) = unpack("N2", substr($buffer, 0, 8));
        my $body_buffer = substr($buffer, 8);
    
        my $header = $class->new_header(
            total_length   => 0,
            request_id     => $request_id,
            sequence_id    => $sequence_id,
        );
    
        my $body;
        if ($request_id == Smgp::Constant::LOGIN_RESP_ID) {
            $body = $class->decode_login_resp($body_buffer);
        } else {
            die "Unsupported request_id: $request_id";
        }
    
        return $class->new_pdu(
            header => $header,
            body   => $body,
        );
    }
    
    1;
    复制代码

    constant.pm存放相关requestId

    复制代码
    package Smgp::Constant;
    
    use strict;
    use warnings FATAL => 'all';
    
    use constant {
        LOGIN_ID               => 0x00000001,
        LOGIN_RESP_ID          => 0x80000001,
        SUBMIT_ID              => 0x00000002,
        SUBMIT_RESP_ID         => 0x80000002,
        DELIVER_ID             => 0x00000003,
        DELIVER_RESP_ID        => 0x80000003,
        ACTIVE_TEST_ID         => 0x00000004,
        ACTIVE_TEST_RESP_ID    => 0x80000004,
        FORWARD_ID             => 0x00000005,
        FORWARD_RESP_ID        => 0x80000005,
        EXIT_ID                => 0x00000006,
        EXIT_RESP_ID           => 0x80000006,
        QUERY_ID               => 0x00000007,
        QUERY_RESP_ID          => 0x80000007,
        MT_ROUTE_UPDATE_ID     => 0x00000008,
        MT_ROUTE_UPDATE_RESP_ID => 0x80000008,
    };
    
    1;
    复制代码

    实现client以及login方法

    复制代码
    package Smgp::Client;
    use strict;
    use warnings FATAL => 'all';
    use IO::Socket::INET;
    
    use Smgp::Protocol;
    use Smgp::Constant;
    
    sub new {
        my ($class, %args) = @_;
        my $self = {
            host => $args{host} // 'localhost',
            port => $args{port} // 9000,
            socket => undef,
            sequence_id => 1,
        };
        bless $self, $class;
        return $self;
    }
    
    sub connect {
        my ($self) = @_;
        $self->{socket} = IO::Socket::INET->new(
            PeerHost => $self->{host},
            PeerPort => $self->{port},
            Proto => 'tcp',
        ) or die "Cannot connect to $self->{host}:$self->{port} $!";
    }
    
    sub login {
        my ($self, $body) = @_;
        my $header = Smgp::Protocol->new_header(
            request_id     => Smgp::Constant::LOGIN_ID,
            sequence_id    => 1,
        );
    
        my $pdu = Smgp::Protocol->new_pdu(
            header => $header,
            body   => $body,
        );
    
        $self->{socket}->send($pdu->encode_login_pdu());
    
        $self->{socket}->recv(my $response_length_bytes, 4);
    
        my $total_length = unpack("N", $response_length_bytes);
        my $remain_length = $total_length - 4;
        $self->{socket}->recv(my $response_data, $remain_length);
    
        return Smgp::Protocol->decode_pdu($response_data)->{body};
    }
    
    sub disconnect {
        my ($self) = @_;
        close($self->{socket}) if $self->{socket};
    }
    
    1;
    复制代码

    运行example,验证连接成功

    复制代码
    package smgp_client_login_example;
    
    use strict;
    use warnings FATAL => 'all';
    
    use Smgp::Client;
    use Smgp::Protocol;
    use Smgp::Constant;
    
    sub main {
        my $client = Smgp::Client->new(
            host => 'localhost',
            port => 9000,
        );
    
        $client->connect();
    
        my $login = Smgp::Protocol->new_login(
            clientId            => '12345678',
            authenticatorClient => '1234567890123456',
            loginMode           => 1,
            timeStamp           => time(),
            version             => 0,
        );
    
        my $response = $client->login($login);
    
        if ($response->{status} == 0) {
            print "Login successful!\n";
        }
        else {
            print "Login failed! Status: ", (defined $response->{status} ? $response->{status} : 'undefined'), "\n";
        }
    
        $client->disconnect();
    }
    
    main() unless caller;
    
    1;
    复制代码

    image.png

    相关开源项目

    总结

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

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

  • 相关阅读:
    【Ubuntu-20.04】OpenCV-3.4.16的安装并对图片与视频处理
    2022年高教社杯国赛A题思路——波浪能最大输出功率设计
    全国大学生数学建模A题目更新中…… 欢迎订阅
    (附源码)ssm高校运动会管理系统 毕业设计 020419
    计算机毕业设计ssm图书馆自习室占座选座zg09h系统+程序+源码+lw+远程部署
    LeetCode高频题88. 合并两个有序数组
    广度优先搜索
    静态版通讯录
    代码随想录算法训练营第二十八天 | LeetCode 491. 递增子序列、46. 全排列、47. 全排列 II
    Java从文件路径中获取文件名的方法
  • 原文地址:https://www.cnblogs.com/huaweiyun/p/18174373