专栏首页后端开发随笔验证调用HttpServletResponse.getWriter().close()方法是否真的会关闭http连接

验证调用HttpServletResponse.getWriter().close()方法是否真的会关闭http连接

起因

线上项目突然遭到大量的非法参数攻击,由于历史问题,之前的代码从未对请求参数进行校验。 导致大量请求落到了数据访问层,给应用服务器和数据库都带来了很大压力。 针对这个问题,只能对请求真正到Controller方法调用之前直接将非法参数请求拒绝掉,所以在Filter中对参数进行统一校验,非法参数直接返回400。 我的建议是不但要设置响应状态码设置为400,还应该明确调用HttpServletResponse.getWriter().close(),希望此举能在服务端主动断开连接,释放资源。 但是同事认为不必要明确调用HttpServletResponse.getWriter().close(),于是就有了这个验证实验。

实验

1.应用容器:tomcat 7.0.59

2.如何验证服务器是否真的断开连接:观察http响应消息头“Connection”值是否为“close”。

不明确close时httpresponse返回的消息头

HTTP/1.1 400 Bad Request
Server: Apache-Coyote/1.1
Content-Length: 21
Date: Tue, 05 Sep 2017 11:39:00 GMT
Connection: close

明确close时httpresponse返回的消息头

HTTP/1.1 400 Bad Request
Server: Apache-Coyote/1.1
Content-Length: 0
Date: Tue, 05 Sep 2017 11:39:25 GMT
Connection: close

结论

1.根据上述结果,如果根据http响应消息头“Connection”值是否为“close”来验证服务端是否会主动断开连接。 那么在servlet中是否明确调用“HttpServletResponse.getWriter().close()”结果都是一样的。 因为在org.apache.coyote.http11.AbstractHttp11Processor中会根据响应状态码判断返回消息头Connection值。

    private void prepareResponse() {
        ...
        // If we know that the request is bad this early, add the
        // Connection: close header.
        keepAlive = keepAlive && !statusDropsConnection(statusCode);
        if (!keepAlive) {
            // Avoid adding the close header twice
            if (!connectionClosePresent) {
                headers.addValue(Constants.CONNECTION).setString(
                        Constants.CLOSE);
            }
        } else if (!http11 && !getErrorState().isError()) {
            headers.addValue(Constants.CONNECTION).setString(Constants.KEEPALIVE);
        }
        ...
    }

    /**
    * Determine if we must drop the connection because of the HTTP status
    * code.  Use the same list of codes as Apache/httpd.
    */
    protected boolean statusDropsConnection(int status) {
    return status == 400 /* SC_BAD_REQUEST */ ||
            status == 408 /* SC_REQUEST_TIMEOUT */ ||
            status == 411 /* SC_LENGTH_REQUIRED */ ||
            status == 413 /* SC_REQUEST_ENTITY_TOO_LARGE */ ||
            status == 414 /* SC_REQUEST_URI_TOO_LONG */ ||
            status == 500 /* SC_INTERNAL_SERVER_ERROR */ ||
            status == 503 /* SC_SERVICE_UNAVAILABLE */ ||
            status == 501 /* SC_NOT_IMPLEMENTED */;
    }

也就是说,当响应状态码为400时,不论是否明确调用“HttpServletResponse.getWriter().close()”,都会在响应消息头中设置“Connection: close”。 那么,问题来了:HTTP的响应消息头“Connection”值为“close”时是否就意味着服务端会主动断开连接了呢? 根据rfc2616的对于HTTP协议的定义(详见:https://www.ietf.org/rfc/rfc2616.txt):

HTTP/1.1 defines the "close" connection option for the sender to
signal that the connection will be closed after completion of the
esponse. For example,

Connection: close

也就是说,一旦在服务端设置响应消息头“Connection”为“close”,就意味着在本次请求响应完成后,对应的连接应该会被关闭。 然而,这对于不同的Servlet容器实现来说,真的就会关闭连接吗? 跟踪tomcat源码发现,即使明确调用close()方法也不是直接就关闭连接。

2.明确调用“HttpServletResponse.getWriter().close()”时tomcat又做了什么事情

    (1)org.apache.catalina.connector.CoyoteWriter
    @Override
    public void close() {

        // We don't close the PrintWriter - super() is not called,
        // so the stream can be reused. We close ob.
        try {
            ob.close();
        } catch (IOException ex ) {
            // Ignore
        }
        error = false;

    }

    (2)org.apache.catalina.connector.OutputBuffer
    /**
     * Close the output buffer. This tries to calculate the response size if
     * the response has not been committed yet.
     *
     * @throws IOException An underlying IOException occurred
     */
    @Override
    public void close()
        throws IOException {

        if (closed) {
            return;
        }
        if (suspended) {
            return;
        }

        // If there are chars, flush all of them to the byte buffer now as bytes are used to
        // calculate the content-length (if everything fits into the byte buffer, of course).
        if (cb.getLength() > 0) {
            cb.flushBuffer();
        }

        if ((!coyoteResponse.isCommitted()) && (coyoteResponse.getContentLengthLong() == -1) &&
                !coyoteResponse.getRequest().method().equals("HEAD")) {
            // If this didn't cause a commit of the response, the final content
            // length can be calculated. Only do this if this is not a HEAD
            // request since in that case no body should have been written and
            // setting a value of zero here will result in an explicit content
            // length of zero being set on the response.
            if (!coyoteResponse.isCommitted()) {
                coyoteResponse.setContentLength(bb.getLength());
            }
        }

        if (coyoteResponse.getStatus() ==
                HttpServletResponse.SC_SWITCHING_PROTOCOLS) {
            doFlush(true);
        } else {
            doFlush(false);
        }
        closed = true;

        // The request should have been completely read by the time the response
        // is closed. Further reads of the input a) are pointless and b) really
        // confuse AJP (bug 50189) so close the input buffer to prevent them.
        Request req = (Request) coyoteResponse.getRequest().getNote(
                CoyoteAdapter.ADAPTER_NOTES);
        req.inputBuffer.close();

        coyoteResponse.finish();

    }

    (3)org.apache.coyote.Response
    public void finish() {
        action(ActionCode.CLOSE, this);
    }

    public void action(ActionCode actionCode, Object param) {
        if (hook != null) {
            if( param==null ) 
                hook.action(actionCode, this);
            else
                hook.action(actionCode, param);
        }
    }

    (4)org.apache.coyote.http11.AbstractHttp11Processor
    /**
     * Send an action to the connector.
     *
     * @param actionCode Type of the action
     * @param param Action parameter
     */
    @Override
    @SuppressWarnings("deprecation") // Inbound/Outbound based upgrade mechanism
    public final void action(ActionCode actionCode, Object param) {

        switch (actionCode) {
        case CLOSE: {
            // End the processing of the current request
            try {
                getOutputBuffer().endRequest();
            } catch (IOException e) {
                setErrorState(ErrorState.CLOSE_NOW, e);
            }
            break;
        }
        ...
        }
    }

    (5)org.apache.coyote.http11.InternalNioOutputBuffer
    /**
     * End request.
     *
     * @throws IOException an underlying I/O error occurred
     */
    @Override
    public void endRequest() throws IOException {
        super.endRequest();
        flushBuffer();
    }

    /**
     * Callback to write data from the buffer.
     */
    private void flushBuffer() throws IOException {

        //prevent timeout for async,
        SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
        if (key != null) {
            NioEndpoint.KeyAttachment attach = (NioEndpoint.KeyAttachment) key.attachment();
            attach.access();
        }

        //write to the socket, if there is anything to write
        if (socket.getBufHandler().getWriteBuffer().position() > 0) {
            socket.getBufHandler().getWriteBuffer().flip();
            writeToSocket(socket.getBufHandler().getWriteBuffer(),true, false);
        }
    }

实际上,明确调用“HttpServletResponse.getWriter().close()”时只是确保将数据发送给客户端,并不会执行关闭连接。 因此,回到我一开始的疑问:是否需要在代码中明确调用close()方法?在我遇到的这个校验非法参数的场景,其实是不必要的。但是,当HTTP状态码返回400时,Connection值一定会被设置为close。 那么,这个问题被引申一下:Http协议头中的“Connection”字段到底有和意义呢?这需要从HTTP协议说起。在Http1.0中是没有这个字段的,也就是说每一次HTTP请求都会建立新的TCP连接。而随着Web应用的发展,通过HTTP协议请求的资源越来越丰富,除了文本还可能存在图片等其他资源了,为了能够在一次TCP连接中能最快地获取到这些资源,在HTTP1.1中增加了“Connection”字段,取值为close或keep-alive。其作用在于告诉使用HTTP协议通信的2端在建立TCP连接并完成第一次HTTP数据响应之后不要直接断开对应的TCP连接,而是维持这个TCP连接,继续在这个连接上传输后续的HTTP数据,这样可以大大提高通信效率。当然,当“Connection”字段值为close时,说明双方不再需要通信了,希望断开TCP连接。 所以,对于使用HTTP协议的Web应用来讲,如果希望服务器端与客户端在本次HTTP协议通信之后断开连接,需要将“Connection”值设置为close;否则应该设置为keep-alive。

3.针对非法参数的DDoS攻击的请求,都应该在应用服务器前端进行拦截,杜绝请求直接到应用层。 如:在nginx端进行IP拦截,参考:https://zhangge.net/5096.html。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ajax方式下载文件

    在web项目中需要下载文件,由于传递的参数比较多(通过参数在服务器端动态下载指定文件),所以希望使用post方式传递参数。 通常,在web前端需要下载文件,都是...

    2Simple
  • 学习go语言编程系列之定义变量

    func main() { // 1. 定义变量名age,不初始化,使用对应类型的默认值 var age int fmt.Println("My age ...

    2Simple
  • Spring Boot应用上传文件时报错

    Spring Boot应用(使用默认的嵌入式Tomcat)在上传文件时,偶尔会出现上传失败的情况,后台报错日志信息如下:“The temporary uploa...

    2Simple
  • python菜鸟教程 | if elif else 判断

    上一讲主要学习了 if else 内容,本讲将要学习最后一个语句 elif(else if)。

    week
  • ShardingSphere多数据源,读写分离等的实现

    有关分表的实现可以参考Springboot2使用shardingsphere分表攻略

    算法之名
  • A股AI芯片第一股,寒武纪2个月火速过会,产业正迎来新契机

    寒武纪的过会,无疑为国内AI芯片产业开了个好头,也让其他AI芯片创企看到了新契机。

    镁客网
  • 【CodeForces 624D/623B】Array GCD

    You are given array ai of length n. You may consecutively apply two operations t...

    饶文津
  • 聊聊skywalking的jedis-pulgin

    skywalking-6.6.0/apm-sniffer/apm-sdk-plugin/jedis-2.x-plugin/src/main/resources/...

    codecraft
  • 聊聊skywalking的jedis-pulgin

    skywalking-6.6.0/apm-sniffer/apm-sdk-plugin/jedis-2.x-plugin/src/main/resources/...

    codecraft
  • 找找规律——LeetCode题目6:Z字形变换

    将一个给定字符串根据给定的行数,以从上往下、从左到右进行Z字形排列,然后输出。(此处需要示例解释一下)

    二环宇少

扫码关注云+社区

领取腾讯云代金券