monkeyWie / proxyee

HTTP proxy server,support HTTPS&websocket.MITM impl,intercept and tamper HTTPS traffic.
MIT License
1.54k stars 573 forks source link

一个请求会收到多次HttpResponse是哪里的原因 #258

Open ZhangYY008 opened 1 year ago

ZhangYY008 commented 1 year ago

继承了HttpProxyIntercept,实现了afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline),在方法里打印了日志,发现一个请求在此方法里会打印多次日志。 一个请求理论上不是应该只有一个response吗?是什么原因导致有多条response情况呢?

monkeyWie commented 1 year ago

可以参考下:InterceptFullResponseProxyServer

默认情况下netty的HttpResponse Content是以流式接收的,所以会有多个response的情况。

ZhangYY008 commented 1 year ago

再确认下哈,在源码里HttpProxyClientHandler里 ` @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //客户端channel已关闭则不转发了 if (!clientChannel.isOpen()) { ReferenceCountUtil.release(msg); this.exceptionCaught(ctx, new Exception("客户端已断开")); return; } if (msg instanceof HttpResponse) { DecoderResult decoderResult = ((HttpResponse) msg).decoderResult(); Throwable cause = decoderResult.cause(); if(cause != null){ ReferenceCountUtil.release(msg); this.exceptionCaught(ctx, cause); return; } interceptPipeline.afterResponse(clientChannel, ctx.channel(), (HttpResponse) msg); //下面HttpContent有多个可以理解,这个HttpResponse一个请求也会调用多次吗?

    } else if (msg instanceof HttpContent) {
        interceptPipeline.afterResponse(clientChannel, ctx.channel(), (HttpContent) msg);
    } else {
        clientChannel.writeAndFlush(msg);
    }
}`
monkeyWie commented 1 year ago

有可以复现的代码吗

ZhangYY008 commented 1 year ago

还没找到能浮现的请求。但是线上有大量这种情况

monkeyWie commented 1 year ago

最好是能提供下复现代码和步骤

ZhangYY008 commented 1 year ago

发现HttpProxyInterceptPipeline不是每次代理请求都new出来一个,当放一个变量a=0到这个类中,在HttpProxyClientHandler的channelRead中的 if (msg instanceof HttpResponse) 中对a++。测试时,代理到同一个url,连续发送,发现a会一直累加,理论上每次代理,HttpProxyInterceptPipeline都是一个新对象,那么a=0,再累加,每次代理这个a加1,每个请求这个a 都等于1才对。

这也复现了我上面的问题,if (msg instanceof HttpResponse) 中interceptPipeline.afterResponse会多次调用

monkeyWie commented 1 year ago

因为http会存在tcp复用的情况,同一个tcp连接会有多个请求响应是正常的,你可以特殊处理下,比如在每次请求进来都标记一下是新的请求

ZhangYY008 commented 1 year ago

大佬,不太理解,客户端 -> 代理系统 -> 目标服务器,代理系统 -> 目标服务器会存在tcp连接复用,那客户端 -> 代理系统也会存在复用吧,那在哪里能区分是新请求呢?

ZhangYY008 commented 1 year ago

还有这个也不太理解,每个新请求都会buildPipeline,为啥这个也没执行呢?难道这个也不执行

monkeyWie commented 1 year ago

Pipeline是跟着tcp连接走的,也就是说每次创建新的tcp连接的时候才创建pipeline,现在你那边一个连接收到多个请求+响应业务处理上会有啥问题吗

ZhangYY008 commented 1 year ago

现在想统计每个请求的处理时间,就是从接收到一个请求到返回响应的时间。想把开始时间放在pipeline中,结束时间在afterResponse中减去开始时间,但是由于有时连接会同用pipeline,导致不能统计到每个请求耗时。大佬,看下这个,该怎么处理呢?

monkeyWie commented 1 year ago

这样试试:

pipeline.addLast(new HttpProxyIntercept() {

                            private Map<HttpRequest, Long> requestUsedTimeMap = new HashMap<>();

                            @Override
                            public void beforeRequest(Channel clientChannel, HttpRequest httpRequest,
                                                      HttpProxyInterceptPipeline pipeline) throws Exception {
                                requestUsedTimeMap.put(httpRequest, System.currentTimeMillis());
                            }

                            @Override
                            public void afterResponse(Channel clientChannel, Channel proxyChannel,
                                                      HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) throws Exception {
                                Long startTime = requestUsedTimeMap.remove(pipeline.getHttpRequest());
                                System.out.println("请求耗时:" + (System.currentTimeMillis() - startTime));
                            }
                        });
ZhangYY008 commented 1 year ago

感谢,按照你这个,我再试试。 还有一点疑问,在HttpProxyServerHandler的channelRead方法中,看到只要消息是HttpRequest,都会setInterceptPipeline(buildPipeline());,应该是每个Http请求都会有HttpReqeust啊,都会new一个HttpProxyInterceptPipeline出来啊,为啥上面说buildPipeline()是跟着tcp连接走的呢? 从测试来看,确实是跟着tcp连接走的,但是没理解,这个一步为啥没执行

ZhangYY008 commented 1 year ago

这样试试:

pipeline.addLast(new HttpProxyIntercept() {

                            private Map<HttpRequest, Long> requestUsedTimeMap = new HashMap<>();

                            @Override
                            public void beforeRequest(Channel clientChannel, HttpRequest httpRequest,
                                                      HttpProxyInterceptPipeline pipeline) throws Exception {
                                requestUsedTimeMap.put(httpRequest, System.currentTimeMillis());
                            }

                            @Override
                            public void afterResponse(Channel clientChannel, Channel proxyChannel,
                                                      HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) throws Exception {
                                Long startTime = requestUsedTimeMap.remove(pipeline.getHttpRequest());
                                System.out.println("请求耗时:" + (System.currentTimeMillis() - startTime));
                            }
                        });

测试了这种方式,还是不行,在afterResponse中startTime大量报空指针。还是证明了,很对请求不执行beforeReqeust,却多次执行afterResponse(httpreponse不是httpcontent),一般测试没法复现,用locust测试工具,挂代理,压测就能稳定复现

monkeyWie commented 1 year ago

有用FullHttpProxyIntercept之类的吗

ZhangYY008 commented 1 year ago

没有使用,但是测试过FullHttpProxyIntercept,会出现多次FullHttpResponse

ZhangYY008 commented 1 year ago

一开始的表述的有问题,纠正下,不是说一次请求会调用多次afterResponse,而是多次同样的请求,会调用多次afterResponse,却只调用一次beforeReqeust,也就是HttpProxyServerHandler的channelRead方法中,并不是每次请求都执行setInterceptPipeline(buildPipeline());

lax0214 commented 1 year ago

我认为buildPipeline()是每一次http请求都会调用的,如果只是每次tcp创建的时候调用,由于tcp复用原因,多个相同的http请求一定会存在多线程问题。

你的需求按照正常情况是行的通的,如果方便可以po出更完整代码看下是什么问题。