Android Kotlin语言实现+TCP客户端开发+粘包处理

不喜勿喷,个人不足的地方还有很多,有问题及解决思路的直接在留言方留言,或者私信给我,一起解决问题.

情景:
连接硬件板块,和硬件进行通信

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+粘包处理

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我来回答你的问题。 首先,Kotlin 是一种基于 JVM 的静态类型编程语言,它的语法简洁易懂,支持函数式编程和面向对象编程。 协程是 Kotlin 中的一种轻量级线程,可以实现异步编程和并发执行。Retrofit 是一款网络请求库,它可以帮助我们轻松地实现网络请求和数据解析。MVVM 是一种软件架构模式,可以将应用程序分为三个部分:模型、视图和视图模型。 下面是一个基于 Kotlin + 协程 + Retrofit + MVVM 的网络请求的优雅实现: 1. 定义 API 接口 首先定义 API 接口,使用 Retrofit 注解来描述请求方法和参数。 ```kotlin interface ApiService { @GET("api/news") suspend fun getNews(@Query("category") category: String): NewsResponse } ``` 2. 创建数据模型 根据 API 接口的返回数据,我们可以创建一个数据模型。 ```kotlin data class News(val title: String, val content: String) data class NewsResponse(val code: Int, val message: String, val newsList: List<News>) ``` 3. 创建 ViewModel ViewModel 是连接数据模型和视图的中间层,它处理数据逻辑并提供可观察的数据。 ```kotlin class NewsViewModel : ViewModel() { private val _newsList = MutableLiveData<List<News>>() val newsList: LiveData<List<News>> = _newsList fun loadNews(category: String) { viewModelScope.launch { val response = retrofit.create(ApiService::class.java).getNews(category) if (response.code == 200) { _newsList.value = response.newsList } } } } ``` 4. 创建视图 视图负责展示数据,并与用户交互。 ```kotlin class NewsActivity : AppCompatActivity() { private val viewModel by viewModels<NewsViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_news) viewModel.newsList.observe(this, Observer { newsList -> // 更新视图 }) viewModel.loadNews("tech") } } ``` 通过使用 Kotlin + 协程 + Retrofit + MVVM,我们可以实现优雅地网络请求,代码简洁易懂,逻辑清晰。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值