Netty上的Vert.x 2 HTTP服务器:写入响应字节但从未刷新,通道从未关闭

时间:2017-09-07 11:14:11

标签: netty vert.x

使用Vert.x 2.1.6中的HTTP服务器(基于Netty 4.0.21)处理HTTP请求,从文件系统读取静态文件并写入客户端。 有时,在多个同时请求(读取:在浏览器中快速单击)期间,随机请求永远不会得到任何响应。 例如。单击注销链接将触发对背景图像,徽标,几个css文件等的请求。 除了徽标文件之外,所有文件都会成功返回,http请求只会挂起。 这种情况并不总是会发生,但似乎是因为在启动新的请求之前没有等待先前的请求完成。

为了弄清楚发生了什么,我将LoggingHandlers添加到Vert.x DefaultHttpServer创建的netty管道中。 下面的示例显示了file logo.png(大小为12754B)的成功和不成功请求的输出。 输出给我留下了两个问题:

  1. 在成功示例中,在写入实际文件的12754字节后,写入另外12774 + 10个字节。这些字节是什么?
  2. 在不成功的示例中,在WRITE操作之前存在FLUSH操作,之后不再能够看到活动。即使明确关闭响应也会被忽略。发生了什么事?
  3. 关于问题1的

    EDIT:我意识到该通道用于多个HTTP请求,因此尾随字节只是正在写入的另一个文件。

    请求:

    GET /authenticate/res/images/logo.png HTTP/1.1
    Host: 192.168.0.12:18443
    Connection: keep-alive
    Pragma: no-cache
    Cache-Control: no-cache
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
    Accept: image/webp,image/apng,image/*,*/*;q=0.8
    Referer: https://192.168.0.12:18443/authenticate/res/css/login.css
    Accept-Encoding: gzip, deflate, br
    Accept-Language: en-GB,en-US;q=0.8,en;q=0.6
    

    成功时输出:

    $ grep 0xafd76300 /var/log/server.log
    2017-09-06 15:17:12,609 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] REGISTERED
    2017-09-06 15:17:12,609 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] REGISTERED
    2017-09-06 15:17:12,609 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] ACTIVE
    2017-09-06 15:17:12,609 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] ACTIVE
    2017-09-06 15:17:12,625 [SslHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] HANDSHAKEN: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    2017-09-06 15:17:12,625 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] USER_EVENT: io.netty.handler.ssl.SslHandshakeCompletionEvent@38f7a12a
    2017-09-06 15:17:12,625 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] USER_EVENT: io.netty.handler.ssl.SslHandshakeCompletionEvent@38f7a12a
    2017-09-06 15:17:12,629 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] RECEIVED: DefaultHttpRequest(decodeResult: success)
    2017-09-06 15:17:12,633 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] RECEIVED(0B)
    2017-09-06 15:17:12,634 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] WRITE(12754B)
    2017-09-06 15:17:12,634 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] FLUSH
    2017-09-06 15:17:12,635 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] WRITE(0B)
    2017-09-06 15:17:12,635 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] WRITE(12774B)
    2017-09-06 15:17:12,635 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] FLUSH
    2017-09-06 15:17:12,636 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] WRITE(0B)
    2017-09-06 15:17:12,636 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] FLUSH
    2017-09-06 15:17:12,636 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] WRITE(10B)
    2017-09-06 15:17:12,636 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] WRITE(0B)
    2017-09-06 15:17:12,636 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] FLUSH
    2017-09-06 15:17:12,636 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] CLOSE()
    2017-09-06 15:17:12,636 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 => /172.16.238.11:8443] CLOSE()
    2017-09-06 15:17:12,636 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 :> /172.16.238.11:8443] INACTIVE
    2017-09-06 15:17:12,637 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 :> /172.16.238.11:8443] UNREGISTERED
    2017-09-06 15:17:12,637 [LoggingHandler] DEBUG: [id: 0xafd76300, /192.168.10.113:60180 :> /172.16.238.11:8443] UNREGISTERED
    

    不成功时的输出:

    $ grep 0x09db306c /var/log/server.log
    2017-09-06 15:17:25,569 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] REGISTERED
    2017-09-06 15:17:25,569 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] REGISTERED
    2017-09-06 15:17:25,569 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] ACTIVE
    2017-09-06 15:17:25,569 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] ACTIVE
    2017-09-06 15:17:25,571 [SslHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] HANDSHAKEN: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    2017-09-06 15:17:25,571 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] USER_EVENT: io.netty.handler.ssl.SslHandshakeCompletionEvent@38f7a12a
    2017-09-06 15:17:25,572 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] USER_EVENT: io.netty.handler.ssl.SslHandshakeCompletionEvent@38f7a12a
    2017-09-06 15:17:25,608 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] RECEIVED: DefaultHttpRequest(decodeResult: success)
    2017-09-06 15:17:25,608 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] RECEIVED(0B)
    2017-09-06 15:17:25,609 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] FLUSH
    2017-09-06 15:17:25,610 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] WRITE(12754B)
    2017-09-06 15:17:25,610 [LoggingHandler] DEBUG: [id: 0x09db306c, /192.168.10.113:60300 => /172.16.238.11:8443] WRITE(0B)
    

    在HttpServer上注册的请求处理程序如下所示:

    Vertx vertx = ...
    HttpServerRequest req = ...
    HttpServerResponse response = req.response();
    vertx.fileSystem().readFile(file, (AsyncResult<Buffer> e) -> {
        try {
            if (e.succeeded()) {
                Buffer buf = e.result();
                response.putHeader("Content-Type", MimeMapper.getTypeForFile(file)+";charset=UTF-8");
                response.putHeader("Content-Length", Integer.toString(buf.length()));
                response.write(buf);
                response.end();
                response.close();
                LOG.trace("Write success (%d) %s...", buf.length(), file);
            } else {
               LOG.error("Write failure", e.cause());
            }
        } catch (Exception x) {
            LOG.error("Write failure", x);
        }
    });
    

    编辑:调用response.close()具有立即取消注册和取消激活频道的效果。如果没有该呼叫,则在最后一次写入后约5分钟自动触发取消注册。在不成功的情况下,永远不会触发取消注册,通道将无限期地保持在活动模式。

    我通过添加两个LoggingHandler修改了Vert.x 2 DefaultHttpServer(版本2.1.6,ssl和压缩激活)中的Netty管道:

    ServerBootstrap bootstrap = new ServerBootstrap();
    bootstrap.group(availableWorkers);
    bootstrap.channel(NioServerSocketChannel.class);
    tcpHelper.applyConnectionOptions(bootstrap);
    tcpHelper.checkSSL(vertx);
    bootstrap.childHandler(new ChannelInitializer<Channel>() {
        @Override
        protected void initChannel(Channel ch) throws Exception {
          ChannelPipeline pipeline = ch.pipeline();
          if (tcpHelper.isSSL()) {
            pipeline.addLast("ssl", tcpHelper.createSslHandler(vertx, false));
          }
          pipeline.addLast("flashpolicy", new FlashPolicyHandler());
          pipeline.addLast("httpDecoder", new HttpRequestDecoder(4096, 8192, 8192, false));
          pipeline.addLast("httpEncoder", new VertxHttpResponseEncoder());
          pipeline.addLast("reqlog", new LoggingHandler());
          if (compressionSupported) {
            pipeline.addLast("deflater", new HttpChunkContentCompressor());
          }
          if (tcpHelper.isSSL() || compressionSupported) {
            // only add ChunkedWriteHandler when SSL is enabled otherwise it is not needed as FileRegion is used.
            pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());       // For large file / sendfile support
          }
          pipeline.addLast("handler", new ServerHandler());
          pipeline.addLast("rsplog", new LoggingHandler());
        }
    });
    

1 个答案:

答案 0 :(得分:1)

所以我似乎找到了错误的来源。对我来说,它看起来像是一个经典的并发问题。我通过更改vertx-core版本2.1.6中的单行代码来修复它,将write操作更改为writeAndFlush。该修复可能绕过了原作者的意图,并将导致在netty频道上进行一些不必要的刷新操作。

这就是我认为正在发生的事情。要在代码中遵循我的推理,请查看2.1.6中的org.vertx.java.core.net.impl.ConnectionBase。涉及两个标志:readneedsFlush。我在这里假设有两个线程同时访问该通道。

  1. Vert.x HttpServer收到http请求,打开Netty频道并成功读取整个请求。处理程序在读取时处于READ模式,然后返回WRITE模式,刷新写缓冲区:read = falseneedsFlush = false
  2. 在Vertx HttpServer开始将响应写入第一个请求之前,它会收到另一个http请求并再次开始读取。这使处理程序处于READ模式:read = true
  3. http处理程序现在想要将响应写入第一个请求,但是从read = true开始,写入将通过设置needsFlush = true开始,并准备对响应进行排队。
  4. 但是,在任何字节排队之前,读取线程再次接管,完成读取第二个请求,从而设置read = false,刷新(空)写缓冲区(自needsFlush = true起)并设置{ {1}}。
  5. 现在写作线程又回来了。由于它已经过去检查needsFlush = false标志,它只是将字节写入通道而不进行刷新。现在永远不会刷新频道,因为不会再发生read
  6. 要解决此问题,我已更新

    endReadAndFlush

    执行org.vertx.java.core.http.impl.VertxHttpHandler.write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) 而非ctx.writeAndFlush(msg, promise)

    可能有一些我忽略的东西,这将解决问题而无需修补vertx2。也许可以配置vertx2为每个http请求创建一个新的netty通道,而不是为多个请求重用相同的通道?如果有人可以提议,我很乐意接受这样一个替代解决方案。

    编辑:也许我应该补充一点,我在重新编译Vertx2时将Netty升级到4.0.51。但两个版本的问题都是一样的,升级后可能会有点糟糕。