ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

VB.net实现UDP服务器:实时在线消费考勤门禁系统源码解析

VB.net实现UDP服务器:实时在线消费考勤门禁系统源码解析 简介这是一份基于VB.net开发的Socket UDP通讯源代码定位为实时在线型消费考勤门禁系统的UDP服务器端适合有VB.net基础、正在从事门禁消费设备对接或网络通讯开发的技术人员。代码围绕UDP端口侦听、客户端数据包解析、UDP广播与点对点发送、大数据包连续发送等核心环节展开并在线程中安全更新UI控件附带数字金额转中文大写功能能帮助开发者快速理解在线设备通讯的整体流程。资源包为rar格式共96个文件包含18个vb源码、16个resources资源文件、12个dll动态库、6个exe可执行文件以及resx、config、xml、txt等辅助文件整体压缩包大小约2.45MB目录结构清晰便于定位核心模块。目前已有274人学习下载适合作为项目参考或学习样本。从源码中可学习到多线程UDP服务的设计思路、消息解析与回包策略、界面与网络线程的交互方式以及完整项目如何组织Form、资源与配置既可用于实际系统二次开发也可作为VB.net网络编程的进阶练习。1. 实时在线消费门禁场景下为什么网关层要选UDP而不是TCP在食堂消费机、订餐机、考勤门禁这类设备联网方案里很多人第一反应是TCP毕竟有握手、有重传貌似更可靠。但真正在VB.net里做过一轮设备联调之后你会发现TCP在这个场景下反而容易把自己套住消费机和门禁控制器大多是嵌入式主板并发连接数有限断线重连逻辑写得稍不严谨服务端就会积累大量半开连接。而UDP虽然“丢包不负责”但胜在无连接、开销低一台工控机同时轮询几十台设备时内存和线程压力都小很多。这套「实时在线消费考勤门禁VB.net」源码给我的直观印象是它不是为了演示Socket API怎么调而是把UDP服务器端真正接到了业务上——开启线程侦听端口设备主动上报的数据、解析设备编号和消费金额、按需回发广播或点对点指令并且在接收线程里安全更新WinForm界面。对于需要对接刷卡消费、订餐扣款、门禁记录这类实时设备的.NET开发者来说这是一份可以直接抄框架的参考。适合已经会用VB.net写简单窗体但没真正碰过Socket协议栈的工程师。2. VB.net工程结构与UDP通信链路的代码脉络拿到压缩包后先别急着开Form1.vb这份源码真正的主干不在界面上而在线程和Socket的交互方式里。工程里能看到Form1到Form7多个窗体外加PayApiFun.dll和App.config其中Form1是主监控窗体其他窗体分别承担设备配置、消费记录、人员信息等功能。UDP服务器的核心代码分散在Form1和部分公共模块中。2.1 项目文件层级怎么读从源码包的文件列表可以直接还原整个解决方案的组织方式WindowsApplication1/ ├── Form1.vb / Form1.Designer.vb // 主窗体UDP监听与业务分发 ├── Form2 ~ Form7 // 设备管理、消费记录、门禁设置等 ├── PayApiFun.dll // 封装消费扣款和支付协议相关接口 ├── App.config // 端口号、数据库连接等配置 ├── My Project/ // 程序集信息与资源 └── 实时在线消费订餐考勤门禁机VB.net.sln主窗体Form1是入口设备上报数据到达后先在这里做协议解析再根据功能码决定是更新界面、写数据库还是转发到业务窗体。Form3和Form4负责设备参数下发Form6和Form7处理人员档案和充值记录。2.2 UDP服务器端的初始化与端口绑定UDP通信的第一步是创建Socket并绑定本地端口。源码里监听端口的写法在VB.net中很典型我结合常见的工控机部署方式把核心骨架整理如下Imports System.Net Imports System.Net.Sockets Imports System.Threading Public Class Form1 Private udpServer As Socket Private listenThread As Thread Private isRunning As Boolean False Private Const BUFFER_SIZE As Integer 4096 Private Sub StartUdpServer(port As Integer) Try udpServer New Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) Dim localEndPoint As New IPEndPoint(IPAddress.Any, port) 绑定本机所有网卡的指定端口 udpServer.Bind(localEndPoint) isRunning True listenThread New Thread(AddressOf ReceiveLoop) listenThread.IsBackground True listenThread.Start() AppendLog(UDP服务已启动端口 port) Catch ex As SocketException AppendLog(端口绑定失败请检查端口是否被占用。错误码 ex.ErrorCode) End Try End Sub逻辑说明Socket对象用Dgram类型创建UDP套接字IPAddress.Any表示监听本机所有网卡不区分设备从哪个IP进来这对现场多网段布线很关键。绑定失败时SocketException的ErrorCode能帮助你快速区分地址占用和权限问题。参数调整建议如果现场设备固定的网段把IPAddress.Any换成IPAddress.Parse(192.168.1.100)可以过滤其他网卡过来的报文减轻干扰端口号要在App.config里集中管理不要写死在代码里否则换端口还要重新编译。2.3 接收线程的轮询循环设计UDP没有连接概念服务端要在一个死循环里持续接收数据。这里最忌讳的是在UI线程里直接接收否则界面卡死一次设备上报的数据就会堵在缓冲区里直接导致消费记录丢失。正确的做法是独立线程跑接收循环Private Sub ReceiveLoop() Dim buffer( BUFFER_SIZE - 1) As Byte Dim remoteEndPoint As IPEndPoint New IPEndPoint(IPAddress.Any, 0) While isRunning Try 注意EndReceiveFrom会阻塞当前线程 Dim length As Integer udpServer.ReceiveFrom(buffer, remoteEndPoint) If length 0 Then Dim data(length - 1) As Byte Array.Copy(buffer, data, length) 将原始数据交给解析模块处理 ParseAndDispatch(data, remoteEndPoint) End If Catch ex As Exception AppendLog(接收数据异常 ex.Message) Thread.Sleep(100) End Try End While End Sub这段代码是整份源码里最核心的运行骨架。ReceiveFrom是同步阻塞调用接收线程会一直停在这里等数据设备不发数据时CPU占用几乎为零。收到数据后先按实际长度拷贝到新数组避免上一次接收的残留字节干扰解析。注意一个容易被忽略的细节remoteEndPoint变量在调用ReceiveFrom时会被重写拿到的是发送方设备的IP和端口后续点对点回复时必须用这个地址不能用配置的设备列表里的IP直接回——设备如果走DHCPIP可能已经变了。3. 设备数据包解析帧结构、功能码与粘包处理UDP接收线程拿到的是原始字节数组直接转ASCII字符串是不行的。消费机和门禁控制器上报的数据往往按照自定义协议组帧常见结构是帧头2字节 设备编号4字节 功能码1字节 数据长度2字节 数据体N字节 校验1字节 帧尾2字节。解析的关键是找到帧头然后按长度字段切帧。3.1 解析循环与缓冲区设计UDP虽然不像TCP那样有连续字节流粘包问题但如果设备连续发送多个数据报某个包的解析逻辑处理时间过长缓冲区里就可能积压多帧数据。所以解析函数不能只处理一帧就退出Private Sub ParseAndDispatch(data() As Byte, remote As IPEndPoint) Dim offset As Integer 0 While offset data.Length 查找帧头 0xAA 0x55 If data(offset) HAA AndAlso data(offset 1) H55 Then If offset 3 data.Length Then Dim dataLen As Integer (data(offset 2) 8) Or data(offset 3) If offset 4 dataLen data.Length Then Dim frame(dataLen - 1) As Byte Array.Copy(data, offset 4, frame, 0, dataLen) ProcessFrame(frame, remote) offset 4 dataLen Else 数据不完整可能是分片需要等待下一包 Exit While End If Else Exit While End If Else offset 1 End If End While End Sub这段解析逻辑的关键是“边扫描边切帧”。每次从当前offset位置开始找0xAA 0x55帧头找到后读取两字节长度字段大端序然后把完整的一帧交给ProcessFrame处理。如果长度字段声称的帧长超过了实际收到的字节数说明是分片包直接退出循环等待下一批数据到来。这里的长度字段换算方式是项目中最容易踩坑的地方。设备协议里如果约定的是小端序低字节在前那(data(offset2) 8) Or data(offset3)的读法就会得到错误长度解析必然错位。建议先拿两个已知长度的报文做字节对比确认设备文档里的字节序标注。3.2 功能码分发帧结构里的功能码决定了这条数据是消费扣款记录、门禁开门请求、还是设备心跳。源码里通过Select Case进行分发Private Sub ProcessFrame(frame() As Byte, remote As IPEndPoint) Dim cmd As Byte frame(0) Select Case cmd Case H01 心跳包设备在线状态上报 UpdateDeviceStatus(remote.Address.ToString(), DateTime.Now) Case H02 消费记录卡号金额时间 Dim cardNo As String Encoding.ASCII.GetString(frame, 1, 8) Dim amount As Integer BitConverter.ToInt32(frame, 9) 金额单位是分转成元需要除以100 AppendConsumeRecord(cardNo, amount / 100.0, remote.Address.ToString()) Case H03 门禁请求校验卡权限后决定是否开门 CheckDoorPermission(frame, remote) Case Else AppendLog(未知功能码 cmd.ToString(X2)) End Select End SubSelect Case分发的好处是后期加新指令类型时改动范围小。每个Case分支对应一类业务操作心跳包只更新设备在线表格消费记录要经过金额换算后写入数据库并更新UI门禁请求则需要查询本机卡权限表再通过UDP回发开门指令。金额字段值得单独强调设备端一般用整数传输金额单位是分比如500代表5.00元。如果直接把整数装进文本框展示用户看到的会是“500”而不是“5.00”。消费金额的精度问题会在第四章的中文大写转换里进一步体现。3.3 设备报文的鲁棒性处理现场设备经常不按规范出牌解析代码要有容错设计异常情况典型现象处理策略报文长度不足帧头找到但长度字段越界跳出循环等待剩余数据设置超时丢失计数帧头错位0xAA后不是0x55跳过当前字节继续向后扫描功能码未知设备固件升级后新增指令记录日志但不中断接收线程校验失败传输过程中数据被干扰丢弃当前帧同时累计错误包数我在实际部署中遇到过一款消费机在电量低时频繁发送空数据报长度为0这时ProcessFrame会收到一个空数组索引frame(0)直接抛异常。处理方式是在ProcessFrame入口先判断frame.Length 2就直接返回这比在异常里捕获更省资源。4. 线程内更新UI与UDP回包广播、点对点和Invoke机制UDP服务器端不只是被动接收还要主动下发指令。比如系统开启消费时段后需要通知所有在线设备切换到扣费模式而针对某个指定设备需要单发开门指令或余额查询指令。这两种回包方式在源码里分得很清楚。4.1 跨线程更新UI的Invoke正确姿势ReceiveLoop跑在后台线程直接操作文本框会触发“线程间操作无效”的异常。VB.net里的标准解法是借助控件的Invoke方法把更新操作切回UI线程Private Sub AppendLog(message As String) If txtLog.InvokeRequired Then txtLog.Invoke(New Action(Of String)(AddressOf AppendLog), message) Else If txtLog.TextLength 50000 Then txtLog.Clear() End If txtLog.AppendText(DateTime.Now.ToString(HH:mm:ss) message vbCrLf) End If End Sub Private Sub AppendConsumeRecord(cardNo As String, amount As Double, ip As String) If dgvRecords.InvokeRequired Then dgvRecords.Invoke(New Action(Of String, Double, String)(AddressOf AppendConsumeRecord), cardNo, amount, ip) Else dgvRecords.Rows.Add(cardNo, amount.ToString(F2), ip, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss)) End If End SubInvokeRequired属性用于判断当前线程是否与控件所在线程相同返回True时说明拿到了后台线程需要Invoke切换到UI线程。这两个辅助方法里每个操作前都做了跨线程判断避免了重复代码的同时也保证了线程安全。这里有三个细节需要注意。第一日志框必须限制长度否则长时间运行后内存持续增长界面会越来越卡第二Invoke和BeginInvoke的选择上消费记录更新用Invoke同步调用即可高频日志可以用BeginInvoke异步调用第三关闭窗体时要把isRunning置为False并调用udpServer.Close()否则后台线程还阻塞在ReceiveFrom上进程退出不干净。4.2 UDP广播与点对点发送广播用于一次性通知所有设备点对点发送用于精确控制单台设备。源码里两类发送方法都有Private Sub SendBroadcast(data() As Byte, port As Integer) Dim broadcastAddr As IPEndPoint New IPEndPoint(IPAddress.Broadcast, port) udpServer.SendTo(data, broadcastAddr) End Sub Private Sub SendToDevice(data() As Byte, targetIP As String, port As Integer) Dim remoteEp As IPEndPoint New IPEndPoint(IPAddress.Parse(targetIP), port) udpServer.SendTo(data, remoteEp) End Sub 统一组装待下发指令 Private Function BuildCommand(cmd As Byte, payload() As Byte) As Byte() Dim frameDataLength As Integer payload.Length 1 Dim frame(frameDataLength 5) As Byte frame(0) HAA frame(1) H55 frame(2) CByte((frameDataLength 8) And HFF) frame(3) CByte(frameDataLength And HFF) frame(4) cmd Array.Copy(payload, 0, frame, 5, payload.Length) frame(frame.Length - 1) H0D Return frame End Function广播发送时用IPAddress.Broadcast交换机/路由器会把报文扩散到当前子网内的所有设备设备收到后根据帧里的功能码判断是否需要响应。点对点发送则使用设备刚才上报数据时的源IP这也是为什么接收线程里要保存remoteEndPoint的原因。BuildCommand函数用来统一组装协议帧方便业务代码只关心功能码和载荷数据。长度字段计算时要把功能码占用的1字节也算进去很多人在组装时在这里错位导致设备收到后解析出错误时长拒收指令。4.3 回包策略的选择逻辑什么时候用广播、什么时候用点对点源码里的调用场景是很好的参考业务场景发送方式原因系统进入消费时段广播所有消费机需要同时切换到工作模式门禁开门指令点对点只针对请求的那一台控制器设备校时广播所有设备统一时间基准余额查询点对点响应要回到特定设备的界面广播并不总是最优解。如果子网很大或者设备数量超过50台广播风暴会拖垮网络。一个折中方案是维护在线设备列表用循环遍历列表逐个点对点发送虽然耗时稍长但网络负载可控。5. 金额转中文大写与大数据包连续发送的实战细节源码里有两个功能点在业务上很讨巧一个是消费金额转中文大写汉字另一个是大数据包比如人员照片或指纹特征码连续发送机制。这两个功能在现场联调时经常被卡住值得单独展开。5.1 数字金额转中文大写的边界处理消费扣款后小票要打印“壹佰贰拾叁元肆角伍分”这个转换函数看着简单但有几个边界条件很容易漏零的处理、元整的处理、亿级单位的处理。Private Function MoneyToChinese(amount As Double) As String Dim arrNum() As String {零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖} Dim arrUnit() As String {分, 角, 元, 拾, 佰, 仟, 万, 拾, 佰, 仟, 亿} Dim strAmount As String (amount * 100).ToString(F0).PadLeft(15, 0c) Dim result As New System.Text.StringBuilder() Dim zeroFlag As Boolean False For i As Integer 0 To strAmount.Length - 1 Dim digit As Integer CInt(strAmount(i).ToString()) Dim unitIndex As Integer strAmount.Length - 1 - i If digit 0 Then 连续的零只写一次且零不在元/万/亿单位前时跳过 If unitIndex 2 OrElse unitIndex 6 OrElse unitIndex 10 Then result.Append(arrUnit(unitIndex)) zeroFlag False ElseIf Not zeroFlag Then If unitIndex 2 Then result.Append(零) zeroFlag True End If Else result.Append(arrNum(digit) arrUnit(unitIndex)) zeroFlag False End If Next 处理元整情况 Dim temp As String result.ToString() If temp.EndsWith(元) Then temp 整 ElseIf temp.Contains(元) AndAlso Not temp.EndsWith(角) AndAlso Not temp.EndsWith(分) Then temp 整 End If Return temp End Function这个函数的处理思路是先把金额乘100换算成整数分再左填充到15位确保每一位都能映射到对应的单位下标。元、万、亿是关口位即使数字为0也要保留单位而角分位的零要尽可能省略。实际使用时要先做金额范围校验负数或超过亿级的金额直接丢弃。5.2 大数据包的分片与重组机制门禁控制器下载人员照片或指纹模板时单包数据可能超过512字节——UDP的理论上限是65507字节但设备端的缓冲区没这么大。源码里走的是分片发送把大数据拆成多个小包每包带序号接收方组装。Private Sub SendLargeData(data() As Byte, targetIP As String, port As Integer, packetSize As Integer) Dim totalPackets As Integer Math.Ceiling(data.Length / packetSize) Dim sequence As Short 0 For i As Integer 0 To totalPackets - 1 Dim offset As Integer i * packetSize Dim length As Integer Math.Min(packetSize, data.Length - offset) Dim payload(length 3) As Byte 前两字节是总包数后两字节是当前包序号 payload(0) CByte((totalPackets 8) And HFF) payload(1) CByte(totalPackets And HFF) payload(2) CByte((i 8) And HFF) payload(3) CByte(i And HFF) Array.Copy(data, offset, payload, 4, length) Dim cmdPayload As Byte() New Byte() {H10} Dim cmdFrame As Byte() BuildCommand(cmdPayload, payload) SendToDevice(cmdFrame, targetIP, port) 简单流控等待50ms避免设备接收缓冲区溢出 Thread.Sleep(50) Next End Sub分片发送的关键点有两个。第一每片之间必须加延时不能无脑连续发送否则设备端的数据缓冲区一满就会丢弃后续包第二接收方要根据总包数和序号做重组校验缺包时向发送方请求重传指定片序号。调试分片逻辑时建议用Wireshark抓包筛选UDP流量重点看每个分片的时间间隔是否均匀、序号是否连续。如果能看到乱序但序号不连续大概率是前面有包被丢弃了。本文还有配套的精品资源点击获取
返回列表