14、WiFi P2P数据传输:建立Socket连接与文件传输

好,到了这一步,咱们的WiFi P2P已经成功连上了。但连上只是开始,真正的重头戏是——怎么把数据传过去?

说白了,WiFi P2P只是帮你搭好了一条“路”。至于路上跑什么车、怎么跑,那是Socket的事。我个人习惯把P2P连接比作“牵线”,而Socket才是真正的“握手”。

为什么需要Socket?

你想想看,WiFi P2P建立连接后,Android系统只是告诉你“你们两个设备已经在一个局域网里了”。但它并没有帮你把数据从A搬到B。这时候,就需要我们手动创建Socket连接。

Socket分两种角色:

  • ServerSocket:服务端,负责监听端口,等待别人连过来。
  • Socket:客户端,主动去连接服务端。

在P2P场景里,通常让Group Owner(GO)作为服务端,Client作为客户端。但这不是绝对的——我在项目里也见过反过来做的,不过GO做服务端更符合直觉,因为GO本身就有“主机”的味道。

核心要点:WiFi P2P只负责建立网络连接,不负责数据传输。数据传输必须由应用层自己实现,最常用的方式就是Socket。

建立ServerSocket(服务端)

先看服务端怎么写。假设我们的设备是Group Owner,它需要开一个端口等着。

// 在GO设备上启动ServerSocket
private fun startServerSocket(port: Int) {
    Thread {
        try {
            val serverSocket = ServerSocket(port)
            Log.d(TAG, "ServerSocket已启动,端口:$port")
            
            while (isRunning) {
                // 阻塞等待客户端连接
                val socket = serverSocket.accept()
                Log.d(TAG, "有客户端连接过来:${socket.inetAddress.hostAddress}")
                
                // 处理这个连接(建议开新线程)
                handleClientSocket(socket)
            }
        } catch (e: IOException) {
            Log.e(TAG, "ServerSocket异常:${e.message}")
        }
    }.start()
}

嗯,这里要注意几个坑:

  • 端口选择:建议用 8988 或 8888 这种高位端口,别用 80、443 这些系统保留端口。
  • 线程问题:ServerSocket.accept() 是阻塞的,必须在子线程里跑。
  • 多客户端:accept() 一次只能处理一个连接,如果有多个客户端,需要循环 accept 并开新线程处理。

我曾经踩过的坑:有一次我忘了在子线程里启动ServerSocket,结果直接在UI线程调了accept(),整个界面卡死。用户点哪儿都没反应,还以为是手机坏了……从那以后,我每次写Socket都会先检查一遍线程环境。

建立Socket(客户端)

客户端这边就简单多了。它只需要知道GO的IP地址和端口号,然后主动连过去。

// 在Client设备上连接GO
private fun connectToServer(host: String, port: Int) {
    Thread {
        try {
            val socket = Socket()
            // 设置超时时间,避免一直傻等
            socket.connect(InetSocketAddress(host, port), 5000)
            
            Log.d(TAG, "连接成功!服务器:$host:$port")
            
            // 开始收发数据
            communicateWithServer(socket)
        } catch (e: IOException) {
            Log.e(TAG, "连接失败:${e.message}")
        }
    }.start()
}

这里有个细节:超时时间一定要设。我见过不少新手不设超时,结果网络一波动,客户端就卡在connect()那里,点取消都没反应。

数据传输:从理论到实践

连接建立好了,接下来就是传数据。最常见的场景是传文件。我们来看一个完整的文件发送流程。

发送文件(客户端)

private fun sendFile(socket: Socket, filePath: String) {
    try {
        val file = File(filePath)
        val fileInputStream = FileInputStream(file)
        val outputStream = socket.getOutputStream()
        
        // 第一步:先发送文件名和大小
        val metaData = "${file.name}|${file.length()}"
        outputStream.write(metaData.toByteArray())
        outputStream.flush()
        
        // 第二步:发送文件内容
        val buffer = ByteArray(8192)
        var bytesRead: Int
        var totalSent: Long = 0
        
        while (fileInputStream.read(buffer).also { bytesRead = it } != -1) {
            outputStream.write(buffer, 0, bytesRead)
            totalSent += bytesRead
            
            // 可以在这里更新进度
            val progress = (totalSent * 100 / file.length()).toInt()
            Log.d(TAG, "发送进度:$progress%")
        }
        
        outputStream.flush()
        Log.d(TAG, "文件发送完成")
        
        fileInputStream.close()
    } catch (e: IOException) {
        Log.e(TAG, "发送文件失败:${e.message}")
    }
}

接收文件(服务端)

private fun receiveFile(socket: Socket, saveDir: String) {
    try {
        val inputStream = socket.getInputStream()
        
        // 第一步:读取元数据(文件名和大小)
        val metaBuffer = ByteArray(1024)
        val metaLength = inputStream.read(metaBuffer)
        val metaString = String(metaBuffer, 0, metaLength)
        val metaParts = metaString.split("|")
        
        val fileName = metaParts[0]
        val fileSize = metaParts[1].toLong()
        
        // 第二步:接收文件内容
        val saveFile = File(saveDir, fileName)
        val fileOutputStream = FileOutputStream(saveFile)
        
        val buffer = ByteArray(8192)
        var bytesRead: Int
        var totalReceived: Long = 0
        
        while (totalReceived < fileSize) {
            bytesRead = inputStream.read(buffer)
            if (bytesRead == -1) break
            
            fileOutputStream.write(buffer, 0, bytesRead)
            totalReceived += bytesRead
            
            val progress = (totalReceived * 100 / fileSize).toInt()
            Log.d(TAG, "接收进度:$progress%")
        }
        
        Log.d(TAG, "文件接收完成:$fileName")
        fileOutputStream.close()
    } catch (e: IOException) {
        Log.e(TAG, "接收文件失败:${e.message}")
    }
}

我的经验之谈:传文件时,我习惯先传一个“元数据包”,里面包含文件名和大小。这样接收端可以提前知道要收多大的文件,方便做进度条和校验。如果不这么做,接收端读到EOF才知道结束,那就没法做进度显示了。

知识体系总览

下面这张图把整个流程串起来了,你可以对照着看:

WiFi P2P 数据传输流程 阶段一:WiFi P2P 连接建立 设备发现 → 邀请 → Group Owner协商 → 连接成功 阶段二:Socket 连接建立 GO设备:启动 ServerSocket(端口监听) Client设备:创建 Socket(主动连接) 阶段三:数据传输 发送元数据(文件名+大小) 传输文件内容 接收端校验与保存 数据传输完成,关闭Socket

常见问题与避坑指南

问题 原因 解决方案
连接超时 IP地址写错,或者端口被占用 先ping一下对方IP,确认网络通;换一个端口试试
文件传一半断了 WiFi信号不稳定,或者Socket超时 加断点续传逻辑;增大Socket超时时间
接收到的文件打不开 字节流顺序乱了,或者元数据解析出错 先传元数据再传文件内容;用固定分隔符解析
多设备同时传文件冲突 ServerSocket单线程处理 每个客户端连接开独立线程处理

我曾经遇到过一个特别坑的问题:有一次在小米手机上测试,ServerSocket一直bind失败,报“权限不足”。查了半天才发现,是MIUI的“联网控制”把App的局域网通信权限给关了。所以,如果发现Socket创建失败,先检查一下系统权限设置。

小结

WiFi P2P的数据传输,说白了就是三步:

  1. GO开ServerSocket等着
  2. Client用Socket连过来
  3. 两边用流(InputStream/OutputStream)读写数据

代码本身不复杂,但线程管理、超时处理、权限检查这些细节,才是真正考验功力的地方。我建议你在写的时候,先把最简单的“发字符串”调通,再升级到文件传输。一步一步来,稳得很。

公众号:蓝海资料掘金营,微信deep3321