凌峰创科服务平台

Android文件上传服务器,如何实现高效稳定?

目录

  1. 核心概念:上传是如何工作的?
  2. 准备工作:服务器端
  3. 使用 HttpURLConnection (原生 API)
    • 代码示例
    • 优缺点分析
  4. 使用 OkHttp (现代网络库)
    • 代码示例
    • 优缺点分析
  5. 使用 Retrofit + OkHttp (强烈推荐)
    • 什么是 Retrofit?
    • 项目配置
    • 定义 API 接口
    • 创建 Retrofit 实例
    • 上传实现
    • 优缺点分析
  6. 进阶与最佳实践
    • 上传进度监听
    • 取消上传请求
    • 处理大文件与 Multipart
    • 权限申请 (Android 6.0+)
    • 错误处理
  7. 如何选择?

核心概念:上传是如何工作的?

Android 客户端上传文件,本质上是 HTTP POST 请求的一种,最常见的方式是 multipart/form-data

Android文件上传服务器,如何实现高效稳定?-图1
(图片来源网络,侵删)
  • multipart/form-data: 这是一种 MIME 类型,允许你在一个 HTTP 请求中发送多种类型的数据(比如文本字段和文件),它会将表单拆分成多个“部分”(part),每个部分都包含自己的内容类型(Content-Type)和内容(文件内容或文本)。
  • 请求流程:
    1. Android 客户端构建一个 POST 请求。
    2. 设置请求头 Content-Typemultipart/form-data,并附带一个 boundary(边界字符串)用于分隔不同的数据部分。
    3. 、文件名、文件类型等信息打包成一个或多个 Part
    4. 将所有 Part 拼接在一起,通过请求体(Request Body)发送给服务器。
    5. 服务器端解析这个 multipart 请求体,提取出文件并保存。

准备工作:服务器端

在开始写 Android 代码之前,你需要一个可以接收文件上传的服务器端,你可以使用任何后端语言(如 Node.js, Python, Java, PHP)来实现。

这里以一个非常简单的 Node.js (Express) 示例为例,它会将上传的文件保存在服务器的 uploads 目录下。

安装依赖:

npm install express multer

服务器端代码 (server.js):

Android文件上传服务器,如何实现高效稳定?-图2
(图片来源网络,侵删)
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
const port = 3000;
// 确保上传目录存在
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
    fs.mkdirSync(uploadDir);
}
// 配置 multer 存储
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, 'uploads/'); // 文件保存目录
    },
    filename: function (req, file, cb) {
        // 使用原始文件名,也可以自定义
        cb(null, file.originalname);
    }
});
const upload = multer({ storage: storage });
// 文件上传接口
// 'file' 是前端表单中 file input 的 name 属性
app.post('/upload', upload.single('file'), (req, res) => {
    if (!req.file) {
        return res.status(400).send('No file uploaded.');
    }
    console.log('File uploaded:', req.file);
    res.status(200).send('File uploaded successfully!');
});
app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
});

运行这个服务器,它将在 http://localhost:3000 上监听,并等待 /upload 请求。


方案一:使用 HttpURLConnection (原生 API)

这是最基础的方式,不需要引入第三方库,但代码相对繁琐。

代码示例

import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "FileUpload";
    private static final String SERVER_URL = "http://10.0.2.2:3000/upload"; // Android 模拟器访问 localhost
    // private static final String SERVER_URL = "http://your_real_server_ip:3000/upload"; // 真机访问
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 模拟一个要上传的文件,通常来自用户选择
        File file = new File(getExternalFilesDir(null), "test_image.jpg");
        // 注意:这里需要先有一个文件,你可以把一张图片放到手机模拟器的对应目录
        // 或者从其他地方复制过来
        uploadFile(file);
    }
    public void uploadFile(File file) {
        if (!file.exists()) {
            Log.e(TAG, "File does not exist: " + file.getAbsolutePath());
            return;
        }
        String boundary = "*****" + Long.toString(System.currentTimeMillis()) + "*****";
        String lineEnd = "\r\n";
        String twoHyphens = "--";
        try {
            URL url = new URL(SERVER_URL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setDoInput(true);
            conn.setDoOutput(true);
            conn.setUseCaches(false);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Connection", "Keep-Alive");
            conn.setRequestProperty("ENCTYPE", "multipart/form-data");
            conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
            conn.setRequestProperty("file", file.getName()); // 'file' 必须和服务器端 multer 的 field name 一致
            DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
            // 添加文件部分
            dos.writeBytes(twoHyphens + boundary + lineEnd);
            dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"" + lineEnd);
            dos.writeBytes("Content-Type: image/jpeg" + lineEnd); // 根据文件类型设置
            dos.writeBytes(lineEnd);
            // 读取文件并发送
            FileInputStream fis = new FileInputStream(file);
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                dos.write(buffer, 0, bytesRead);
            }
            fis.close();
            dos.writeBytes(lineEnd);
            dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
            // 获取服务器响应
            int responseCode = conn.getResponseCode();
            Log.d(TAG, "HTTP Response code: " + responseCode);
            if (responseCode == HttpURLConnection.HTTP_OK) {
                Log.d(TAG, "File uploaded successfully");
            } else {
                Log.e(TAG, "File upload failed. Response code: " + responseCode);
            }
            dos.flush();
            dos.close();
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "Error uploading file: " + e.getMessage());
        }
    }
}

优缺点分析

  • 优点:
    • 无需任何第三方依赖。
    • 适合理解 HTTP 协议底层工作原理。
  • 缺点:
    • 代码冗长且易错: 手动拼接 multipart 请求体非常繁琐,容易出错。
    • 功能有限: 难以处理进度监听、取消请求、并发等复杂场景。
    • 性能一般: 默认配置下性能不如 OkHttp。

方案二:使用 OkHttp (现代网络库)

OkHttp 是目前 Android 上最流行的网络库之一,它更高效、更易用。

代码示例

import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class OkHttpUploadActivity extends AppCompatActivity {
    private static final String TAG = "OkHttpUpload";
    private static final String SERVER_URL = "http://10.0.2.2:3000/upload";
    private OkHttpClient client;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        client = new OkHttpClient();
        File file = new File(getExternalFilesDir(null), "test_image.jpg");
        uploadFileWithOkHttp(file);
    }
    public void uploadFileWithOkHttp(File file) {
        // 1. 创建 MultipartBody.Part
        // "file" 是表单字段名,必须和服务器端一致
        RequestBody fileBody = RequestBody.create(file, MediaType.parse("image/jpeg"));
        MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), fileBody);
        // 2. (可选) 添加其他文本字段
        RequestBody descriptionBody = RequestBody.create("This is a test image", MediaType.parse("text/plain"));
        // 3. 构建请求
        Request request = new Request.Builder()
                .url(SERVER_URL)
                .post(new MultipartBody.Builder()
                        .setType(MultipartBody.FORM)
                        .addPart(filePart)
                        .addFormDataPart("description", "This is a test image") // 添加文本字段
                        .build())
                .build();
        // 4. 异步执行请求
        client.newCall(request).enqueue(new okhttp3.Callback() {
            @Override
            public void onFailure(okhttp3.Call call, IOException e) {
                Log.e(TAG, "Upload failed: " + e.getMessage());
                runOnUiThread(() -> {
                    // 在主线程更新UI
                });
            }
            @Override
            public void onResponse(okhttp3.Call call, Response response) throws IOException {
                if (response.isSuccessful()) {
                    Log.d(TAG, "Upload successful: " + response.body().string());
                } else {
                    Log.e(TAG, "Upload failed with code: " + response.code());
                }
                response.body().close();
            }
        });
    }
}

优缺点分析

  • 优点:
    • API 简洁: 使用 MultipartBody.Builder 可以轻松构建 multipart 请求。
    • 性能卓越: 支持连接池、GZIP 压缩等,性能很好。
    • 支持异步/同步: enqueue() 用于异步,execute() 用于同步。
    • 功能强大: 内置拦截器、缓存等机制。
  • 缺点:
    • 需要添加依赖。
    • 对于复杂的 API 管理和类型安全,不如 Retrofit。

方案三:使用 Retrofit + OkHttp (强烈推荐)

Retrofit 不是一个网络请求库,而是一个 RESTful API 的类型安全 HTTP 客户端,它将网络请求接口化,让你用类似调用方法的方式发起网络请求,它底层默认使用 OkHttp。

Android文件上传服务器,如何实现高效稳定?-图3
(图片来源网络,侵删)

项目配置 (build.gradle)

dependencies {
    // Retrofit & OkHttp
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // 用于解析 JSON 响应
    implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' // 用于打印日志
}

定义 API 接口

创建一个接口,定义你的上传方法。

import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.Part;
public interface FileUploadService {
    // @Multipart 表示这是一个 multipart/form-data 请求
    // @POST 指定请求路径
    // @Part("file") 中的 "file" 是表单字段名,必须和服务器端一致
    @Multipart
    @POST("/upload")
    Call<ResponseBody> uploadFile(@Part MultipartBody.Part file, @Part("description") RequestBody description);
}

创建 Retrofit 实例

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.scalars.ScalarsConverterFactory;
import java.util.concurrent.TimeUnit;
public class RetrofitClient {
    private static Retrofit retrofit = null;
    public static Retrofit getClient(String baseUrl) {
        if (retrofit == null) {
            // 创建 OkHttp 客户端
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
            logging.setLevel(HttpLoggingInterceptor.Level.BODY); // 打印详细日志
            OkHttpClient client = new OkHttpClient.Builder()
                    .addInterceptor(logging)
                    .connectTimeout(30, TimeUnit.SECONDS)
                    .writeTimeout(30, TimeUnit.SECONDS)
                    .readTimeout(30, TimeUnit.SECONDS)
                    .build();
            // 创建 Retrofit 实例
            retrofit = new Retrofit.Builder()
                    .baseUrl(baseUrl)
                    .client(client)
                    // ScalarsConverterFactory 用于处理纯文本响应
                    .addConverterFactory(ScalarsConverterFactory.create()) 
                    // 如果服务器返回 JSON,使用 GsonConverterFactory
                    // .addConverterFactory(GsonConverterFactory.create()) 
                    .build();
        }
        return retrofit;
    }
}

上传实现

import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
public class RetrofitUploadActivity extends AppCompatActivity {
    private static final String TAG = "RetrofitUpload";
    private static final String SERVER_URL = "http://10.0.2.2:3000/";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        File file = new File(getExternalFilesDir(null), "test_image.jpg");
        Retrofit retrofit = RetrofitClient.getClient(SERVER_URL);
        FileUploadService service = retrofit.create(FileUploadService.class);
        // 1. 准备文件 Part
        RequestBody fileBody = RequestBody.create(file, MediaType.parse("image/jpeg"));
        MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), fileBody);
        // 2. 准备文本字段 Part
        RequestBody description = RequestBody.create("This is a description", MediaType.parse("text/plain"));
        // 3. 发起请求
        Call<String> call = service.uploadFile(filePart, description);
        call.enqueue(new Callback<String>() {
            @Override
            public void onResponse(Call<String> call, Response<String> response) {
                if (response.isSuccessful()) {
                    Log.d(TAG, "Upload successful: " + response.body());
                } else {
                    Log.e(TAG, "Upload failed with code: " + response.code());
                }
            }
            @Override
            public void onFailure(Call<String> call, Throwable t) {
                Log.e(TAG, "Upload failed: " + t.getMessage());
            }
        });
    }
}

优缺点分析

  • 优点:
    • 类型安全: 将 API 定义为 Java 接口,编译时检查。
    • 代码简洁: 将网络请求与业务逻辑分离,代码结构清晰。
    • 易于维护: 修改 API 只需修改接口定义。
    • 功能强大: 支持同步/异步、RxJava、Kotlin 协程等。
    • 生态系统完善: 拥有大量转换器(Gson, Jackson, Protobuf 等)和拦截器。
  • 缺点:
    • 引入的依赖较多。
    • 有一定的学习成本,需要理解接口注解和 Retrofit 的工作方式。

进阶与最佳实践

上传进度监听

Retrofit 本身不直接支持进度监听,但可以结合 OkHttp 的 Interceptor 来实现。

  1. 创建进度回调接口

    public interface ProgressListener {
        void onProgress(long bytesWritten, long contentLength);
    }
  2. 创建 ProgressRequestBody

    public class ProgressRequestBody extends RequestBody {
        // ... 实现细节,参考 OkHttp 官方示例或第三方库如 "com.github.ihsanbal:LoggingInterceptor"
        // 这个类会包装原始的 RequestBody,并在写入数据时回调 ProgressListener
    }
  3. 在 Retrofit 接口中使用

    @Multipart
    @POST("/upload")
    Call<ResponseBody> uploadFile(@Part MultipartBody.Part file, @Part("description") RequestBody description);
    // 将文件 Part 包装成 ProgressRequestBody

取消上传请求

非常简单,只需要保存 Call 对象,然后在需要时调用 call.cancel()

private Call<String> uploadCall;
// ...
uploadCall = service.uploadFile(filePart, description);
uploadCall.enqueue(...);
// 在 Activity/Fragment 的 onDestroy 或取消按钮点击事件中
@Override
protected void onDestroy() {
    super.onDestroy();
    if (uploadCall != null && !uploadCall.isCanceled()) {
        uploadCall.cancel();
    }
}

处理大文件与 Multipart

Retrofit/OkHttp 的 MultipartBody 内部使用流式处理,不会一次性将整个文件加载到内存中,因此非常适合大文件上传,服务器端也必须配置好以接收大文件(Nginx 的 client_max_body_size)。

权限申请 (Android 6.0+)

如果要从外部存储(如 Environment.getExternalStorageDirectory())读取文件,必须在 AndroidManifest.xml 中声明权限,并在运行时动态请求。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

错误处理

使用 try-catch 处理本地 IO 异常,在 CallbackonFailure 中处理网络异常和服务器返回的错误状态码,对于服务器返回的错误,可以定义一个统一的错误处理机制。


如何选择?

方案 适用场景 推荐指数
HttpURLConnection 学习目的、极简应用、对依赖有严格要求的项目 ★☆☆☆☆
OkHttp 需要直接进行网络操作,但不希望代码过于冗长,适用于需要拦截器、自定义客户端等中高级功能的场景。 ★★★☆☆
Retrofit + OkHttp 绝大多数 App 的首选,特别是当你的 App 有多个 API 接口时,它能提供最好的代码结构、可维护性和类型安全。 ★★★★★

最终建议:

对于任何新的 Android 项目,直接从 Retrofit + OkHttp 开始,它能为你节省大量的开发时间,并构建出更健壮、更易于维护的网络层,只有在你确定只需要一个简单的、一次性的上传功能,并且不想引入任何额外依赖时,才考虑使用 HttpURLConnection

分享:
扫描分享到社交APP
上一篇
下一篇