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 2020/12/23 10:46:20 UTC

[tomcat] branch master updated: Add support for Unix domain sockets for NIO

This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/master by this push:
     new 884b997  Add support for Unix domain sockets for NIO
884b997 is described below

commit 884b997f5a9a7da9f696d00574d3b727afbfae8c
Author: remm <re...@apache.org>
AuthorDate: Wed Dec 23 11:45:58 2020 +0100

    Add support for Unix domain sockets for NIO
    
    This requires Java 16 or later, and NIO (NIO2 did not get the feature).
    This does not remove the socket on shutdown, not sure what the best
    behavior is there. The socket is closed and Java doesn't do anything
    about that. This uses a bit of reflection, maybe the
    unixDomainSocketPath attribute can be added to avoid that, if the
    feature is actually popular.
    When using the feature, the JMX and thread names are slightly adjusted,
    and using the port attribute is optional.
    Based on a PR submitted by Graham Leggett
    https://github.com/apache/tomcat/pull/382
---
 java/org/apache/catalina/connector/Connector.java  |  56 ++++++-----
 java/org/apache/coyote/AbstractProtocol.java       |  31 +++---
 .../org/apache/tomcat/util/compat/Jre16Compat.java |  15 +++
 java/org/apache/tomcat/util/compat/JreCompat.java  |   9 ++
 .../apache/tomcat/util/net/AbstractEndpoint.java   |   2 +-
 java/org/apache/tomcat/util/net/NioEndpoint.java   | 107 +++++++++++++++++++--
 webapps/docs/changelog.xml                         |   7 ++
 webapps/docs/config/http.xml                       |  21 ++++
 8 files changed, 205 insertions(+), 43 deletions(-)

diff --git a/java/org/apache/catalina/connector/Connector.java b/java/org/apache/catalina/connector/Connector.java
index c2e7e25..4f4be1c 100644
--- a/java/org/apache/catalina/connector/Connector.java
+++ b/java/org/apache/catalina/connector/Connector.java
@@ -950,23 +950,30 @@ public class Connector extends LifecycleMBeanBase  {
 
         StringBuilder sb = new StringBuilder("type=");
         sb.append(type);
-        sb.append(",port=");
-        int port = getPortWithOffset();
-        if (port > 0) {
-            sb.append(port);
+        Object path = getProperty("unixDomainSocketPath");
+        if (path != null) {
+            // Maintain MBean name compatibility, even if not accurate
+            sb.append(",port=0,address=");
+            sb.append(ObjectName.quote(path.toString()));
         } else {
-            sb.append("auto-");
-            sb.append(getProperty("nameIndex"));
-        }
-        String address = "";
-        if (addressObj instanceof InetAddress) {
-            address = ((InetAddress) addressObj).getHostAddress();
-        } else if (addressObj != null) {
-            address = addressObj.toString();
-        }
-        if (address.length() > 0) {
-            sb.append(",address=");
-            sb.append(ObjectName.quote(address));
+            sb.append(",port=");
+            int port = getPortWithOffset();
+            if (port > 0) {
+                sb.append(port);
+            } else {
+                sb.append("auto-");
+                sb.append(getProperty("nameIndex"));
+            }
+            String address = "";
+            if (addressObj instanceof InetAddress) {
+                address = ((InetAddress) addressObj).getHostAddress();
+            } else if (addressObj != null) {
+                address = addressObj.toString();
+            }
+            if (address.length() > 0) {
+                sb.append(",address=");
+                sb.append(ObjectName.quote(address));
+            }
         }
         return sb.toString();
     }
@@ -1059,7 +1066,7 @@ public class Connector extends LifecycleMBeanBase  {
     protected void startInternal() throws LifecycleException {
 
         // Validate settings before starting
-        if (getPortWithOffset() < 0) {
+        if (getProperty("unixDomainSocketPath") == null && getPortWithOffset() < 0) {
             throw new LifecycleException(sm.getString(
                     "coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset())));
         }
@@ -1125,12 +1132,17 @@ public class Connector extends LifecycleMBeanBase  {
         StringBuilder sb = new StringBuilder("Connector[");
         sb.append(getProtocol());
         sb.append('-');
-        int port = getPortWithOffset();
-        if (port > 0) {
-            sb.append(port);
+        Object path = getProperty("unixDomainSocketPath");
+        if (path != null) {
+            sb.append(path.toString());
         } else {
-            sb.append("auto-");
-            sb.append(getProperty("nameIndex"));
+            int port = getPortWithOffset();
+            if (port > 0) {
+                sb.append(port);
+            } else {
+                sb.append("auto-");
+                sb.append(getProperty("nameIndex"));
+            }
         }
         sb.append(']');
         return sb.toString();
diff --git a/java/org/apache/coyote/AbstractProtocol.java b/java/org/apache/coyote/AbstractProtocol.java
index d4d2d55..bbe393c 100644
--- a/java/org/apache/coyote/AbstractProtocol.java
+++ b/java/org/apache/coyote/AbstractProtocol.java
@@ -347,22 +347,27 @@ public abstract class AbstractProtocol<S> implements ProtocolHandler,
     private String getNameInternal() {
         StringBuilder name = new StringBuilder(getNamePrefix());
         name.append('-');
-        if (getAddress() != null) {
-            name.append(getAddress().getHostAddress());
-            name.append('-');
-        }
-        int port = getPortWithOffset();
-        if (port == 0) {
-            // Auto binding is in use. Check if port is known
-            name.append("auto-");
-            name.append(getNameIndex());
-            port = getLocalPort();
-            if (port != -1) {
+        String path = getProperty("unixDomainSocketPath");
+        if (path != null) {
+            name.append(path);
+        } else {
+            if (getAddress() != null) {
+                name.append(getAddress().getHostAddress());
                 name.append('-');
+            }
+            int port = getPortWithOffset();
+            if (port == 0) {
+                // Auto binding is in use. Check if port is known
+                name.append("auto-");
+                name.append(getNameIndex());
+                port = getLocalPort();
+                if (port != -1) {
+                    name.append('-');
+                    name.append(port);
+                }
+            } else {
                 name.append(port);
             }
-        } else {
-            name.append(port);
         }
         return name.toString();
     }
diff --git a/java/org/apache/tomcat/util/compat/Jre16Compat.java b/java/org/apache/tomcat/util/compat/Jre16Compat.java
index 406824f..c142417 100644
--- a/java/org/apache/tomcat/util/compat/Jre16Compat.java
+++ b/java/org/apache/tomcat/util/compat/Jre16Compat.java
@@ -22,6 +22,7 @@ import java.net.ProtocolFamily;
 import java.net.SocketAddress;
 import java.net.StandardProtocolFamily;
 import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
 
 import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
@@ -35,15 +36,18 @@ class Jre16Compat extends Jre9Compat {
     private static final Class<?> unixDomainSocketAddressClazz;
     private static final Method openServerSocketChannelFamilyMethod;
     private static final Method unixDomainSocketAddressOfMethod;
+    private static final Method openSocketChannelFamilyMethod;
 
     static {
         Class<?> c1 = null;
         Method m1 = null;
         Method m2 = null;
+        Method m3 = null;
         try {
             c1 = Class.forName("java.net.UnixDomainSocketAddress");
             m1 = ServerSocketChannel.class.getMethod("open", ProtocolFamily.class);
             m2 = c1.getMethod("of", String.class);
+            m3 = SocketChannel.class.getMethod("open", ProtocolFamily.class);
         } catch (ClassNotFoundException e) {
             if (c1 == null) {
                 // Must be pre-Java 16
@@ -56,6 +60,7 @@ class Jre16Compat extends Jre9Compat {
         unixDomainSocketAddressClazz = c1;
         openServerSocketChannelFamilyMethod = m1;
         unixDomainSocketAddressOfMethod = m2;
+        openSocketChannelFamilyMethod = m3;
     }
 
     static boolean isSupported() {
@@ -82,4 +87,14 @@ class Jre16Compat extends Jre9Compat {
         }
     }
 
+    @Override
+    public SocketChannel openUnixDomainSocketChannel() {
+        try {
+            return (SocketChannel) openSocketChannelFamilyMethod.invoke
+                    (null, StandardProtocolFamily.valueOf("UNIX"));
+        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+            throw new UnsupportedOperationException(e);
+        }
+    }
+
 }
diff --git a/java/org/apache/tomcat/util/compat/JreCompat.java b/java/org/apache/tomcat/util/compat/JreCompat.java
index 543eb52..b12f80b 100644
--- a/java/org/apache/tomcat/util/compat/JreCompat.java
+++ b/java/org/apache/tomcat/util/compat/JreCompat.java
@@ -25,6 +25,7 @@ import java.net.SocketAddress;
 import java.net.URL;
 import java.net.URLConnection;
 import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
 import java.util.Deque;
 import java.util.jar.JarFile;
 
@@ -311,4 +312,12 @@ public class JreCompat {
         throw new UnsupportedOperationException(sm.getString("jreCompat.noUnixDomainSocket"));
     }
 
+    /**
+     * Create socket channel using the specified socket domain socket address.
+     * @return the socket channel
+     */
+    public SocketChannel openUnixDomainSocketChannel() {
+        throw new UnsupportedOperationException(sm.getString("jreCompat.noUnixDomainSocket"));
+    }
+
 }
diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
index 1dcdb2a..fe9957b 100644
--- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
@@ -942,7 +942,7 @@ public abstract class AbstractEndpoint<S,U> {
     /**
      * Unlock the server socket acceptor threads using bogus connections.
      */
-    private void unlockAccept() {
+    protected void unlockAccept() {
         // Only try to unlock the acceptor if it is necessary
         if (acceptor == null || acceptor.getState() != AcceptorState.RUNNING) {
             return;
diff --git a/java/org/apache/tomcat/util/net/NioEndpoint.java b/java/org/apache/tomcat/util/net/NioEndpoint.java
index 328a53a..a1743a4 100644
--- a/java/org/apache/tomcat/util/net/NioEndpoint.java
+++ b/java/org/apache/tomcat/util/net/NioEndpoint.java
@@ -22,6 +22,7 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.SocketAddress;
 import java.net.SocketTimeoutException;
 import java.nio.ByteBuffer;
 import java.nio.channels.CancelledKeyException;
@@ -35,8 +36,15 @@ import java.nio.channels.Selector;
 import java.nio.channels.ServerSocketChannel;
 import java.nio.channels.SocketChannel;
 import java.nio.channels.WritableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
 import java.util.ConcurrentModificationException;
 import java.util.Iterator;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
@@ -51,6 +59,7 @@ import org.apache.tomcat.util.collections.SynchronizedQueue;
 import org.apache.tomcat.util.collections.SynchronizedStack;
 import org.apache.tomcat.util.compat.JreCompat;
 import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
+import org.apache.tomcat.util.net.Acceptor.AcceptorState;
 import org.apache.tomcat.util.net.jsse.JSSESupport;
 
 /**
@@ -111,6 +120,27 @@ public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel>
     public void setUseInheritedChannel(boolean useInheritedChannel) { this.useInheritedChannel = useInheritedChannel; }
     public boolean getUseInheritedChannel() { return useInheritedChannel; }
 
+
+    /**
+     * Path for the Unix domain socket, used to create the socket address.
+     */
+    private String unixDomainSocketPath = null;
+    public String getUnixDomainSocketPath() { return this.unixDomainSocketPath; }
+    public void setUnixDomainSocketPath(String unixDomainSocketPath) {
+        this.unixDomainSocketPath = unixDomainSocketPath;
+    }
+
+
+    /**
+     * Permissions which will be set on the Unix domain socket if it is created.
+     */
+    private String unixDomainSocketPathPermissions = null;
+    public String getUnixDomainSocketPathPermissions() { return this.unixDomainSocketPathPermissions; }
+    public void setUnixDomainSocketPathPermissions(String unixDomainSocketPathPermissions) {
+        this.unixDomainSocketPathPermissions = unixDomainSocketPathPermissions;
+    }
+
+
     /**
      * Priority of the poller thread.
      */
@@ -164,12 +194,7 @@ public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel>
     // Separated out to make it easier for folks that extend NioEndpoint to
     // implement custom [server]sockets
     protected void initServerSocket() throws Exception {
-        if (!getUseInheritedChannel()) {
-            serverSock = ServerSocketChannel.open();
-            socketProperties.setProperties(serverSock.socket());
-            InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
-            serverSock.socket().bind(addr,getAcceptCount());
-        } else {
+        if (getUseInheritedChannel()) {
             // Retrieve the channel provided by the OS
             Channel ic = System.inheritedChannel();
             if (ic instanceof ServerSocketChannel) {
@@ -178,6 +203,28 @@ public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel>
             if (serverSock == null) {
                 throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
             }
+        } else if (getUnixDomainSocketPath() != null) {
+            SocketAddress sa = JreCompat.getInstance().getUnixDomainSocketAddress(getUnixDomainSocketPath());
+            serverSock = JreCompat.getInstance().openUnixDomainServerSocketChannel();
+            serverSock.bind(sa, getAcceptCount());
+            if (getUnixDomainSocketPathPermissions() != null) {
+                FileAttribute<Set<PosixFilePermission>> attrs =
+                        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString(getUnixDomainSocketPathPermissions()));
+                Path path = Paths.get(getUnixDomainSocketPath());
+                if (attrs != null) {
+                    Files.setAttribute(path, attrs.name(), attrs.value());
+                } else {
+                    java.io.File file = path.toFile();
+                    file.setReadable(true, false);
+                    file.setWritable(true, false);
+                    file.setExecutable(false, false);
+                }
+            }
+        } else {
+            serverSock = ServerSocketChannel.open();
+            socketProperties.setProperties(serverSock.socket());
+            InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
+            serverSock.bind(addr, getAcceptCount());
         }
         serverSock.configureBlocking(true); //mimic APR behavior
     }
@@ -304,8 +351,43 @@ public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel>
         serverSock = null;
     }
 
+
     // ------------------------------------------------------ Protected Methods
 
+
+    @Override
+    protected void unlockAccept() {
+        if (getUnixDomainSocketPath() == null) {
+            super.unlockAccept();
+        } else {
+            // Only try to unlock the acceptor if it is necessary
+            if (acceptor == null || acceptor.getState() != AcceptorState.RUNNING) {
+                return;
+            }
+            try {
+                SocketAddress sa = JreCompat.getInstance().getUnixDomainSocketAddress(getUnixDomainSocketPath());
+                try (SocketChannel socket = JreCompat.getInstance().openUnixDomainSocketChannel()) {
+                    // With a UDS, expect no delay connecting and no defer accept
+                    socket.connect(sa);
+                }
+                // Wait for upto 1000ms acceptor threads to unlock
+                long waitLeft = 1000;
+                while (waitLeft > 0 &&
+                        acceptor.getState() == AcceptorState.RUNNING) {
+                    Thread.sleep(5);
+                    waitLeft -= 5;
+                }
+            } catch(Throwable t) {
+                ExceptionUtils.handleThrowable(t);
+                if (getLog().isDebugEnabled()) {
+                    getLog().debug(sm.getString(
+                            "endpoint.debug.unlock.fail", String.valueOf(getPortWithOffset())), t);
+                }
+            }
+        }
+    }
+
+
     protected SynchronizedStack<NioChannel> getNioChannels() {
         return nioChannels;
     }
@@ -361,7 +443,9 @@ public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel>
             // Set socket properties
             // Disable blocking, polling will be used
             socket.configureBlocking(false);
-            socketProperties.setProperties(socket.socket());
+            if (getUnixDomainSocketPath() == null) {
+                socketProperties.setProperties(socket.socket());
+            }
 
             socketWrapper.setReadTimeout(getConnectionTimeout());
             socketWrapper.setWriteTimeout(getConnectionTimeout());
@@ -983,6 +1067,15 @@ public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel>
 
         public NioSocketWrapper(NioChannel channel, NioEndpoint endpoint) {
             super(channel, endpoint);
+            if (endpoint.getUnixDomainSocketPath() != null) {
+                // Pretend localhost for easy compatibility
+                localAddr = "127.0.0.1";
+                localName = "localhost";
+                localPort = 0;
+                remoteAddr = "127.0.0.1";
+                remoteHost = "localhost";
+                remotePort = 0;
+            }
             nioChannels = endpoint.getNioChannels();
             poller = endpoint.getPoller();
             socketBufferHandler = channel.getBufHandler();
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 89c8d18..7576672 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -151,6 +151,13 @@
         combination with the Servlet non-blocking IO API. It was possible that
         some requests could get dropped. (markt)
       </fix>
+      <add>
+        Add support for using Unix domain sockets for NIO when running
+        on Java 16 or later. This uses NIO specific
+        <code>unixDomainSocketPath</code> and
+        <code>unixDomainSocketPathPermissions</code> attributes.
+        Based on a PR submitted by Graham Leggett. (remm)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Jasper">
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 1616aa8..32c300b 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -894,6 +894,27 @@
         <code>-1</code> for unlimited cache and <code>0</code> for no cache.</p>
       </attribute>
 
+      <attribute name="unixDomainSocketPath" required="false">
+        <p>Where supported, the path to a Unix Domain Socket that this
+        <strong>Connector</strong> will create and await incoming connections.
+        Tomcat will NOT automatically remove the socket on server shutdown.
+        If the socket already exists, care must be taken by the administrator
+        to remove the socket after verifying that the socket isn't already
+        being used by an existing Tomcat process. Using this requires
+        Java 16 or later. When this is specified, the otherwise mandatory
+        <code>port</code> attribute may be omitted.</p>
+      </attribute>
+
+      <attribute name="unixDomainSocketPathPermissions" required="false">
+        <p>Where supported, the posix permissions that will be applied to the
+        to the Unix Domain Socket specified with
+        <code>unixDomainSocketPath</code> above. The
+        permissions are specified as a string of nine characters, in three sets
+        of three: (r)ead, (w)rite and e(x)ecute for owner, group and others
+        respectively. If a permission is not granted, a hyphen is used. If
+        unspecified, the permissions default to <code>rw-rw-rw-</code>.</p>
+      </attribute>
+
       <attribute name="useInheritedChannel" required="false">
         <p>(bool)Defines if this connector should inherit an inetd/systemd network socket.
         Only one connector can inherit a network socket. This can option can be


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