web应用(Nginx,Apache)使用 x-sendfile 下载文件

本文介绍了X-Sendfile机制,一种将文件下载请求由后端应用转交给前端web服务器处理的方法,能有效提高服务器效率,尤其适用于大文件下载场景。文章详细解释了X-Sendfile的工作原理,并提供了在Nginx中的具体配置示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

很多时候用户需要从网站下载文件,如果文件是可以通过一个固定链接公开获取的,那么我们只需将文件存放到 webroot下的目录里就好。但大多数情况下,我们需要做权限控制,例如下载 PDF 账单,又例如下载网盘里的档案。这时,我们通常借助于脚本代码来实现,而这无疑会增加服务器的负担。

例如下面的代码:

<?php
   // 用户身份认证,若验证失败跳转
   authenticate();
   // 获取需要下载的文件,若文件不存在跳转
   $file = determine_file();
   // 读取文件内容
   $content = file_get_contents ( $file );
   // 发送合适的 HTTP 头
   header( "Content-type: application/octet-stream" );
   header( 'Content-Disposition: attachment; filename="' . basename ( $file ) . '"' );
   header( "Content-Length: " . filesize ( $file ));
   echo $content ; // 或者 readfile($file);
?>



一、这样做有什么问题?

这样做意味着我们的程序需要将文件内容从磁盘经过一个固定的 buffer 去循环读取到内存,再发送给前端 web 服务器,最后才到达用户。当需要下载的文件很大的时候,这种方式将消耗大量内存,甚至引发 php 进程超时或崩溃。Cache 也很头疼,更不用说中断重连的情况了。
一个理想的解决方式应该是,由 php 程序进行权限检查等逻辑判断,一切通过后,让前台的 web 服务器直接将文件发送给用户——像 Nginx 这样的前台更善于处理静态文件。这样一来 php 脚本就不会被 I/O 阻塞了。

二、什么是 X-Sendfile?

X-Sendfile 是一种将文件下载请求由后端应用转交给前端 web 服务器处理的机制,它可以消除后端程序既要读文件又要处理发送的压力,从而显著提高服务器效率,特别是处理大文件下载的情形下。

X-Sendfile 通过一个特定的 HTTP header 来实现:在 X-Sendfile 头中指定一个文件的地址来通告前端 web 服务器。当 web 服务器检测到后端发送的这个 header 后,它将忽略后端的其他输出,而使用自身的组件(包括 缓存头 和 断点重连 等优化)机制将文件发送给用户。

不过,在使用 X-Sendfile 之前,我们必须明白这并不是一个标准特性,在默认情况下它是被大多数 web 服务器禁用的。而不同的 web 服务器的实现也不一样,包括规定了不同的 X-Sendfile 头格式。如果配置失当,用户可能下载到 0 字节的文件。

使用 X-Sendfile 将允许下载非 web 目录中的文件(例如/root/),即使文件在 .htaccess 保护下禁止访问,也会被下载。

不同的 web 服务器实现了不同的 HTTP 头

SENDFILE 头使用的 WEB 器
X-SendfileApache, Lighttpd v1.5, Cherokee
X-LIGHTTPD-send-fileLighttpd v1.4
X-Accel-RedirectNginx, Cherokee

使用 X-SendFile 的缺点是你失去了对文件传输机制的控制。例如如果你希望在完成文件下载后执行某些操作,比如只允许用户下载文件一次,这个 X-Sendfile 是没法做到的,因为后台的 php 脚本并不知道下载是否成功。

三、怎样使用?

Apache 请参考mod_xsendfile模块。下面我介绍 Nginx 的用法。

Nginx 默认支持该特性,不需要加载额外的模块。只是实现有些不同,需要发送的 HTTP 头为 X-Accel-Redirect。另外,需要在配置文件中做以下设定

location /protected/ {
  internal;
  root  /some/path;
}



internal表示这个路径只能在 Nginx 内部访问,不能用浏览器直接访问防止未授权的下载。


于是 PHP 发送 X-Accel-Redirect 给 Nginx:

<?php
   $filePath = '/protected/iso.img' ;
   header( 'Content-type: application/octet-stream' );
   header( 'Content-Disposition: attachment; filename="' . basename ( $file ) . '"' );
   //让Xsendfile发送文件
   header( 'X-Accel-Redirect: ' . $filePath );
?>



这样用户就会下载到 /some/path/protected/iso.img 这个路径下的文件。
如果你想发送的是 /some/path/iso.img 文件,那么 Nginx 配置应该是

location /protected/ {
  internal;
  alias  /some/path/; # 注意最後的斜杠
}

String userAgent = request.getHeader("User-Agent");
//
originalName = this.encodeNameByBrowserType(originalName, userAgent);

response.setCharacterEncoding("UTF-8");

String contentDispositionType = "inline";
response.addHeader("Content-Disposition", contentDispositionType + ";filename=\"" + originalName + "\"");


response.setContentType(this.getContentType(savepath, request));

private String encodeNameByBrowserType(String fileName, String userAgent) throws UnsupportedEncodingException {
        if (StringUtils.isNotBlank(fileName)) {
            fileName = fileName.replaceAll("\\s", "");
            if (StringUtils.isNotEmpty(userAgent)) {
                userAgent = userAgent.toLowerCase();
            }

            if (!StringUtils.isNotEmpty(userAgent) || !userAgent.contains("msie") && !userAgent.contains("edge") && !userAgent.contains("rv:11.")) {
                fileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1");
            } else {
                fileName = URLEncoder.encode(fileName, "UTF-8");
            }

            return fileName;
        } else {
            throw new IllegalArgumentException("fileName is null.");
        }
}

private String getContentType(String savepath, HttpServletRequest request) {
        String contentTypeStr = "application/octet-stream";
        if (savepath.matches(".*\\.(((?i)doc)|((?i)docx))$")) {
            contentTypeStr = "application/msword";
        } else if (savepath.matches(".*\\.(((?i)txt)|((?i)pdf))$")) {
            contentTypeStr = "application/octet-stream";
        } else {
            String contentType = request.getServletContext().getMimeType(savepath);
            if (StringUtils.isNotEmpty(contentType)) {
                contentTypeStr = contentType;
            }
        }

        return contentTypeStr;
}

    private String responseOutPutFile(Attachment attach, HttpServletResponse response) {
        String errorMessage = "";
        InputStream ins = null;
        BufferedInputStream bufferIn = null;
        OutputStream out = null;
        response.addHeader("Content-Length", "" + attach.getFileSize());

        try {
            ins = this.getIAttachService().executeDownload(attach);
            if (null == ins) {
                response.reset();
                response.setContentType("application/json; charset=utf-8");
                errorMessage = "输出文件失败,attachId:" + attach.getAttachId();
            } else {
                out = response.getOutputStream();
                bufferIn = new BufferedInputStream(ins);
                byte[] buffer = new byte[1024];
                boolean var8 = false;

                int realLength;
                while((realLength = bufferIn.read(buffer, 0, buffer.length)) != -1) {
                    out.write(buffer, 0, realLength);
                }
            }
        } catch (Exception var9) {
            LOGGER.error("download---输出文件失败,attachId=" + attach.getAttachId(), var9);
        }

        return errorMessage;
    }
 

private String reponseAttachmentByConfigure(Attachment attach, HttpServletResponse response) throws IOException {
    String errorMessage = "";
    String var4 = this.uploadProperties.getDownloadStrategy();
    byte var5 = -1;
    switch(var4.hashCode()) {
    case -1411517106:
        if (var4.equals("apache")) {
            var5 = 2;
        }
        break;
    case -891990144:
        if (var4.equals("stream")) {
            var5 = 1;
        }
        break;
    case 104760218:
        if (var4.equals("nginx")) {
            var5 = 0;
        }
        break;
    case 991980690:
        if (var4.equals("lighttpd")) {
            var5 = 3;
        }
    }

    switch(var5) {
    case 0:
        if (StringUtils.isNoneBlank(new CharSequence[]{this.uploadProperties.getDownloadUrl()})) {
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Headers", "X-Requested-With");
            response.sendRedirect(this.uploadProperties.getDownloadUrl() + attach.getDownloadUrl());
        } else {
            response.addHeader("X-Accel-Redirect", attach.getDownloadUrl());
        }
        break;
    case 1:
        errorMessage = this.responseOutPutFile(attach, response);
        break;
    case 2:
        response.addHeader("X-Sendfile", attach.getDownloadUrl());
        break;
    case 3:
        response.addHeader("X-Sendfile", attach.getDownloadUrl());
        break;
    default:
        throw new GboatAttachmentException("please config download strategy!");
    }

    return errorMessage;
}

private String responseOutPutFile(Attachment attach, HttpServletResponse response) {
    String errorMessage = "";
    InputStream ins = null;
    BufferedInputStream bufferIn = null;
    OutputStream out = null;
    response.addHeader("Content-Length", "" + attach.getFileSize());

    try {
        ins = this.getIAttachService().executeDownload(attach);
        if (null == ins) {
            response.reset();
            response.setContentType("application/json; charset=utf-8");
            errorMessage = "输出文件失败,attachId:" + attach.getAttachId();
        } else {
            out = response.getOutputStream();
            bufferIn = new BufferedInputStream(ins);
            byte[] buffer = new byte[1024];
            boolean var8 = false;

            int realLength;
            while((realLength = bufferIn.read(buffer, 0, buffer.length)) != -1) {
                out.write(buffer, 0, realLength);
            }
        }
    } catch (Exception var9) {
        LOGGER.error("download---输出文件失败,attachId=" + attach.getAttachId(), var9);
    }

    return errorMessage;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值