驗證調用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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM