祗疼妳一个 发表于 2024-5-19 05:55:10

教你用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协议中,无需同步等待相应就可以发送下一个指令,实现者可以根据自己的需要,实现同步、异步两种消息传输模式,满足不同场景下的性能要求。
时序图

连接乐成,发送短信

   https://static001.geekbang.org/infoq/4a/4ad5b0ec3eb733de75e2e3c0f5113f70.png连接乐成,从SMGW吸收到短信

   https://static001.geekbang.org/infoq/2a/2a5ab8ba74c3d22660a7bc182027cfb3.png协议帧介绍

https://bbs-img.huaweicloud.com/blogs/img/20240504/1714810889452181377.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.pmMakefile.PL:用来生成Makefileexamples:存放示例代码

[*]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;https://bbs-img.huaweicloud.com/blogs/img/20240504/1714810956453856035.png
相关开源项目


[*]netty-codec-sms 存放各种SMS协议(如cmpp、sgip、smpp)的netty编解码器
[*]sms-client-java 存放各种SMS协议的Java客户端
[*]sms-server-java 存放各种SMS协议的Java服务端
[*]cmpp-python cmpp协议的python实现
[*]cngp-zig cmpp协议的python实现
[*]smgp-perl smgp协议的perl实现
[*]smpp-rust smpp协议的rust实现
总结

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 教你用Perl实现Smgp协议