多台4G信号设备(如ofo)连接到指定服务器的一套架构
工程类型 idea-kotlin
资源下载 1-客户端UI下载 2-服务器UI下载 3-代码下载
硬件定时发送心跳给服务器 建立连接 多台移动设备通过服务器控制设备或者查状态
该需求有点类似多人聊天室 就比如ofo一样多个用户和多个设备之间处理
以队列安全的形势查询和控制准确发出指令和接收指令
设备长连接就可以套用该模板设计各种命令提供第三方调用,至于硬件指令都可以通过模板在衍生出来从而解决各种需求
这里用软件客户端模拟硬件测试(包括16进制编码传输),
测试效果图如下
发送心跳"1" 与设备建立连接
自定义命令 $php-> IsLive 1 (1是连接的设备名) 是否已连接
自定义命令 $php-> SearchList 查询当前连接的服务器列表
开启服务后 发送设备号“2” 发送 2 心跳与设备建立连接
发送自定义命令 $php-> Receive 2 “等待接收数据” (等待设备2回传数据)
如果设备2不响应时超时会提示code 1002
多次发送指令会按顺序加入队列依次返回数据
在响应时间内 设备2返回数据时会返回指令发起端。
原理:
名称解释:
App->手机app
Servce->Http服务器或第三方平台
TcpScrit->一个脚本服务提供给Service调用
Device->4G设备
模板设计的技术难点在于如何解决异步和多台设备通信
保证每个设备都收到一条信息和回传的信息能够准确的回传给发起端
通常4G设备会不断发送心跳给服务器上的脚本
2:服务器将设备加入到连接的列表中
3:服务向设备应答回传信息
之所以用脚本因为它可能会被其他服务或者其他语言调用,如下图
单个移动设备请求单个物联网设备从发起到结束的时序图如下
1:一个App端向第三方后台发送http请求
2:后台收到请求后向脚本发出指令
3:脚本发送指令(单向或者双向等待回传)给设备
4:脚本响应回传后台是否成功
5:后台返回app端数据
当有多个用户和多台设备时多并发的问题就来了
1:如果多个app端对同一台设备发起操作和查询指令 如何保证每一个设备的查询是正确的并且不会串联
2:如果有一台设备没有响应了,如何作出超时处理
关于这点可以使用队列的来区分每一次的请求,这里默认是长连接的方式如下图所示
流程如下:
⓪多台设备发送心跳给服务器从而建立连接
①服务器接收到某个http请求向脚本发起指令
②定义多个指令类型的集合,根据是哪一条指令类型作后续动作,如果是需要监听回调信息还会注册某个设备的监听器
③将指令在超时队列中运行
④向4G设备发送控制或者查询指令
⑤4G设备向脚本回传当前自己的状态,进入⑥流程
⑥设备监听器接收到指定设备回传的消息作动作
⑦继续回到队列流程处理-如果没有数据就做出超时处理
⑧向服务端回传数据
从一个请求发起到结束可以看作一次事件 以下类图是每个流程需要实现的方法
首先是服务器和多个设备如何建立连接
package org.lxz.tools.syncdevice.client
import org.lxz.tools.syncdevice.event.*
import org.lxz.tools.syncdevice.helper.HexHelper
import java.io.*
import java.net.*
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.locks.ReentrantLock
/**
* Created by linxingzhu on 2018/6/28.
*/
interface DeviceConnectListListen {
fun list(connectMap:HashMap<String, DeviceConnect>)
fun addDevice(deviceConn: DeviceConnect)
fun remove(deviceConn: DeviceConnect)
}
interface LogListen{
/**输入与触发事件监听*/
fun received(received:String, hex:String, device: DeviceConnect, event:EventTask, code:Int)
/**输出监听*/
fun write(connect: DeviceConnect, send:String, mode: OutPutSteamMode)
}
interface ServiceEventListen{
fun event(code:Int)
}
enum class OutPutSteamMode(val desc: String) {
DEFAULT("字符串形式"),
HEX("16进制形式");
}
class TcpService private constructor():Runnable {
companion object {
@JvmStatic
fun main(args: Array<String>) {
instaince.startServer(arrayListOf(
HeartbeatEventTask::class.java,
OpenLockEventTask::class.java,
CheckStatusEventTask::class.java,
CheckAllLockStatusEventTask::class.java,
SearchConnectListEventTask::class.java,
IsLiveEventTask::class.java
))
}
val instaince: TcpService by lazy{ TcpService() }
const val ERROR_STATUS="error"
const val SUCCESS_STATUS="success"
const val ERROR_CODE_INTERRRUPTED=1001
const val ERROR_CODE_TIME_OUT=1002
const val ERROR_CODE_CANNET_NOT_CALLBACK_DATA=1003
const val ERROR_CODE_CANNET_NOT_FIND_DEVICE=1004
/**处理*/
const val STATUS_CODE_DISPOSE=10
const val STATUS_CODE_ACCEPT=11
const val STATUS_CODE_ACCEPT_BUT_NOT_CALLBACK_DATA=12
const val STATUS_CODE_DISPOSE_BUT_NOT_CALLBACK_DATA=13
const val EVENT_INIT_CODE=9001;
const val EVENT_CLOSE_CODE=9002
}
var deviceConnectThreaadPool: ExecutorService?=null
val version = "1.2"
val port = 9501
/**异步读写锁*/
val synclock = ReentrantLock()
var eventChainTask = arrayListOf<Class<out EventAction>>()
/**TCP服务设备连接列表*/
val connectDeviceMap = HashMap<String, DeviceConnect>()
/**TCP服务非设备列表*/
val connecNonDeviceList= arrayListOf<DeviceConnect>()
init{println("Build TcpService Version:" + version +" port:"+port) }
private var mDeviceConnectListListen: DeviceConnectListListen?=null
fun setDeviceConnectListListen(deviceConn: DeviceConnectListListen?): TcpService {
mDeviceConnectListListen=deviceConn
return this
}
fun getDeviceConnectListListen(): DeviceConnectListListen?=mDeviceConnectListListen
private var mLogListen: LogListen?=null
fun setLogListen(log: LogListen): TcpService {
mLogListen=log
return this
}
fun getLogListen(): LogListen?=mLogListen
private var mServiceEventListen: ServiceEventListen?=null
fun setServiceEventListen(event: ServiceEventListen): TcpService {
mServiceEventListen=event
return this
}
fun getServiceEventListen(): ServiceEventListen?=mServiceEventListen
/**创建一条本地指令*/
fun command(content:String,mode: OutPutSteamMode,listen:DeviceConnect.ReadDataListen){
var conn=DeviceConnect(Socket("127.0.0.1",port))
conn.setReadDataListen(listen)
write(conn,content,mode)
}
fun syncExecute(action:()->Unit){
try {
synclock.lock()
action()
synclock.unlock()
} catch (e: Exception) {
}
}
fun write(connect: DeviceConnect, content:String, mode: OutPutSteamMode)
{
syncExecute {
when(mode)
{
OutPutSteamMode.DEFAULT ->{
connect.printWriter.write(content)
connect.printWriter.flush()
}
OutPutSteamMode.HEX ->{
var bytes=HexHelper.hexStr2Bytes(content)
connect.outStream.write(bytes)
connect.printWriter.flush()
}
}
mLogListen?.write(connect,content,mode);
}
}
fun startServer(clazzs:List<Class<out EventAction>>) {
eventChainTask.clear()
eventChainTask.addAll(clazzs)
println("TCP端口监听启动")
serverSocket?.close()
if(deviceConnectThreaadPool==null||deviceConnectThreaadPool!!.isShutdown)
{
deviceConnectThreaadPool = Executors.newWorkStealingPool()
}
deviceConnectThreaadPool!!.submit(this)
}
var serverSocket: ServerSocket?=null
override fun run() {
try {
var socket: Socket? = null
serverSocket = ServerSocket(port)// 1024-65535的某个端口
// 调用accept()方法开始监听,等待客户端的连接
fun getSocket(): Socket? {
socket = serverSocket!!.accept()
return socket;
}
mServiceEventListen?.event(EVENT_INIT_CODE)
while (serverSocket!=null&&!(serverSocket!!.isClosed)&&getSocket() != null) {
println("服务器监听到新连接:" + socket!!.inetAddress)
var deviceConnect:DeviceConnect=DeviceConnect(socket!!)
addNonDeviceList(deviceConnect)
deviceConnectThreaadPool?.submit(deviceConnect)
}
} catch (e: IOException) {
e.printStackTrace()
}
println("服务器停止监听")
mServiceEventListen?.event(EVENT_CLOSE_CODE)
}
private fun addNonDeviceList(deviceConnect: DeviceConnect) {
syncExecute {
connecNonDeviceList.add(deviceConnect)
}
}
fun removeNonDeviceSocketConnectList(deviceConnect: DeviceConnect) {
syncExecute {
connecNonDeviceList.remove(deviceConnect)
}
}
fun saveSocketConnectMap(deviceId: String, tcpSocketConnect: DeviceConnect) {
syncExecute{
print("添加新设备${deviceId}")
instaince.connectDeviceMap.put(deviceId, tcpSocketConnect)
mDeviceConnectListListen?.list(connectDeviceMap)
connecNonDeviceList.remove(tcpSocketConnect)
mDeviceConnectListListen?.addDevice(tcpSocketConnect)
}
}
fun removeSocketConnectMap(deviceId: String, tcpSocketConnect: DeviceConnect) {
syncExecute {
print("移除设备${deviceId}")
instaince.connectDeviceMap.remove(deviceId)
mDeviceConnectListListen?.list(connectDeviceMap)
mDeviceConnectListListen?.remove(tcpSocketConnect)
}
}
fun stopServect() {
//关闭所有连接
closeAllConnect()
deviceConnectThreaadPool?.shutdownNow()
serverSocket?.close()
serverSocket=null;
}
private fun closeAllConnect() {
closeDeviceConnect()
closeNonDeviceConnect()
}
private fun closeNonDeviceConnect() {
syncExecute {
var it=connecNonDeviceList.iterator()
while (it.hasNext())
{
it.next().outStream.close()
}
connecNonDeviceList.clear()
}
}
private fun closeDeviceConnect() {
var it=connectDeviceMap.iterator()
while (it.hasNext())
{
it.next().value.outStream.close()
}
connectDeviceMap.clear()
}
}
为每一个连接插件读写流和监听
class DeviceConnect(val socket: Socket) : Runnable {
var outStream: OutputStream
var inStream: InputStream
var reader: BufferedReader
var deviceId: String? = null
val createTime: String
var printWriter: PrintWriter
var events:ArrayList<EventAction> = arrayListOf()
interface ReadDataListen{
fun read(connect: DeviceConnect,message:String,hex:String,byte:ByteArray,length:Int)
}
private var mReadDataListen:ReadDataListen?=null
fun setReadDataListen(listen:ReadDataListen):DeviceConnect
{ mReadDataListen=listen
return this }
fun getReadDataListen():ReadDataListen?=mReadDataListen
init {
createTime = TimeHelper.getNowTime()
outStream = socket.getOutputStream()
printWriter = PrintWriter(outStream)
inStream = socket.getInputStream()
val streamReader = InputStreamReader(inStream)
reader = BufferedReader(streamReader)
for(eventClass in TcpService.instaince.eventChainTask)
{
events.add(eventClass.newInstance())
}
}
var openEnabledReceiverTask:ArrayList<EventAction> = arrayListOf();
override fun run() {
val buffer = ByteArray(1024 * 4)
var n = 0
fun read(): Int {
n = inStream.read(buffer)
return n;
}
while ((read()) != -1) {
val message = String(buffer, 0, n)
val instruct = message.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray().toList()
val hex = HexHelper.byte2HexStr(buffer, n)
mReadDataListen?.read(this,message,hex,buffer,n);
println("message:->" + message);
println("hex:->" + hex);
TcpService.instaince.syncExecute {
for (task in openEnabledReceiverTask) {
println("当前有一条任务等待回调")
task.deviceAcceptTaskInQueue(message, hex, this, TcpService.instaince)
}
}
for(task in events) {
var isProcessing = task.receiveProcess(message, hex, instruct)
if (isProcessing) {
task.disposeEventTaskInQueue(message, hex, instruct, this, TcpService.instaince)
events.remove(task)
events.add(task.javaClass.newInstance())
break
}
}
}
when(deviceId!=null){
true->TcpService.instaince.removeSocketConnectMap(deviceId!!, this)
else->TcpService.instaince.removeNonDeviceSocketConnectList(this)
}
println("IP:" + socket.inetAddress + " 命令 " + deviceId + " 断开连接")
}
}
创建一个从发起到接收数据的事件接口
/**一个连接事件到结束的生命周期*/
interface EventAction{
/**
* @param message 接收到的原始消息
* @param hex 转化成16进制
* @param instruct 转化以空格分割开的指令
*/
fun receiveProcess(message:String,hex:String, instruct:List<String>):Boolean
/**设置超时时间*/
fun timeOut():Long
/**返回第一个连接的客户端*/
fun getFristConnectClient(): DeviceConnect
/**返回待监听的客户端*/
fun getAcceptConnectClient(): DeviceConnect?
/**
* @param cmd 接收的指令
* @param client 当前发送消息的设备
* @param connectList 已连接设备服务列表
*/
fun disposeEvent(message:String, hex:String, instruct:List<String>, client: DeviceConnect, service: TcpService)
/**监听指定的设备*/
fun deviceAccept(receive:String, hex:String, client: DeviceConnect, service: TcpService)
/**加入超时队列-处理第一个连接的设备指令*/
fun disposeEventTaskInQueue(message: String, hex: String, instruct: List<String>, client: DeviceConnect, service: TcpService)
/**加入超时队列-监听指定的设备*/
fun deviceAcceptTaskInQueue(receive: String, hex: String, client: DeviceConnect, service: TcpService)
/**对指定设备开启消息监听*/
fun openDeviceAccept(conn: DeviceConnect)
/**对指定设备关闭消息监听*/
fun closeDeviceAccept(conn: DeviceConnect)
/**是否有设备已经开启消息监听*/
fun isDeviceAccept(conn: DeviceConnect):Boolean
/**是否完成*/
fun isFinish():Boolean
/**设置完成*/
fun setFinish()
/**等待任务完成*/
fun waitTaskUntilFinish()
/**添加*/
fun addTask(run:Runnable)
}
定义一个超时任务队列
abstract class EventTask : EventAction {
private lateinit var fristClient: DeviceConnect;
private lateinit var acceptClient: DeviceConnect;
var isFinished=false;
private lateinit var acitons: LinkedList<Runnable>
@Synchronized
override fun isFinish():Boolean{
return isFinished
}
@Synchronized
override fun setFinish(){
try {
syncTaskLock.lock()
isFinished=true
syncTaskCondition.signalAll()
syncTaskLock.unlock()
} catch (e: Exception) {
}
}
override fun waitTaskUntilFinish(){
if(!isFinish()) {
syncTaskLock.lock()
syncTaskCondition.await()
syncTaskLock.unlock()
}
}
val syncTaskLock = ReentrantLock()
val syncTaskCondition = syncTaskLock.newCondition()
companion object {
var exeService: ExecutorService = Executors.newCachedThreadPool()
var timeOutService: ExecutorService = Executors.newCachedThreadPool()
}
/**是否有设备已经开启消息监听*/
override fun isDeviceAccept(conn: DeviceConnect):Boolean{
var isAccept:Boolean=false;
TcpService.instaince.syncExecute {
isAccept= !(conn.openEnabledReceiverTask.isEmpty())
TcpService.instaince.synclock.unlock()
}
return isAccept;
}
override fun openDeviceAccept(conn: DeviceConnect) {
TcpService.instaince.syncExecute {
conn.openEnabledReceiverTask.add(this)
}
}
override fun closeDeviceAccept(conn: DeviceConnect) {
TcpService.instaince.syncExecute {
conn.openEnabledReceiverTask.clear()
}
}
override fun getFristConnectClient(): DeviceConnect {
return fristClient
}
/**返回待监听的客户端*/
override fun getAcceptConnectClient(): DeviceConnect?=acceptClient
abstract override fun receiveProcess(message: String, hex: String, instruct: List<String>): Boolean
abstract override fun timeOut(): Long
abstract override fun disposeEvent(message: String, hex: String, instruct: List<String>, client: DeviceConnect, service: TcpService)
abstract override fun deviceAccept(receive: String, hex: String, client: DeviceConnect, service: TcpService)
override fun addTask(run: Runnable) {
syncTaskLock.lock()
acitons.add(run)
syncTaskCondition.signalAll()
syncTaskLock.unlock()
}
override fun disposeEventTaskInQueue(message: String, hex: String, instruct: List<String>, client: DeviceConnect, service: TcpService) {
fristClient=client;
acitons=LinkedList()
addTask(Runnable { disposeEvent(message, hex, instruct, client, service)})
TcpService.instaince.getLogListen()?.received(message,hex,client,this,STATUS_CODE_DISPOSE)
timeOutService.submit({
var future = exeService.submit(object : Callable<Boolean> {
override fun call(): Boolean {
while (!isFinish())
{
while (!acitons.isEmpty())
{
acitons.pollFirst().run()
}
waitTaskUntilFinish()
}
println("执行结束")
return true
}
})
timeOut(message,hex,future,client,false);
})
}
private fun timeOut(message: String, hex: String, future: Future<Boolean>, client: DeviceConnect, isReceive: Boolean) {
var data=DataBean("","","",null);
try {
future.get(timeOut(), TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
e.printStackTrace() //get为一个等待过程,异常中止get会抛出异常
data.result=ERROR_STATUS
data.code=ERROR_CODE_INTERRRUPTED;
} catch (e: ExecutionException) {
e.printStackTrace() //submit计算出现异常
data.result=ERROR_STATUS
data.code=ERROR_CODE_INTERRRUPTED;
} catch (e: TimeoutException) {
e.printStackTrace() //超时异常
data.result=ERROR_STATUS
data.code= ERROR_CODE_TIME_OUT;
}
if(data.code!=null)
{
print("给首个连接设备发消息")
TcpService.instaince.write(getFristConnectClient(),Gson().toJson(data), OutPutSteamMode.DEFAULT)
if(acceptClient!=null) {
closeDeviceAccept(acceptClient)
}
print("回调结束")
when(isReceive)
{
true-> TcpService.instaince.getLogListen()?.received(message,hex,client,this, TcpService.STATUS_CODE_ACCEPT_BUT_NOT_CALLBACK_DATA)
else-> TcpService.instaince.getLogListen()?.received(message,hex,client,this, TcpService.STATUS_CODE_DISPOSE_BUT_NOT_CALLBACK_DATA)
}
}
}
override fun deviceAcceptTaskInQueue(receive: String, hex: String, client: DeviceConnect, service: TcpService) {
acceptClient=client
TcpService.instaince.getLogListen()?.received(receive,hex,client,this,STATUS_CODE_DISPOSE)
addTask(Runnable { deviceAccept(receive, hex, client, service) })
}
}
```java
以上模板思路完成后编写一个心跳事件
class HeartbeatEventTask : EventTask() {
override fun receiveProcess(message:String,hex:String,instruct:List<String>):Boolean{
var pattern = Pattern.compile("[0-9]*");
return pattern.matcher(message).matches()
}
override fun timeOut(): Long = 5000
override fun disposeEvent(message: String, hex: String, instruct: List<String>, client: DeviceConnect, service: TcpService) {
//默认收到的第一消息是设备的id 并加入到存活的服务器连接列表
if(client.deviceId==null)
{
client.deviceId=message
service.saveSocketConnectMap(message,client)
}
TcpService.instaince.write(client,message, OutPutSteamMode.DEFAULT)
setFinish()
}
override fun deviceAccept(receive: String, hex: String, client: DeviceConnect, service: TcpService) {
}
}
编写一个对设备控制的指令
/**
* Created by linxingzhu on 2018/7/10.
*/
class SendDeviceEventTask : EventTask() {
/**
* php-> Device 2 "22" false
* */
override fun receiveProcess(message: String, hex: String, instruct: List<String>): Boolean {
val b= instruct.size >= 4
&& "\$php->".equals(instruct[0])
&& "Device".equals(instruct[1])
&& Pattern.compile("[0-9]*").matcher(instruct[2]).matches();
return b;
}
override fun regx():String="[\"]+.+[\"]+|[^\\s]+[ ]*+"
override fun timeOut(): Long = 5000
private val timeinterval = 1000L;
private val CODE_SUSSCESS = 0
//1表示参数错误
private val ERROR_CODE_PARAMETER = 1
//2表示像连接成功但写入数据失败
private val ERROR_CODE_WRITE = 2
//3表示锁控板未连接到服务器
private val ERROR_CODE_NO_CONNECT = 3
override fun disposeEvent(message: String, hex: String, instruct: List<String>, client: DeviceConnect, service: TcpService) {
/** 将数据提交到队列*/
val deviceId = instruct[2]
var message = instruct[3]
if(message.indexOf("\"")==0&&message.lastIndexOf("\"")==message.length-1)
{
message=message.substring(1,message.length-1)
println(message)
}
var isHex=false
if(instruct.size==4)
{
isHex=false;
}else
{
isHex = java.lang.Boolean.valueOf(instruct[4])
}
var connect = service.connectDeviceMap[deviceId]
when (true) {
connect == null -> {
callBackData(client, "error code:${ERROR_CODE_NO_CONNECT}")
}
(connect?.socket?.isConnected)==false->{
callBackData(client, "error code:${ERROR_CODE_NO_CONNECT}")
}
else -> {
try {
TcpService.instaince.write(connect!!,message,if(isHex) OutPutSteamMode.HEX else OutPutSteamMode.DEFAULT )
callBackData(client, "error code:${CODE_SUSSCESS}")
} catch (e: Exception) {
callBackData(client, "error code:${ERROR_CODE_WRITE}")
}
}
}
}
fun callBackData(client: DeviceConnect, content:String) {
TcpService.instaince.write(client,content, OutPutSteamMode.DEFAULT)
setFinish()
}
override fun deviceAccept(receive: String, hex: String, client: DeviceConnect, service: TcpService) {
}
}
编写一个设备发出指令并等待设备回传事件
class ReceiveDeviceEventTask : EventTask() {
/**
* php-> Receive 2 "22" false
* */
override fun receiveProcess(message: String, hex: String, instruct: List<String>): Boolean {
return instruct.size >= 4
&& "\$php->".equals(instruct[0])
&& "Receive".equals(instruct[1])
&& Pattern.compile("[0-9]*").matcher(instruct[2]).matches();
}
override fun regx():String="[\"]+.+[\"]+|[^\\s]+[ ]*+"
override fun timeOut(): Long = 5000
private val timeinterval = 1000L;
private val CODE_SUSSCESS = 0
//1表示参数错误
private val ERROR_CODE_PARAMETER = 1
//2表示像连接成功但写入数据失败
private val ERROR_CODE_WRITE = 2
//3表示锁控板未连接到服务器
private val ERROR_CODE_NO_CONNECT = 3
override fun disposeEvent(message: String, hex: String, instruct: List<String>, client: DeviceConnect, service: TcpService) {
/** 将数据提交到队列*/
val deviceId = instruct[2]
var message = instruct[3]
if(message.indexOf("\"")==0&&message.lastIndexOf("\"")==message.length-1)
{
message=message.substring(1,message.length-1)
println(message)
}
var isHex=false
if(instruct.size==4)
{
isHex=false;
}else
{
isHex = java.lang.Boolean.valueOf(instruct[4])
}
var connect = service.connectDeviceMap[deviceId]
when (true) {
connect == null -> {
callBackData(client, "error code:${ERROR_CODE_NO_CONNECT}")
}
(connect?.socket?.isConnected)==false->{
callBackData(client, "error code:${ERROR_CODE_NO_CONNECT}")
}
else -> {
//如果当前有监听等待
while (isDeviceAccept(connect!!))
{
Thread.sleep(200)
}
//对设备开启监听、
openDeviceAccept(connect!!)
try {
TcpService.instaince.write(connect!!,message,if(isHex) OutPutSteamMode.HEX else OutPutSteamMode.DEFAULT )
} catch (e: Exception) {
callBackData(client, "error code:${ERROR_CODE_WRITE}")
}
}
}
}
fun callBackData(client: DeviceConnect, content:String) {
TcpService.instaince.write(client,content, OutPutSteamMode.DEFAULT)
setFinish()
}
override fun deviceAccept(receive: String, hex: String, client: DeviceConnect, service: TcpService) {
//如果为非数字
if(!Pattern.compile("[0-9]*").matcher(receive).matches()) {
TcpService.instaince.write(getFristConnectClient(),receive,OutPutSteamMode.DEFAULT )
setFinish()
}
}
}