C#短信群发源码实战:基于短信猫AT指令与PDU模式全解析 📅 发布时间:2026/9/10 1:21:34 👁 浏览次数: 简介这是C#编写的企业短信群发系统源代码基于短信猫硬件设备面向需要快速搭建短信批量发送功能的.NET开发者。源码完整可编译运行内置短信模板管理、号码列表处理、发送状态跟踪等模块适用于营销通知、企业内部消息触达等场景。压缩包共114个文件以38个cs主程序文件为核心搭配17个resx界面资源配置、dll依赖库及exe可执行程序另有gif/jpg图形素材和mdb数据库文件整个rar包仅721KB结构清晰。已有119人学习下载项目覆盖串口通信、GSM/AT指令、多线程并发、数据库操作、WinForms界面设计等关键知识点适合希望深入理解短信猫开发或进行二次开发的开发者参考。 短信群发这东西听起来简单实际做起来坑不少。尤其是用短信猫很多人以为买回来插上就完事结果不是乱码就是漏发要么串口打不开要么模块死机。这篇文章我就把自己做C#短信群发源码基于短信猫的完整思路、代码、踩坑过程都摊开了讲从原理到落地一条龙说清楚。这套方案的核心是通过C#操作短信猫GSM Modem调用底层AT指令实现短信的批量发送、状态确认和失败重发。适合的场景很明确企业内部通知、会员营销短信、物流提醒、故障告警等需要批量发送短信且对成本敏感的中小项目。它不依赖第三方短信平台的按条计费一次买断硬件资费就是SIM卡本身的短信套餐。1. 整体设计与选型思路1.1 短信猫的工作原理先把底层的逻辑搞明白。短信猫本质上就是一个工业级的GSM/GPRS通信模块最常见的是华为的MG323、EM310、SIMCOM的SIM800系列外置一个串口或者USB转串口接口再插一张能发短信的SIM卡就构成了一个可以独立发短信的设备。和手机发短信不一样短信猫没有屏幕也没有按键它只认命令。这些命令就是AT指令集是计算机和模块之间通信的标准语言。整个流程是这样的C#程序通过串口把AT指令发出去模块收到指令后返回OK或ERROR如果指令带数据请求就返回具体的应答信息比如ATCMGS发送指令会返回一个提示符然后程序把短信内容发过去最后再发送十六进制的1A表示确认结束。这个逻辑和HTTP请求是完全不同的思维模式。HTTP无状态、请求就响应AT指令则是有状态的会话模块在发短信的每一个阶段都有自己的状态。刚开始做的时候容易犯的错误就是把HTTP那种发出-返回的思维硬套在AT指令上结果总是拿不到预期的响应或者拿到的响应慢了半拍。1.2 为什么选短信猫而不是短信平台市面上面向开发者的短信服务商不少按条收费接入也方便。但为什么还是有很多项目在用短信猫核心原因就两个成本控制和数据私有化。成本上如果发送量不大——比如一天几百条用短信平台每条三到五分钱一年下来也是一笔不小的开支。短信猫是买断制的一个工业级模块从几十到两百块不等一张SIM卡用企业短信套餐能发几千条长期来看对低频发送场景很有优势。数据私有化就更直接了手机号名单、发送内容、发送成功与否都存放在自己的服务器和数据库里不会经过第三方平台对某些敏感行业来说是硬需求。当然短信猫的缺点也很明显受信号环境影响并发量低一条短信发送需要好几秒不可能像平台那样一秒发几百条。这个方案更适合低频高价值的发送场景比如药品入库通知、门禁异常告警、客户预约提醒这类。2. 核心实现与关键代码2.1 串口通信的建立C#里面操作短信猫本质上就是操作串口。.NET自带的System.IO.Ports.SerialPort类就够用了不需要额外装驱动库。关键参数就四个波特率、数据位、停止位、校验位。大多数短信猫出厂默认波特率是9600或115200数据位8、停止位1、无校验。注意有些工业模块默认是19200这跟设备出厂设置有关。我在项目里遇到过必须先把波特率设为115200发AT指令确认模块在线然后再切到指定波特率的奇葩情况因为那个模块固件重置后默认值不是9600。private SerialPort _port; private void OpenPort(string portName, int baudRate 9600) { _port new SerialPort { PortName portName, BaudRate baudRate, DataBits 8, StopBits StopBits.One, Parity Parity.None, ReadTimeout 500, WriteTimeout 500, Encoding Encoding.GetEncoding(ISO-8859-1) }; _port.Open(); }这里有一个很重要的细节Encoding必须设置成ISO-8859-1而不能是默认的UTF-8。因为AT指令的应答和短信内容在文本模式下是按照单字节字符处理的如果用UTF-8去解码中文内容会被编码成多字节模块收到之后根本识别不了返回的信息也会乱掉。2.2 发送AT指令与等待响应发AT指令最忌讳的就是发完就忘。短信猫的执行是有延迟的有的指令在信号不好的时候能迟滞好几秒。所以核心代码必须是一个发送并等待指定响应的同步方法不能靠Sleep死等否则程序会卡死也不能发了就不管否则下一个指令就会因为上一个还没执行完而出错。private readonly object _lockObj new object(); private readonly StringBuilder _buffer new StringBuilder(); private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e) { string data _port.ReadExisting(); lock (_lockObj) { _buffer.Append(data); } } private string SendCommandWithWait(string command, string[] expectedResponses, int timeout 3000) { lock (_lockObj) { _buffer.Clear(); _port.WriteLine(command); DateTime deadline DateTime.Now.AddMilliseconds(timeout); while (DateTime.Now deadline) { string current _buffer.ToString(); foreach (string response in expectedResponses) { if (current.Contains(response)) return current; } Thread.Sleep(50); } throw new TimeoutException($指令超时: {command}, 当前返回: {_buffer.ToString()}); } }这里把_lockObj锁定保证了同一时刻只有一个线程能往串口里写指令。有人会问为什么要这样锁因为短信猫模块本身没有并发处理能力你前一条指令还没处理完后一条又进来了模块会直接返回ERROR甚至死机需要断电重启。2.3 短信内容的编码处理短信内容分两种情况纯英文/数字和包含中文。纯英文直接用文本模式ATCMGF1发送的数据就是ASCII简单粗暴。但只要内容里有中文就麻烦了因为GSM协议最早是为英文字符设计的中文不在它的内置字符集里。处理办法是把短信内容转成UCS2编码也叫UTF-16 BE然后用PDU模式发送。C#里面用UnicodeEncoding就能转private string StringToUCS2(string message) { UnicodeEncoding ucs2 new UnicodeEncoding(true, false); byte[] bytes ucs2.GetBytes(message); StringBuilder sb new StringBuilder(); foreach (byte b in bytes) { sb.Append(b.ToString(X2)); } return sb.ToString(); }UnicodeEncoding(true, false)的第一个参数true表示大端字节序false表示不输出BOM头。UCS2编码要求每两个字节表示一个字符大端序是传输标准顺序不能搞反。很多新人栽在这里用BitConverter转出来的字节序是反的小端结果发出去的中文是一串乱码。2.4 PDU模式下的短信发送前面说的是文本模式它虽然简单但处理中文很麻烦。更通用的做法是直接走PDU模式。PDU是一个二进制的协议数据单元短信中心号、发送方号码、内容编码、有效期等信息都按特定格式打包在一起。C#实现的发送核心代码这里给出一个已经跑通的版本三个核心参数短信息中心号码、目标手机号、消息内容。public string BuildPdu(string smscNumber, string phoneNumber, string message) { // 1. 短信中心号码处理86开头后补F补齐奇数位 string smsc 00 ParityChange(86 smscNumber); // 2. 发送方号码处理86 手机号 string from 00 ParityChange(86 phoneNumber); // 3. 协议头14表示基本参数号码协议标识编码方案时间 string head 1100; // 4. 有效期A7表示最高优先级24小时 string validity A7; // 5. UCS2编码的消息内容 string content StringToUCS2(message); int contentLength content.Length / 2; // PDU全文 短信中心长度 短信中心 消息头 发送号码长度 发送号码 协议头 有效期 内容长度 内容 string pdu string.Format({0:X2}{1}{2}{3:X2}{4}{5}{6}{7:X2}{8}, smsc.Length / 2, smsc, head, from.Length / 2, from, validity, contentLength, content); return pdu; }发送时用ATCMGS内容长度内容长度是指短信内容的字节数不是整个PDU的长度等模块返回后把PDU字符串发过去再发送十六进制的1A结束。public bool SendByPdu(SmsData sms) { string pdu BuildPdu(sms.Smsc, sms.Phone, sms.Content); string cmd ATCMGS (pdu.Length / 2); string response SendCommandWithWait(cmd, new[] { }, 2000); if (!response.Contains()) return false; _port.Write(pdu (char)26); string final WaitForResponse(new[] { OK, ERROR, CMS ERROR }, 10000); return final.Contains(OK); }注意(char)26就是0x1A的ASCII字符用_port.Write直接写进去。在C#里不能用WriteLine写因为WriteLine会自动追加换行符模块会把换行也算进去导致PDU解析失败。3. 实操过程与核心环节实现3.1 硬件选型与SIM卡准备做这个项目之前硬件选择是最容易踩坑的第一步。网上能买到的模块大致分三类。第一类是早年很流行的USB短信猫插头长得像U盘插上电脑就多出一个COM口。这类模块优点是便宜、免拆机缺点也很明显USB转串口芯片质量参差不齐驱动兼容性差大批量发送时容易掉设备。如果你只是学习练手或者公司内部几十条的通知可以选这个。第二类是串口短信猫外壳有DB9串口接口用USB转串口线跟电脑连接。这类模块一般是工规级的设计稳定性比USB短信猫强不少发送吞吐量也更好。我就是用这个方案跑的连续发几百条没出过设备掉线的事。第三类是原生的GSM/GPRS开发板或模块比如SIM800C、SIM7600CE这种需要自己做电源电路、电平转换和天线接口。难度上了一个台阶但是灵活性最高适合集成到自己的硬件设备里。表格对比一下方案成本稳定性开发难度适用场景USB短信猫低中低学习、小批量通知串口短信猫中高低企业生产环境GSM开发板高高高嵌入式集成项目SIM卡的选择也马虎不得。必须找运营商开通短信功能的卡如果是要发大量内容的营销短信还要确认是否有企业短信资质否则用个人卡发营销内容很容易触发运营商的垃圾短信拦截机制发不出去。3.2 电源与天线的细节很多人做完硬件接线后发一两条好好的连续发几条后模块突然没响应重启又好了。很大概率是电源问题。GSM模块在发射信号的瞬间电流峰值高达2A如果电源的电流输出能力不足电压就会被拉低模块直接重启或死机。所以电源纹波要小额定电流最好在3A以上线材也要够粗够短。天线也是模块工作在高频天线接口要有良好的射频匹配和接地屏蔽这样才能保证信号质量和稳定的发送成功率。我实测过一个项目天线放在金属机箱内部信号强度始终只有一格短信发送失败率超过30%把天线引到机箱外面后成功率立刻回到99%。这类问题用软件很难发现所以硬件阶段就排掉能省掉后续大量排障时间。3.3 发送调度与队列设计短信猫是串行设备同一时刻只能发一条短信。而群发业务又是一个典型的生产者-消费者模型主线程不断把待发送短信丢进队列后台的发送线程从队列里取出一条完成AT指令的全流程后再取下一条。public class SmsQueue { private readonly ConcurrentQueueSmsTask _queue new ConcurrentQueueSmsTask(); private readonly AutoResetEvent _signal new AutoResetEvent(false); private readonly SmsModem _modem; public SmsQueue(SmsModem modem) { _modem modem; var thread new Thread(ProcessLoop) { IsBackground true }; thread.Start(); } public void Enqueue(SmsTask task) { _queue.Enqueue(task); _signal.Set(); } private void ProcessLoop() { while (true) { _signal.WaitOne(); while (_queue.TryDequeue(out SmsTask task)) { try { bool success _modem.SendByPdu(task.ToSmsData()); task.Result success ? 成功 : 失败; } catch (Exception ex) { task.Result $异常: {ex.Message}; } SaveToDatabase(task); Thread.Sleep(200); // 模块处理间隔 } } } }这个设计看着简单但它解决了两个实际问题。第一短信发送是慢操作一条至少三秒如果直接在UI线程里同步发送界面必卡死。第二任务入库和短信发送解耦即使程序中途崩溃数据库里的任务状态也可以用来做恢复和补发。3.4 失败重试与状态记录短信发送失败的原因五花八门SIM卡欠费、信号弱、对方关机、短信中心返回错误代码等等。所以发送结果必须详细记录到数据库便于排查和精确补发。数据库表至少要有这几个字段CREATE TABLE SmsLog ( Id INT IDENTITY PRIMARY KEY, PhoneNumber VARCHAR(20), Content NVARCHAR(500), SendTime DATETIME, Status VARCHAR(10), ErrorCode VARCHAR(50), AttemptCount INT DEFAULT 0 );重试逻辑不宜无脑重试超过两次。因为如果模块本身出了问题比如信号差导致大量失败不断重试只会加深积压。比较好的做法是第一次失败先检查错误码如果错误码是SIM卡未注册或内存已满先重置模块再重试如果错误码是对端关机这个重试没有意义直接标记失败。重试间隔可以设计成指数退避第一次3秒、第二次10秒、第三次30秒超过三次就不再自动处理进入人工确认队列。4. 常见问题与排查技巧实录4.1 模块无响应或返回乱码这个在串口调试阶段极其常见。先确认串口是不是被占用了比如你去打开设备管理器双击COM口查看属性一些调试工具也会占住串口。典型的案例是程序启动时打开串口失败提示拒绝访问。然后是波特率问题。如果设置不对你发AT过去模块要么没反应要么返回不可读的乱码。可以通过尝试几个常见的波特率组合来定位9600、19200、115200发AT等OK哪个通了就用哪个。再有就是模块本身的串口线的问题特别是DB9线有几根是交叉线有几根是直连线低级错误却经常让人排查半天。4.2 中文短信全是乱码这种问题十有八九是编码转换问题。如果你用的是文本模式那就是ATCSMP设置不对或者发送的编码方式根本就不是UCS2直接用GB2312字节流当PDU发了。PDU模式下要严格按照XX十六进制字符串拼接格式且内容长度必须是字节数不是字符数。我给出一个自检方法在程序里把要发的消息转成UCS2后先打印出来看是否符合Unicode码点。比如你好对应的UCS2是4F606D不带空格你打印出来必须是这个十六进制序列如果出来的是604F266D这种倒序那就是大小端方向搞反了。4.3 明明发送成功但对方收不到这是最让人抓狂的问题。AT指令返回OK数据库记了成功但用户那边就是收不到。排查思路要分成几条线并行。先看是不是模块GSM网络信号太弱。发送时信号强度ATCSQ返回值小于10的基本就是信号盲区短信虽然提交成功了但会被短信中心延迟发送甚至丢弃。再看是不是被运营商风控了。连续短时间给不同号码发送相同内容的短信会被运营商的垃圾短信过滤系统拦截这部分短信发送后看起来OK实际没有投递成功。需要控制发送频率比如同一内容每分钟不超过10条每条间隔5到10秒发送时段也避开凌晨这样的敏感时间窗口。4.4 长时间运行后发送越来越慢这个我有过切身教训。程序跑了一天后发短信越来越卡刚开始一条三秒到后来一条十几秒甚至超时。后来排查下来有两个原因一个是队列里堆积了大量失败重试的任务不断重试把正常任务挤在后面另一个是模块串口缓冲区里残留了上一次的大量数据每次发送前先清空缓冲区再发指令问题明显缓解。要特别说明一下短信发送的官方限流。运营商短信中心对单卡发短信有频率限制当你发得太多太快短信中心会临时限制该号码的短信业务此时模块会返回类似CMS ERROR302SIM ME short message storage full或者干脆长时间不给响应。做群发功能频率控制是必须做的别以为设备快就拼命地塞。5. 群发策略与合法合规注意事项短信猫虽然能发几条、几百条甚至上千条但它的本质还是民用/工业级通信设备跟运营商正规的多通道短信平台不同。所以群发场景下一定要特别注意发送策略和合规问题。频率控制是刚需。我建议按下面这个经验值来控制限制项推荐值单条间隔200-300ms同内容每分钟不超过10条单卡日发送量300条以内连续发送总时长不超过2小时为什么单卡一天不超过300条因为运营商的阈值检测是动态的不同地区、不同卡类型不一样但超过一定量后轻则限制短信功能重则停机。宁可发得慢一点也不要让卡被关了。合规方面也必须讲清楚。短信群发涉及两个核心原则一是必须有用户授权群发营销类短信需要接收者明确同意否则就是垃圾短信二是发送内容不得涉及违法信息发前务必经过内部内容审核。有条件的公司还应该按国家相关规定办理正规资质。这不是套话是做这类系统绕不过去的红线开发之前就要在需求层面确定好赠送短信用途的合规性。我自己在做类似系统时都会在群发前把号码池做一次过滤剔除历史投诉过的、退订过的、无效的号码从源头降低被投诉举报的概率。6. 项目扩展与优化方向短信猫方案做到能用不难但要做到好用还要几个方向可以优化。多通道并发是提升发送量的常见做法。一台电脑用USB HUB接多个短信猫每个猫一张卡程序里面做一个简单的负载均衡哪个猫空闲就给它分配任务。但要注意每张卡的发送量和频率都要独立控制不能把总频率限制在几台设备之间共享否则还是会被运营商识别。数据统计功能也得跟上。发送成功率按小时统计、失败原因占比分析、各号码段的表现差异这些数据不仅能体现系统的运行状况还能反过来指导发送策略的调整。我用的方案是把发送日志实时汇总到一张统计表用定时任务每天出报表对接企业自身的报表平台即可。还有一点值得做的是把发送模块抽成独立的Windows服务。这样即使没有用户登录程序也能在后台定时发送。配合一个控制台管理页面运营人员可以随时查看队列状态和发送结果不用每次发短信都要打开一个窗口程序。按我长期使用的经验一套短信猫群发系统稳定运行的关键不在于代码写得多花哨而在于TE指令流程是否可靠、重试机制是否合理、硬件电源信号是否稳定、频率控制是否严格。这四个环节把控住了短信发送成功率长期维持在98%以上是没问题的。最后再分享一个很多人忽略的小技巧给模块写一个简单的看门狗机制。每发50条短信后主动发一条AT指令探测模块状态如果连续探测3次都无响应就通过串口DTR置位或者继电器断电的方式给模块做一次硬重启然后重新初始化。这个操作能解决95%以上的模块长期运行死机问题比任何代码层面的优化都管用。本文还有配套的精品资源点击获取