返回

Java HttpsUrlConnection断点续传:解决文件上传中断难题

java

Java HttpsUrlConnection 文件上传中断恢复难题

使用 HttpsUrlConnection 上传大文件,特别是当网络环境不稳定时,是一个常见的挑战。开发者经常期望在网络中断后能够恢复上传,避免因一次连接失败就必须重新传输整个文件。默认情况下,HttpsUrlConnection可能会缓冲全部数据,造成内存溢出。即使采用 setFixedLengthStreamingMode() 方法解决内存问题,上传过程中的网络中断却依然会导致 "broken pipe" 异常,导致上传失败,不能继续。这里,我们将分析这一问题,并探讨一些有效的解决策略。

问题分析:setFixedLengthStreamingMode() 与中断恢复的冲突

setFixedLengthStreamingMode() 旨在减少内存占用。它通过设置预定义的 Content-Length,允许 HttpsUrlConnection 在上传过程中分块发送数据。但这种模式会带来一个问题:服务器通常假设上传的数据长度与 Content-Length 头部指定的一致,并在连接中断时会重置连接状态。一旦连接中断后重新尝试上传,客户端已经发送的一部分数据无法被服务端正确识别,从而触发“broken pipe” 错误。服务器不保存之前传输的内容,只识别 Content-Length 规定的总量。

简单地说,采用 setFixedLengthStreamingMode() 后,虽然客户端数据以分块方式发送,服务端仍然将本次上传视为一次独立的、完整的请求。因此断线后重连不会被视为同一上传的延续,服务器拒绝这种分片上传模式,造成中断后无法恢复。

解决方案: Range Header 实现断点续传

核心在于让服务端理解“上传分片”这一概念,使用 Range Header 是最常见的策略。此 HTTP 头可以指明客户端要传输的范围,告诉服务端数据应该追加到之前已经上传的部分。

实现步骤:

  1. 记录已上传字节数: 在客户端记录已成功发送的字节数。上传时先读取并比对上次发送量。
  2. 设置 Range Header: 构造一个 Range 头,其格式如 bytes=start-end 。 例如,如果要继续从第 1000 个字节开始上传,则 header 可以设置为 Range: bytes=1000-
  3. 处理服务端返回状态码:
    • 206 Partial Content : 表示服务器支持断点续传并接收了部分上传。
    • 416 Range Not Satisfiable : 表明请求的范围超出服务端接受的范围。这种情况表示文件上传完成或服务端需要你重新上传整个文件。
  4. 循环读取与上传 : 读取未上传的部分,并继续通过 HttpsUrlConnection 发送。直到传输完成或遭遇不可恢复错误。
  5. 注意重连: 如果发送过程中发生网络异常,客户端应该重新创建 HttpsUrlConnection 对象,按照步骤 1~4 执行上传过程。每次重连,需重新获取需要上传的偏移量并设置Range 头。
  6. 设置Content-Length : 不使用 setFixedLengthStreamingMode 方法,使用实际读取数据块的长度设定 connection.setRequestProperty("Content-Length", String.valueOf(chunkSize));

代码示例:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
public class ResumableUpload {

  private static final int CHUNK_SIZE = 4096; // 4KB
  private static final Path filePath = Paths.get("path_to_your_file.dat"); // 请替换为你自己的文件路径

    public static void upload() {
        try {
           long uploadedBytes = 0; // 保存已上传的字节
            HttpURLConnection connection;
           while (uploadedBytes < Files.size(filePath)) {
            
            URL url = new URL("YOUR_UPLOAD_URL");
              connection = (HttpURLConnection) url.openConnection();
              connection.setRequestMethod("PUT");
              connection.setDoOutput(true); // 输出

            
             long fileSize = Files.size(filePath);
               if (uploadedBytes > 0) {
                  // 设置 Range 头,断点续传
                   connection.setRequestProperty("Range", "bytes=" + uploadedBytes + "-"+ fileSize );
                }


              // 发送数据
                try(InputStream fileInputStream = Files.newInputStream(filePath);
                   OutputStream outputStream = connection.getOutputStream();
                   ) {

                  fileInputStream.skip(uploadedBytes); //跳过已上传部分
                  byte[] buffer = new byte[CHUNK_SIZE];
                   int bytesRead;
                   while ((bytesRead = fileInputStream.read(buffer)) != -1) {
                         outputStream.write(buffer,0, bytesRead);
                           uploadedBytes += bytesRead;
                      }


                int responseCode = connection.getResponseCode();
               if (responseCode == HttpURLConnection.HTTP_OK ||  responseCode ==  206)
                    System.out.println("Upload completed successfully or continue");
               else{

                     System.err.println("Server returned " + responseCode + ", abort uploading"  );

                   }

               }  catch (IOException ex){

                     System.out.println("Failed to upload chunk because :"+ ex.getMessage());
                        
              }  finally {

                      if (Objects.nonNull(connection))
                              connection.disconnect();

                  }

         }
          
             System.out.println("file uploaded sucessfully");

        }  catch (Exception ex){
           ex.printStackTrace();
       }
    }


    public static void main(String[] args) {

      upload();

   }

}

操作步骤:

  1. 将以上代码中的 YOUR_UPLOAD_URL 替换为你上传目标的 URL,path_to_your_file.dat 替换为你的本地文件路径。
  2. 编译并运行Java程序。上传将开始并支持中断恢复。程序将根据实际的上传进度动态调整 Range Header,并在每次连接恢复后继续上传剩余的数据。

注意事项与安全建议:

  • 服务端支持: 确保你的服务器支持 Range 请求。多数云存储服务 (例如 AWS S3) 默认支持。
  • 数据完整性: 在断点恢复中需要仔细处理服务端返回的状态码,如 206 (Partial Content) 416 (Requested Range Not Satisfiable), 以保证数据正确。可以使用 SHA 或者其他校验和机制进行数据验证。
  • 错误处理: 代码示例没有完整错误处理,需要在真实生产环境下处理网络错误以及上传过程的其他潜在问题。
  • 超时设置: 对于不稳定的网络环境,应该合理配置超时时间 setConnectTimeout setReadTimeout 方法,以避免程序卡住等待响应。
  • 缓存: 确保应用程序不缓冲上传内容到内存。而是从文件流读取并按块发送,保证不会产生 OOM 问题。

通过以上步骤,HttpsUrlConnection 也能有效地实现大文件上传中的中断恢复。 采用 Range header 替代直接依赖 setFixedLengthStreamingMode(), 可以避免"broken pipe"问题并能上传大型文件。使用标准 HTTP 技术来实现分片上传在可扩展性与稳定性方面具备优势。