教你如何使用Zig實現Cmpp協議

华为云开发者联盟發表於2024-04-07
本文分享自華為雲社群《華為雲簡訊服務教你用Zig實現Cmpp協議》,作者: 張儉。

引言&協議概述

中國網路通訊集團簡訊閘道器協議(CNGP)是中國網通為實現簡訊業務而制定的一種通訊協議,全稱叫做China Netcom Short Message Gateway Protocol,用於在PHS短訊息閘道器(SMGW)和服務提供商(SP)之間、短訊息閘道器(SMGW)和短訊息閘道器(SMGW)之間通訊。

Zig 是一種效能優異、安全性高的系統程式語言,適合用於實現底層網路協議。它提供了強大的型別系統、編譯時計算和錯誤處理機制。

CNGP 協議基於客戶端/服務端模型工作。由客戶端(簡訊應用,如手機,應用程式等)先和簡訊閘道器(SMGW Short Message Gateway)建立起 TCP 長連線,並使用 CNGP 命令與SMGW進行互動,實現簡訊的傳送和接收。在CNGP協議中,無需同步等待響應就可以傳送下一個指令,實現者可以根據自己的需要,實現同步、非同步兩種訊息傳輸模式,滿足不同場景下的效能要求。

時序圖

連線成功,傳送簡訊

教你如何使用Zig實現Cmpp協議

連線成功,從SMGW接收到簡訊

教你如何使用Zig實現Cmpp協議

協議幀介紹

在CNGP協議中,每個PDU都包含兩個部分:CNGP Header和CNGP Body

image.png

CNGP Header

Header包含以下欄位,大小長度都是4位元組

  • Total Length:整個PDU的長度,包括Header和Body。
  • Command ID:用於標識PDU的型別(例如,Login、Submit等)。
  • Common Status:命令狀態
  • Sequence Id:序列號,用來匹配請求和響應。

使用Zig實現CNGP協議棧裡的建立連線

.
├── src
│   └── bound_atomic.zig
│   ├── cngp_client.zig
│   ├── cngp_client_login_example.zig
│   ├── constant.zig
│   ├── protocol.zig
  • bound_atomic.zig:原子遞增工具類,用來做SequenceId
  • cngp_client.zig:這個檔案包含Cngp的定義,該類負責與CNGP服務進行通訊,例如建立連線、傳送簡訊等
  • cngp_client_login_example.zig:示例程式碼
  • constant.zig:存放常量
  • protocol.zig:這個檔案包含 CNGP 協議相關的定義和實現,例如協議的命令 ID、PDU 格式等。

constant.zig存放相關commandId

pub const CommandId = enum(u32) {
    Login = 0x00000001,
    LoginResp = 0x80000001,
    Submit = 0x00000002,
    SubmitResp = 0x80000002,
    Deliver = 0x00000003,
    DeliverResp = 0x80000003,
    ActiveTest = 0x00000004,
    ActiveTestResp = 0x80000004,
    Exit = 0x00000006,
    ExitResp = 0x80000006,
};

protocol.zig協議編解碼

const std = @import("std");
const io = std.io;
const CommandId = @import("constant.zig").CommandId;

pub const CngpLogin = struct {
    clientId: []const u8,
    authenticatorClient: []const u8,
    loginMode: u8,
    timeStamp: i32,
    version: u8,

    pub fn length(self: *const CngpLogin) usize {
        return self.clientId.len + self.authenticatorClient.len + 1 + 4 + 1;
    }

    pub fn encode(self: *const CngpLogin, writer: anytype) !void {
        try writer.writeAll(self.clientId);
        try writer.writeAll(self.authenticatorClient);
        try writer.writeByte(self.loginMode);
        try writer.writeIntBig(i32, self.timeStamp);
        try writer.writeByte(self.version);
    }
};

pub const CngpLoginResp = struct {
    authenticatorServer: []const u8,
    version: u8,

    pub fn decode(buffer: []u8) !CngpLoginResp {
        var stream = std.io.fixedBufferStream(buffer);
        var reader = stream.reader();
        var authenticatorServerBuffer: [16]u8 = undefined;
        var fixedSize = try reader.read(&authenticatorServerBuffer);
        if (fixedSize != 16) {
            return error.InvalidLength;
        }
        const version = try reader.readByte();
        return CngpLoginResp{
            .authenticatorServer = authenticatorServerBuffer[0..],
            .version = version,
        };
    }
};

pub const CngpHeader = struct {
    total_length: i32,
    command_id: CommandId,
    command_status: i32,
    sequence_id: i32,
};

pub const CngpBody = union(enum) {
    Login: CngpLogin,
    LoginResp: CngpLoginResp,
};

pub const CngpPdu = struct {
    header: CngpHeader,
    body: CngpBody,

    pub fn length(self: *const CngpPdu) usize {
        return 16 + switch (self.body) {
            .Login => |login| login.length(),
            else => 0,
        };
    }

    pub fn encode(self: *const CngpPdu) ![]u8 {
        const len = self.length();
        var buffer = try std.heap.page_allocator.alloc(u8, len);
        var stream = std.io.fixedBufferStream(buffer);
        var writer = stream.writer();
        try writer.writeInt(i32, @as(i32, @intCast(len)), .Big);
        try writer.writeInt(u32, @intFromEnum(self.header.command_id), .Big);
        try writer.writeInt(i32, self.header.command_status, .Big);
        try writer.writeInt(i32, self.header.sequence_id, .Big);
        switch (self.body) {
            .Login => |login| try login.encode(writer),
            else => unreachable,
        }
        return buffer;
    }

    pub fn decode_login_resp(buffer: []u8) !CngpPdu {
        var header: CngpHeader = undefined;
        header.total_length = 0;
        header.command_id = CommandId.LoginResp;
        header.command_status = std.mem.readIntLittle(i32, buffer[8..12]);
        header.sequence_id = std.mem.readIntLittle(i32, buffer[12..16]);

        const body = try CngpLoginResp.decode(buffer[12..]);
        return CngpPdu{
            .header = header,
            .body = CngpBody{ .LoginResp = body },
        };
    }
};

利用原子型別實現sequenceId遞增

const std = @import("std");

pub const BoundAtomic = struct {
    min: i32,
    max: i32,
    integer: std.atomic.Atomic(i32),

    pub fn new(min: i32, max: i32) BoundAtomic {
        return BoundAtomic{
            .min = min,
            .max = max,
            .integer = std.atomic.Atomic(i32).init(min),
        };
    }

    pub fn nextVal(self: *BoundAtomic) i32 {
        while (true) {
            const current = self.integer.load(.SeqCst);
            const next = if (current == self.max) self.min else current + 1;
            if (self.integer.compareAndSwap(current, next, .SeqCst, .SeqCst) == null) {
                return next;
            }
        }
    }
};

實現client以及login方法

const std = @import("std");
const net = std.net;
const CngpBody = @import("protocol.zig").CngpBody;
const CngpLogin = @import("protocol.zig").CngpLogin;
const CngpPdu = @import("protocol.zig").CngpPdu;
const CommandId = @import("constant.zig").CommandId;
const BoundAtomic = @import("bound_atomic.zig").BoundAtomic;

pub const CngpClient = struct {
    host: []const u8,
    port: u16,
    sequenceId: BoundAtomic,
    stream: ?std.net.Stream,

    pub fn init(host: []const u8, port: u16) CngpClient {
        return CngpClient{
            .host = host,
            .port = port,
            .sequenceId = BoundAtomic.new(1, 0x7FFFFFFF),
            .stream = null,
        };
    }

    pub fn connect(self: *CngpClient) !void {
        const peer = try net.Address.parseIp4(self.host, self.port);
        self.stream = try net.tcpConnectToAddress(peer);
    }

    pub fn login(self: *CngpClient, body: CngpLogin) !CngpPdu {
        const sequenceId = self.sequenceId.nextVal();
        const pdu = CngpPdu{
            .header = .{
                .total_length = 0, // Will be calculated in encode method
                .command_id = CommandId.Login,
                .command_status = 0,
                .sequence_id = sequenceId,
            },
            .body = CngpBody{ .Login = body },
        };
        const data = try pdu.encode();
        if (self.stream) |s| {
            const size = try s.write(data);
            if (size != data.len) {
                return error.WriteFailed;
            }

            var buffer: [4]u8 = undefined;
            const readLengthSize = try s.read(buffer[0..]);
            if (readLengthSize != 4) {
                return error.ReadFailed;
            }
            const remainLength = std.mem.readInt(u32, buffer[0..], .Big) - 4;

            var responseBuffer = try std.heap.page_allocator.alloc(u8, remainLength);
            defer std.heap.page_allocator.free(responseBuffer);

            var reader = s.reader();
            const readSize = try reader.read(responseBuffer[0..remainLength]);
            if (readSize != remainLength) {
                return error.ReadFailed;
            }

            const response = try CngpPdu.decode_login_resp(responseBuffer);
            return response;
        } else {
            return error.UnexpectedNull;
        }
    }

    pub fn close(self: *CngpClient) void {
        if (self.stream) |s| {
            s.close();
            self.stream = null;
        }
    }
};

執行Example,驗證連線成功

const std = @import("std");
const CngpClient = @import("cngp_client.zig").CngpClient;
const CngpLogin = @import("protocol.zig").CngpLogin;

pub fn main() !void {
    const host = "127.0.0.1";
    const port: u16 = 9890;

    var client = CngpClient.init(host, port);
    defer client.close();

    const clientId = "1234567890";
    const authenticatorClient = "1234567890123456";
    const loginMode: u8 = 1;
    const timeStamp: i32 = 123456789;
    const version: u8 = 1;

    const loginBody = CngpLogin{
        .clientId = clientId,
        .authenticatorClient = authenticatorClient,
        .loginMode = loginMode,
        .timeStamp = timeStamp,
        .version = version,
    };

    try client.connect();
    const response = try client.login(loginBody);
    try std.io.getStdOut().writer().print("Login response: {}\n", .{response});
}

image.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實現
  • smpp-rust smpp協議的rust實現

總結

本文簡單對CNGP協議進行了介紹,並嘗試用zig實現協議棧,但實際商用傳送簡訊往往更加複雜,面臨諸如流控、運營商對接、傳輸層安全等問題,可以選擇華為雲訊息&簡訊(Message & SMS)服務,華為雲簡訊服務是華為雲攜手全球多家優質運營商和渠道,為企業使用者提供的通訊服務。企業呼叫API或使用群發助手,即可使用驗證碼、通知簡訊服務。

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

相關文章