尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C#短信发送程序:PDU编码与串口AT指令实现
简介面向希望深入GSM短信通信与C#底层编码实践的开发者这份C#短信发送程序围绕PDU类展开重点解决短信提交、号码与SMSC地址编码、7位/8位/16位字符转换以及长短信拆分重组等问题。包内共45个文件以cs源码、exe程序、resources与resx界面资源、config配置和pdb调试文件为主另含项目解决方案与说明文本压缩包约484KB整体呈现为可运行的SMSCSharp工程骨架。已有157人学习下载适合作为理解短信协议与桌面端串口/网络发送流程的参考案例。读者可从中梳理PDU构造函数参数、Submit PDU封装、短信中心号码处理、异常与超时处理、状态报告及异步发送思路并借助测试场景观察短文本、长文本和特殊字符下的编码差异。项目目录紧凑便于按PDU编解码、发送逻辑、界面与配置几个层面拆读适合具备C#基础、想补齐通信协议与编码技巧的中级开发者。1. 一个被低估的 C# 短信发送程序从 PDU 编码讲起一个做设备集成的朋友曾拿着串口日志来问AT 指令明明回了OK短信还是发不出去内容全是乱码。查到最后问题不在串口也不在运营商而是短信内容被直接按 UTF-8 的原始字节送进了发送缓冲区真正的中文短信必须先做 PDU 编码Protocol Data Unit短信协议数据单元再拼帧、再发。这份 C# 短信发送程序pdu_class针对的就是这件事它把 GSM 07.05、07.10 里那套 PDU 组包、拆包的麻烦封装成了可直接调用的类库适合做设备报警、验证码下发、工控现场短信通知的开发者拿去二改。它不依赖厂商专有 SDK串口加 AT 指令就能跑也方便接入上位机系统。2. 技术栈与目录结构pdu_class 的模块分工2.1 这套程序靠什么实现短信发送这套程序的核心是纯 C# 实现运行在 .NET Framework 上语言层面没有引入任何第三方 PDU 库PDU 的编解码逻辑是手写的。通信层走System.IO.Ports.SerialPort通过串口向短信猫或带短信功能的通信模块发送 AT 指令读出响应后判断CMGS、OK、ERROR等关键字。之所以手写 PDU是因为现成的短信 SDK 通常绑定特定厂商硬件换个模块就不认。PDU 编码本身是公开协议自己实现反而更稳组包规则确定编解码可单测硬件只负责收字节。整套代码的逻辑分四块PDU 编解码把号码、中心号码、短信正文、编码方案、时间戳拼成十六进制字符串也能反向解析收到的短信。串口通信打开端口、设置波特率、写入 AT 指令、按超时读响应。发送流程控制AT 握手、设文本模式或 PDU 模式、发ATCMGS、等待提示、写入 PDU、等待最终结果。配置与日志串口号、波特率、中心号码、发送超时都放在配置里串口报文写入日志方便排查。注意串口短信对时序很敏感等待提示、等待CMGS回执都有超时。很多发不出去其实不是编码错而是超时太短、读取时机不对。2.2 目录结构与文件职责工程按协议层 / 通信层 / 业务层三层组织源码目录大概是这个形态不同版本文件名会有出入但职责划分基本一致PduSms/ ├── Class/ │ ├── PduClass.cs # PDU 编解码核心组包、拆包、号码与时间处理 │ ├── SmsSender.cs # 发送流程AT 握手、CMGS、回执判断 │ ├── SerialHelper.cs # 串口封装打开、写、按超时读 │ └── SmsConfig.cs # 配置实体串口、中心号码、超时 ├── Forms/ │ └── MainForm.cs # 演示窗体填号码、填内容、点发送 ├── App.config # 串口与短信参数 └── Program.cs # 入口文件职责二次开发时常改的地方PduClass.csPDU 组包拆包补 UCS2 以外的编码、支持长短信分段SmsSender.csAT 指令流程增加重试、并发队列、发送状态回调SerialHelper.cs串口读写调整超时、增加断线重连SmsConfig.cs参数承载增加多设备配置、从数据库读参数这种分法的好处是PDU 逻辑可以脱离串口单独测串口层可以脱离业务单独造模拟器。改造时只要不动PduClass的入参出参通信层怎么换都不影响上层。3. PDU 核心代码拆解一条中文短信怎么拼出来3.1 PDU 串的组成与短信中心号码处理一条完整的 PDU 提交串SMS-SUBMIT大致由这几段拼成短信中心号码、PDU 类型、被叫号码长度与类型、被叫号码半字节交换、协议标识、数据编码方案、有效期、用户数据长度、用户数据。程序里对应的处理思路如下// 短信中心号码补位 奇偶交换 private string EncodeSmsc(string smsc) { if (string.IsNullOrEmpty(smsc)) return 00; // 无中心号码时填 00用卡内默认 smsc smsc.TrimStart(); string len smsc.Length.ToString(X2); // 长度按十进制数转十六进制 string swapped SwapNibbles(smsc); // 8613800138000 - 68310810380000 return len 91 swapped; // 91 表示国际号码格式 } // 半字节交换每两位数字前后互换 private string SwapNibbles(string digits) { if (digits.Length % 2 ! 0) digits F; // 奇数位补 F var sb new StringBuilder(); for (int i 0; i digits.Length; i 2) sb.Append(digits[i 1]).Append(digits[i]); return sb.ToString(); }这里有两个容易翻车的点。第一短信中心号码的长度写的是号码位数不是交换后的字符串长度新手常把len按交换后长度算结果整个 PDU 错位。第二奇数位补F国内手机号 11 位交换后最后会多一个F这是协议规定不是 bug。号码类型字节91代表国际格式带 号如果是本地号码应换81。3.2 UCS2 编码中文内容与长度字段短信正文支持多种编码7 位 GSM 字母表管英文中文必须走 UCS2即 UTF-16BE。类库中一般是这么处理的// 按编码方案生成用户数据部分 private string EncodeUserData(string content, out int dataLen, out int codingScheme) { if (IsGsm7(content)) // 纯 ASCII 走 7 位编码能多塞字符 { codingScheme 0x00; string hex7 Gsm7Encode(content); dataLen content.Length; // 7 位编码长度按字符数算 return hex7; } else // 含中文统一走 UCS2 { codingScheme 0x08; byte[] bytes Encoding.BigEndianUnicode.GetBytes(content); dataLen bytes.Length; // UCS2 长度按字节数算 return BitConverter.ToString(bytes).Replace(-, ); } }dataLen这个字段最容易出错UCS2 下它是字节数一个汉字 2 字节7 位编码下它是字符数。同一条短信一条 70 字的上限就是从这来的。如果长度字段和实际数据对不上模块可能直接回ERROR也可能收下短信但内容截断两种现象都很难查。我在项目里一般会在拼完后做一次自校验解析回自己刚拼的 PDU号码和内容都对上才真正下发。3.3 长短信分段UDH 与参考号超过 70 个汉字UCS2或 160 个字符7 位就要分段走长短信Concatenated SMS。核心是在用户数据前插入 UDHUser Data Header包含分段参考号、总段数、当前段号// 构造一条长短信的某一分段 UDH6 字节头 // 05 00 03 ref total seq private string BuildUdh(byte reference, byte total, byte seq) { // 05: UDH 总长00: IEI 为拼接标识03: 后续 3 字节 return 050003 reference.ToString(X2) total.ToString(X2) seq.ToString(X2); }带 UDH 时用户数据长度字段要算上 UDH 的字节数可用正文长度也会相应减少 6 字节UCS2 下少 3 个汉字。这里没有标准参考号同一组短信必须用同一个reference否则手机端拼不起来表现就是收到好几条零散短信。常见做法是取时间戳低字节或自增计数器只要在短时间内不重复即可。4. 串口通信与 AT 指令把 PDU 真正发出去4.1 SerialPort 初始化与 AT 握手PDU 拼好后下一步是走串口。短信模块的波特率常见 9600、115200数据位 8、无校验、停止位 1、无流控。初始化后先发AT探活再切 PDU 模式public bool Open(string portName, int baudRate) { _port new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _port.ReadTimeout 3000; _port.WriteTimeout 3000; _port.DataReceived OnDataReceived; // 异步收避免阻塞 _port.Open(); if (!SendAt(AT, OK, 1000)) return false; // 探活 if (!SendAt(ATE0, OK, 1000)) return false; // 关回显减少干扰 return SendAt(ATCMGF0, OK, 1000); // 0 PDU 模式 } // 发送指令并等待预期回执 private bool SendAt(string cmd, string expect, int timeoutMs) { _port.Write(cmd \r\n); return WaitFor(expect, timeoutMs); }ATE0关回显很有必要开着回显时你写的指令会原样回来和模块响应混在一起关键字匹配容易误判。ATCMGF0切 PDU 模式如果硬件只支持文本模式CMGF1中文照样会乱码这一点在选模块时就要确认。4.2 CMGS 发送流程与回执判断PDU 模式下发短信的完整流程是先发ATCMGSPDU长度模块回提示符再写 PDU 串最后以CtrlZ0x1A结束等CMGS: mr和OKpublic SmsResult Send(string phone, string content) { string pdu PduClass.BuildSubmit(phone, content, _config.Smsc); int tpduLen pdu.Length / 2 - 1; // 减去中心号码那段单位是字节 _port.Write($ATCMGS{tpduLen}\r\n); if (!WaitFor(, 3000)) return SmsResult.Fail(未收到 提示符); _port.Write(pdu \u001A); // CtrlZ 结束 if (WaitFor(CMGS:, 15000)) // 回执超时要放大 { WaitFor(OK, 2000); return SmsResult.Ok(); } return SmsResult.Fail(未收到 CMGS 回执); }参数说明tpduLen是去掉短信中心号码后的 TPDU 字节长度不是整个 PDU 串长度这是这里最容易出错的地方很多代码在这里直接写全串长度模块回ERROR日志里看不出原因。CMGS的回执通常要等网络侧15 秒超时不算长信号差的时候 30 秒也有。WaitFor的实现建议是循环读缓冲区、拼字符串、查关键字而不是一次ReadLine因为模块的响应不总以换行结尾。提示SerialPort的DataReceived事件触发次数不确定收到半包很常见。缓冲区要自己管理读到完整回执再判断别在事件里直接解析业务。5. 运行与联调从零把第一条短信发出去5.1 环境准备与首次运行需要一台 Windows 机器、.NET Framework 运行环境或对应版本的 Visual Studio 打开工程以及一个能正常插卡、已在运营商侧开通短信的模块。步骤建议这么走插卡上电用串口调试助手确认模块能回AT先排除硬件和驱动问题。打开工程在App.config或界面里填串口号、波特率、短信中心号码。中心号码可以先用ATCSCA?查出来别自己编。填一个自己的手机号作为收件人发送内容写纯英文先测通链路再换中文。发送后看界面返回和日志文件重点看模块最后回的是OK还是ERROR。通了之后再测长短信、连续发送观察是否需要间隔。英文先通、中文后测这个顺序能快速区分链路问题和编码问题。我第一次调的时候直接发中文折腾半天最后发现是串口线松动。5.2 日志与问题定位程序和硬件打交道日志就是黑匣子。建议在串口层把每次写入和读出的原始字节都记下来十六进制和字符串各存一份。排查时按这个顺序看AT 探活是否成功、CMGF0是否成功、CMGS后是否出现、PDU 是否完整写入、最后回执是什么。如果回执是ERROR多半在 PDU 格式或长度字段如果根本没有回执先怀疑超时和串口占用。可以装个虚拟串口对把另一端当模拟模块手工回和OK用来验证发送流程而不必反复插拔硬件。6. 二次开发避坑与改造思路6.1 常见问题排查清单现象模块回OK但手机收不到。原因ATCMGS的长度字段填了整串长度模块按错误长度截断了 PDU表面成功实际发的是残帧。解决长度只算 TPDU 部分发完用另一台手机核对内容。现象中文全是问号或乱码。原因编码方案字节没设成08或者正文直接用了 UTF-8 字节。解决含非 ASCII 字符统一走 UCS2长度字段同步改成字节数。现象长短信被拆成多条独立短信。原因每段 UDH 的参考号不一致或总段数字段写错。解决同一组消息共用一个参考号段号从 1 连续编到总数。现象连续发几条后卡住不返回。原因发送间隔太短模块还在等网络回执串口缓冲区堆了脏数据。解决两条之间加 12 秒间隔发之前清空缓冲区超时后主动发AT重新同步。现象换台电脑就发不出去。原因串口号写死在代码里换机器后端口号变了或者 USB 转串口驱动版本不同导致波特率不稳。解决串口参数全部外置到配置启动时枚举可用端口。6.2 从 Demo 到可用的发送服务拿来能跑只是第一步真上线一般要做三件事把同步发送改成队列加后台线程避免界面卡死给每条短信记状态待发、已发、失败、重试次数失败按指数退避重试把配置、日志、发送记录落到本地文件或数据库。改造的关键是把PduClass当纯函数用它只负责字符串进、字符串出不碰串口这样编码逻辑可以写单测回归时不怕改坏。发送层则抽一个接口串口实现和模拟实现各一份联调时不依赖硬件。// 发送队列的最小骨架 public interface ISmsTransport { SmsResult Send(string phone, string content); } public class SmsQueue { private readonly BlockingCollectionSmsTask _queue new BlockingCollectionSmsTask(); public void Enqueue(SmsTask task) _queue.Add(task); public void Start(ISmsTransport transport) { Task.Run(() { foreach (var task in _queue.GetConsumingEnumerable()) { task.Result transport.Send(task.Phone, task.Content); task.FinishedAt DateTime.Now; // 失败入重试队列成功归档 } }); } }接口一抽串口换 USB 模块、换 HTTP 网关都不用动业务代码。我自己的习惯是每次改 PDU 相关代码先用固定号码和固定内容跑一遍自解析校验确认组的包自己能解回来再上真机。被长度字段坑过一次之后这个校验我再没省过。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

物理坐标驱动自投影拍照技术解析与应用

物理坐标驱动自投影拍照技术解析与应用

1. 物理坐标驱动的自投影拍照:概念与价值 第一次听说"物理坐标驱动的自投影拍照"这个概念时,我脑海中浮现的是科幻电影里全息投影的场景。但当我真正开始研究这个技术时,发现它远比想象中更接地气、更有实用价值。简单来说&#xf…

📅 2026/10/26 18:49:02
解决PyTorch音频后端API兼容性问题

解决PyTorch音频后端API兼容性问题

1. 问题背景与现象分析 最近在PyTorch 2.10.0cu130环境下运行Fish Speech项目时,不少开发者遇到了一个典型的AttributeError错误: torchaudio.list_audio_backends 属性不存在。这个错误看似简单,实则涉及PyTorch生态系统的多个关键组件协同…

📅 2026/10/26 18:44:01
前端数组遍历与交互选择的高效实现方案

前端数组遍历与交互选择的高效实现方案

1. 数组遍历与点击交互的核心逻辑 在前端开发中,处理列表项的选择与取消选择是一个高频需求。这个功能看似简单,但实现起来需要考虑数组操作、DOM更新和状态同步等多个环节。核心在于利用数组的遍历方法结合点击事件,通过判断当前元素是否已存…

📅 2026/10/26 18:44:01
MORE NEWS

更多资讯

📰

Windows10纯CPU跑YOLOv5:PyCharm环境搭建与推理调优

1. 先把预期摆正:CPU 跑 YOLOv5 到底能干什么windows10cpupycharm跑yolov5 这个组合,这几年被问到的频率一点没降。原因很现实:大多数人手里只有一台办公本或者台式机,没有独立显卡,但又想亲手把目标检测跑起来看看效果…

📰

Ubuntu 18.04 安装 GAMMA:SAR 处理环境配置与排错指南

1. 为什么要在 Ubuntu 18.04 上折腾 GAMMAGAMMA 安装这件事,说难不难,说简单也不简单。它不是那种apt install一条命令就能解决的软件,而是一整套命令行形式的 SAR 处理工具集,涵盖干涉测量、差分干涉、地理编码、点目标时序分析等…

📰

Ubuntu 18.04部署GAMMA:依赖库、环境变量与四层自检

1. 先摆正预期:Ubuntu 18.04 上装 GAMMA 到底是件什么事在 Ubuntu 18.04 上装 GAMMA,这事儿说白了不是"执行一条命令等它跑完",而是一次把一套上世纪末就成型的科学计算软件栈,硬塞进一个 2018 年发布的现代发行版里的兼…

📰

OpenClaw与Nanobot:高性能分布式系统架构解析

1. OpenClaw与Nanobot项目概述 OpenClaw是一个基于多智能体协同的分布式系统框架,而Nanobot则是其核心组件之一。这个开源项目最初由金融量化领域的研究团队开发,后来逐渐演变为通用的任务自动化解决方案。通过分析Nanobot的源码,我们可以深入…

📰

e2e测试的本质:可控现实模拟器与工程临界点

1. “e2e”不是缩写,是工程实践的临界点很多人第一次看到“e2e”三个字母,下意识会去查全称——End-to-End?没错,但这个答案只开了个门缝。真正让“e2e”在工程师日常中高频出现、反复调试、深夜报错、团队争论的,从来…

📰

Java游戏开发:任务状态管理系统的设计与优化

1. 项目概述"魔法森林冒险"是一款基于Java开发的角色扮演游戏,其中的Allen类作为核心系统之一,负责处理游戏中的任务进度与状态管理模块。这个模块的设计直接关系到玩家的游戏体验流畅度,也是游戏开发中最容易出问题的环节之一。我…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬