引言&協議概述
中國網路通訊集團簡訊閘道器協議(CNGP)是中國網通為實現簡訊業務而制定的一種通訊協議,全稱叫做China Netcom Short Message Gateway Protocol,用於在PHS短訊息閘道器(SMGW)和服務提供商(SP)之間、短訊息閘道器(SMGW)和短訊息閘道器(SMGW)之間通訊。
Zig 是一種效能優異、安全性高的系統程式語言,適合用於實現底層網路協議。它提供了強大的型別系統、編譯時計算和錯誤處理機制。
CNGP 協議基於客戶端/服務端模型工作。由客戶端(簡訊應用,如手機,應用程式等)先和簡訊閘道器(SMGW Short Message Gateway)建立起 TCP 長連線,並使用 CNGP 命令與SMGW進行互動,實現簡訊的傳送和接收。在CNGP協議中,無需同步等待響應就可以傳送下一個指令,實現者可以根據自己的需要,實現同步、非同步兩種訊息傳輸模式,滿足不同場景下的效能要求。
時序圖
連線成功,傳送簡訊
連線成功,從SMGW接收到簡訊
協議幀介紹
在CNGP協議中,每個PDU都包含兩個部分:CNGP Header和CNGP Body
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}); }
相關開源專案
- 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或使用群發助手,即可使用驗證碼、通知簡訊服務。
點選關注,第一時間瞭解華為雲新鮮技術~