You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2017/10/31 17:45:30 UTC

svn commit: r1813918 - in /tomcat/trunk: java/org/apache/coyote/http2/ webapps/docs/

Author: remm
Date: Tue Oct 31 17:45:30 2017
New Revision: 1813918

URL: http://svn.apache.org/viewvc?rev=1813918&view=rev
Log:
Add sendfile support for HTTP/2 NIO2 using mapped files. Performance seems to be improved with h2load.

Modified:
    tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
    tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java
    tomcat/trunk/java/org/apache/coyote/http2/Stream.java
    tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java
    tomcat/trunk/webapps/docs/changelog.xml

Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java?rev=1813918&r1=1813917&r2=1813918&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java Tue Oct 31 17:45:30 2017
@@ -16,9 +16,14 @@
  */
 package org.apache.coyote.http2;
 
+import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
 import java.nio.channels.CompletionHandler;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -27,8 +32,12 @@ import org.apache.coyote.Adapter;
 import org.apache.coyote.ProtocolException;
 import org.apache.coyote.Request;
 import org.apache.tomcat.util.http.MimeHeaders;
+import org.apache.tomcat.util.net.SendfileState;
 import org.apache.tomcat.util.net.SocketWrapperBase;
 import org.apache.tomcat.util.net.SocketWrapperBase.BlockingMode;
+import org.apache.tomcat.util.net.SocketWrapperBase.CompletionCheck;
+import org.apache.tomcat.util.net.SocketWrapperBase.CompletionHandlerCall;
+import org.apache.tomcat.util.net.SocketWrapperBase.CompletionState;
 
 public class Http2AsyncUpgradeHandler extends Http2UpgradeHandler {
 
@@ -69,6 +78,11 @@ public class Http2AsyncUpgradeHandler ex
     }
 
     @Override
+    boolean hasAsyncIO() {
+        return true;
+    }
+
+    @Override
     protected void writeSettings() {
         // Send the initial settings frame
         socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(),
@@ -230,6 +244,159 @@ public class Http2AsyncUpgradeHandler ex
         }
     }
 
+    @Override
+    protected void processWrites() throws IOException {
+        if (socketWrapper.isWritePending()) {
+            socketWrapper.registerWriteInterest();
+        }
+    }
+
+    @Override
+    protected SendfileState processSendfile(Stream stream) {
+        String fileName = (String) stream.getCoyoteRequest().getAttribute(
+                org.apache.coyote.Constants.SENDFILE_FILENAME_ATTR);
+        if (fileName != null) {
+            java.nio.file.Path path = new File(fileName).toPath();
+            SendfileData sendfile = new SendfileData();
+            sendfile.pos = ((Long) stream.getCoyoteRequest().getAttribute(
+                    org.apache.coyote.Constants.SENDFILE_FILE_START_ATTR)).longValue();
+            sendfile.end = ((Long) stream.getCoyoteRequest().getAttribute(
+                    org.apache.coyote.Constants.SENDFILE_FILE_END_ATTR)).longValue();
+            sendfile.left = sendfile.end - sendfile.pos;
+            try {
+                try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
+                    sendfile.mappedBuffer = channel.map(MapMode.READ_ONLY, sendfile.pos, sendfile.end - sendfile.pos);
+                    sendfile.stream = stream;
+                }
+                // Reserve as much as possible right away
+                int reservation = (sendfile.end - sendfile.pos > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) (sendfile.end - sendfile.pos);
+                sendfile.streamReservation  = stream.reserveWindowSize(reservation, true);
+                sendfile.connectionReservation = reserveWindowSize(stream, sendfile.streamReservation);
+            } catch (IOException e) {
+                return SendfileState.ERROR;
+            }
+            // Actually perform the write
+            int frameSize = Integer.min(getMaxFrameSize(), sendfile.connectionReservation);
+            boolean finished = (frameSize == sendfile.left) && stream.getCoyoteResponse().getTrailerFields() == null;
+
+            // Need to check this now since sending end of stream will change this.
+            boolean writeable = stream.canWrite();
+            byte[] header = new byte[9];
+            ByteUtil.setThreeBytes(header, 0, frameSize);
+            header[3] = FrameType.DATA.getIdByte();
+            if (finished) {
+                header[4] = FLAG_END_OF_STREAM;
+                stream.sentEndOfStream();
+                if (!stream.isActive()) {
+                    activeRemoteStreamCount.decrementAndGet();
+                }
+            }
+            if (writeable) {
+                ByteUtil.set31Bits(header, 5, stream.getIdentifier().intValue());
+                sendfile.mappedBuffer.limit(sendfile.mappedBuffer.position() + frameSize);
+                socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(),
+                        TimeUnit.MILLISECONDS, sendfile, COMPLETE_WRITE_WITH_COMPLETION,
+                        new SendfileCompletionHandler(), ByteBuffer.wrap(header), sendfile.mappedBuffer);
+                try {
+                    handleAsyncException();
+                } catch (IOException e) {
+                    return SendfileState.ERROR;
+                }
+            }
+            return SendfileState.PENDING;
+        } else {
+            return SendfileState.DONE;
+        }
+    }
+
+    private static final CompletionCheck COMPLETE_WRITE_WITH_COMPLETION = new CompletionCheck() {
+        @Override
+        public CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers,
+                int offset, int length) {
+            for (int i = 0; i < length; i++) {
+                if (buffers[offset + i].remaining() > 0) {
+                    return CompletionHandlerCall.CONTINUE;
+                }
+            }
+            return CompletionHandlerCall.DONE;
+        }
+    };
+
+    protected class SendfileCompletionHandler implements CompletionHandler<Long, SendfileData> {
+        @Override
+        public void completed(Long nBytes, SendfileData sendfile) {
+            long bytesWritten = nBytes.longValue() - 9;
+            sendfile.left -= bytesWritten;
+            if (sendfile.left == 0) {
+                try {
+                    sendfile.stream.getOutputBuffer().close();
+                } catch (IOException e) {
+                    failed(e, sendfile);
+                }
+                return;
+            }
+            sendfile.streamReservation -= bytesWritten;
+            sendfile.connectionReservation -= bytesWritten;
+            sendfile.pos += bytesWritten;
+            try {
+                if (sendfile.connectionReservation == 0) {
+                    if (sendfile.streamReservation == 0) {
+                        int reservation = (sendfile.end - sendfile.pos > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) (sendfile.end - sendfile.pos);
+                        sendfile.streamReservation = sendfile.stream.reserveWindowSize(reservation, true);
+                    }
+                    sendfile.connectionReservation = reserveWindowSize(sendfile.stream, sendfile.streamReservation);
+                }
+            } catch (IOException e) {
+                failed (e, sendfile);
+                return;
+            }
+            int frameSize = Integer.min(getMaxFrameSize(), sendfile.streamReservation);
+            boolean finished = (frameSize == sendfile.left) && sendfile.stream.getCoyoteResponse().getTrailerFields() == null;
+
+            // Need to check this now since sending end of stream will change this.
+            boolean writeable = sendfile.stream.canWrite();
+            byte[] header = new byte[9];
+            ByteUtil.setThreeBytes(header, 0, frameSize);
+            header[3] = FrameType.DATA.getIdByte();
+            if (finished) {
+                header[4] = FLAG_END_OF_STREAM;
+                sendfile.stream.sentEndOfStream();
+                if (!sendfile.stream.isActive()) {
+                    activeRemoteStreamCount.decrementAndGet();
+                }
+            }
+            if (writeable) {
+                ByteUtil.set31Bits(header, 5, sendfile.stream.getIdentifier().intValue());
+                sendfile.mappedBuffer.limit(sendfile.mappedBuffer.position() + frameSize);
+                socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(),
+                        TimeUnit.MILLISECONDS, sendfile, COMPLETE_WRITE_WITH_COMPLETION,
+                        this, ByteBuffer.wrap(header), sendfile.mappedBuffer);
+                try {
+                    handleAsyncException();
+                } catch (IOException e) {
+                    failed(e, sendfile);
+                }
+            }
+        }
+
+        @Override
+        public void failed(Throwable t, SendfileData sendfile) {
+            applicationErrorCompletion.failed(t, null);
+        }
+    }
+
+    protected class SendfileData {
+        protected Stream stream;
+        // Note: a mapped buffer is a special construct with an underlying file
+        // that doesn't need to be closed
+        protected MappedByteBuffer mappedBuffer;
+        protected int frameSize;
+        protected long left;
+        protected int streamReservation;
+        protected int connectionReservation;
+        protected long pos;
+        protected long end;
+    }
 
     protected class AsyncPingManager extends PingManager {
         @Override

Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java?rev=1813918&r1=1813917&r2=1813918&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java Tue Oct 31 17:45:30 2017
@@ -50,6 +50,7 @@ import org.apache.tomcat.util.codec.bina
 import org.apache.tomcat.util.http.MimeHeaders;
 import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
 import org.apache.tomcat.util.net.SSLSupport;
+import org.apache.tomcat.util.net.SendfileState;
 import org.apache.tomcat.util.net.SocketEvent;
 import org.apache.tomcat.util.net.SocketWrapperBase;
 import org.apache.tomcat.util.res.StringManager;
@@ -699,7 +700,12 @@ class Http2UpgradeHandler extends Abstra
     }
 
 
-    private void processWrites() throws IOException {
+    boolean hasAsyncIO() {
+        return false;
+    }
+
+
+    protected void processWrites() throws IOException {
         synchronized (socketWrapper) {
             if (socketWrapper.flush(false)) {
                 socketWrapper.registerWriteInterest();
@@ -799,6 +805,10 @@ class Http2UpgradeHandler extends Abstra
     }
 
 
+    protected SendfileState processSendfile(Stream stream) {
+        return SendfileState.DONE;
+    }
+
     private synchronized Set<AbstractStream> releaseBackLog(int increment) {
         Set<AbstractStream> result = new HashSet<>();
         if (backLogSize < increment) {

Modified: tomcat/trunk/java/org/apache/coyote/http2/Stream.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Stream.java?rev=1813918&r1=1813917&r2=1813918&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/Stream.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/Stream.java Tue Oct 31 17:45:30 2017
@@ -104,8 +104,7 @@ class Stream extends AbstractStream impl
             // TODO Assuming the body has been read at this point is not valid
             state.receivedEndOfStream();
         }
-        // No sendfile for HTTP/2 (it is enabled by default in the request)
-        this.coyoteRequest.setSendfile(false);
+        this.coyoteRequest.setSendfile(handler.hasAsyncIO());
         this.coyoteResponse.setOutputBuffer(outputBuffer);
         this.coyoteRequest.setResponse(coyoteResponse);
         this.coyoteRequest.protocol().setString("HTTP/2.0");
@@ -198,7 +197,7 @@ class Stream extends AbstractStream impl
     }
 
 
-    private final synchronized int reserveWindowSize(int reservation, boolean block)
+    final synchronized int reserveWindowSize(int reservation, boolean block)
             throws IOException {
         long windowSize = getWindowSize();
         while (windowSize < 1) {

Modified: tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java?rev=1813918&r1=1813917&r2=1813918&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java Tue Oct 31 17:45:30 2017
@@ -30,6 +30,7 @@ import org.apache.juli.logging.LogFactor
 import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
 import org.apache.tomcat.util.net.DispatchType;
+import org.apache.tomcat.util.net.SendfileState;
 import org.apache.tomcat.util.net.SocketEvent;
 import org.apache.tomcat.util.net.SocketWrapperBase;
 import org.apache.tomcat.util.res.StringManager;
@@ -41,6 +42,7 @@ class StreamProcessor extends AbstractPr
 
     private final Http2UpgradeHandler handler;
     private final Stream stream;
+    private SendfileState sendfileState = null;
 
 
     StreamProcessor(Http2UpgradeHandler handler, Stream stream, Adapter adapter,
@@ -102,7 +104,10 @@ class StreamProcessor extends AbstractPr
 
     @Override
     protected final void finishResponse() throws IOException {
-        stream.getOutputBuffer().close();
+        sendfileState = handler.processSendfile(stream);
+        if (!(sendfileState == SendfileState.PENDING)) {
+            stream.getOutputBuffer().close();
+        }
     }
 
 
@@ -261,7 +266,9 @@ class StreamProcessor extends AbstractPr
             setErrorState(ErrorState.CLOSE_NOW, e);
         }
 
-        if (getErrorState().isError()) {
+        if (sendfileState == SendfileState.PENDING) {
+            return SocketState.SENDFILE;
+        } else if (getErrorState().isError()) {
             action(ActionCode.CLOSE, null);
             request.updateCounters();
             return SocketState.CLOSED;

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1813918&r1=1813917&r2=1813918&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Tue Oct 31 17:45:30 2017
@@ -77,6 +77,9 @@
       <fix>
         Improve NIO2 syncing for async IO operations. (remm)
       </fix>
+      <add>
+        Sendfile support for HTTP/2 and NIO2. (remm)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Jasper">



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org