You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

大文件下载时客户端连接断开问题解决(Java/Jersey/HTTP GET)

解决Jersey大文件下载的ClientAbortException问题(无需修改客户端)

这问题我之前帮团队解决过几乎一样的情况——核心就是用**HTTP标准的范围请求(Range Requests)**来拆分大文件传输,这也是Dropbox这类服务能绕过大文件网络限制的关键,同时完全兼容现有客户端,还能保留你的会话认证逻辑。

问题根源分析

你遇到的ClientAbortException本质是防火墙/代理/NAT在传输超过阈值的连续数据流时主动断开连接。默认的一次性全量文件传输会触发这个阈值,而范围请求可以把大文件拆成多个小片段(比如每个1GB)分别发送,每个片段都在阈值内,客户端会自动拼接这些片段完成下载。

具体解决方案(无需引入新框架)

1. 修改资源方法,支持Range请求

首先要在你的download方法中处理Range请求头,返回206 Partial Content状态码和对应的响应头,同时保留原有的会话认证逻辑:

@GET
@Path("download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response download(@HeaderParam("x-session-token") String sSessionId, 
                         @QueryParam("filename") String sFileName,
                         @HeaderParam("Range") String rangeHeader) {
    // 会话认证逻辑保持不变
    User oUser = MyLib.GetUserFromSession(sSessionId);
    if (oUser == null) {
        return Response.status(Status.UNAUTHORIZED).build();
    }

    File oFile = getEntryFile(sFileName);
    if (oFile == null) {
        return Response.serverError().build();
    }

    long fileLength = oFile.length();
    ResponseBuilder responseBuilder;

    // 处理Range请求
    if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
        try {
            // 解析Range头,格式:bytes=start-end
            String[] rangeParts = rangeHeader.substring(6).split("-");
            long start = Long.parseLong(rangeParts[0]);
            long end = rangeParts.length > 1 ? Long.parseLong(rangeParts[1]) : fileLength - 1;

            // 校验范围合法性
            if (start >= fileLength) {
                // 请求范围超出文件大小,返回416
                return Response.status(Status.REQUESTED_RANGE_NOT_SATISFIABLE)
                        .header("Content-Range", "bytes */" + fileLength)
                        .build();
            }
            end = Math.min(end, fileLength - 1);
            long contentLength = end - start + 1;

            // 返回部分内容
            FileStreamingOutput stream = new FileStreamingOutput(oFile, start, end);
            responseBuilder = Response.status(Status.PARTIAL_CONTENT)
                    .entity(stream)
                    .header("Content-Length", contentLength)
                    .header("Content-Range", "bytes " + start + "-" + end + "/" + fileLength)
                    .header("Accept-Ranges", "bytes");
        } catch (NumberFormatException e) {
            // Range格式错误,返回完整文件
            responseBuilder = getFullFileResponse(oFile);
        }
    } else {
        // 没有Range请求,返回完整文件
        responseBuilder = getFullFileResponse(oFile);
    }

    // 保留下载文件名头
    responseBuilder.header("Content-Disposition", "attachment; filename=" + oFile.getName());
    return responseBuilder.build();
}

// 提取完整文件响应的复用方法
private ResponseBuilder getFullFileResponse(File file) {
    FileStreamingOutput stream = new FileStreamingOutput(file, 0, file.length() - 1);
    return Response.ok(stream)
            .header("Content-Length", file.length())
            .header("Accept-Ranges", "bytes");
}

2. 修改FileStreamingOutput,支持范围写入

更新你的FileStreamingOutput类,添加起始和结束位置参数,只传输指定范围的文件内容,同时不要手动关闭容器的OutputStream(原来的finally里关闭oOutputStream是错误的,会导致Tomcat抛出Broken Pipe异常):

public class FileStreamingOutput implements StreamingOutput {
    final File m_oFile;
    final long m_start;
    final long m_end;

    public FileStreamingOutput(File oFile) {
        this(oFile, 0, oFile.length() - 1);
    }

    public FileStreamingOutput(File oFile, long start, long end) {
        if (null == oFile) {
            throw new NullPointerException("FileStreamingOutput: passed a null File");
        }
        m_oFile = oFile;
        m_start = start;
        m_end = end;
    }

    @Override
    public void write(OutputStream oOutputStream) throws IOException, WebApplicationException {
        if (null == oOutputStream) {
            throw new NullPointerException("FileStreamingOutput.write: passed a null OutputStream");
        }
        try (InputStream oInputStream = new FileInputStream(m_oFile)) {
            // 跳过起始位置之前的字节
            IOUtils.skipFully(oInputStream, m_start);
            // 复制指定范围的内容(end - start + 1 字节)
            long bytesToCopy = m_end - m_start + 1;
            IOUtils.copyLarge(oInputStream, oOutputStream, 0, bytesToCopy);
            oOutputStream.flush();
        } catch (Exception e) {
            // 这里可以根据需要添加日志,不要直接打印栈轨迹到控制台
            e.printStackTrace();
        }
        // 不要关闭oOutputStream!这是Tomcat管理的容器输出流,手动关闭会导致异常
    }
}

3. 调整Tomcat配置,确保大文件传输不受限

打开Tomcat的conf/server.xml,找到你的Connector节点,添加/修改以下参数:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           maxSwallowSize="-1" />
  • maxSwallowSize="-1":允许Tomcat接收/发送无限大小的请求体/响应体,默认值可能会限制大文件传输。

为什么这能解决问题?

  1. 范围请求拆分传输:每个请求只传输文件的一小部分(比如1GB),不会触发防火墙/代理的阈值,客户端会自动发起后续范围请求直到文件下载完成。
  2. 完全兼容标准HTTP:所有现代浏览器和HTTP客户端都支持Range请求,不需要修改任何客户端代码。
  3. 保留会话认证:每个下载请求都会带上x-session-token头,你的认证逻辑会正常验证每个请求的合法性。
  4. 修正流管理错误:不再手动关闭容器输出流,避免不必要的Broken Pipe异常。

内容的提问来源于stack exchange,提问作者kr1zz

火山引擎 最新活动