本文档主要讨论如下内容:

  1. 使用 Java Servlet 接口的服务端;
  2. 使用 Java HttpClient 工具的客户端;
  3. 使用 Curl 工具的 C 语言客户端;
  4. 文件名非 ASCII 字符问题

概述

根据 HTTP 协议的普适的实现,使用 Content-Type 和 Content-Disposition 两个 Header 属性,并将数据作为二进制流写入 Response Body。这样使用可以大部分已有的工具或者类库,便于开发和调试。

Header 写法如下:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="sample.txt"

这样实现, HTTP URL 直接在浏览器地址栏直接输入,下载的文件也会自动命名为 sample.txt。

使用 Java Servlet 接口的服务端

代码如下:

@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
    private final int ARBITARY_SIZE = 1048;
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
      throws ServletException, IOException {
     
        resp.setContentType("text/plain");
        resp.setHeader("Content-disposition", "attachment; filename=sample.txt");
 
        try(InputStream in = req.getServletContext().getResourceAsStream("/WEB-INF/sample.txt");
          OutputStream out = resp.getOutputStream()) {
 
            byte[] buffer = new byte[ARBITARY_SIZE];
         
            int numBytesRead;
            while ((numBytesRead = in.read(buffer)) > 0) {
                out.write(buffer, 0, numBytesRead);
            }
        }
    }
}

使用 Java HttpClient 工具的客户端

HttpEntity entity = response.getEntity();
if (entity != null) {
    String name = response.getFirstHeader('Content-Disposition').getValue();
    String fileName = disposition.replaceFirst("(?i)^.*filename=\"([^\"]+)\".*$", "$1");
    FileOutputStream fos = new FileOutputStream("C:\\" + fileName);
    entity.writeTo(fos);
    fos.close();
}

使用 Curl 工具的 C 语言客户端

Curl 工具版本 7.21.2 以上,if you specify --remote-header-name / -J,下载的文件也会自动按照 Header 里的信息命名。

参考 https://github.com/curl/curl/blob/0c76795cafe0fccab41b3adc1be08cb81d55024f/src/toolcbhdr.c#L157。

文件名非 ASCII 字符问题

RFC 5987 中,Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters 章节讨论了字符集问题。

如下代码实现了 encode 的功能:

public static String rfc5987_encode(final String s) throws UnsupportedEncodingException {
    final byte[] s_bytes = s.getBytes("UTF-8");
    final int len = s_bytes.length;
    final StringBuilder sb = new StringBuilder(len << 1);
    final char[] digits = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    final byte[] attr_char = {'!','#','$','&','+','-','.','0','1','2','3','4','5','6','7','8','9',           'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','^','_','`',                        'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','|', '~'};
    for (int i = 0; i < len; ++i) {
        final byte b = s_bytes[i];
        if (Arrays.binarySearch(attr_char, b) >= 0)
            sb.append((char) b);
        else {
            sb.append('%');
            sb.append(digits[0x0f & (b >>> 4)]);
            sb.append(digits[b & 0x0f]);
        }
    }

    return sb.toString();
}

也可以参考 https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/http/ContentDisposition.java

附录

参考:

  1. Do I need Content-Type: application/octet-stream for file download? https://stackoverflow.com/questions/20508788/do-i-need-content-type-application-octet-stream-for-file-download
  2. Content-Disposition https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
  3. Example of Downloading file https://www.baeldung.com/servlet-download-file
  4. Curl to grab remote filename after following location https://stackoverflow.com/questions/6881034/curl-to-grab-remote-filename-after-following-location
  5. handling filename* parameters with spaces via RFC 5987 results in '+' in filenames
  6. Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters
  • No labels