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

时序图

连接成功,发送短信

 
 
 
教你用Perl实现Smgp协议

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

 
 
 
教你用Perl实现Smgp协议

协议帧介绍

教你用Perl实现Smgp协议

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;

教你用Perl实现Smgp协议

相关开源项目

总结

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

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

发表评论

相关文章

当前内容话题
  • 0