
简介这是一份面向VB.NET网络编程初学者与一卡通系统开发者的UDP通信服务端示例源码聚焦实时在线云消费机场景解决如何用Socket监听UDP端口、收发报文并搭建消费终端与服务器通信骨架的问题。压缩包共105个文件约2.91MB包含8个vb源码文件、26个dll依赖库、7个exe可执行程序、6个xml与5个config配置项以及resx资源、bat注册脚本、ocx控件等覆盖32位与64位系统控件注册、项目工程与运行环境配置结构完整便于直接编译调试。目前已有199人学习下载。读者可从中掌握VB.NET下UDP端口监听、消息发送与接收的完整实现思路并在此基础上接入数据库增删查改快速扩展为实时一卡通消费系统适合作为课程设计、毕业设计或企业原型开发的参考底稿。1. 从一台收银机的“心跳”说起vb.net Socket Udp 实时在线云消费机服务器端到底在做什么一台食堂消费机、一台会员刷卡机插上网线之后它凭什么能让后台知道“刚刚有人消费了 8 块 5”答案往往不是 HTTP而是一段跑在 UDP 上的私有心跳协议。标题里的 vb.net Socket Udp 实时在线云消费机服务器端源码讲的就是这套东西用 VB.NET 的 System.Net.Sockets 里的 UdpClient / Socket在服务器端开一个端口持续接收分布在各个门店的消费机上报的在线状态、刷卡流水、心跳包再把结果落到数据库或内存表里让“实时在线”这四个字成立。它解决的核心问题有三个设备在线状态怎么实时感知、消费流水怎么低延迟上报、断网重连后怎么补数据。适合谁看做一卡通、食堂、会员、门禁这类终端联网项目的开发者尤其是被 TCP 长连接数量压得喘不过气、想换成 UDP 降低服务端连接开销的人。UDP 无连接、开销小几百上千台设备同时上报也不会像 TCP 那样占着一堆连接句柄代价是可靠性要自己补——这正是这份服务器端源码最值得拆的地方。2. 为什么消费机服务器端偏爱 UDP协议选型与报文结构设计2.1 UDP 和 TCP 在设备上报场景下的取舍先把这个选型讲透不然后面所有代码都是空中楼阁。消费机上报的本质是“小包、高频、可容忍偶发丢失、但要求低延迟”。一台设备一次上报可能就几十字节设备号、流水号、卡号、金额、时间戳、校验。用 TCP 的话每台设备要维持一条长连接服务端要处理 Accept、心跳保活、半开连接检测设备一多连接数和内存都是压力。UDP 没有连接概念服务端一个 Socket 就能收所有设备的数据ReceiveFrom直接拿到来源 IP 和端口天然适合“多设备上报到一个中心”。但 UDP 的坑也在这不保证到达、不保证顺序、不保证不重复。所以服务器端源码里必须自己补三样东西——应用层应答ACK、流水号去重、CRC 校验。常见做法是设备发一条上报服务端回一条 ACK设备没收到 ACK 就重发服务端用“设备号 流水号”做唯一键去重。这套机制在 VB.NET 里用 UdpClient 就能实现不需要额外框架。提示UDP 单包建议控制在 512 字节以内超过 MTU 容易被分片分片丢一个整包就废了。消费流水这种小包场景刚好合适。2.2 一份可落地的报文格式报文结构是服务器端和设备的“合同”定错了后面全是血泪。下面这份是我一般会用的定长头 变长体的结构字段清晰、解析简单VB.NET 里用 BitConverter 就能拆。字段长度(字节)说明帧头 Magic2固定 0xAA55用于快速丢弃脏包协议版本1便于以后升级兼容命令字 Cmd10x01 心跳0x02 消费上报0x03 补传设备号 DeviceId8ASCII 或 BCD全局唯一流水号 Seq4设备内自增用于去重和 ACK数据长度 Len2变长体字节数数据体 BodyLen卡号、金额、时间等CRC162从帧头到 Body 的校验CRC16 是热词里反复出现的东西这里必须用上。VB.NET 没有内置 CRC16需要自己写查表或按位计算。校验不过的包直接丢不要试图“猜”脏包进业务层是灾难的开始。2.3 用 VB.NET 写一个最小可跑的 UDP 接收循环下面这段是服务器端的骨架跑起来就能收到设备发来的包并回 ACK。用 UdpClient 绑定端口循环 Receive解析后回发。Imports System.Net Imports System.Net.Sockets Imports System.Text Module UdpServer 监听端口消费机统一往这个端口上报 Private Const ListenPort As Integer 9000 Sub Main() Dim server As New UdpClient(ListenPort) 设置接收缓冲区避免大包被截断 server.Client.ReceiveBufferSize 1024 * 64 Console.WriteLine(UDP 消费机服务器已启动端口 ListenPort) Dim remoteEP As New IPEndPoint(IPAddress.Any, 0) While True Try 阻塞接收拿到字节流和来源地址 Dim data As Byte() server.Receive(remoteEP) HandlePacket(server, data, remoteEP) Catch ex As Exception 单个包异常不能拖垮整个循环 Console.WriteLine(接收异常 ex.Message) End Try End While End Sub 解析并处理一个报文 Private Sub HandlePacket(server As UdpClient, data As Byte(), remoteEP As IPEndPoint) 最小长度校验帧头2版本1命令1设备8流水4长度2CRC2 20 If data.Length 20 Then Return 校验帧头 If data(0) HAA OrElse data(1) H55 Then Return Dim cmd As Byte data(3) Dim deviceId As String Encoding.ASCII.GetString(data, 4, 8) Dim seq As UInt32 BitConverter.ToUInt32(data, 12) Dim bodyLen As Integer BitConverter.ToUInt16(data, 16) CRC 校验最后两字节是 CRC前面全部参与计算 Dim crcCalc As UInt16 Crc16.Compute(data, 0, data.Length - 2) Dim crcRecv As UInt16 BitConverter.ToUInt16(data, data.Length - 2) If crcCalc crcRecv Then Return Select Case cmd Case H1 心跳更新在线表回 ACK OnlineTable.Touch(deviceId, remoteEP) SendAck(server, remoteEP, deviceId, seq) Case H2 消费上报先落库去重再回 ACK Dim body As Byte() New Byte(bodyLen - 1) {} Array.Copy(data, 18, body, 0, bodyLen) If ConsumptionStore.SaveOnce(deviceId, seq, body) Then SendAck(server, remoteEP, deviceId, seq) End If Case H3 补传断网恢复后设备批量补数据逻辑同上 SendAck(server, remoteEP, deviceId, seq) End Select End Sub 回一条 ACK告诉设备这条我收到了 Private Sub SendAck(server As UdpClient, remoteEP As IPEndPoint, deviceId As String, seq As UInt32) Dim ack As Byte() BuildAck(deviceId, seq) server.Send(ack, ack.Length, remoteEP) End Sub End Module逻辑说明主循环用UdpClient.Receive阻塞收包拿到remoteEP就知道是哪台设备、哪个端口发来的回 ACK 时直接用它。HandlePacket里先做长度、帧头、CRC 三层过滤脏包在进业务前就被拦掉。命令字分流后心跳只更新在线表消费上报走SaveOnce去重落库两条路径都回 ACK。参数说明ListenPort是设备配置里要填的服务器端口改这里设备端也要同步改。ReceiveBufferSize设 64KB 是为了防止突发大包被截断UDP 默认缓冲区偏小。CRC 计算范围是0 到 Length-2最后两字节是校验位本身不能算进去。SaveOnce返回 True 才回 ACK保证“落库成功才确认”避免设备以为成功其实没存。3. 让“实时在线”真正实时在线表、心跳超时与并发处理3.1 在线状态表怎么设计才不误判“实时在线”最容易翻车的地方是误判设备明明在线后台显示离线或者设备早断电了后台还显示在线。根子在于在线表的设计。我一般用一个ConcurrentDictionary(Of String, DeviceState)key 是设备号value 里存最后心跳时间、来源 IP、端口、最后流水号。每次收到心跳就更新LastSeen DateTime.Now。判断在线不是靠一个布尔标志而是靠“当前时间 - LastSeen 是否小于阈值”。阈值怎么定看设备心跳间隔。如果设备 30 秒发一次心跳阈值设 90 秒3 倍比较稳能容忍偶发丢包和网络抖动。设太短一次丢包就误报离线设太长设备真断了半天才发现。这个值我一般做成配置项不同项目现场调。3.2 心跳超时扫描与离线判定在线表更新是“被动”的离线判定需要“主动”扫描。开一个后台线程每隔 10 秒扫一遍在线表把超时的设备标记为离线并触发告警。VB.NET 里用Task或独立Thread都行。 后台扫描线程定期把超时设备标记为离线 Private Sub StartOfflineScanner() Task.Run(Sub() While True Threading.Thread.Sleep(10000) 每 10 秒扫一次 Dim now As DateTime DateTime.Now For Each kv In OnlineTable.Snapshot() 超过 90 秒没心跳判定离线 If (now - kv.Value.LastSeen).TotalSeconds 90 Then OnlineTable.MarkOffline(kv.Key) 这里可以触发告警、写日志、推送消息 Console.WriteLine(设备离线 kv.Key) End If Next End While End Sub) End Sub逻辑说明扫描周期 10 秒比心跳间隔小保证判定及时。Snapshot()返回当前在线表的副本避免遍历时被接收线程修改导致集合异常——这是并发场景下最常见的翻车点。MarkOffline只改状态不删记录保留最后在线时间方便排查。参数说明Sleep(10000)是扫描周期太小浪费 CPU太大判定延迟。90 秒阈值要和设备心跳间隔匹配设备改心跳周期这里必须同步改。生产环境建议把离线事件写进日志表方便回溯“这台机器几点几分掉的”。3.3 接收与业务解耦别在收包线程里做重活上面那个最小循环有个隐患HandlePacket里如果直接做数据库写入数据库一慢收包就堵UDP 缓冲区一满后面的包全丢。这是 UDP 服务器端最典型的性能坑。正确做法是收包线程只做“解析 入队”业务处理交给独立的工作线程或线程池。 收包线程只入队业务线程消费队列 Private ReadOnly _queue As New ConcurrentQueue(Of RawPacket)() 收包处 _queue.Enqueue(New RawPacket With {.Data data, .Remote remoteEP}) 业务线程 Private Sub StartWorker() Task.Run(Sub() While True Dim pkt As RawPacket Nothing If _queue.TryDequeue(pkt) Then HandlePacket(server, pkt.Data, pkt.Remote) Else Threading.Thread.Sleep(1) 队列空时让出 CPU End If End While End Sub) End Sub逻辑说明ConcurrentQueue是线程安全的收包线程只管入队几乎不耗时UDP 缓冲区不会被业务拖慢。业务线程从队列取包处理数据库慢也只影响队列长度不影响收包。队列积压时可以加监控积压过多说明业务处理能力不足该加线程或优化 SQL 了。参数说明Sleep(1)是空队列时的让步避免死循环空转吃满 CPU。生产环境可以换成BlockingCollection的Take队列空时自动阻塞更省 CPU。队列长度建议加个上限告警比如超过 1 万条就报警这是系统扛不住的早期信号。4. 避坑与排查UDP 消费机服务器端最常见的 5 个翻车现场4.1 端口被占用服务起不来现象程序一启动就抛异常提示“通常每个套接字地址只允许使用一次”服务直接挂掉。原因端口被上一个没退干净的进程占着或者被别的程序用了。UDP 端口虽然不像 TCP 有 TIME_WAIT但进程没释放时照样占用。解决启动前先检测端口或者用SocketOptionName.ReuseAddress允许重用更稳的做法是捕获异常后提示具体端口运维一看就知道去杀哪个进程。排查时用netstat -ano | findstr 9000看谁占着。4.2 收到包但解析全是乱码现象日志里能看到收到数据但设备号、金额全是乱码。原因编码不一致。设备端用 GBK 发服务端用 ASCII 或 UTF-8 解中文或特殊字符就乱。解决协议里约定死编码纯数字和 ASCII 设备号用 ASCII 最稳涉及中文备注统一用 UTF-8 并在协议文档里写清楚。别一边 GBK 一边 UTF-8这是血泪经验。4.3 设备显示已上报后台却没流水现象设备端提示上报成功后台查不到这笔消费。原因ACK 回早了。服务端收到包就回 ACK但落库失败了设备以为成功不再重发数据就丢了。解决必须“落库成功才回 ACK”SaveOnce返回 True 再发确认。落库失败就不回设备超时重发靠去重保证不重复入库。这个顺序错了丢数据是必然的。4.4 重复流水入库现象同一笔消费在数据库里出现两三条。原因设备没收到 ACK 重发了服务端没做去重。解决用“设备号 流水号”建唯一索引插入时用INSERT ... ON DUPLICATE KEY UPDATE或先查后插配合数据库唯一约束兜底。内存里也可以用一个近期流水号的滑动窗口快速判重减少数据库压力。4.5 高峰期大量丢包现象平时正常一到饭点刷卡高峰就丢包后台流水对不上。原因收包线程被业务阻塞UDP 缓冲区溢出内核直接丢包。解决按 3.3 的做法把收包和业务解耦收包只入队同时调大ReceiveBufferSize并在系统层面调大 UDP 接收缓冲区。监控队列积压积压持续增长就是处理能力不够的信号。5. 进阶技巧用抓包和压测把服务器端调到能扛住饭点高峰前面把能跑通的骨架和坑都过了一遍最后落到一个具体技巧怎么验证你这套服务器端到底能扛多少设备、多少并发。别等上线了饭点崩了才后悔本地先用工具压一遍。第一步是抓包验证协议。用 Wireshark 过滤udp.port 9000看设备实际发出来的字节流和你协议文档对不对得上。我一般会重点看三处帧头是不是 0xAA55、CRC 算出来和包里带的是否一致、设备号字段有没有偏移错位。抓包是排查“设备说发了、服务端说没收到”这类玄学问题的黑匣子比在代码里加一堆日志快得多。第二步是压测。写一个模拟器用多个线程模拟几百台设备按真实心跳间隔和消费频率往服务器打流。VB.NET 里用UdpClient循环Send就行重点是把设备号、流水号做成变化的模拟真实去重压力。 压测模拟器模拟 N 台设备持续上报 Private Sub StressTest(serverIP As String, port As Integer, deviceCount As Integer) Dim tasks As New List(Of Task)() For i As Integer 1 To deviceCount Dim devId As String DEV i.ToString(D5) tasks.Add(Task.Run(Sub() Dim client As New UdpClient() Dim ep As New IPEndPoint(IPAddress.Parse(serverIP), port) Dim seq As UInt32 0 While True seq 1 Dim pkt As Byte() BuildHeartbeat(devId, seq) client.Send(pkt, pkt.Length, ep) Threading.Thread.Sleep(30000) 30 秒一次心跳 End While End Sub)) Next Task.WaitAll(tasks.ToArray()) End Sub逻辑说明每台模拟设备一个 Task独立 UdpClient按 30 秒心跳持续发。BuildHeartbeat按第 2 章的报文格式拼包CRC 必须算对否则服务端直接丢压测就白压了。跑起来后观察服务端的 CPU、内存、队列积压和数据库写入延迟。参数说明deviceCount从 100 开始往上加找到服务端开始丢包或队列持续积压的临界点那就是当前配置的容量上限。Sleep(30000)要和真实设备心跳一致压测才有参考价值。压测时把消费上报频率也加上比如每台设备每分钟一笔更接近真实饭点场景。第三步是看系统层面的 UDP 统计。Linux 上用netstat -su看receive buffer errors和packet receive errors这两个数持续增长说明内核在丢包要么调大缓冲区要么服务端处理太慢。Windows 上可以用性能计数器看 UDP 相关指标。这一步能区分“是网络丢的还是我程序丢的”定位方向完全不同。我自己的习惯是任何 UDP 服务器端上线前必须先在本地用模拟器压到目标设备数的 1.5 倍跑满一小时不丢包、队列不积压才敢往生产放。UDP 这东西没有 TCP 的背压机制服务端扛不住就是静默丢包用户那边只看到“钱扣了没记录”排查起来极其痛苦。宁可压测多花半天也别在饭点被电话叫起来。希望帮到你。本文还有配套的精品资源点击获取