`

HTTP - 304实现

    博客分类:
  • JAVA
 
阅读更多

最近公司做了一个CMS项目,其中涉及到图片上传后页面展现,如果图片过大就会带来过多的带宽消耗,同时也会影响客户体验;

 

解决方案:利用浏览器缓存,HTTP - 304来解决;

 

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@SuppressWarnings("serial")
public class FileServlet extends HttpServlet {

    // Constants ----------------------------------------------------------------------------------

    private static final int DEFAULT_BUFFER_SIZE = 10240; // ..bytes = 10KB.
    private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
    private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

    // Properties ---------------------------------------------------------------------------------

//    private String basePath;

    // Actions ------------------------------------------------------------------------------------

    /**
     * Initialize the servlet.
     * @see HttpServlet#init().
     */
    public void init() throws ServletException {

        /**
         * 按照我们的业务,不需要这个init的自配置,直接去掉了
         */
        // Get base path (path to get all resources from) as init parameter.
//        this.basePath = getServletContext().getRealPath(getInitParameter("basePath"));

        // Validate base path.
//        if (this.basePath == null) {
//            throw new ServletException("FileServlet init param 'basePath' is required.");
//        } else {
//            File path = new File(this.basePath);
//            if (!path.exists()) {
//                throw new ServletException("FileServlet init param 'basePath' value '"
//                    + this.basePath + "' does actually not exist in file system.");
//            } else if (!path.isDirectory()) {
//                throw new ServletException("FileServlet init param 'basePath' value '"
//                    + this.basePath + "' is actually not a directory in file system.");
//            } else if (!path.canRead()) {
//                throw new ServletException("FileServlet init param 'basePath' value '"
//                    + this.basePath + "' is actually not readable in file system.");
//            }
//        }
    }

    /**
     * Process HEAD request. This returns the same headers as GET request, but without content.
     * @see HttpServlet#doHead(HttpServletRequest, HttpServletResponse).
     */
    protected void doHead(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        // Process request without content.
        processRequest(request, response, false);
    }

    /**
     * Process GET request.
     * @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse).
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        // Process request with content.
        processRequest(request, response, true);
    }

    /**
     * Process the actual request.
     * @param request The request to be processed.
     * @param response The response to be created.
     * @param content Whether the request body should be written (GET) or not (HEAD).
     * @throws IOException If something fails at I/O level.
     */
    private void processRequest
        (HttpServletRequest request, HttpServletResponse response, boolean content)
            throws IOException
    {
        // Validate the requested file ------------------------------------------------------------

        // Get requested file by path info.
        String requestedFile = (String) request.getAttribute("filepath");

        // Check if file is actually supplied to the request URL.
        if (requestedFile == null) {
            // Do your thing if the file is not supplied to the request URL.
            // Throw an exception, or send 404, or show default/warning page, or just ignore it.
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // URL-decode the file name (might contain spaces and on) and prepare file object.
        File file = new File(requestedFile);

        // Check if file actually exists in filesystem.
        if (!file.exists()) {
            // Do your thing if the file appears to be non-existing.
            // Throw an exception, or send 404, or show default/warning page, or just ignore it.
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // Prepare some variables. The ETag is an unique identifier of the file.
        String fileName = file.getName();
        long length = file.length();
        long lastModified = file.lastModified();
        String eTag = fileName + "_" + length + "_" + lastModified;
        long expires = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;


        // Validate request headers for caching ---------------------------------------------------

        // If-None-Match header should contain "*" or ETag. If so, then return 304.
        String ifNoneMatch = request.getHeader("If-None-Match");
        if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.setHeader("ETag", eTag); // Required in 304.
            response.setDateHeader("Expires", expires); // Postpone cache with 1 week.
            return;
        }

        // If-Modified-Since header should be greater than LastModified. If so, then return 304.
        // This header is ignored if any If-None-Match header is specified.
        long ifModifiedSince = request.getDateHeader("If-Modified-Since");
        if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.setHeader("ETag", eTag); // Required in 304.
            response.setDateHeader("Expires", expires); // Postpone cache with 1 week.
            return;
        }


        // Validate request headers for resume ----------------------------------------------------

        // If-Match header should contain "*" or ETag. If not, then return 412.
        String ifMatch = request.getHeader("If-Match");
        if (ifMatch != null && !matches(ifMatch, eTag)) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }

        // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
        long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
        if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }


        // Validate and process range -------------------------------------------------------------

        // Prepare some variables. The full Range represents the complete file.
        Range full = new Range(0, length - 1, length);
        List<Range> ranges = new ArrayList<Range>();

        // Validate and process Range and If-Range headers.
        String range = request.getHeader("Range");
        if (range != null) {

            // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return;
            }

            // If-Range header should either match ETag or be greater then LastModified. If not,
            // then return full file.
            String ifRange = request.getHeader("If-Range");
            if (ifRange != null && !ifRange.equals(eTag)) {
                try {
                    long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
                    if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
                        ranges.add(full);
                    }
                } catch (IllegalArgumentException ignore) {
                    ranges.add(full);
                }
            }

            // If any valid If-Range header, then process each part of byte range.
            if (ranges.isEmpty()) {
                for (String part : range.substring(6).split(",")) {
                    // Assuming a file with length of 100, the following examples returns bytes at:
                    // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                    long start = sublong(part, 0, part.indexOf("-"));
                    long end = sublong(part, part.indexOf("-") + 1, part.length());

                    if (start == -1) {
                        start = length - end;
                        end = length - 1;
                    } else if (end == -1 || end > length - 1) {
                        end = length - 1;
                    }

                    // Check if Range is syntactically valid. If not, then return 416.
                    if (start > end) {
                        response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                        return;
                    }

                    // Add range.
                    ranges.add(new Range(start, end, length));
                }
            }
        }


        // Prepare and initialize response --------------------------------------------------------

        // Get content type by file name and set default GZIP support and content disposition.
        String contentType = getServletContext().getMimeType(fileName);
        boolean acceptsGzip = false;
        String disposition = "inline";

        // If content type is unknown, then set the default value.
        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
        // To add new content types, add new mime-mapping entry in web.xml.
        if (contentType == null) {
            contentType = "application/octet-stream";
        }

        // If content type is text, then determine whether GZIP content encoding is supported by
        // the browser and expand content type with the one and right character encoding.
        if (contentType.startsWith("text")) {
            String acceptEncoding = request.getHeader("Accept-Encoding");
            acceptsGzip = acceptEncoding != null && accepts(acceptEncoding, "gzip");
            contentType += ";charset=UTF-8";
        } 

        // Else, expect for images, determine content disposition. If content type is supported by
        // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
        else if (!contentType.startsWith("image")) {
            String accept = request.getHeader("Accept");
            disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";
        }

        // Initialize response.
        response.reset();
        response.setBufferSize(DEFAULT_BUFFER_SIZE);
        response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("ETag", eTag);
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", expires);


        // Send requested file (part(s)) to client ------------------------------------------------

        // Prepare streams.
        RandomAccessFile input = null;
        OutputStream output = null;

        try {
            // Open streams.
            input = new RandomAccessFile(file, "r");
            output = response.getOutputStream();

            if (ranges.isEmpty() || ranges.get(0) == full) {

                // Return full file.
                Range r = full;
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);

                if (content) {
                    if (acceptsGzip) {
                        // The browser accepts GZIP, so GZIP the content.
                        response.setHeader("Content-Encoding", "gzip");
                        output = new GZIPOutputStream(output, DEFAULT_BUFFER_SIZE);
                    } else {
                        // Content length is not directly predictable in case of GZIP.
                        // So only add it if there is no means of GZIP, else browser will hang.
                        response.setHeader("Content-Length", String.valueOf(r.length));
                    }

                    // Copy full range.
                    copy(input, output, r.start, r.length);
                }

            } else if (ranges.size() == 1) {

                // Return single part of file.
                Range r = ranges.get(0);
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                response.setHeader("Content-Length", String.valueOf(r.length));
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                if (content) {
                    // Copy single part range.
                    copy(input, output, r.start, r.length);
                }

            } else {

                // Return multiple parts of file.
                response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                if (content) {
                    // Cast back to ServletOutputStream to get the easy println methods.
                    ServletOutputStream sos = (ServletOutputStream) output;

                    // Copy multi part range.
                    for (Range r : ranges) {
                        // Add multipart boundary and header fields for every range.
                        sos.println();
                        sos.println("--" + MULTIPART_BOUNDARY);
                        sos.println("Content-Type: " + contentType);
                        sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

                        // Copy single part range of multi part range.
                        copy(input, output, r.start, r.length);
                    }

                    // End with multipart boundary.
                    sos.println();
                    sos.println("--" + MULTIPART_BOUNDARY + "--");
                }
            }
        } finally {
            // Gently close streams.
            close(output);
            close(input);
        }
    }

    // Helpers (can be refactored to public utility class) ----------------------------------------

    /**
     * Returns true if the given accept header accepts the given value.
     * @param acceptHeader The accept header.
     * @param toAccept The value to be accepted.
     * @return True if the given accept header accepts the given value.
     */
    private static boolean accepts(String acceptHeader, String toAccept) {
        String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
        Arrays.sort(acceptValues);
        return Arrays.binarySearch(acceptValues, toAccept) > -1
            || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
            || Arrays.binarySearch(acceptValues, "*/*") > -1;
    }

    /**
     * Returns true if the given match header matches the given value.
     * @param matchHeader The match header.
     * @param toMatch The value to be matched.
     * @return True if the given match header matches the given value.
     */
    private static boolean matches(String matchHeader, String toMatch) {
        String[] matchValues = matchHeader.split("\\s*,\\s*");
        Arrays.sort(matchValues);
        return Arrays.binarySearch(matchValues, toMatch) > -1
            || Arrays.binarySearch(matchValues, "*") > -1;
    }

    /**
     * Returns a substring of the given string value from the given begin index to the given end
     * index as a long. If the substring is empty, then -1 will be returned
     * @param value The string value to return a substring as long for.
     * @param beginIndex The begin index of the substring to be returned as long.
     * @param endIndex The end index of the substring to be returned as long.
     * @return A substring of the given string value as long or -1 if substring is empty.
     */
    private static long sublong(String value, int beginIndex, int endIndex) {
        String substring = value.substring(beginIndex, endIndex);
        return (substring.length() > 0) ? Long.parseLong(substring) : -1;
    }

    /**
     * Copy the given byte range of the given input to the given output.
     * @param input The input to copy the given range to the given output for.
     * @param output The output to copy the given range from the given input for.
     * @param start Start of the byte range.
     * @param length Length of the byte range.
     * @throws IOException If something fails at I/O level.
     */
    private static void copy(RandomAccessFile input, OutputStream output, long start, long length)
        throws IOException
    {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;

        if (input.length() == length) {
            // Write full range.
            while ((read = input.read(buffer)) > 0) {
                output.write(buffer, 0, read);
            }
        } else {
            // Write partial range.
            input.seek(start);
            long toRead = length;

            while ((read = input.read(buffer)) > 0) {
                if ((toRead -= read) > 0) {
                    output.write(buffer, 0, read);
                } else {
                    output.write(buffer, 0, (int) toRead + read);
                    break;
                }
            }
        }
    }

    /**
     * Close the given resource.
     * @param resource The resource to be closed.
     */
    private static void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException ignore) {
                // Ignore IOException. If you want to handle this anyway, it might be useful to know
                // that this will generally only be thrown when the client aborted the request.
            }
        }
    }

    // Inner classes ------------------------------------------------------------------------------

    /**
     * This class represents a byte range.
     */
    protected class Range {
        long start;
        long end;
        long length;
        long total;

        /**
         * Construct a byte range.
         * @param start Start of the byte range.
         * @param end End of the byte range.
         * @param total Total length of the byte source.
         */
        public Range(long start, long end, long total) {
            this.start = start;
            this.end = end;
            this.length = end - start + 1;
            this.total = total;
        }

    }

}

 

分享到:
评论

相关推荐

    usr-TCP232-304-software

    USR-TCP232-304是一款功能强大的485转以太网转换器,它允许设备通过RS-485接口连接到以太网网络,从而实现远程监控和控制。本文将深入探讨USR-TCP232-304的软件设计与应用,旨在帮助用户更好地理解和使用这款产品。 ...

    USR-TCP232-304_AT_V2.0.zip

    USR-TCP232-304_AT_V2.0是一个专为串口通信设计的模块,它将传统的RS-232串行接口转换为TCP/IP网络接口,使得串口设备能够方便地接入到以太网环境中。该模块通常用于工业自动化、物联网(IoT)应用以及任何需要串口到...

    pywin32-304.0-cp310-cp310-win_amd64.zip

    总之,PyWin32是Python在Windows环境中开发的强大工具,它的正确安装和使用是实现系统级编程的关键。通过理解其版本匹配、安装步骤以及提供的功能,开发者可以充分利用Python的灵活性和Windows的强大功能,创建出...

    pywin32-304.0-cp38-cp38-win_amd64.zip

    "cp38"是Python 3.8的代号,"cp"代表CPython(Python的官方实现),后面的数字是主次版本号。 3. **64位与32位环境**:64位Python环境可以处理更大的内存,更适合处理大数据或者内存密集型应用。在64位系统上,32位...

    USR-TCP232-304_basic

    USR-TCP232-304是一种串口服务器产品,它能够实现串口通信到网络通信的转换,适用于智慧工业物联网领域,充当网络和串口设备之间的桥梁。在基本测试案例中,涉及了产品的结构框图、数据流向、资料下载、测试环境搭建...

    信捷 DP-304步进驱动器用户手册.rar

    通过阅读《信捷 DP-304步进驱动器用户手册》,用户不仅能了解步进驱动器的基本原理,还能掌握DP-304的具体操作和维护技巧,从而充分发挥其性能,实现精确的运动控制。在实践中,用户应结合手册与实际设备,逐步熟悉...

    pywin32-304.0-cp39-cp39-win_amd64.zip

    "pywin32-304.0-cp39-cp39-win_amd64.zip" 是PyWin32的一个特定版本,版本号为304.0,针对Python 3.9版本设计,并且是专为64位(win_amd64)的Windows系统编译的。这个压缩包包含了必要的文件,使得Python开发者能够...

    AZ-304 - Q237 2022.pdf

    此外,结合 Azure Monitor 可以实现更全面的应用监控,包括警报设置、自定义日志查询等功能。 参考链接: - [Application Insights 文档]...

    CW104-204-304型可调负输出稳压电源电路

    当电路的负端接地时,正端会输出正向电压,这是因为稳压器内部的晶体管会在调节过程中改变其导通状态,使得电流可以从正端流出,实现正电压的调节。 总的来说,CW104-204-304型可调负输出稳压电源电路设计灵活,...

    TechEd2009 横滨 官方资料 T1-304

    - 定期任务可以通过 PowerShell 来实现。 - 命令行提供了统一的操作接口,基于文本的输入输出,便于工具间的交互和脚本编写。 - **兼容性问题:** - 对于 Windows XP SP3, Windows Vista SP1, Windows Server ...

    AZ-304 411题.pdf

    12. 身份管理:案例中提到计划建立混合身份模型,这暗示了在混合云环境中实现单点登录和跨本地和云服务的身份管理需求。 从文档中可以看出,涉及的IT知识点和技能非常广泛,这包括了微软Azure云服务平台的架构设计...

    chromiumembedded-java-cef-304f3e3de03f.zip

    【描述】该压缩包包含了Java版的CEF实现,具体版本为304f3e3de03f。CEF的主要目标是提供一种方式来在各种平台上无缝地嵌入Chromium浏览器引擎,包括Windows、macOS和Linux。这个特定的版本可能包含源代码、编译库、...

    0-0 M4531 demo板原理图

    Type-C接口通过**TYPEC-304A-ACP**实现,提供了更多的功能性和兼容性。具体来说,Type-C接口的引脚包括: - DP/DM: 数据正/负线 - SBU: 侧边带使用 - CC: 配置通道 - VBUS/GND: 供电/接地 #### J-Link调试接口 为了...

    使用 VMware Workstation Pro 17.0-304页

    【使用 VMware Workstation Pro 17.0-304页】 VMware Workstation Pro 是一款强大的桌面虚拟化软件,允许用户在单个物理主机上运行多个独立的操作系统实例,以便进行开发、测试、演示和运行应用程序。在版本 17.0 ...

    基于java-304_垃圾分类微信小程序的设计与实现-源码.zip

    同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的...

    Oracle Solaris 11.2 Programming Interfaces Guide-304

    5. **安全性**:Solaris提供了强大的安全框架,如Solaris Security Kernel、zones和IMA(Identity Management Architecture),开发者可以利用这些接口实现安全的应用程序和系统服务。 6. **文件系统**:包括对多种...

    SMIV-304_FOllowing_the_Rules_Earnestv3

    它们可以组合多个标准业务规则的功能,实现更复杂的逻辑和更精细的控制。超级业务规则支持更复杂的条件判断和多步动作,使得自动化流程能够适应更广泛和更复杂的工作场景。 总之,BMC ServiceDesk Express通过其...

Global site tag (gtag.js) - Google Analytics