有趣的clientAbortException

遇到一个非常有意思的异常.
在使用tomcat(jetty也一样)时,如果client请求,server已经收到请求后,但是client突然关闭了请求,会出现无线循环的抛异常.

Resolved exception caused by Handler execution: org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe

这种异常会导致栈溢出.但是实际上并不影响服务的正常运行,也就是说如果你不看日志的话,几乎感知不到这种异常.所以这种异常的最大问题就是导致日志疯长.当然这取决于你服务器对栈深的规定,等到栈溢出了,异常就停止了.因为抛出栈溢出了.
下面的程序通过调整执行次数和sleep时间,可以重现这种异常.

public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            sendRequest();
        }
    }

    public static String sendRequest() {
        CloseableHttpClient httpCilent = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://127.0.0.1:8080");
        new Thread(() -> {
            try {
                CloseableHttpResponse resp = httpCilent.execute(httpGet);
                System.out.println(resp.getEntity().toString());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    httpCilent.close();// 释放资源
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {
            try {
                //设定sleep,来更好的实现close的时机
                Thread.sleep(10);
                httpCilent.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        return null;
    }

至于说为啥不影响服务的运行,我认为不管是tomcat还是spring,对于每一个新的请求都是开辟一个新的thread来处理,所以每个请求之间相互不干扰.
现在说说这个错误出现的原因.
先看看clientAbortException都在哪里调用了(我用的是springboot),可以用你的IDE查看下:

org.apache.catalina.connector.ClientAbortException.ClientAbortException(Throwable)
    org.apache.catalina.connector.OutputBuffer.doFlush(boolean)
    org.apache.catalina.connector.OutputBuffer.realWriteBytes(ByteBuffer)

这里显示有俩处调用.经过调试发现实际错误发生在doFlush里面.

protected void doFlush(boolean realFlush) throws IOException {

        if (suspended) {
            return;
        }

        try {
            doFlush = true;
            if (initial) {
                coyoteResponse.sendHeaders();
                initial = false;
            }
            if (cb.remaining() > 0) {
                flushCharBuffer();
            }
            if (bb.remaining() > 0) {
                flushByteBuffer();
            }
        } finally {
            doFlush = false;
        }

        if (realFlush) {
            coyoteResponse.action(ActionCode.CLIENT_FLUSH, null);
            // If some exception occurred earlier, or if some IOE occurred
            // here, notify the servlet with an IOE
            if (coyoteResponse.isExceptionPresent()) {
                throw new ClientAbortException(coyoteResponse.getErrorException());
            }
        }

    }

可以看到最后判断是否有异常,如果有,抛出clientAbortException.抛出异常后(是tomcat抛出的),spring就捕获了异常,然后spring的dispatch就会经过各种封装各种filter处理.

filterChain.doFilter(request, response);

目的就是向client输出错误信息,最后会调用:
org.springframework.util.StreamUtils里的copy方法.

public static int copy(InputStream in, OutputStream out) throws IOException {
        Assert.notNull(in, "No InputStream specified");
        Assert.notNull(out, "No OutputStream specified");

        int byteCount = 0;
        byte[] buffer = new byte[BUFFER_SIZE];
        int bytesRead = -1;
        while ((bytesRead = in.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
            byteCount += bytesRead;
        }
        out.flush();
        return byteCount;
    }

看到没,最后调用了out.flush()后,又重新会调用到tomcat里面的doFlush方法,就又发现client丢失,又再次抛出异常.循环就此开始了.
那么如何避免这种错误了?
我的答案是避免不了.也可能是我的见识目前不知道咋解决.不过可以避免大量的日志输出,我们定义个filter,然后在filter里面捕获异常.

 @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        final HttpServletResponse res = (HttpServletResponse) response;
        final HttpServletRequest req = (HttpServletRequest) request;
        try {
            // 请求转发
            chain.doFilter(req, res);
        } catch (Exception e) {
            /*
             * 异常拦截,server请求收到,但是client取消,导致死循环栈溢出
             * Resolved exception caused by Handler execution:
             * org.apache.catalina.connector.ClientAbortException:
             * java.io.IOException: Broken pipe
             * 但是这里捕获的其实是栈溢出错误.
             */
            logger.warn("捕获到ClientAbortException");
        }
    }

这样我们就可以避免大量日志输出了.
这个异常还是蛮有意思的,有种你可以忽略她,但不可以忘记她的感觉.

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值