从网络下载文件本质上是通过网络协议(如 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
}