Android中tcp的通信

Android中tcp的通信

1.前言:

​ 最近公司要做自研的iot项目,包含扫地机器人、喂食器、猫马桶、饮水机等等设备,本来是pwp+websocket,但是由于设备底层的p2p断连有问题,至于为啥设备端pwp连接会有断不开的问题,目前看不到底层源码排查不出来,所以改成tcp+websocket,这2种方案不能说谁特别好,只能说选择一种适合自己项目的方式.

2.简介:

TCP 是什么

TCP(Transmission Control Protocol,传输控制协议) 是一种面向连接的、可靠的、基于字节流的传输层通信协议。它是互联网协议套件(TCP/IP)的核心协议之一,位于 OSI 模型的第 4 层(传输层),负责在不可靠的网络层(IP)之上提供可靠的端到端数据传输。

3.TCP 的核心特性:

特性 说明
面向连接 通信前必须先建立连接(三次握手),通信结束后释放连接(四次挥手)
可靠传输 通过确认应答(ACK)、超时重传、序列号等机制保证数据不丢、不错、不乱序
流量控制 通过滑动窗口机制,防止发送方发送过快导致接收方缓冲区溢出
拥塞控制 通过慢启动、拥塞避免、快速重传、快速恢复等算法,防止网络拥塞
全双工通信 连接建立后,双方可以同时发送和接收数据
字节流 不保留消息边界,将数据视为连续的字节流

4.tcp连接建立:

三次握手(Three-way Handshake)用于在客户端和服务器之间建立连接。

客户端 服务器

| |

| -------- SYN, seq=x --------> |

| |

| <--- SYN, ACK, seq=y, ack=x+1 ---|

| |

| -------- ACK, ack=y+1 ------> |

| |

步骤 方向 说明
客户端 → 服务器 发送 SYN 包,携带初始序列号 seq=x,进入 SYN_SENT 状态
服务器 → 客户端 回复 SYN+ACK 包,携带自己的初始序列号 seq=y,确认号 ack=x+1,进入 SYN_RCVD 状态
客户端 → 服务器 回复 ACK 包,确认号 ack=y+1,双方进入 ESTABLISHED 状态

为什么是三次而不是两次?

  • 防止历史重复连接请求造成错误(如延迟到达的 SYN 包)
  • 同步双方的初始序列号(ISN),确保可靠传输

5.TCP 数据传输

连接建立后,双方可以开始传输数据:

  1. 发送方:将数据分段,每段附加序列号,发送给接收方
  2. 接收方:收到数据后,发送 ACK 确认(累积确认或选择确认)
  3. 超时重传:发送方如果在超时时间内未收到 ACK,则重传该段数据
  4. 滑动窗口:接收方通过 Window Size 字段告知发送方自己的接收能力,实现流量控制

6.tcp与UDP对比:

特性 TCP UDP
连接方式 面向连接 无连接
可靠性 可靠(确认、重传、排序) 不可靠(尽力而为)
传输顺序 保证有序 不保证
流量控制 有(滑动窗口)
拥塞控制
头部开销 较大(20~60 字节) 较小(8 字节)
传输效率 较低(有额外机制开销) 较高
适用场景 HTTP/HTTPS、FTP、邮件等 DNS、视频流、游戏、VoIP

7.流程总览

整个通信链路分为 两大阶段注册阶段(HTTP) + 连接阶段(TCP)

阶段 APP(服务端) 设备(客户端)
准备 ① 自动获取本地 IP ② 启动 ServerSocket 监听端口(默认 12306) ③ accept() 阻塞等待 HTTP 服务器持续运行,监听 /register 接口
注册 ④ 构造 P2pRegisterRequest(IP + Port + P2pType) ⑤ 通过 OkHttp POST 发送给设备 HTTP 服务器 ⑥ 解析 POST 请求体,提取 APP 的 IP 和端口
握手 accept() 返回新 Socket,三次握手已完成 ⑦ 作为 TCP 客户端,向 APP 的 IP:Port 发起 connect()
通信 ⑨ 开启线程,通过 InputStream / OutputStream 循环收发二进制数据 ⑩ 发送二进制报文,接收 APP 响应,保持长连接

关键代码对应

步骤 对应代码位置
获取 IP P2PManager.getLocalIpAddress() / getDeviceIp()
启动服务端 P2PManager.startTcpServer()ServerSocket(tcpPort)
发送注册 P2PManager.sendRegisterPostRequest() → OkHttp POST
处理连接 P2PManager.handleDeviceCommunication()accept() + read/write
三次握手 由操作系统 TCP 协议栈自动完成,代码中体现为 connect() / accept() 成功返回

tcp通信有5个阶段:

阶段 内容
阶段一 准备阶段 --- APP 获取本地 IP + 启动 TCP 服务端
阶段二 HTTP 注册阶段 --- APP POST 注册信息 → 设备解析
阶段三 TCP 三次握手 --- 设备主动 connect(),操作系统自动完成
阶段四 双向数据传输 --- 二进制字节流收发
阶段五 持续通信 / 四次挥手断开

8.TcpManager:

kotlin 复制代码
package com.example.tcpdemo.tcp

import android.util.Log
import com.example.tcpdemo.bean.DeviceStatusResponse
import com.google.gson.Gson
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.net.ServerSocket
import java.net.Socket
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executors

/**
 * @作者: smile
 * @时间: 2025/12/5 12:05
 * @描述:
 **/
object TcpManager {


    private const val TAG = "TcpManager"
    private val gson = Gson()
    private val threadPool = Executors.newCachedThreadPool() // 线程池管理连接
    private var serverSocket: ServerSocket? = null
    private var clientSocket: Socket? = null
    private var isServerRunning = false

    // -------------------------- 服务端(固定端口8888) --------------------------
    /**
     * 启动服务端(监听8888端口,与真实手机端口一致)
     */
    fun startServer(onMessageReceived: (String) -> Unit) {
        if (isServerRunning) {
            onMessageReceived("⚠️ 服务端已在运行中")
            return
        }
        isServerRunning = true
        threadPool.execute {
            try {
                // 绑定8888端口(必须与客户端端口一致)
                serverSocket = ServerSocket(8888)
                serverSocket?.reuseAddress = true // 允许端口复用,避免重启时端口占用
                Log.d(
                    TAG,
                    "✅ 服务端启动成功!监听:${serverSocket?.inetAddress?.hostAddress ?: "0.0.0.0"}:8888"
                )
                onMessageReceived("✅ 服务端启动成功!等待客户端连接...")

                while (isServerRunning) {
                    val client = serverSocket?.accept() ?: break // 阻塞等待客户端连接
                    val clientIp = client.inetAddress.hostAddress
                    Log.d(TAG, "📲 客户端连接成功:$clientIp:${client.port}")
                    onMessageReceived("📲 客户端连接成功:$clientIp:${client.port}")

                    // 单独线程处理该客户端的通信(支持多客户端,可选)
                    threadPool.execute {
                        handleClientCommunication(client, onMessageReceived)
                    }
                }
            } catch (e: Exception) {
                isServerRunning = false
                val errorMsg = "❌ 服务端启动失败:${e.message}"
                Log.e(TAG, errorMsg, e)
                onMessageReceived(errorMsg)
            }
        }
    }

    /**
     * 处理单个客户端的收发数据
     */
    private fun handleClientCommunication(client: Socket, onMessageReceived: (String) -> Unit) {
        val clientIp = client.inetAddress.hostAddress
        try {
            // 接收客户端数据
            val bufferedReader = BufferedReader(
                InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)
            )
            // 发送数据给客户端
            val printWriter = PrintWriter(
                OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8),
                true // 自动刷新缓冲区,无需手动flush
            )

            var receivedData: String?
            while (bufferedReader.readLine().also { receivedData = it } != null) {
                if (receivedData.isNullOrBlank()) continue
                Log.d(TAG, "📥 服务端接收($clientIp):$receivedData")

                // 解析JSON并格式化显示
                val formattedMsg = parseDeviceStatus(receivedData!!)
                onMessageReceived("📥 收到客户端($clientIp)消息:\n$formattedMsg")

                // 服务端回复客户端(真实通信必需,让客户端确认接收)
                val response = "✅ 服务端已接收!你的数据:$receivedData"
                printWriter.println(response)
                Log.d(TAG, "📤 服务端回复($clientIp):$response")
            }
        } catch (e: Exception) {
            val errorMsg = "❌ 与客户端($clientIp)通信异常:${e.message}"
            Log.e(TAG, errorMsg, e)
            onMessageReceived(errorMsg)
        } finally {
            // 关闭连接
            client.close()
            Log.d(TAG, "🔌 客户端($clientIp)断开连接")
            onMessageReceived("🔌 客户端($clientIp)断开连接")
        }
    }

    /**
     * 停止服务端
     */
    fun stopServer(onResult: (String) -> Unit) {
        isServerRunning = false
        serverSocket?.close()
        serverSocket = null
        Log.d(TAG, "🛑 服务端已停止")
        onResult("🛑 服务端已停止")
    }

    // -------------------------- 客户端(连接指定服务端IP+8888端口) --------------------------
    /**
     * 客户端连接服务端并发送数据
     * @param serverIp 服务端真实IP(192.168.1.68 或 192.168.0.243)
     */
    fun clientConnectAndSend(
        serverIp: String,
        deviceStatus: DeviceStatusResponse,
        onResult: (String) -> Unit
    ) {
        if (clientSocket?.isConnected == true) {
            onResult("⚠️ 客户端已连接,无需重复连接")
            return
        }
        threadPool.execute {
            try {
                // 连接服务端(IP+8888端口,超时10秒,适配真实网络延迟)
                clientSocket = Socket().apply {
                    connect(java.net.InetSocketAddress(serverIp, 8888), 10000)
                }
                Log.d(TAG, "✅ 客户端连接成功:$serverIp:8888")
                onResult("✅ 客户端连接成功:$serverIp:8888")

                // 序列化JSON数据并发送
                val jsonData = gson.toJson(deviceStatus)
                val printWriter = PrintWriter(
                    OutputStreamWriter(clientSocket?.getOutputStream(), StandardCharsets.UTF_8),
                    true
                )
                printWriter.println(jsonData)
                Log.d(TAG, "📤 客户端发送:$jsonData")
                onResult("📤 客户端发送数据:\n$jsonData")

                // 接收服务端回复
                val bufferedReader = BufferedReader(
                    InputStreamReader(clientSocket?.getInputStream(), StandardCharsets.UTF_8)
                )
                val serverResponse = bufferedReader.readLine() ?: "❌ 服务端无回复"
                Log.d(TAG, "📥 客户端接收回复:$serverResponse")
                onResult("📥 服务端回复:\n$serverResponse")

            } catch (e: Exception) {
                val errorMsg = "❌ 连接/发送失败:${e.message}"
                Log.e(TAG, errorMsg, e)
                onResult(errorMsg)
            } finally {
                // 关闭客户端连接(如需长连接,可注释此行)
                clientSocket?.close()
                clientSocket = null
                Log.d(TAG, "🔌 客户端断开连接")
                onResult("🔌 客户端断开连接")
            }
        }
    }

    /**
     * 解析设备状态JSON,格式化显示
     */
    private fun parseDeviceStatus(json: String): String {
        return try {
            val response = gson.fromJson(json, DeviceStatusResponse::class.java)
            val deviceId = response.data?.keys?.firstOrNull() ?: "未知设备ID"
            val status = response.data?.get(deviceId) ?: return "解析失败:无设备状态数据"
            buildString {
                append("设备ID:$deviceId\n")
                append("在线状态:${if (status.isOnline) "✅ 在线" else "❌ 离线"}\n")
                append("上线时间:${status.upAt}(时间戳)\n")
                append("下线时间:${status.downAt}(时间戳)\n")
                append("在线时长:${status.liveTime}秒\n")
                append("客户端地址:${status.clientAddr}\n")
                append("本地地址:${status.localAddr}")
            }
        } catch (e: Exception) {
            "❌ 解析失败:${e.message}"
        }
    }
}

9.P2PManager

kotlin 复制代码
package com.example.tcpdemo.tcp

import android.content.Context
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.util.Log
import com.example.tcpdemo.bean.P2pRegisterRequest
import com.google.gson.Gson
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.net.Inet4Address
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket
import java.nio.charset.StandardCharsets
import java.util.Formatter
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

/**
 * @作者: smile
 * @时间: 2025/12/5 15:30
 * @描述:
 **/
object P2PManager {
    private const val TAG = "P2pManager"
    private val gson = Gson()
    private val threadPool = Executors.newCachedThreadPool()
    private val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(15, TimeUnit.SECONDS) // HTTP连接超时15秒
        .readTimeout(15, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .build()

    // TCP 服务端相关
    private var serverSocket: ServerSocket? = null
    private var isServerRunning = false
    private var tcpPort = 12306 // 默认TCP端口,可配置
    private var localIp = "" // 默认本地IP,会自动获取

    // HTTP 服务器相关(设备的HTTP服务器地址,需从后台获取)
    private var deviceHttpServerUrl = "" // 例如:http://192.168.0.243:8080/register

    // -------------------------- 1. 自动获取本地IP(关键:无需手动输入) --------------------------
    /**
     * 获取APP所在设备的本地局域网IP(适配WiFi环境)
     */
    fun getLocalIpAddress(): String {
        try {
            val interfaces = NetworkInterface.getNetworkInterfaces()
            for (intf in interfaces) {
                val addrs = intf.inetAddresses
                for (addr in addrs) {
                    if (!addr.isLoopbackAddress && addr is Inet4Address) {
                        return addr.hostAddress ?: ""
                    }
                }
            }
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
        return ""
    }

    // -------------------------- 2. TCP服务端(APP作为服务端,等待设备连接) --------------------------
    /**
     * 启动TCP服务端
     * @param port 监听端口(默认12306)
     * @param onServerStatus 服务端状态回调(启动成功/失败/设备连接)
     */
    fun startTcpServer(
        port: Int = 12306,
        onServerStatus: (String) -> Unit
    ) {
        if (isServerRunning) {
            onServerStatus("⚠️ TCP服务端已在运行")
            return
        }

        // 自动获取本地IP
        localIp = getLocalIpAddress()
        Log.d(TAG,"===获取到的本地ip地址为=== $localIp")

        tcpPort = port

        isServerRunning = true
        threadPool.execute {
            try {
                serverSocket = ServerSocket(tcpPort)
                serverSocket?.reuseAddress = true
                Log.d(TAG, "✅ TCP服务端启动成功:$localIp:$tcpPort")
                onServerStatus("✅ TCP服务端启动成功\nIP:$localIp\n端口:$tcpPort\n等待设备连接...")

                // 阻塞等待设备TCP客户端连接(P2P关键步骤)
                while (isServerRunning) {
                    val deviceSocket = serverSocket?.accept() ?: break
                    val deviceIp = deviceSocket.inetAddress.hostAddress
                    Log.d(TAG, "📲 设备连接成功(P2P建立):$deviceIp:${deviceSocket.port}")
                    onServerStatus("📲 设备连接成功(P2P建立):$deviceIp:${deviceSocket.port}")

                    // 处理设备与APP的TCP通信(收发数据)
                    handleDeviceCommunication(deviceSocket, onServerStatus)
                }
            } catch (e: Exception) {
                isServerRunning = false
                val errorMsg = "❌ TCP服务端启动失败:${e.message}"
                Log.e(TAG, errorMsg, e)
                onServerStatus(errorMsg)
            }
        }
    }

    /**
     * 处理设备与APP的TCP通信(接收设备数据 + 发送响应)
     */
    private fun handleDeviceCommunication(
        deviceSocket: Socket,
        onServerStatus: (String) -> Unit
    ) {
        val deviceIp = deviceSocket.inetAddress.hostAddress
        // 增大读取缓冲区(根据设备数据大小调整,建议1024-8192)
        val buffer = ByteArray(4096)
        try {
            val inputStream = deviceSocket.getInputStream()
            val outputStream = deviceSocket.getOutputStream()

            // 循环读取二进制数据
            var bytesRead: Int
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                if (bytesRead <= 0) continue
                val receivedBytes = buffer.copyOfRange(0, bytesRead)
                val hexString = bytesToHex(receivedBytes)
                Log.d(TAG, "📥 接收设备二进制数据($deviceIp):\n原始字节数:$bytesRead\n16进制:$hexString")
                onServerStatus("📥 接收设备数据:\n字节数:$bytesRead\n16进制:$hexString")

                val responseBytes = "APP_RECEIVED_OK".toByteArray(StandardCharsets.UTF_8)
                outputStream.write(responseBytes)
                outputStream.flush() // 确保数据立即发送
                Log.d(TAG, "📤 向设备发送二进制响应:${bytesToHex(responseBytes)}")
                onServerStatus("📤 向设备发送响应(二进制):${bytesToHex(responseBytes)}")

                // 如果设备是字符串
                // val responseStr = "✅ APP已接收数据(字节数:$bytesRead)"
                // val responseBytes = responseStr.toByteArray(StandardCharsets.UTF_8)
                // outputStream.write(responseBytes)
                // outputStream.flush()
                // Log.d(TAG, "📤 向设备发送字符串响应:$responseStr")
                // onServerStatus("📤 向设备发送响应:$responseStr")
            }
        } catch (e: Exception) {
            val errorMsg = "❌ 与设备通信异常:${e.message}"
            Log.e(TAG, errorMsg, e)
            onServerStatus(errorMsg)
        } finally {
            deviceSocket.close()
            Log.d(TAG, "🔌 设备断开连接:$deviceIp")
            onServerStatus("🔌 设备断开连接:$deviceIp")
        }
    }

    /**
     * 辅助工具:将二进制字节数组转为16进制字符串
     */
    private fun bytesToHex(bytes: ByteArray): String {
        val hexChars = CharArray(bytes.size * 2)
        val hexArray = "0123456789ABCDEF".toCharArray()
        for (i in bytes.indices) {
            val v = bytes[i].toInt() and 0xFF // 确保无符号
            hexChars[i * 2] = hexArray[v ushr 4] // 高4位
            hexChars[i * 2 + 1] = hexArray[v and 0x0F] // 低4位
        }
        // 每2个字符加一个空格,增强可读性
        return String(hexChars).chunked(2).joinToString(" ")
    }

    /**
     * 停止TCP服务端
     */
    fun stopTcpServer(onResult: (String) -> Unit) {
        isServerRunning = false
        serverSocket?.close()
        serverSocket = null
        Log.d(TAG, "🛑 TCP服务端已停止")
        onResult("🛑 TCP服务端已停止")
    }

    // -------------------------- 3. HTTP POST请求(向设备HTTP服务器注册TCP地址) --------------------------
    /**
     * 向设备HTTP服务器发送注册请求(核心P2P步骤)
     * @param deviceHttpUrl 设备HTTP服务器地址(从后台获取)
     * @param p2pType P2P模式(Map/Video)
     * @param onPostResult 请求结果回调
     */
    fun sendRegisterPostRequest(
        deviceHttpUrl: String,
        p2pType: String = "Map",
        onPostResult: (Boolean, String) -> Unit
    ) {
        if (!isServerRunning) {
            onPostResult(false, "❌ 请先启动TCP服务端")
            return
        }
        if (deviceHttpUrl.isBlank()) {
            onPostResult(false, "❌ 设备HTTP服务器地址为空")
            return
        }

        val registerRequest = P2pRegisterRequest(
            ip = localIp,
            port = tcpPort.toString(),
            p2pType = p2pType
        )
        val requestJson = gson.toJson(registerRequest)
        Log.d(TAG, "📤 构造POST请求体:$requestJson")

        val mediaType = "application/json; charset=utf-8".toMediaType()
        val requestBody = requestJson.toRequestBody(mediaType)


        // 发送HTTP请求
        threadPool.execute {
            try {
                val request = Request.Builder()
                    .url(deviceHttpUrl)
                    .post(requestBody)
                    .addHeader("Content-Type", "application/json")
                    .addHeader("Content-Length", requestJson.toByteArray(StandardCharsets.UTF_8).size.toString()) // 必需
                    .addHeader("Connection", "close") // 短连接
                    .build()
                val response = okHttpClient.newCall(request).execute()
                if (response.isSuccessful) {
                    val responseBody = response.body?.string() ?: ""
                    Log.d(TAG, "✅ POST请求成功,服务器响应:$responseBody")
                    onPostResult(true, "✅ POST注册成功\n服务器响应:$responseBody")
                } else {
                    val errorMsg = "❌ POST请求失败,响应码:${response.code}"
                    Log.e(TAG, errorMsg)
                    onPostResult(false, errorMsg)
                }
            } catch (e: Exception) {
                val errorMsg = "❌ POST请求异常:${e.message}"
                Log.e(TAG, errorMsg, e)
                onPostResult(false, errorMsg)
            }
        }
    }

    fun getDeviceIp(context: Context): String {
        // 优先使用新 API
        getIpUsingConnectivityManager(context).takeIf { it.isNotEmpty() }?.let { return it }

        // 其次尝试 WiFi(更快)
        getWifiIpAddress(context).takeIf { it.isNotEmpty() }?.let { return it }

        // 最后尝试所有网卡
        return getLocalIpAddress()
    }
    fun getWifiIpAddress(context: Context): String {
        val wifi = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
        val ip = wifi.connectionInfo.ipAddress

        if (ip == 0) return ""

        return ((ip and 0xFF).toString() + "." +
                ((ip shr 8) and 0xFF) + "." +
                ((ip shr 16) and 0xFF) + "." +
                ((ip shr 24) and 0xFF))
    }

    fun getIpUsingConnectivityManager(context: Context): String {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = cm.activeNetwork ?: return ""
        val linkProperties = cm.getLinkProperties(network) ?: return ""

        linkProperties.linkAddresses.forEach { addr ->
            val address = addr.address
            if (address is Inet4Address && !address.isLoopbackAddress) {
                return address.hostAddress ?: ""
            }
        }
        return ""
    }


    // -------------------------- 4. 辅助方法:设置设备HTTP服务器地址 --------------------------
    fun setDeviceHttpServerUrl(url: String) {
        deviceHttpServerUrl = url
        Log.d(TAG, "📌 设备HTTP服务器地址已设置:$url")
    }
}

10.TestCompose:

kotlin 复制代码
package com.example.tcpdemo.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.blankj.utilcode.util.LogUtils
import com.example.tcpdemo.callback.LogActionCallback

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun P2pDebugScreen(
    initHttpUrl: String,
    initPort: String,
    p2pOptions: List<String>,
    initSelectedP2p: String,
    onUrlSet: (String) -> Unit,
    onP2pModeChanged: (String) -> Unit,
    onStartTcpClick: (LogActionCallback) -> Unit,
    onStopTcpClick: (LogActionCallback) -> Unit,
    onSendRegisterClick: (LogActionCallback) -> Unit
) {
    var urlInput by remember { mutableStateOf(initHttpUrl) }
    var portInput by remember { mutableStateOf(initPort) }
    var selectedP2p by remember { mutableStateOf(initSelectedP2p) }
    var expandedDropDown by remember { mutableStateOf(false) }
    var logContent by remember { mutableStateOf("") }

    // 日志回调实现
    val logCallback = remember {
        object : LogActionCallback {
            override val port: String
                get() = portInput

            override fun appendLog(text: String) {
                logContent += text + "\n\n"
                LogUtils.d(text)
            }
        }
    }

    // 主色调紫色
    val primaryPurple = Color(0xFFC8B6F0)
    val purpleText = Color(0xFF5A2E91)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White)
            .padding(16.dp)
    ) {
        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(text = "设备HTTP地址:", fontSize = 14.sp)
            Spacer(modifier = Modifier.width(6.dp))
            TextField(
                value = urlInput,
                onValueChange = { urlInput = it },
                modifier = Modifier.weight(1f),
                singleLine = true,
                textStyle = androidx.compose.ui.text.TextStyle(fontSize = 14.sp)
            )
            Spacer(modifier = Modifier.width(8.dp))
            Button(
                onClick = {
                    if(urlInput.isBlank()){
                        return@Button
                    }
                    onUrlSet(urlInput)
                    logCallback.appendLog("📌 设备HTTP服务器地址已设置:$urlInput")
                },
                colors = ButtonDefaults.buttonColors(
                    containerColor = primaryPurple,
                    contentColor = purpleText
                ),
                shape = RoundedCornerShape(24.dp)
            ) {
                Text("设置", fontSize = 14.sp)
            }
        }

        Spacer(modifier = Modifier.height(14.dp))

        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(text = "TCP服务端端口:", fontSize = 14.sp)
            Spacer(modifier = Modifier.width(6.dp))
            TextField(
                value = portInput,
                onValueChange = { portInput = it },
                modifier = Modifier.width(100.dp),
                singleLine = true,
                textStyle = androidx.compose.ui.text.TextStyle(fontSize = 14.sp)
            )
            Spacer(modifier = Modifier.width(12.dp))
            Text(text = "当前P2P模式:$selectedP2p", fontSize = 14.sp)
            Spacer(modifier = Modifier.width(8.dp))

            ExposedDropdownMenuBox(
                expanded = expandedDropDown,
                onExpandedChange = { expandedDropDown = !expandedDropDown }
            ) {
                TextField(
                    value = selectedP2p,
                    onValueChange = {},
                    readOnly = true,
                    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedDropDown) },
                    modifier = Modifier.menuAnchor(),
                    textStyle = androidx.compose.ui.text.TextStyle(fontSize = 14.sp)
                )
                ExposedDropdownMenu(
                    expanded = expandedDropDown,
                    onDismissRequest = { expandedDropDown = false }
                ) {
                    p2pOptions.forEach { option ->
                        DropdownMenuItem(
                            text = { Text(option, fontSize = 14.sp) },
                            onClick = {
                                selectedP2p = option
                                onP2pModeChanged(option)
                                expandedDropDown = false
                            }
                        )
                    }
                }
            }
        }

        Spacer(modifier = Modifier.height(20.dp))

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            ActionButton(
                modifier = Modifier.weight(1f),
                text = "1.启动TCP服务",
                onClick = {
                    logContent = ""
                    onStartTcpClick(logCallback)
                }
            )
            ActionButton(
                modifier = Modifier.weight(1f),
                text = "2.发送POST注册",
                onClick = { onSendRegisterClick(logCallback) }
            )
        }

        Spacer(modifier = Modifier.height(12.dp))

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            ActionButton(
                modifier = Modifier.weight(1f),
                text = "停止TCP服务端",
                onClick = { onStopTcpClick(logCallback) }
            )
            ActionButton(
                modifier = Modifier.weight(1f),
                text = "清空日志",
                onClick = { logContent = "" }
            )
        }

        Spacer(modifier = Modifier.height(20.dp))

        // --------日志滚动区域--------
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .background(Color(0xFFF5F5F5), shape = RoundedCornerShape(8.dp))
                .padding(10.dp)
                .verticalScroll(rememberScrollState())
        ) {
            Text(
                text = logContent,
                fontSize = 15.sp,
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
}


@Composable
fun ActionButton(modifier: Modifier, text: String, onClick: () -> Unit) {
    val primaryPurple = Color(0xFFC8B6F0)
    val purpleText = Color(0xFF5A2E91)
    Button(
        onClick = onClick,
        modifier = modifier,
        colors = ButtonDefaults.buttonColors(
            containerColor = primaryPurple,
            contentColor = purpleText
        ),
        shape = RoundedCornerShape(28.dp)
    ) {
        Text(text = text, fontSize = 15.sp, fontWeight = FontWeight.Medium)
    }
}

11.测试代码:

kotlin 复制代码
package com.example.tcpdemo

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.ComposeView
import com.example.tcpdemo.tcp.P2PManager
import com.example.tcpdemo.ui.P2pDebugScreen

/**
 * @作者: smile
 * @时间: 2025/12/5 15:34
 * @描述: Compose版P2P调试页面
 **/
class P2PCommunicationActivity : AppCompatActivity() {
    private val mainHandler = Handler(Looper.getMainLooper())
    private val P2P_TYPES = arrayOf("Map", "Video")

    private var selectedP2pType = "Map"
    private var deviceHttpServerUrl = "http://192.168.1.239:12305/register"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContentView(
            ComposeView(this).apply {
                setContent {
                    P2pDebugScreen(
                        initHttpUrl = deviceHttpServerUrl,
                        initPort = "12305",
                        p2pOptions = P2P_TYPES.toList(),
                        initSelectedP2p = selectedP2pType,

                        onUrlSet = { newUrl ->
                            deviceHttpServerUrl = newUrl
                            P2PManager.setDeviceHttpServerUrl(newUrl)
                        },
                        onP2pModeChanged = { newMode ->
                            selectedP2pType = newMode
                        },
                        onStartTcpClick = { callback ->
                            clearAllLog()
                            val portNum = callback.port.toIntOrNull() ?: 12305
                            P2PManager.startTcpServer(portNum) { status ->
                                mainHandler.post { callback.appendLog(status) }
                            }
                        },
                        onStopTcpClick = { callback ->
                            P2PManager.stopTcpServer { result ->
                                mainHandler.post { callback.appendLog(result) }
                            }
                        },
                        onSendRegisterClick = { callback ->
                            if (deviceHttpServerUrl.isBlank()) {
                                Toast.makeText(this@P2PCommunicationActivity, "请先设置设备HTTP服务器地址", Toast.LENGTH_SHORT).show()
                                return@P2pDebugScreen
                            }
                            callback.appendLog("📤 正在发送POST注册请求...")
                            P2PManager.sendRegisterPostRequest(
                                deviceHttpUrl = deviceHttpServerUrl,
                                p2pType = selectedP2pType
                            ) { _, msg ->
                                mainHandler.post { callback.appendLog(msg) }
                            }
                        }
                    )
                }
            }
        )
    }

    override fun onDestroy() {
        super.onDestroy()
        P2PManager.stopTcpServer { }
    }

    fun clearAllLog() {}
}

12.实现效果:

13.项目demo地址:

https://gitee.com/jackning_admin/tcpdemo

14.总结:

tcp通信有2种方式,第1种使用okhttp请求的方式,但是需要设置设备请求地址为https,如果项目中有多个变体和资源都

要修改一下,这样非常麻烦,而且设备的地址是固定的,有可能不支持https,我这里是使用socket的方式,这种不用设置https,使用起来简单方便,我这里的自己的iot项目,大家可以根据自己的协议来封装实现.

相关推荐
Loongproxy26 分钟前
抖音企业号更换公司主体怎么操作?矩阵账号配静态长效IP的稳定方案
服务器·网络·网络协议·tcp/ip
Escalating_xu1 小时前
【C++入门基础(下)】默认参数、函数重载、引用、inline 与 nullptr
android·c++·redis
dog25010 小时前
再论 TCP 的流式传输
网络·网络协议·tcp/ip
wen_zhufeng12 小时前
用 vLLM 加速 TTS 推理:通用改造指南
android·vllm
长友cy14 小时前
windows 搭建Qt编译Android 应用程序,快速搭建
android·windows
傻啦嘿哟14 小时前
某宝商品爬虫:破解反爬的终极方案(含IP代理池+User-Agent轮换)
爬虫·网络协议·tcp/ip
布莱克60514 小时前
如何实现断点续传和分片上传?
网络·tcp/ip·状态模式
智驭未来掌门人15 小时前
Android Kotlin 开发避坑指南:从语言特性到工程化实践
android
码农coding15 小时前
android12 ViewRootImpl分析
android