从网络下载文件分析与应用

5 阅读3分钟

从网络下载文件本质上是通过网络协议(如 HTTP、FTP 等)与远程服务器进行通信,向服务器发送请求以获取指定文件的数据,然后把这些数据存储到本地文件系统。

以 HTTP 协议为例,其原理主要涉及以下几个方面:
请求 - 响应模型:客户端(下载程序)向服务器发送 HTTP 请求,服务器接收请求后进行处理,再返回 HTTP 响应。请求中包含了要下载文件的地址(URL),响应中则包含文件的数据。
数据传输:服务器将文件数据以二进制流的形式发送给客户端,客户端接收这些数据并逐步写入本地文件。
文件操作:客户端在本地创建一个文件,把接收到的数据依次写入该文件,直至文件下载完成。

从网络下载文件的一般流程:
解析 URL:
对要下载文件的 URL 进行解析,从中提取出服务器地址、端口号、文件路径等信息。例如,对于 URL example.com/path/to/fil… example.com,端口号默认为 80,文件路径为 /path/to/file.txt。
建立网络连接:
利用解析得到的服务器地址和端口号,客户端与服务器建立网络连接。以 HTTP 协议为例,通常使用 TCP 协议建立连接。
发送请求:
客户端向服务器发送 HTTP 请求,请求中包含请求方法(如 GET)、请求的 URL、请求头信息等。
接收响应:
服务器接收到请求后进行处理,然后返回 HTTP 响应。响应包含状态码、响应头信息和响应体。状态码用于表示请求的处理结果(如 200 OK 表示请求成功),响应体包含要下载的文件数据。
检查响应状态:
客户端接收到响应后,首先检查状态码。若状态码为 200 OK,则表示请求成功,可以开始下载文件;若状态码为其他值(如 404 Not Found),则表示请求失败,需要进行相应处理。
创建本地文件:
在本地文件系统中创建一个文件,用于存储下载的数据。可以指定文件的路径和文件名。
下载文件数据:
从响应体中读取文件数据,并将其写入本地文件。通常采用循环的方式,每次读取一定量的数据(如 4KB),然后写入文件,直到所有数据都被读取并写入文件。
关闭连接和文件:
文件下载完成后,关闭与服务器的网络连接和本地文件。

fun downloadFileFromUrl() {
        Log.v(TAG, "downloadFileFromUrl...")
        val upgradeOtaUrl = "http://ipAddress:port/eric/file/app-eric-release.apk"
        if (upgradeOtaUrl.isBlank()) {
            Log.e(TAG, "Failed to extract real URL.")
            return
        }
        Log.d(TAG, "downloadFileFromUrl...realUrl: $upgradeOtaUrl")

        // 检查是否有外部存储写入权限
        if (!isExternalStorageWritable()) {
            Log.e(TAG, "External storage is not writable.")
            return
        }

        // 创建文件对象,创建一个 File 对象 otaFile
        // 此文件位于应用的外部存储目录下,文件名为 app-eric-ota.apk。
        val otaFile = File(context.getExternalFilesDir(null), "app-eric-ota.apk")
        // 输出详细级别的日志,显示文件的路径。
        Log.v(TAG, "downloadFileFromUrl...otaFile: $otaFile")

        // 在 IO 线程中启动协程进行文件下载
        // 创建一个 CoroutineScope,并指定在 Dispatchers.IO 线程中启动一个协程,目的是执行文件下载操作。
        CoroutineScope(Dispatchers.IO).launch {
            try {
                // 创建一个 OkHttpClient 实例,用于发送 HTTP 请求。
                val client = OkHttpClient()
                // 接着构建一个 Request 对象,将请求的 URL 设置为 upgradeOtaUrl。
                val request = Request.Builder()
                    .url(upgradeOtaUrl)
                    .build()
                // 发送 HTTP 请求并获取响应。
                val response = client.newCall(request).execute()
                Log.v(TAG, "downloadFileFromUrl...response: $response")
                // 检查响应是否成功
                if (response.isSuccessful) {
                    Log.v(TAG, "downloadFileFromUrl...response is successful")
                    // 获取响应体的输入流,若响应体为空则抛出 IOException。
                    val inputStream = response.body?.byteStream()
                        ?: throw IOException("Response body is null.")
                    // 创建一个 FileOutputStream 用于将数据写入 otaFile。
                    val fileOutputStream = FileOutputStream(otaFile)

                    // 使用 try-with-resources 自动关闭流
                    // 利用 use 函数确保输入流和输出流在使用完毕后自动关闭。
                    // 将输入流中的数据copyTo复制到输出流,也就是将下载的文件写入 otaFile。
                    inputStream.use {
                        input ->
                        fileOutputStream.use {
                            output ->
                            input.copyTo(output)
                        }
                    }
                    Log.v(TAG, "File downloaded successfully to $otaFile")
                } else {
                    Log.v(TAG, "downloadFileFromUrl...response is not successful")
                    Log.v(TAG, "Response code: ${response.code}")
                }
            } catch (e: Exception) {
                Log.e(TAG, "downloadFileFromUrl exception... ${e.message}", e)
            }
        }
    }

    // 提取真实 URL 的辅助函数
    private fun extractRealUrl(otaUrl: String): String {
        // 从传入的 URL 中提取真实的 URL。它把 URL 按 = 分割成多个部分,若分割后的部分数量不少于 2 个,则返回第二个部分;否则返回空字符串。
        val realUrlParts = otaUrl.split("=")
        return if (realUrlParts.size >= 2) realUrlParts[1] else ""
    }

    // 检查外部存储是否可写的辅助函数
    private fun isExternalStorageWritable(): Boolean {
        // 获取外部存储的状态,若状态为 Environment.MEDIA_MOUNTED,则表明外部存储可写,返回 true;否则返回 false。
        val state = Environment.getExternalStorageState()
        return Environment.MEDIA_MOUNTED == state
    }