Android使用usb线传输大文件笔记

使用usb线传输大文件

参考资料:

  1. 使用USB数据线连接PC端和Android端进行数据的交互
  2. 安卓设备通过USB接口实现与pc端的简单数据通信
  3. Socket TCP/IP协议数据传输过程中的粘包和分包问题
  4. 【Android学习】socket长连接,数据粘包问题

  由于客户应用使用的场景比较特殊。明确要求不能使用网络进行数据交互,所以不得不研究了一下使用usb通信这方面。原理就是当连上usb线后,通过socket进行数据通信。只不过android设备作为socket服务端,pc作为socket的客户端。pc端在与服务端建立连接之前需要使用adb命令设置转发端口(具体可参考参考资料1和参考资料2)。
  端口这玩意随便填,不跟别人冲突就行。

adb shell am broadcast -a NotifyServiceStop
adb forward tcp:9999 tcp:9000
adb shell am broadcast -a NotifyServiceStart

##使用代码调用命令行
  如果想要完善android通过usb先进行数据交互,这里应该有不少的命令能用到,这里先记录一下调用命令的基本使用。

import java.io.BufferedReader
import java.io.InputStreamReader

fun main() {

  //读取连接设备
  val process = Runtime.getRuntime().exec("adb devices")
  val devices = BufferedReader(InputStreamReader(process.inputStream))
  val stringBuilder = StringBuilder()
  var line: String? = null
  while (devices.readLine().apply { line = this } != null) {
    stringBuilder.append("$line\n")
  }
  println(stringBuilder.toString())

  //读取安装的应用
  val process2 = Runtime.getRuntime().exec("adb shell pm list packages")
  val packages = BufferedReader(InputStreamReader(process2.inputStream))
  val sb = StringBuilder()
  var line2: String? = null
  while (packages.readLine().apply { line2 = this } != null) {
    sb.append("$line2\n")
  }
  println(sb.toString())

}

  打印结果如下图所示:
Android使用usb线传输大文件笔记_第1张图片
  上面运行结果不错就是代码量有点多,用python简化一下就清晰不少。

import subprocess

if __name__ == '__main__':
    # 读取连接设备
    subprocess.call("adb devices", shell=True)
    # 读取安装的应用
    subprocess.call("adb shell pm list packages", shell=True)

Android端

  Android端作为Socket的服务端,用来接收文件。为了防止分包的问题这里我定义了一个封包和解包方式。
Android使用usb线传输大文件笔记_第2张图片
  在文件传输的时候,数据的收发都会以这种结构去发送或者接收。(实际情况下肯定跟这个不一样,我这为了方便携带参数用的就是json格式的数据)。理论上就是每次读取的字节大小都在携带信息中。如果在循环读取中有一次没有读满,那就把它应当读取的字节都读取出来,再循环下一次数据读取,省着出现粘包的现象。(byte也别设太大,要不内存溢出)

package com.lyan.usbtestphone

import android.annotation.SuppressLint
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import com.blankj.utilcode.constant.PermissionConstants
import com.blankj.utilcode.util.GsonUtils
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.PermissionUtils
import com.blankj.utilcode.util.ToastUtils
import kotlinx.android.synthetic.main.activity_main.*
import java.io.*
import java.net.ServerSocket
import java.net.Socket

class MainActivity : AppCompatActivity() {

    @SuppressLint("SetTextI18n")
    private val handler = Handler(Handler.Callback {
        when (it.what) {
            1 -> progressTv.text = "${it.obj}%"
        }
        false
    })

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        testBtn.setOnClickListener {
            startSocketServer { LogUtils.i("${Thread.currentThread().name} : $it") }
        }
    }

    private fun startSocketServer(callback: (name: Socket) -> Unit) = Thread {
        val file = File(Environment.getExternalStorageDirectory(), "copy.db3")
        if (file.exists()) {
            if (file.delete()) {
                file.createNewFile()
            }
        } else {
            file.createNewFile()
        }
        ServerSocket(9000).apply {
            logI("移动服务端等待客户端的连接...")
            val client = this.accept()
            callback.invoke(client)
            val bufferedOutputStream = BufferedOutputStream(DataOutputStream(FileOutputStream(file)))
            val bufferedInputStream = BufferedInputStream(DataInputStream(client.getInputStream()))
            val tagBytes = ByteArray(6)
            val infoBytes = ByteArray(4)
            var hTag: String//标记头
            var fTag: String//标记尾
            var infoSize: Int//携带数据的一些信息
            var jsonBytes: ByteArray//携带数据的byte数组
            var sendInfoData: SendInfoData//解析后的携带信息
            var readBytes: ByteArray//真正传输的数据的byte数组
            var readSize: Int//真正传输的数据的byte长度
            while (true) {

                val len = bufferedInputStream.read(tagBytes)
                if (len == -1) {
                    break
                }
                hTag = String(tagBytes, 0, len)//标记头
                //读取进度信息
                infoSize = bufferedInputStream.read(infoBytes).run { bytesToInt(infoBytes) }
                jsonBytes = ByteArray(infoSize)
                sendInfoData = bufferedInputStream.read(jsonBytes).run {
                    val infoJson = when (infoSize) {
                        this -> {//读取数据完整
                            LogUtils.i("读取数据完整")
                            String(jsonBytes, 0, this)
                        }
                        else -> {//读取数据不完整(此处只要将分包处理后、粘包的问题自然就不会出现了)
                            LogUtils.i("读取数据不完整")
                            bufferedInputStream.read(jsonBytes, this, infoSize - this).run {
                                String(jsonBytes, 0, infoSize)
                            }
                        }
                    }
                    LogUtils.i("其他信息 ------> $infoJson")
                    GsonUtils.getGson().fromJson(infoJson, SendInfoData::class.java)
                }
                handler.obtainMessage(1, sendInfoData.percent).sendToTarget()
                //读取数据信息 传输数据大小 解析数据长度
                readBytes = ByteArray(sendInfoData.sendSize)
                readSize = bufferedInputStream.read(readBytes)//已读数据大小
                LogUtils.i("读流的长度:$readSize")
                if (readSize < sendInfoData.sendSize) {
                    LogUtils.w("读取数据不完整!已读数据少于应读取数据的大小……",
                            "应读:${sendInfoData.sendSize}", "实读:$readSize")
                    bufferedInputStream.read(readBytes, readSize, sendInfoData.sendSize - readSize)
                }
                fTag = bufferedInputStream.read(tagBytes).run { String(tagBytes, 0, this) }
                LogUtils.w("header:$hTag", "携带信息的字节数:$infoSize", "携带信息内容:$sendInfoData",
                        "每次应读取的数据字节数量:${sendInfoData.sendSize}", "footer:$fTag")
                bufferedOutputStream.write(readBytes)
                bufferedOutputStream.flush()
            }
        }
    }.start()

    //请求权限(文件读写权限)
    override fun onResume() {
        super.onResume()
        PermissionUtils.permission(PermissionConstants.STORAGE).callback(object : PermissionUtils.SimpleCallback {
            override fun onGranted() {
                ToastUtils.showShort("获取文件读写权限成功!")
            }

            override fun onDenied() {
            }
        }).request()
    }

    private fun bytesToInt(bytes: ByteArray): Int {
        return (0 until bytes.size).sumBy { (bytes[it].toInt() and 0xFF) shl it * 8 }
    }

    //每次接收数据的信息 进度 和 要保存的数据字节大小
    data class SendInfoData(val percent: String, val sendSize: Int)
}

pc端(正常应该是后台)

  在这种情况下后台就成客户端了。这里为了减少socket客户端的代码量,所以使用ptyhon来写:

import subprocess
import socket
import time
import copy
import json
import os


class SendInfoData(object):

    def __init__(self, percent="", sendSize=0):
        self.__percent = percent
        self.__sendSize = sendSize

    def toJson(self):
        return json.dumps({
            "percent": self.__percent,
            "sendSize": self.__sendSize,
        })


# int转byte数组
def intToBytes(intNumber=0): return intNumber.to_bytes(4, "little")


# str转utf-8 byte数组
def strToUtf8Bytes(value): return bytes(value, encoding="utf-8")


if __name__ == '__main__':

    subprocess.call("adb shell am broadcast -a NotifyServiceStop", shell=True)
    subprocess.call("adb forward tcp:9999 tcp:9000", shell=True)
    subprocess.call("adb shell am broadcast -a NotifyServiceStart", shell=True)

    client = socket.socket()
    result = client.connect(("127.0.0.1", 9999))

    # 文件路径
    filePath = "/Users/apple/Downloads/base.db3"

    # 文件大小
    allSize = os.path.getsize(filePath)
    fileSize = copy.deepcopy(allSize)
    print("%s\n" % fileSize)
    defaultReadSize = 1024 * 10
    progress = 0

    h = time.time()
    # 读取 文件
    with open(filePath, mode="rb") as readFile:
        while True:
            if fileSize <= 0: break
            readSize = defaultReadSize if fileSize > defaultReadSize else fileSize
            progress += readSize
            percent = '{: .2f}'.format(progress * 1.0 / allSize * 100)
            print("进度:%s" % percent)
            # 读取内容
            readBytes = readFile.read(readSize)
            if not readBytes: break
            tagH = strToUtf8Bytes("@tag:h")
            tagF = strToUtf8Bytes("@tag:f")
            # 携带信息
            infoJson = SendInfoData(percent, readSize).toJson()
            print("json:%s\n" % infoJson)
            infoBytes = strToUtf8Bytes(infoJson)
            infoSize = intToBytes(len(infoBytes))
            # 包裹传输数据
            client.send(tagH)  # 标记开头
            client.send(infoSize)  # 携带参数byte数据长度
            client.send(infoBytes)  # 携带参数内容
            client.send(readBytes)  # 真实传输的内容
            client.send(tagF)  # 标记结尾
            fileSize -= readSize

    client.close()

    f = time.time()
    print("用时:{: .0f}s".format((f - h)))

  这个例子的界面比较简单,就一个按钮和一个文本。手上测试的文件是一个900多兆的文件,在单位的时候试过一个4个多G的sqlite文件。而且文件在传输后一样可以正常使用。说明这个方式还是可行的。(当然在实际项目中这个例子仅仅是证明这个方式可行而已,具体优化部分肯定不带少的)
Android使用usb线传输大文件笔记_第3张图片
Android使用usb线传输大文件笔记_第4张图片

笔记:byte[]未写满,补全的方式

  目的是验证,这里以读取文件的内容为例,文件是txt格式的这里放了一段字符串“一二三四五六七八九十”。一共是10字符(一个字符2个字节,也就是20个字节)。定义一个byte数组长度为20。先读一半,然后再读剩下的一半。

import java.io.File
import java.io.FileInputStream
import java.nio.charset.Charset

fun main() {

    val path = "/Users/apple/Downloads/O.txt"
    val file = File(path)
    val inputStream = FileInputStream(file)
    //available()这个方法本质的意义是 剩余未被读取的字节数量
    println("文件的字节总数:${inputStream.available()}")

    val byte = ByteArray(20)

    val read1 = inputStream.read(byte, 0, 10)
    println(read1)
    println("read1后剩余字节数量:${inputStream.available()}")
    val read1Msg = String(byte, 0, read1, Charset.forName("GBk"))
    println(read1Msg)

    inputStream.read(byte, 10, 20 - read1)
    println("read2后剩余字节数量:${inputStream.available()}")
    val read2Msg = String(byte, 0, byte.size, Charset.forName("GBk"))
    println(read2Msg)

}

  这块结合Android端那段代码看正好(一旦索引那想不明白,真不如运行代码来的实在),运行结果如下。
Android使用usb线传输大文件笔记_第5张图片

你可能感兴趣的:(Android学习笔记)