1. 项目缘起:为什么要在Android上搞USB Socket通信?
做Android开发或者搞机玩机的朋友,对ADB(Android Debug Bridge)肯定不陌生。我们最熟悉的场景,就是用USB线连上手机,在电脑终端里敲adb shell、adb install,或者用adb logcat看日志。这背后,其实就是一套基于C/S架构的通信机制。但大多数人可能没深究过,这条USB线缆里流淌的,除了调试指令和文件,还能承载我们自定义的、双向的、高速的数据流。这就是“Android Adb USB Socket 通信”的核心价值。
简单来说,它允许我们在PC端(客户端)和Android设备端(服务端)之间,建立一个基于ADB协议的、可靠的Socket连接通道。这个通道不走Wi-Fi网络,不依赖设备的IP地址和端口暴露,完全通过USB物理连接,利用ADB守护进程(adbd)作为中转,实现进程间的数据交换。听起来是不是有点像“USB网络共享”的底层玩法?没错,原理上相通,但这里我们拥有更高的控制权。
那么,这玩意儿到底能干嘛?应用场景其实非常广泛。比如,你想开发一个PC端的手机管理工具,需要实时获取手机屏幕截图、传输大文件、或者执行一系列复杂的自动化脚本,传统的ADB命令一次一交互,效率低且难以管理状态。而建立一个常驻的Socket连接,就可以实现长会话、双向异步通信。再比如,一些硬件开发场景,Android设备作为主机(Host),需要通过USB控制外设,但标准ADB命令集不包含你的自定义协议,这时就可以在设备上运行一个自定义服务,通过ADB Socket与PC端的控制程序通信,PC端发送指令,设备端解析并操控硬件。对于安全研究、自动化测试框架(如Appium底层的一部分通信机制)、甚至是游戏辅助工具,这种稳定、低延迟的通信方式都是基石。
所以,今天我就以一个实践者的角度,带你从原理到实操,彻底打通这条“隐秘”的通信通道。我们会从最基础的ADB通信模型讲起,然后一步步拆解如何建立连接、如何设计协议、如何处理数据,最后分享几个我趟过的坑和性能优化的技巧。无论你是想深化对Android系统的理解,还是需要为你的项目寻找一个稳定的跨进程通信方案,这篇文章都能给你一份可直接“抄作业”的指南。
2. 理解基石:ADB的通信模型与USB转发机制
要玩转ADB USB Socket,首先得摸清ADB的老底。很多人以为adb shell就是全部,其实那只是冰山一角。ADB是一个三层架构的守护进程系统。
2.1 ADB的三组件模型
- ADB Client(客户端):这就是我们在PC上执行的
adb命令行工具。它的职责是解析用户命令,并将其发送给ADB Server。 - ADB Server(服务端):一个在PC后台运行的守护进程(
adb -L或adb start-server会启动它)。它负责管理所有连接到PC的Android设备(或模拟器),并与每个设备上的ADB Daemon建立连接。它是通信的枢纽。 - ADB Daemon(守护进程,adbd):运行在Android设备(或模拟器)上的后台服务。它监听来自ADB Server的连接,并执行具体的命令(如启动shell、安装APK、转发端口等)。
当你键入adb devices时,发生的是:Client询问Server当前连接的设备列表,Server与每个设备的adbd通信后,将列表返回给Client。关键在于,Client和Server之间、Server和Daemon之间,都是通过Socket进行通信的。Client-Server之间通常是本地TCP Socket(比如127.0.0.1:5037),而Server-Daemon之间,在USB连接下,就是通过USB协议封装的虚拟网络Socket。
2.2 USB连接下的数据流
当Android设备通过USB连接PC并启用调试模式后,PC端的ADB Server会通过USB驱动(如WinUSB、libusb)与设备建立连接。设备端的adbd进程会创建一个名为adbd的Socket服务,监听特定的“端口”(注意,这里不是网络端口,是ADB协议内部的端点标识)。PC端的Server会与这个服务建立一条控制连接。
我们常用的adb shell,其本质是ADB Server通过这条控制连接,向设备的adbd发送一个shell:命令字符串。adbd收到后,会fork一个新的进程(比如/system/bin/sh),并将这个新进程的输入/输出与另一条新创建的数据通道连接起来。这条数据通道,同样是一个Socket。
2.3 核心命令:adb forward与adb reverse
这是我们实现自定义Socket通信的关键。这两个命令用于在PC和设备之间建立端口转发。
adb forward tcp:<pc_port> localabstract:<socket_name>:将PC端的TCP端口转发到Android设备上的一个Unix Domain Socket(通常是localabstract:命名空间下的)。这是我们实现PC主动连接设备服务的主要方式。PC上任何连接到localhost:<pc_port>的程序,其数据都会被ADB转发到设备上名为<socket_name>的Socket。adb reverse tcp:<device_port> tcp:<pc_port>:将Android设备上的一个TCP端口转发到PC端的TCP端口。这是实现设备端程序主动连接PC服务的反向隧道。设备上任何连接到localhost:<device_port>的程序,其数据都会被转发到PC的<pc_port>端口。
我们的“USB Socket通信”,狭义上通常指利用adb forward,在PC端创建一个TCP Server Socket,等待连接;在设备端创建一个localabstractSocket服务。然后通过adb forward将两者桥接起来。广义上,adb reverse也是构建双向通信的重要一环。
注意:
localabstract是Android特有的一种Unix Domain Socket命名空间,它不依赖于文件系统路径,因此不需要文件系统权限,更适合应用之间的通信。adbd本身就在这个命名空间下监听。
理解了这些,我们就知道,所谓的“Android Adb USB Socket 通信”,技术上是利用ADB的端口转发功能,将PC上的TCP Socket与Android设备上的Unix Domain Socket(或TCP Socket)进行桥接,从而实现跨USB的数据透传。接下来,我们就从零开始搭建一个这样的通信实例。
3. 实战演练:构建一个双向ECHO服务器
光说不练假把式。我们来实现一个经典的ECHO服务器:设备端作为服务端,监听Socket;PC端作为客户端,连接并发送字符串,设备端原样返回。我们将分设备端(Android)和PC端(以Python为例)两部分进行。
3.1 设备端(Android)服务实现
在Android设备上,我们需要创建一个守护进程或一个Service来监听Socket。这里以在Native层(C/C++)或使用LocalServerSocket为例,因为这样更接近底层,效率也高。实际上,adb命令adb shell背后也是类似的机制。
方案一:使用Android SDK的LocalServerSocket(Java)虽然adb forward转发到的是localabstract,但Android SDK提供了便捷的类来操作它。
// 在Android Service或后台线程中运行 import android.net.LocalServerSocket; import android.net.LocalSocket; import java.io.InputStream; import java.io.OutputStream; public class AdbSocketServer { private static final String SOCKET_NAME = "my_custom_socket"; private volatile boolean isRunning = false; public void startServer() { new Thread(() -> { try { LocalServerSocket server = new LocalServerSocket(SOCKET_NAME); isRunning = true; System.out.println("Server started on abstract socket: " + SOCKET_NAME); while (isRunning) { LocalSocket clientSocket = server.accept(); // 阻塞等待连接 new Thread(new ClientHandler(clientSocket)).start(); } server.close(); } catch (Exception e) { e.printStackTrace(); } }).start(); } private static class ClientHandler implements Runnable { private LocalSocket socket; ClientHandler(LocalSocket socket) { this.socket = socket; } @Override public void run() { try { InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); byte[] buffer = new byte[4096]; int bytesRead; // 简单的ECHO逻辑 while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); os.flush(); } socket.close(); } catch (Exception e) { e.printStackTrace(); } } } public void stopServer() { isRunning = false; } }这段代码在设备上创建了一个名为my_custom_socket的localabstractSocket服务器。任何连接到这个Socket的客户端发送的数据,都会被原样送回。
方案二:使用Native代码(C/C++)对于性能要求更高或需要与Native库交互的场景,可以直接用C语言编写。这通常需要NDK和jni。
#include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <string.h> #include <stdio.h> #define SOCKET_NAME "my_custom_socket" void start_native_server() { int server_fd, client_fd; struct sockaddr_un addr; char buffer[4096]; socklen_t addr_len; // 创建Unix Domain Socket if ((server_fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { perror("socket"); return; } memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; // 注意:abstract socket的路径名第一个字节是 '\0' strncpy(&addr.sun_path[1], SOCKET_NAME, sizeof(addr.sun_path) - 2); // 对于abstract socket,绑定路径长度需要包含开头的空字符 if (bind(server_fd, (struct sockaddr*)&addr, sizeof(sa_family_t) + strlen(SOCKET_NAME) + 1) < 0) { perror("bind"); close(server_fd); return; } if (listen(server_fd, 5) < 0) { perror("listen"); close(server_fd); return; } printf("Native server listening on abstract socket: %s\n", SOCKET_NAME); while (1) { client_fd = accept(server_fd, (struct sockaddr*)&addr, &addr_len); if (client_fd < 0) { perror("accept"); continue; } // 处理客户端连接(可创建线程或使用非阻塞IO) int n; while ((n = read(client_fd, buffer, sizeof(buffer))) > 0) { write(client_fd, buffer, n); // ECHO } close(client_fd); } close(server_fd); }Native方式更底层,需要注意abstract socket的绑定方式(路径首字符为\0)。将上述Native代码编译成库,通过JNI调用即可。
3.2 PC端(Python)客户端实现
在PC端,我们需要做两件事:1. 建立ADB转发;2. 作为TCP客户端连接被转发的端口。
首先,通过命令行或代码执行ADB转发命令:
adb forward tcp:38300 localabstract:my_custom_socket这条命令将PC的38300端口转发到了设备的my_custom_socket。
然后,用Python编写一个简单的TCP客户端:
import socket import time def test_echo_client(): host = 'localhost' # 因为adb forward到了本地端口 port = 38300 # 与adb forward命令设置的端口一致 # 创建TCP socket client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: client_socket.connect((host, port)) print(f"Connected to {host}:{port}") test_message = b"Hello from PC over ADB Socket!\n" client_socket.sendall(test_message) print(f"Sent: {test_message.decode().strip()}") # 接收回显 data = client_socket.recv(1024) print(f"Received Echo: {data.decode().strip()}") # 可以持续对话 for i in range(3): msg = f"Message {i}\n".encode() client_socket.sendall(msg) echo = client_socket.recv(1024) print(f"Sent: {msg.decode().strip()}, Echo: {echo.decode().strip()}") time.sleep(0.5) except socket.error as e: print(f"Socket error: {e}") finally: client_socket.close() print("Connection closed.") if __name__ == "__main__": # 在实际应用中,执行adb forward的部分可以集成进来 import subprocess try: # 先设置转发(如果已存在,adb会报错,可以忽略或先移除) subprocess.run(['adb', 'forward', 'tcp:38300', 'localabstract:my_custom_socket'], check=True) print("ADB forward set up.") except subprocess.CalledProcessError as e: # 转发可能已存在 print(f"Note on forward setup: {e}") test_echo_client()运行这个Python脚本(确保设备已通过USB连接且adb devices能看到),你应该能看到PC端发送的字符串被设备端原样返回。至此,一个最基本的基于ADB USB的Socket通信链路就打通了。
4. 深入核心:协议设计、数据封装与连接管理
实现了基础的ECHO,这只是万里长征第一步。在实际项目中,我们面对的是复杂的业务数据、可能并发的连接、以及需要稳定长连接的场景。这就涉及到三个核心问题:通信协议、数据封装和连接生命周期管理。
4.1 自定义应用层协议
原始Socket传输的是字节流,没有消息边界。如果你发送“Hello”和“World”,接收方可能一次收到“HelloWorld”。因此,我们必须定义自己的应用层协议来分割消息。常见的有:
- 长度前缀法:在每个消息前面加上固定字节的长度字段(如4字节的int)。接收方先读4字节得到长度N,再读取N字节的数据。这是最常用、最高效的方式。
- 分隔符法:用一个特殊的字符或字节序列(如
\n、\0或<EOF>)作为消息结束标记。适用于文本协议,但需要转义分隔符本身。 - 固定长度法:所有消息长度固定。简单但不够灵活。
我强烈推荐长度前缀法。下面是一个简单的Python和Java实现示例:
PC端(Python)发送带长度前缀的消息:
import struct def send_message(sock, message): """发送一条消息,格式:4字节长度(网络字节序) + 数据""" data = message.encode('utf-8') if isinstance(message, str) else message length = len(data) # '>I' 表示大端序的4字节无符号整数 sock.sendall(struct.pack('>I', length) + data) def recv_message(sock): """接收一条长度前缀格式的消息""" # 先读取4字节长度 raw_len = recv_all(sock, 4) if not raw_len: return None length = struct.unpack('>I', raw_len)[0] # 根据长度读取数据 data = recv_all(sock, length) return data def recv_all(sock, n): """确保读取n个字节,应对TCP拆包""" data = bytearray() while len(data) < n: packet = sock.recv(n - len(data)) if not packet: return None data.extend(packet) return bytes(data)设备端(Android Java)解析:
// 在ClientHandler的run方法中 DataInputStream dis = new DataInputStream(socket.getInputStream()); DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); while (isRunning) { try { int length = dis.readInt(); // 读取4字节长度,DataInputStream默认是大端序 byte[] buffer = new byte[length]; dis.readFully(buffer); // 读取指定长度的数据 String received = new String(buffer, StandardCharsets.UTF_8); // 处理消息... // 回复时也按相同格式 byte[] response = processMessage(received).getBytes(StandardCharsets.UTF_8); dos.writeInt(response.length); dos.write(response); dos.flush(); } catch (EOFException e) { // 连接关闭 break; } catch (IOException e) { e.printStackTrace(); break; } }4.2 连接管理与心跳机制
ADB USB连接本身是相对稳定的,但设备可能休眠、USB线可能被拔出、PC可能进入睡眠。因此,必须在应用层实现连接管理和心跳。
- 连接保持:在业务空闲时,定期(如每30秒)从一端向另一端发送一个心跳包(比如一个长度为0的消息,或特定指令
PING)。对方回复PONG。如果连续多次未收到心跳回复,则认为连接已断开,需要进行重连。 - 重连逻辑:重连时,需要重新执行
adb forward(因为连接断开后,ADB的转发可能依然存在,但Socket连接已失效)。一个健壮的重连逻辑应包括:检测到连接断开 -> 延迟等待 -> 尝试重新建立Socket连接 -> 如果失败,尝试重新执行ADB转发命令 -> 再次尝试连接。 - 多连接处理:设备端的Server需要能处理多个PC客户端的并发连接。上面的示例用了“一个连接一个线程”的模型,简单但并发高时资源消耗大。在生产环境中,可以考虑使用线程池、NIO(
java.nio.channels)或更高效的网络库(如Netty)来处理。
4.3 错误处理与资源清理
这是最容易出问题的地方。
- Socket异常:
IOException是常态。读写操作必须放在try-catch中,确保发生异常时能关闭Socket,释放资源。 - ADB转发冲突:
adb forward tcp:38300 ...如果38300端口已被占用,命令会失败。在程序启动时,可以先尝试移除旧的转发:adb forward --remove tcp:38300,然后再建立新的。 - 设备重启或ADB重启:设备重启或PC上的ADB Server重启(
adb kill-server)会导致所有转发失效。你的程序需要能监听设备连接状态(例如定期检查adb devices列表),并在设备重新上线后重建转发和连接。 - Native Socket的权限:如果设备端服务是以
root权限运行的(例如在su后的shell中),创建的Socket其他非root进程可能无法连接。需要注意SELinux上下文和文件权限(对于文件系统Socket)或确保连接双方权限匹配(对于abstract socket,通常不受文件系统权限限制,但SELinux策略可能影响)。
5. 进阶话题:性能优化与安全考量
当你的通信框架跑通后,接下来就要考虑如何让它跑得更快、更稳、更安全。
5.1 性能优化点
- 缓冲区大小:Socket的发送和接收缓冲区大小会影响吞吐量。可以根据传输的数据量适当调整(通过
Socket.setSendBufferSize()和setReceiveBufferSize())。但要注意,这个值只是一个建议值,实际大小可能受系统限制。 - 禁用Nagle算法:对于需要低延迟、小数据包即时传输的场景(如实时控制指令),可以禁用TCP的Nagle算法(在Java中通过
Socket.setTcpNoDelay(true))。这能减少数据包在发送端的缓冲延迟,但可能会增加网络包数量。 - 批量处理与粘包:上文提到的“长度前缀法”已经解决了粘包问题。对于大量小消息,可以考虑在应用层进行批量打包,减少协议头的开销和系统调用的次数。
- 直接字节缓冲区(Direct Buffer):在Java NIO中,使用
ByteBuffer.allocateDirect()分配的堆外内存进行IO操作,可以减少一次从JVM堆内拷贝到系统内核的步骤,在高吞吐场景下有益。 - USB 2.0 vs 3.0+:物理连接的速度是硬上限。USB 2.0的理论速度是480 Mbps,而USB 3.0可达5 Gbps。确保使用高质量的USB数据线并连接到主机的USB 3.0端口,能显著提升大数据量传输(如视频流、大文件)的性能。
5.2 安全考量
通过ADB USB的通信,数据在USB线缆上传输,相对于Wi-Fi,被中间人攻击的风险较低,但绝非绝对安全。
- 认证与授权:任何拥有USB调试权限的PC都可以尝试连接你的Socket服务。因此,服务端必须实现某种形式的认证。可以在建立连接后,第一时间进行“握手”交换密钥或令牌。例如,PC端发送一个预共享的密钥,设备端验证通过后才开始真正的业务通信。
- 数据加密:即使物理链路相对安全,对敏感数据(如密码、个人数据)也应进行加密。可以在应用层使用TLS/SSL(虽然配置在Socket上稍复杂),或者使用更轻量的对称加密算法(如AES),在消息封装/解封装时进行加解密。密钥需要通过安全的方式交换。
- 最小权限原则:设备端的服务不应以过高权限(如root)运行,除非必要。仔细配置SELinux策略,仅允许必要的访问。
- 输入验证:永远不要信任来自Socket的数据。严格验证所有输入数据的格式、长度和范围,防止缓冲区溢出或逻辑漏洞。
5.3 与adb reverse的配合实现双向对等通信
上面的例子是PC主动连接设备(adb forward)。有时我们需要设备能主动发起请求到PC上的服务。这时就需要adb reverse。
例如,PC上运行一个HTTP API服务在8080端口。我们可以:
adb reverse tcp:8080 tcp:8080然后,在设备端的App或脚本中,就可以像访问本地服务一样访问http://127.0.0.1:8080/,而这个请求会被ADB反向隧道转发到PC的8080端口。
结合adb forward和adb reverse,可以构建出非常灵活的双向通信架构。PC和设备可以互为客户端和服务器,实现真正的对等通信。这在一些分布式计算、设备协同的场景下非常有用。
6. 避坑指南:那些年我踩过的“Socket”坑
理论很美好,实践总是磕磕绊绊。下面分享几个我在实际项目中遇到的典型问题和解决方案,希望能帮你节省大量调试时间。
6.1 “Address already in use” 与 “通常每个套接字地址只允许使用一次”
这是Socket编程中最常见的错误之一。在PC端,当你尝试绑定一个已被占用的端口时,就会报这个错。在ADB上下文中,有几个可能:
- 端口冲突:你设置的转发端口(如38300)已经被PC上其他程序占用。用
netstat -ano | findstr :38300(Windows)或lsof -i :38300(Linux/macOS)找出并结束占用进程,或者换一个端口。 - ADB转发残留:之前的程序异常退出,没有正确清理ADB转发。
adb forward --list查看所有现有转发,用adb forward --remove tcp:38300移除指定的。一个健壮的做法是在程序启动时,先执行remove再add。 - 设备端Socket未关闭:在设备端,如果你的Server Socket没有正确关闭(
close()),下次启动时绑定同一个抽象名(my_custom_socket)就会失败。确保在程序退出或Service销毁时,调用LocalServerSocket.close()。对于abstract socket,进程退出后系统通常会清理,但最好显式关闭。
6.2 连接莫名断开与“Broken pipe”
长连接环境下,连接突然断开是常态。
- 根本原因:对端进程崩溃、网络物理断开(USB松动)、系统休眠、防火墙/杀毒软件干扰、或者长时间空闲被中间路由器或ADB本身超时断开。
- 应对策略:
- 心跳保活:如前所述,这是必须的。心跳间隔建议在20-60秒之间。
- 读写超时设置:在Socket上设置
setSoTimeout(毫秒),这样read操作在指定时间内没数据就会抛出SocketTimeoutException,而不是无限期阻塞。你可以在超时后发送一个心跳包来探测连接。 - 异常捕获与重连:所有IO操作放在try-catch中,捕获
SocketException、IOException(特别是Broken pipe、Connection reset)后,进入重连流程。重连逻辑要有退避策略(比如第一次等1秒,第二次等2秒,最多等30秒),避免疯狂重连。 - 检查USB连接状态:可以定期运行
adb devices,确保目标设备还在列表中,状态是device而不是offline或unauthorized。
6.3 数据传输乱码、丢包或数据不对齐
这通常不是ADB或USB的问题,而是应用层协议处理不当。
- 字符编码:确保发送端和接收端使用相同的字符编码(如UTF-8)。在传输文本时,明确指定编码。二进制数据则无需转换。
- TCP粘包/拆包:这是流式协议的特性,不是Bug。必须使用前面提到的“长度前缀法”或“分隔符法”来界定消息边界。
read()一次返回的数据量是不确定的,不能假设一次read就能拿到一条完整消息。 - 字节序(Endianness):如果你在协议中使用了多字节整数(如长度字段),必须统一字节序。网络字节序是大端序(Big-Endian)。在Java中,
DataInputStream.readInt()和DataOutputStream.writeInt(int)使用大端序。在Python的struct模块中,使用'>I'表示大端序的4字节无符号整数。如果两端不一致,解析出来的长度值会是错的。
6.4 在非Root设备上运行Socket服务
如果你的设备没有Root权限,需要注意:
- 使用
localabstract:如前所述,localabstractSocket不依赖文件系统,因此不需要/data目录的写权限,非常适合普通应用。ADB转发时也指定localabstract:前缀。 - SELinux限制:在某些严格定制的Android系统上,普通应用创建的
localabstractSocket可能无法被adbd(通常以shell或root身份运行)访问。这会导致adb forward成功,但PC端连接时被拒绝。查看logcat可能会有SELinux avc denied的日志。这种情况需要调整SELinux策略,或者让你的服务以更高的权限(如run-as你的应用包名)运行,但这通常需要系统级支持或设备已Root。
6.5 跨平台开发的注意事项
你的PC端程序可能运行在Windows、Linux或macOS上。ADB命令行工具的行为基本一致,但有些细微差别:
- ADB路径:在代码中调用
adb命令时,最好使用绝对路径,或将ADB所在目录加入系统PATH环境变量。 - USB驱动(仅Windows):Windows需要正确的USB驱动才能识别Android设备并安装ADB接口。如果
adb devices列表为空,检查设备管理器里是否有带感叹号的“Android ADB Interface”,并安装合适的驱动(如Google USB Driver)。 - 进程管理:在Windows上,强制结束进程可能不会像Unix-like系统那样触发Socket的优雅关闭。确保你的程序有明确的关闭钩子(Shutdown Hook),在退出时主动关闭Socket并移除ADB转发。
7. 从Demo到生产:一个简单的文件传输服务设计
最后,我们以一个更实用的例子来收尾:设计一个通过ADB USB Socket进行文件传输的服务。这个例子会综合运用前面讲到的协议、心跳、错误处理等知识。
7.1 协议设计
我们定义一个简单的二进制协议:
- 消息类型:1字节。
0x01表示文件信息,0x02表示文件数据块,0x03表示传输完成/确认,0x04表示心跳,0x05表示错误。 - 长度字段:4字节大端序整数,表示后续数据部分的长度。
- 数据部分:根据消息类型变化。
0x01(文件信息):数据部分为UTF-8编码的JSON字符串,包含文件名、文件大小、MD5(可选)。0x02(文件数据块):数据部分就是文件的原始字节块。0x03(传输完成):数据部分可为空,或包含一个状态码。0x04(心跳):数据部分为空(长度为0)。0x05(错误):数据部分为UTF-8编码的错误描述字符串。
7.2 传输流程
- PC端(发送方)连接到设备端服务。
- PC端发送
0x01消息,携带文件信息。 - 设备端(接收方)回复
0x03(确认可以开始)或0x05(错误,如磁盘空间不足)。 - PC端将文件分块(例如每块64KB),依次发送
0x02消息。 - 设备端每收到一个数据块,可以回复一个简单的
0x03(确认收到),也可以累积一定数量后回复。为了简化,这里采用“发送-确认”的停等模式。 - 文件发送完毕,PC端发送一个特殊的
0x03(传输完成)。 - 设备端验证接收到的总字节数与文件信息中的大小一致,然后回复最终的
0x03。 - 传输过程中,任何一方都可以定期发送
0x04心跳。
7.3 关键代码片段(PC端发送文件)
import struct import json import hashlib import os def send_file_over_socket(sock, file_path): file_name = os.path.basename(file_path) file_size = os.path.getsize(file_path) # 计算MD5(可选) # md5 = calculate_md5(file_path) # 1. 发送文件信息 (0x01) file_info = { 'name': file_name, 'size': file_size, # 'md5': md5 } info_json = json.dumps(file_info).encode('utf-8') send_message(sock, b'\x01', info_json) # 假设send_message支持类型字节 # 等待接收方确认 resp_type, resp_data = recv_message(sock) # 假设recv_message也返回类型和数据 if resp_type != 0x03: raise Exception(f"Receiver not ready: {resp_data.decode()}") # 2. 发送文件数据块 chunk_size = 65536 # 64KB sent_bytes = 0 with open(file_path, 'rb') as f: while True: chunk = f.read(chunk_size) if not chunk: break send_message(sock, b'\x02', chunk) sent_bytes += len(chunk) # 等待确认(简单停等) resp_type, _ = recv_message(sock) if resp_type != 0x03: raise Exception("Transfer interrupted") # 可以在这里更新进度条 # 3. 发送传输完成标记 send_message(sock, b'\x03', b'') # 空数据表示完成 print(f"File {file_name} sent successfully. Total bytes: {sent_bytes}")设备端的实现逻辑与之对称,负责解析消息类型,根据类型执行相应操作(创建文件、写入数据、发送确认等)。这个例子虽然简单,但已经具备了生产级文件传输的雏形。你可以在此基础上增加断点续传、压缩、加密、多线程传输等高级功能。
整个探索过程下来,我的体会是,ADB USB Socket通信就像在USB这条“管道”里自己铺设了一条专属的数据铁路。它避开了复杂的网络配置和防火墙,提供了稳定、低延迟的通道,特别适合对可靠性和实时性要求高的PC与设备间通信场景。虽然需要自己处理协议、心跳、重连这些“脏活累活”,但带来的控制力和灵活性是无可替代的。当你看到自定义的数据在USB线里顺畅地双向流动时,那种对系统底层掌控的感觉,正是工程师乐趣的来源之一。希望这篇长文能帮你打开这扇门,在实际项目中少走弯路。