基本功能实现:
- 可设置心跳包的TCP SOCKET远程设备均可以连接;
- 远程设备数量可在限制的范围内任意增加;
- 可对远程设备进行什么任意频率的数据轮询;
- 可处理限制范围内的任意数据量;
- 程序使用尽量少的线程处理设备的连接、数据的处理,增加线程的繁忙度,减少内存的占用。
基本思路与部分代码:
- 创建负责侦听的线程
'循环侦听是否有设备连接上来 Dim threadWatch As Thread = New Thread(AddressOf WatchConnecting) threadWatch.IsBackground = True threadWatch.Start(serverSocket)
- 创建负责接收数据的线程
'对dtu集合中每个dtu的socket进行数据的接收,接收到数据就放到数据队列中 Dim threadReceieveData As Thread = New Thread(AddressOf receieveData) threadReceieveData.IsBackground = True threadReceieveData.Start()
- 创建负责处理数据的线程
'对队列中的每个数据列进行读取,并处理。 Dim threadResolvingData As Thread = New Thread(AddressOf resolvingData) threadResolvingData.IsBackground = True threadResolvingData.Start()
- 创建三个线程,分别处理侦听、数据接收、数据处理
- 侦听的代码
''' <summary> ''' 负责监听的代码:一个死循环接收socket连接,接收到连接后马上存放到 dtu 列表中 ''' </summary> ''' <remarks></remarks> Sub WatchConnecting(ByVal serverSocket As Socket) While True '持续不断的监听客户端的连接请求;侦听只是为了建立连接,建立完成继续侦听 Try '开始监听客户端连接请求,Accept方法会阻断当前的线程; '一旦监听到一个客户端的请求,就返回一个与该客户端通信的 套接字; Dim sokConnection As Socket = serverSocket.Accept() PlcingDTUServiceEventLog.WriteEntry("侦听结果:有客户端连接成功:" & sokConnection.RemoteEndPoint.ToString()) Dim dtu As dtuClass = New dtuClass dtu.socket = sokConnection dtuList.Add(dtu) Catch ex As Exception PlcingDTUServiceEventLog.WriteEntry("侦听客户端连接时出错:" & ex.Message) restartComputer(ex.Message.ToString, "侦听客户端连接时出错") End Try End While End Sub
- 数据接收的代码
''' <summary> ''' 仅接收数据:在一个死循环中接收数据:接收到数据后,存放到附表中,在已经存在的线程中进行处理。 ''' </summary> ''' <remarks></remarks> Sub receieveData() While True Thread.Sleep(100) Try For Each dtu As dtuClass In dtuList 'PlcingDTUServiceEventLog.WriteEntry("dtu的信息:" & dtu.socket.RemoteEndPoint.ToString) If dtu.Socket IsNot Nothing And dtu.Socket.Connected Then Dim sokClient As Socket = dtu.Socket 'Dim dtuNumber As Integer = 0 '定义一个1024字节的缓存区; Dim arrMsgRec(1024) As Byte '将接受到的数据存入到输入 arrMsgRec中; Dim length As Integer = -1 Try '如果有数据,则接收,否则不处理 If sokClient.Available > 0 Then length = sokClient.Receive(arrMsgRec) '接收数据,并返回数据的长度; '如果接收到的数据长度正确,则把数据放到消息队列中 If length > 0 Then dtu.Data = byteToHexStr(arrMsgRec, length) dtu.length = length '接收到数据后,就把数据存放到队列中待处理 dataQuene.Enqueue(dtu) If writeLOG Then PlcingDTUServiceEventLog.WriteEntry("接收到数据,并把数据存放到队列中待处理:" & dtu.Data & ",队列长度:" & dataQuene.Count) End If End If Catch se As SocketException PlcingDTUServiceEventLog.WriteEntry("接收数据SOCKET异常:" & se.Message & vbCrLf & "数据长度:" & Str(length) & vbCrLf & "连接信息:" & sokClient.RemoteEndPoint.ToString() & vbCrLf & " devID:" & getProjectBySocket(sokClient).gprsEquipmentID) PlcingDTUServiceEventLog.WriteEntry(sokClient.RemoteEndPoint.ToString() & " : " & getProjectBySocket(sokClient).gprsEquipmentID & " 号 LQDTU SelectRead: " & sokClient.Poll(1000, SelectMode.SelectRead) & vbCrLf & " SelectWrite: " & sokClient.Poll(1000, SelectMode.SelectWrite) & vbCrLf & " SelectError: " & sokClient.Poll(1000, SelectMode.SelectError)) '---------异常时踢DTU下线 socketDissconnect(sokClient, "接收数据SOCKET异常") '------------------------------ restartComputer(se.Message.ToString, "接收数据SOCKET异常") Continue For Catch ode As ObjectDisposedException PlcingDTUServiceEventLog.WriteEntry("接收数据SOCKET异常:对已释放的对象执行操作时所引发的异常:" & ode.Message & vbCrLf & "数据长度:" & Str(length) & vbCrLf & "连接信息:" & sokClient.RemoteEndPoint.ToString() & vbCrLf & " devID:" & getProjectBySocket(sokClient).gprsEquipmentID) PlcingDTUServiceEventLog.WriteEntry(sokClient.RemoteEndPoint.ToString() & " : " & getProjectBySocket(sokClient).gprsEquipmentID & " 号 LQDTU SelectRead: " & sokClient.Poll(1000, SelectMode.SelectRead) & vbCrLf & " SelectWrite: " & sokClient.Poll(1000, SelectMode.SelectWrite) & vbCrLf & " SelectError: " & sokClient.Poll(1000, SelectMode.SelectError)) '---------异常时踢DTU下线 socketDissconnect(sokClient, "接收数据SOCKET异常:对已释放的对象执行操作时所引发的异常") '------------------------------ restartComputer(ode.Message.ToString, "对已释放的对象执行操作时所引发的异常") Continue For Catch e As Exception PlcingDTUServiceEventLog.WriteEntry("接收数据一般异常:" + e.Message & vbCrLf & " devID:" & getProjectBySocket(sokClient).gprsEquipmentID) '---------异常时踢DTU下线 socketDissconnect(sokClient, "接收数据一般异常") '------------------------------ restartComputer(e.Message.ToString, "接收数据一般异常") Continue For End Try End If Next Catch ex As Exception End Try End While End Sub
- 数据处理的代码
''' <summary> ''' 单独的线程中查看消息队列中的消息,有则处理,没有则循环 ''' </summary> ''' <remarks></remarks> Sub resolvingData() While True Thread.Sleep(100) '处理接收到的数据 Dim dtu As dtuClass = Nothing Dim dtuNumber As Integer = 0 If dataQuene.Count <> 0 Then Try dtu = dataQuene.Dequeue '有时还是会出现异常,所以放在容错范围内 PlcingDTUServiceEventLog.WriteEntry("从队列中取出最早的一条数据进行处理:" & dtu.Data & ",项目编号:" & getProjectBySocket(dtu.Socket).gprsEquipmentID & ",队列长度:" & dataQuene.Count) Catch ex As Exception PlcingDTUServiceEventLog.WriteEntry("取出数据处理时出错,出错信息:" & ex.Message) Continue While End Try Else Continue While End If Try Dim strMsg As String = dtu.Data '以下为数据处理的具体逻辑,不同的需求,有不同的逻辑,自己编写就好 '判断数据长度 If Len(strMsg) = 0 Or dtu.length = -1 Then Continue While If Len(strMsg) <= 4 And Len(strMsg) >= 1 Then PlcingDTUServiceEventLog.WriteEntry(Now.ToString & ":数据太短:" & dtu.Socket.RemoteEndPoint.ToString() & " 数据:" & strMsg) Continue While End If If Len(strMsg) > 4 And Len(strMsg) < 2048 Then '定义数据的类型: '0001、3031:登录 '0003、3032:心跳包 '其他为MODBUS数据 Dim dataType As String = Mid(strMsg, 5, 4) 'GPRS设备登录:'01060001000CD80F If (dataType = "0001" And Left(strMsg, 2) = "00") Or (dataType = "3031" And Left(strMsg, 4) = "3031") Then ' And Left(strMsg, 2) = "00" '从登录信息中取得DTU编号 dtuNumber = HexStringToAsciiString(Mid(strMsg, 13, 8)) '在字符串截取后再处理就会出现invalid cast error: 从字符串“ ”到类型“Integer”的转换无效。system.InvalidCastException '把设备的登录信息记录到eqState表中(设备在线情况表) Dim Sql As String = "update eqstate set gprsState='1',onoffTime='" & Format(Now, "yyyy-MM-dd HH:mm") & "' where eqid='" & Right("0000" & Trim(Str(dtuNumber)), 4) & "'" Call RunSqlTransaction(connectionWords, Sql) '记录该DTU的心跳时间 getProjectByID(dtuNumber).heartBeatTime = Now() If writeLOG Then PlcingDTUServiceEventLog.WriteEntry(Now.ToString & ":" & "处理数据结果:数据:" & strMsg & ",项目编号:" & dtuNumber & ",动作:登录") '设备登录时,检查DTU列表中是否有DTU的信息,有就直接更换键值对应的连接信息 getProjectByID(dtuNumber).socket = dtu.Socket '接收到设备心跳信息 ElseIf (dataType = "0003" And Left(strMsg, 2) = "00") Or (dataType = "3032" And Left(strMsg, 4) = "3032") Then 'And Left(strMsg, 2) = "00" '从心跳信息中取得DTU编号 dtuNumber = HexStringToAsciiString(Mid(strMsg, 13, 8)) '设备心跳时,检查DTU列表中是否有DTU信息,有就直接更换键值对应的连接信息 getProjectByID(dtuNumber).socket = dtu.Socket '记录该DTU的心跳时间 getProjectByID(dtuNumber).heartBeatTime = Now() Dim Sql As String = "update eqstate set gprsState='1',onoffTime='" & Format(Now, "yyyy-MM-dd HH:mm") & "' where eqid='" & Right("0000" & Trim(Str(dtuNumber)), 4) & "'" Call RunSqlTransaction(connectionWords, Sql) If writeLOG Then PlcingDTUServiceEventLog.WriteEntry(Now.ToString & ":" & "处理数据结果:数据:" & strMsg & ",项目编号:" & dtuNumber & ",动作:心跳") Else 'MODBUS数据,用户数据 '设备有用户数据来时,检查DTU列表中是否有DTU信息,有就直接更换键值对应的连接信息 dtuNumber = CInt(getProjectBySocket(dtu.Socket).gprsEquipmentID) getProjectByID(dtuNumber).heartBeatTime = Now() getProjectByID(dtuNumber).socket = dtu.Socket ReDim Preserve getProjectByID(dtuNumber).resolvedDataClass.Data(getProjectByID(dtuNumber).fieldNumber) '根据功能码处理接收到的用户数据 Select Case Mid(strMsg, 3, 2) Case "01", "02" '01对应输出线圈,也就是Q值,02对应输入线圈,也就是I值 F01_02_Object(getProjectByID(dtuNumber).modbusCommand, dtuNumber, Now(), strMsg) Case "03", "04" '03对应保持寄存器,04对应输入寄存器 F03_04_Object(getProjectByID(dtuNumber).modbusCommand, dtuNumber, Now(), strMsg) Case "05", "06", "10" '这是远程控制返回的数据,需要把这些数据回写到远程控制表中,以表示远程控制的结束 F05_06_10(dtuNumber, strMsg) End Select Dim projectTMP As projectClass = getProjectByID(dtuNumber) '根据当前发送的指令和系统中多指令的记录及是否有结束标志,进行数据的写库操作 '在多指令系统中,有的数据来了,并不需要进行任何操作,所以有以下的写法 Select Case True Case projectTMP.modbusCommand = projectTMP.command '单MODBUS指令时 WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第二指令相同 且 第二个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address2Modbus And projectTMP.address2Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第三指令相同 且 第三个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address3Modbus And projectTMP.address3Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第四指令相同 且 第四个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address4Modbus And projectTMP.address4Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第五指令相同 且 第五个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address5Modbus And projectTMP.address5Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第六指令相同 且 第六个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address6Modbus And projectTMP.address6Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第七指令相同 且 第七个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address7Modbus And projectTMP.address7Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第八指令相同 且 第八个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address8Modbus And projectTMP.address8Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第九指令相同 且 第九个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address9Modbus And projectTMP.address9Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) '当前指令与第十指令相同 且 第十个地址有结束标志时 Case projectTMP.modbusCommand = projectTMP.address10Modbus And projectTMP.address10Over WriteToDataBase_Object(dtuNumber, projectTMP.modbusCommand, strMsg) End Select 'PlcingDTUServiceEventLog.WriteEntry("DTUNUMBER:" & dtuNumber & ",数据:" & strMsg & ",指令:" & getProjectByID(dtuNumber).ModbusCommand) End If End If If Len(strMsg) > 4000 Then '如果接收到的数据长度大于188,则断开这个客户端 PlcingDTUServiceEventLog.WriteEntry("数据太长,连接被关闭:(" & dtu.Socket.RemoteEndPoint.ToString() & "):" & strMsg) '从 通信套接字 集合中删除被中断连接的通信套接字: socketDissconnect(dtu.Socket, "数据太长,连接被关闭") End If Catch ex As Exception PlcingDTUServiceEventLog.WriteEntry("处理数据时出错,出错信息:" & ex.Message) Continue While End Try End While End Sub
- 有了这些基本的逻辑,建立一个可以侦听远程TCPSOCKET连接的上位机,基本可以实现了。