微信朋友圈图片上传接口的分片上传与断点续传 Java 实现

微信图片上传协议特征

微信客户端在上传朋友圈高清图时,采用分片上传 + 断点续传机制。其流程为:

  1. 预上传请求获取 upload_id
  2. 将文件按固定块(通常 256KB)切片,依次上传;
  3. 每次上传返回已接收的偏移量(offset),用于断点恢复;
  4. 所有分片完成后提交合并。

模拟此过程需精确处理二进制分片、偏移校验与重试逻辑。

分片上传上下文模型

定义上传任务状态,支持持久化以实现断点续传:

package wlkankan.cn.wx.upload;

import java.io.File;
import java.io.Serializable;

public class UploadContext implements Serializable {
    private String uploadId;
    private String mediaId;
    private File sourceFile;
    private long totalSize;
    private long uploadedOffset;
    private int chunkSize = 256 * 1024; // 256KB per chunk

    public UploadContext(File file) {
        this.sourceFile = file;
        this.totalSize = file.length();
        this.uploadedOffset = 0;
    }

    // getters and setters
    public long getRemaining() { return totalSize - uploadedOffset; }
    public boolean isCompleted() { return uploadedOffset >= totalSize; }
}

预上传接口调用

向微信服务器申请 upload_id

package wlkankan.cn.wx.client;

import wlkankan.cn.wx.upload.UploadContext;
import com.fasterxml.jackson.databind.JsonNode;
import okhttp3.*;

public class WeChatUploadClient {

    private final OkHttpClient httpClient = new OkHttpClient();
    private static final String PRE_UPLOAD_URL = "https://mmsns.qpic.cn/mmsns_upload";

    public String preUpload(UploadContext ctx) throws Exception {
        RequestBody body = new FormBody.Builder()
            .add("file_size", String.valueOf(ctx.getTotalSize()))
            .add("file_type", "image/jpeg")
            .build();

        Request request = new Request.Builder()
            .url(PRE_UPLOAD_URL)
            .post(body)
            .build();

        try (Response resp = httpClient.newCall(request).execute()) {
            if (!resp.isSuccessful()) throw new RuntimeException("Pre-upload failed");
            JsonNode json = objectMapper.readTree(resp.body().string());
            return json.get("upload_id").asText();
        }
    }
}

在这里插入图片描述

分片上传核心逻辑

逐块读取文件并上传,记录成功偏移:

public class ChunkUploader {

    private static final String CHUNK_UPLOAD_URL = "https://mmsns.qpic.cn/mmsns_chunk";

    public void uploadChunk(UploadContext ctx, WeChatUploadClient client) throws Exception {
        RandomAccessFile raf = new RandomAccessFile(ctx.getSourceFile(), "r");
        raf.seek(ctx.getUploadedOffset());

        byte[] buffer = new byte[Math.min(ctx.getChunkSize(), (int) ctx.getRemaining())];
        int read = raf.read(buffer);
        if (read <= 0) {
            raf.close();
            return;
        }

        // 构造 multipart/form-data 分片
        RequestBody requestBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("upload_id", ctx.getUploadId())
            .addFormDataPart("offset", String.valueOf(ctx.getUploadedOffset()))
            .addFormDataPart("chunk", "chunk.dat",
                RequestBody.create(buffer, MediaType.get("application/octet-stream")))
            .build();

        Request request = new Request.Builder()
            .url(CHUNK_UPLOAD_URL)
            .post(requestBody)
            .build();

        try (Response resp = client.getHttpClient().newCall(request).execute()) {
            if (resp.isSuccessful()) {
                JsonNode json = objectMapper.readTree(resp.body().string());
                long confirmedOffset = json.get("offset").asLong();
                // 微信返回的 offset 应等于 ctx.uploadedOffset + read
                if (confirmedOffset == ctx.getUploadedOffset() + read) {
                    ctx.setUploadedOffset(confirmedOffset);
                    persistContext(ctx); // 持久化进度
                } else {
                    throw new IllegalStateException("Offset mismatch");
                }
            } else {
                throw new RuntimeException("Chunk upload failed: " + resp.code());
            }
        } finally {
            raf.close();
        }
    }

    private void persistContext(UploadContext ctx) {
        // 可序列化到本地文件或数据库,例如:
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(ctx.getSourceFile().getPath() + ".upload"))) {
            oos.writeObject(ctx);
        } catch (Exception e) {
            // log error
        }
    }
}

断点续传恢复机制

从本地加载未完成的上传上下文:

public class UploadResumeManager {

    public UploadContext loadOrCreate(File imageFile) {
        File metaFile = new File(imageFile.getPath() + ".upload");
        if (metaFile.exists()) {
            try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(metaFile))) {
                UploadContext ctx = (UploadContext) ois.readObject();
                if (ctx.getTotalSize() == imageFile.length() && !ctx.isCompleted()) {
                    return ctx; // 继续上传
                } else {
                    metaFile.delete(); // 文件变更或已完成,清理
                }
            } catch (Exception ignored) {}
        }
        return new UploadContext(imageFile); // 全新上传
    }
}

完整上传流程编排

整合预上传、分片循环与提交:

public class TimelineImageUploader {

    public String uploadImage(File image) throws Exception {
        UploadResumeManager resumeMgr = new UploadResumeManager();
        UploadContext ctx = resumeMgr.loadOrCreate(image);

        WeChatUploadClient client = new WeChatUploadClient();
        if (ctx.getUploadId() == null) {
            ctx.setUploadId(client.preUpload(ctx));
        }

        ChunkUploader chunkUploader = new ChunkUploader();
        while (!ctx.isCompleted()) {
            try {
                chunkUploader.uploadChunk(ctx, client);
            } catch (Exception e) {
                // 可加入指数退避重试
                Thread.sleep(1000);
                continue; // 下次循环重试当前块
            }
        }

        // 提交并获取 mediaId
        return commitUpload(ctx, client);
    }

    private String commitUpload(UploadContext ctx, WeChatUploadClient client) throws Exception {
        // 调用微信 commit 接口(伪代码)
        RequestBody body = new FormBody.Builder()
            .add("upload_id", ctx.getUploadId())
            .build();
        // ... 发送请求并解析 mediaId
        return "mock_media_id_123";
    }
}

通过分片读取、偏移确认、上下文持久化三重机制,该实现在网络中断或进程崩溃后可无缝恢复上传,精准复刻微信官方客户端的上传行为。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐