教你用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或使用羣發助手,即可使用驗證碼、通知短信服務。

點擊關注,第一時間瞭解華爲雲新鮮技術~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章