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才知道结束,那就没法做进度显示了。
知识体系总览
下面这张图把整个流程串起来了,你可以对照着看:
常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 连接超时 | IP地址写错,或者端口被占用 | 先ping一下对方IP,确认网络通;换一个端口试试 |
| 文件传一半断了 | WiFi信号不稳定,或者Socket超时 | 加断点续传逻辑;增大Socket超时时间 |
| 接收到的文件打不开 | 字节流顺序乱了,或者元数据解析出错 | 先传元数据再传文件内容;用固定分隔符解析 |
| 多设备同时传文件冲突 | ServerSocket单线程处理 | 每个客户端连接开独立线程处理 |
我曾经遇到过一个特别坑的问题:有一次在小米手机上测试,ServerSocket一直bind失败,报“权限不足”。查了半天才发现,是MIUI的“联网控制”把App的局域网通信权限给关了。所以,如果发现Socket创建失败,先检查一下系统权限设置。
小结
WiFi P2P的数据传输,说白了就是三步:
- GO开ServerSocket等着
- Client用Socket连过来
- 两边用流(InputStream/OutputStream)读写数据
代码本身不复杂,但线程管理、超时处理、权限检查这些细节,才是真正考验功力的地方。我建议你在写的时候,先把最简单的“发字符串”调通,再升级到文件传输。一步一步来,稳得很。