不喜勿喷,个人不足的地方还有很多,有问题及解决思路的直接在留言方留言,或者私信给我,一起解决问题.
情景:
连接硬件板块,和硬件进行通信
TCP概念性质的知识可以观看别的博客
// 定义一个类来继承协程使整个类拥有协程的上下文.
internal class TransparentTcp : CoroutineScope{
private val job = Job()
override val coroutineContext: CoroutineContext = IO + job
}
internal 关键字的意义:该变量/类/方法仅在此工程中可见,因为我做这个TCP这个工程最后需要打包为一个SDK,给界面使用,给界面使用的过程中,我不想让他直接操作我的这个TCP工作类,我给他一个代理类,让他对代理类进行操作.他们引入我的SDK之后,该类是可见的,但不可访问,下图就是关键字的意思,
以下是需要工作用到的字段
private val IP = "192.168.xxx.xxx"
private val PORT = 4028
private val TCP_CONNETION_TIME = 5000L // TCP重连
private val TCP_MESSAGE_TIME = 3000L // tcp检测心跳
/**
* TCP 超时连接30s 因为TCP有重发机制,所以时间稍微久一点也可以
*/
private val TCP_CONNECTION_TIMEOUT = 30 * 1000
private var tempTime = 0L // 临时保存时间
private var socket: Socket? = null
private lateinit var socketIn: InputStream
private lateinit var socketOut: OutputStream
/**
* 开始工作协程
*/
private lateinit var startlaun: Job
/**
* 心跳协程
*/
private var heartLaun: Job? = null
/**
* 接收信息协程
*/
private var readLaun: Job? = null
/**
* 发送信息协程
*/
private var sendLaun: Job? = null
工作协程
@Suppress("BlockingMethodInNonBlockingContext")
private fun startConnetion() =
launch(coroutineContext, CoroutineStart.LAZY) {
// 创建socket
while (true) {
try {
socket = Socket()
Log.i(TAG, "连接tcp")
// 创建 socket连接地址
val inetSocketAddress = InetSocketAddress(IP, PORT)
// 等待tcp连接成功,连接失败直接抛异常
socket?.connect(inetSocketAddress, 30000)
socketIn = socket?.getInputStream()!!
socketOut = socket?.getOutputStream()!!
heartLaun = heartPackage()
sendLaun = sendMessageTcp()
readLaun = readTcpData()
// 先暂停5s,如果接收到tcp的消息(因为接收协程已经开启[readLaun]),则会继续往下阻塞
delay(5000)
// 阻塞当前的协程继续循环,等待要是出意外或者超时则会重新遍历继续重连tcp
async {
while (true) {
// 重复检测当前的时间是否超时
delay(1000)
// 接收到任何信息就代表连接成功,接收到信息之后会改变当前的时间
if (TCP_CONNECTION_TIMEOUT < System.currentTimeMillis() - tempTime) {
messageCallBack.transparentConnetionTimeOut()
closeJob()
socketClose()
// 跳出阻塞 再次循环外层的协程,重新连接tcp
return@async
}
}
}.await()
} catch (e: Exception) {
// 重新连接的时候将要对当前的协程阻塞进行关闭
e.printStackTrace()
closeJob()
socketClose()
Log.i(TAG, "start: 重新连接")
delay(TCP_CONNETION_TIME)
}
}
}
注解的含义:忽略检测一些警告,在用TCP或者UDP的过程中,这些需要在子线程调用的API在协程中是给我警告的.不知道为什么.
创建协程的参数(coroutineContext, CoroutineStart.LAZY)有这两个,第一个是协程的上下文,第二个则是延迟开启协程,需要主动调用start(),我这样写的目的是在开启改TCP之前先检测网络是否正常,等一些查询.没有问题之后开启工作协程
startlaun = startConnetion()
startlaun.start()
往下就是创建,连接,重要的是之后的三个协程,心跳协程,发送消息协程,接收消息协程
heartLaun = heartPackage() // 心跳命令
sendLaun = sendMessageTcp() // 发送消息
readLaun = readTcpData() // 接收消息
开启协程之后把当前的协程睡5s,原因:检测TCP是否正常连接.
下一段是一个阻塞协程,细心的朋友可以看到工作线程里是一个无限循环.如果TCP是正常通信的状态下,这个阻塞的协程会持续阻塞当前的无限循环,阻塞的条件就是,当前的时间 - 上次接收消息的时间 < 规定的超时时间,如果大于规定的时间,那就代表TCP服务端没有给我任何回复,那肯定有异常啊.因为我心跳在持续发送消息啊 tempTime这个变量在接收消息的时候会重新赋值
async {
while (true) {
// 重复检测当前的时间是否超时
delay(1000)
// 接收到任何信息就代表连接成功,接收到信息之后会改变当前的时间
if (TCP_CONNECTION_TIMEOUT < System.currentTimeMillis() - tempTime) {
messageCallBack.transparentConnetionTimeOut()
closeJob()
socketClose()
// 跳出阻塞 再次循环外层的协程,重新连接tcp
return@async
}
}
}.await()
// 心跳协程
// 定义一个心跳包
private fun heartPackage(): Job? = launch {
while (true) {
delay(TCP_MESSAGE_TIME)
// 不要管里面的代码,只需要知道这是每隔一段时间发送一个心跳命令
// 在终端未认证之前要定时发送连接终端命令
if (!TransparentMessageManager.terminalAllow) {
TransparentMessageManager.sendConncetRequest()
} else {
TransparentMessageManager.sendLogRequest()
}
}
}
发送消息的协程
// 定义一个发送消息的队列
var messageQueue = ConcurrentLinkedQueue<ByteArray>()
fun messageQueueoffer(tcpMsg: ByteArray) {
synchronized(messageQueue) {
messageQueue.offer(tcpMsg)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private fun sendMessageTcp() = launch {
while (true) {
try {
// 定一个死循环 持续对这个队列进行读取
val message = messageQueue.poll()
// 读取的空消息 就暂停那么100毫秒,跳过此次循环在去遍历
if (message == null) {
delay(100)
continue
}
// 读取到消息之后直接发送
socketOut.write(message)
socketOut.flush()
// 防止粘包
delay(200)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
接收消息协程
// 定一个接收消息的队列,粘包处理我做的是按照字节处理.
val receiveQueue = ConcurrentLinkedQueue<Byte>()
@Suppress("BlockingMethodInNonBlockingContext")
private fun readTcpData(): Job? = launch {
launch {
var count = 0
var number: Int
val lengthByteArray = ByteArray(8)
while (true) {
number = 0
// 获取到消息长度 这样写是因为在if中写async.await()阻塞不了当前的协程,原因未知
// 换个方式写可以阻塞当前的协程也就是阻塞当前的while循环
(count == 8).isSuccess {
async {
// 取出来字节数组钱4为 加解密密钥
val seedBytes = lengthByteArray.copyOfRange(0, 4)
// 取出(copy)字节后四位,之后要取出数据的长度
val messageBytes = lengthByteArray.copyOfRange(4, 8)
val realMsgLenBytes: ByteArray = xorEncode(messageBytes, seedBytes)
val length = TransparentUtils.bytes2int(realMsgLenBytes)
// 创建一个获取数据长度的字节数组 并且
val dataByteArray = ByteArray(4 + length)
while (number < length) {
val poll = receiveQueue.poll() ?: continue
dataByteArray[number + 4] = poll
if (number == length - 1) {
System.arraycopy(messageBytes, 0, dataByteArray, 0, 4)
// 拿到当前的密钥,和当前的数据解析当前的数据
newAnalysisData(xorEncode(dataByteArray, seedBytes))
}
number++
}
count = 0
}.await()
}
val poll = receiveQueue.poll()
if (poll == null) {
delay(100)
continue
}
lengthByteArray[count] = poll
count++
}
}
try {
val b = ByteArray(2048)
var length = 0
while (length != -1) {
length = socketIn.read(b)
if (length != -1) {
val tempByte = ByteArray(length)
tempByte.forEachIndexed { index, byte ->
receiveQueue.offer(b[index])
}
tempTime = System.currentTimeMillis()
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
在这个协程中,定义了一个解析数据(包括处理粘包)的协程,先不要看这个,首先看接收消息处理的,也就是子协程下面的那段代码,三个注释就是主要
try {
val b = ByteArray(2048)
var length = 0
while (length != -1) {
length = socketIn.read(b)
if (length != -1) {
// 定义一个该数据长度的字节
val tempByte = ByteArray(length)
// 把当前的字节数组里的数据全部写进消息队列中
tempByte.forEachIndexed { index, byte ->
receiveQueue.offer(b[index])
}
// 更新一下TCP检测断开的机制,也就是前面的说到的.
tempTime = System.currentTimeMillis()
}
}
} catch (e: IOException) {
e.printStackTrace()
}
再看接收协程中的子协程
launch {
var count = 0
var number: Int
val lengthByteArray = ByteArray(8)
while (true) {
number = 0
// 获取到消息长度 这样写是因为在if中写async.await()阻塞不了当前的协程,原因未知
// 换个方式写可以阻塞当前的协程也就是阻塞当前的while循环
(count == 8).isSuccess {
async {
// 取出来字节数组钱4为 加解密密钥
val seedBytes = lengthByteArray.copyOfRange(0, 4)
// 取出(copy)字节后四位,之后要取出数据的长度
val messageBytes = lengthByteArray.copyOfRange(4, 8)
val realMsgLenBytes: ByteArray = xorEncode(messageBytes, seedBytes)
val length = TransparentUtils.bytes2int(realMsgLenBytes)
// 创建一个获取数据长度的字节数组 并且
val dataByteArray = ByteArray(4 + length)
while (number < length) {
val poll = receiveQueue.poll() ?: continue
dataByteArray[number + 4] = poll
if (number == length - 1) {
System.arraycopy(messageBytes, 0, dataByteArray, 0, 4)
// 拿到当前的密钥,和当前的数据解析当前的数据
newAnalysisData(xorEncode(dataByteArray, seedBytes))
}
number++
}
count = 0
}.await()
}
val poll = receiveQueue.poll()
if (poll == null) {
delay(100)
continue
}
lengthByteArray[count] = poll
count++
}
}
首先我需要讲解一下我的服务端给我发送的消息是什么样子的.前面四个字节是加密的,后面四个字节是整体消息长度
处理TCP粘包的思路:是根据之前接收到的字节数组队列中一个一个字节去拿,当拿到前八个字节,我就去做处理,把消息长度解密,然后得到一个消息的长度的信息length这个变量,在之后继续去在接收字节数组队列中去拿该长度的消息.等拿到的字节的数量等于解密出来的长度的信息,则认为是一个完整的包的数据,在之后就对这个完整的包做处理.
在做的过程中发现一个小问题,在协程中如果达成了取到8个字节的这个时候,写了一个if(count == 8){}
在这个if(){}中 我写阻塞协程async {}.await()起不到阻塞的作用.还没有找到原因.所以我写了一个扩展函数来代替
private inline fun Boolean.isSuccess(function: () -> Unit) {
if (this)
function()
}
顺带说一嘴, 这些协程都是在工作协程之下运行的. 协程有一个很舒服的地方就是,父协程销毁,子协程全部销毁.不需要担心协程没有被回收之类的
好了, 整体的代码就是这样. 有不足之处还望大佬多多提出,我也需要对代码进行改进.
把后来写的java版的TCP粘包处理的链接也放在这里,kotlin的不明白,可以看下java的.
Android Java+TCP+粘包处理