8198372: API to create a SelectableChannel to a FileDescriptor
authoralanb
Sat, 23 Jun 2018 09:34:09 +0100
changeset 50739 4bedc9338137
parent 50738 6cc2dc161c64
child 50740 edf11e5dc766
8198372: API to create a SelectableChannel to a FileDescriptor Reviewed-by: chegar, bpb
src/java.base/share/classes/module-info.java
src/jdk.net/share/classes/jdk/nio/Channels.java
src/jdk.net/share/classes/jdk/nio/package-info.java
src/jdk.net/share/classes/module-info.java
test/jdk/jdk/nio/Basic.java
--- a/src/java.base/share/classes/module-info.java	Sat Jun 23 08:03:52 2018 +0100
+++ b/src/java.base/share/classes/module-info.java	Sat Jun 23 09:34:09 2018 +0100
@@ -242,6 +242,7 @@
     exports sun.nio.ch to
         java.management,
         jdk.crypto.cryptoki,
+        jdk.net,
         jdk.sctp,
         jdk.unsupported;
     exports sun.nio.cs to
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.net/share/classes/jdk/nio/Channels.java	Sat Jun 23 09:34:09 2018 +0100
@@ -0,0 +1,263 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.nio;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.spi.AbstractSelectableChannel;
+import java.nio.channels.spi.SelectorProvider;
+import java.util.Objects;
+
+import sun.nio.ch.IOUtil;
+import sun.nio.ch.Net;
+import sun.nio.ch.SelChImpl;
+import sun.nio.ch.SelectionKeyImpl;
+import sun.nio.ch.SelectorProviderImpl;
+
+/**
+ * Defines static methods to create {@link java.nio.channels.Channel channels}.
+ *
+ * <p> Unless otherwise specified, passing a {@code null} argument to any of the
+ * methods defined here will cause a {@code NullPointerException} to be thrown.
+ *
+ * @since 11
+ */
+
+public final class Channels {
+    private Channels() { }
+
+    /**
+     * An object used to coordinate the closing of a selectable channel created
+     * by {@link Channels#readWriteSelectableChannel readWriteSelectableChannel}.
+     *
+     * @since 11
+     */
+    public interface SelectableChannelCloser {
+
+        /**
+         * Closes a selectable channel.
+         *
+         * <p> This method is invoked by the channel's close method in order to
+         * perform the actual work of closing the channel. This method is only
+         * invoked if the channel has not yet been closed, and it is never
+         * invoked more than once by the channel's close implementation.
+         *
+         * <p> An implementation of this method must arrange for any other
+         * thread that is blocked in an I/O operation upon the channel to return
+         * immediately, either by throwing an exception or by returning normally.
+         * If the channel is {@link SelectableChannel#isRegistered registered}
+         * with one or more {@link java.nio.channels.Selector Selector}s then
+         * the file descriptor should not be released until the {@link
+         * #implReleaseChannel implReleaseChannel} method is invoked. </p>
+         *
+         * @param  sc
+         *         The selectable channel
+         *
+         * @throws IOException
+         *         If an I/O error occurs while closing the file descriptor
+         *
+         * @see java.nio.channels.spi.AbstractInterruptibleChannel#implCloseChannel
+         */
+        void implCloseChannel(SelectableChannel sc) throws IOException;
+
+        /**
+         * Release the file descriptor and any resources for a selectable
+         * channel that closed while registered with one or more {@link
+         * java.nio.channels.Selector Selector}s.
+         *
+         * <p> This method is for cases where a channel is closed when
+         * {@link java.nio.channels.SelectableChannel#isRegistered registered}
+         * with one or more {@code Selector}s. A channel may remain registered
+         * for some time after it is closed. This method is invoked when the
+         * channel is eventually deregistered from the last {@code Selector}
+         * that it was registered with. It is invoked at most once.
+         *
+         * @apiNote This method is invoked while synchronized on the selector
+         * and its selected-key set. Great care must be taken to avoid deadlocks
+         * with other threads that also synchronize on these objects.
+         *
+         * @param  sc
+         *         The closed selectable channel
+         *
+         * @throws IOException
+         *         If an I/O error occurs
+         *
+         * @see java.nio.channels.spi.AbstractSelector#deregister
+         */
+        void implReleaseChannel(SelectableChannel sc) throws IOException;
+    }
+
+    /**
+     * Creates a selectable channel to a file descriptor that supports an
+     * {@link SelectableChannel#validOps() operation-set} of
+     * {@link SelectionKey#OP_READ OP_READ} and
+     * {@link SelectionKey#OP_WRITE OP_WRITE}. The selectable channel will be
+     * created by the default {@link SelectorProvider}.
+     *
+     * <p> The given file descriptor is a socket or resource that can be
+     * multiplexed by a {@link java.nio.channels.Selector} for read and write
+     * readiness. Great care is required to coordinate direct use of the file
+     * descriptor with the use of the selectable channel. In particular,
+     * changing the blocking mode or closing the file descriptor without careful
+     * coordination will result in unspecified and unsafe side effects. The
+     * given {@link SelectableChannelCloser SelectableChannelCloser} is invoked to
+     * close the file descriptor and to coordinate the closing when the channel
+     * is registered with a {@code Selector}. </p>
+     *
+     * <p> If there is a security manager set then its
+     * {@link SecurityManager#checkRead(FileDescriptor) checkRead} and
+     * {@link SecurityManager#checkWrite(FileDescriptor) checkWrite} methods
+     * are invoked to check that the caller has permission to both read from and
+     * write to the file descriptor. </p>
+     *
+     * @implNote This method throws {@code UnsupportedOperationException} if
+     * the default {@code SelectorProvider} is not the JDK built-in implementation.
+     *
+     * @param  fd
+     *         The file descriptor
+     * @param  closer
+     *         The object to close the channel
+     *
+     * @return The selectable channel
+     *
+     * @throws IllegalArgumentException
+     *         If the file descriptor is not {@link FileDescriptor#valid() valid}
+     * @throws SecurityException
+     *         If denied by the security manager
+     */
+    public static SelectableChannel readWriteSelectableChannel(FileDescriptor fd,
+                                                               SelectableChannelCloser closer) {
+        Objects.requireNonNull(closer);
+        if (!fd.valid())
+            throw new IllegalArgumentException("file descriptor is not valid");
+
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkRead(fd);
+            sm.checkWrite(fd);
+        }
+
+        SelectorProvider provider = SelectorProvider.provider();
+        if (!(provider instanceof SelectorProviderImpl))
+            throw new UnsupportedOperationException("custom SelectorProvider");
+
+        return new ReadWriteChannelImpl((SelectorProviderImpl)provider, fd, closer);
+    }
+
+    private static final class ReadWriteChannelImpl
+        extends AbstractSelectableChannel implements SelChImpl
+    {
+        private final FileDescriptor fd;
+        private final int fdVal;
+        private final SelectableChannelCloser closer;
+
+        ReadWriteChannelImpl(SelectorProviderImpl provider,
+                             FileDescriptor fd,
+                             SelectableChannelCloser closer) {
+            super(provider);
+            this.fd = fd;
+            this.fdVal = IOUtil.fdVal(fd);
+            this.closer = closer;
+        }
+
+        @Override
+        public FileDescriptor getFD() {
+            return fd;
+        }
+
+        @Override
+        public int getFDVal() {
+            return fdVal;
+        }
+
+        @Override
+        public int validOps() {
+            return (SelectionKey.OP_READ | SelectionKey.OP_WRITE);
+        }
+
+        private boolean translateReadyOps(int ops,
+                                          int initialOps,
+                                          SelectionKeyImpl ski) {
+            int intOps = ski.nioInterestOps();
+            int oldOps = ski.nioReadyOps();
+            int newOps = initialOps;
+
+            if ((ops & (Net.POLLERR | Net.POLLHUP)) != 0) {
+                newOps = intOps;
+                ski.nioReadyOps(newOps);
+                return (newOps & ~oldOps) != 0;
+            }
+
+            if (((ops & Net.POLLIN) != 0) &&
+                    ((intOps & SelectionKey.OP_READ) != 0))
+                newOps |= SelectionKey.OP_READ;
+
+            if (((ops & Net.POLLOUT) != 0) &&
+                    ((intOps & SelectionKey.OP_WRITE) != 0))
+                newOps |= SelectionKey.OP_WRITE;
+
+            ski.nioReadyOps(newOps);
+            return (newOps & ~oldOps) != 0;
+        }
+
+        @Override
+        public boolean translateAndUpdateReadyOps(int ops, SelectionKeyImpl ski) {
+            return translateReadyOps(ops, ski.nioReadyOps(), ski);
+        }
+
+        @Override
+        public boolean translateAndSetReadyOps(int ops, SelectionKeyImpl ski) {
+            return translateReadyOps(ops, 0, ski);
+        }
+
+        @Override
+        public int translateInterestOps(int ops) {
+            int newOps = 0;
+            if ((ops & SelectionKey.OP_READ) != 0)
+                newOps |= Net.POLLIN;
+            if ((ops & SelectionKey.OP_WRITE) != 0)
+                newOps |= Net.POLLOUT;
+            return newOps;
+        }
+
+        @Override
+        protected void implConfigureBlocking(boolean block) throws IOException {
+            IOUtil.configureBlocking(fd, block);
+        }
+
+        @Override
+        protected void implCloseSelectableChannel() throws IOException {
+            closer.implCloseChannel(this);
+        }
+
+        @Override
+        public void kill() throws IOException {
+            closer.implReleaseChannel(this);
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.net/share/classes/jdk/nio/package-info.java	Sat Jun 23 09:34:09 2018 +0100
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * Defines JDK-specific {@link java.nio.channels.Channel channel} APIs.
+ *
+ * @since 11
+ */
+
+package jdk.nio;
--- a/src/jdk.net/share/classes/module-info.java	Sat Jun 23 08:03:52 2018 +0100
+++ b/src/jdk.net/share/classes/module-info.java	Sat Jun 23 09:34:09 2018 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -31,5 +31,6 @@
  */
 module jdk.net {
     exports jdk.net;
+    exports jdk.nio;
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/jdk/nio/Basic.java	Sat Jun 23 09:34:09 2018 +0100
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8198372
+ * @modules jdk.net java.base/sun.nio.ch:+open
+ * @run testng Basic
+ * @summary Basic tests for jdk.nio.Channels
+ */
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+
+import jdk.nio.Channels;
+import jdk.nio.Channels.SelectableChannelCloser;
+
+import sun.nio.ch.IOUtil;
+
+import org.testng.annotations.Test;
+import static org.testng.Assert.*;
+
+@Test
+public class Basic {
+
+    /**
+     * A loopback connection
+     */
+    static class Connection implements Closeable {
+        private final SocketChannel sc1;
+        private final SocketChannel sc2;
+
+        private Connection(SocketChannel sc1, SocketChannel sc2) {
+            this.sc1 = sc1;
+            this.sc2 = sc2;
+        }
+
+        static Connection open() throws IOException {
+            try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
+                InetAddress lb = InetAddress.getLoopbackAddress();
+                ssc.bind(new InetSocketAddress(lb, 0));
+                SocketChannel sc1 = SocketChannel.open(ssc.getLocalAddress());
+                SocketChannel sc2 = ssc.accept();
+                return new Connection(sc1, sc2);
+            }
+        }
+
+        SocketChannel channel1() {
+            return sc1;
+        }
+
+        SocketChannel channel2() {
+            return sc2;
+        }
+
+        public void close() throws IOException {
+            try {
+                sc1.close();
+            } finally {
+                sc2.close();
+            }
+        }
+    }
+
+    /**
+     * A SelectableChannelCloser that tracks if the implCloseChannel and
+     * implReleaseChannel methods are invoked
+     */
+    static class Closer implements SelectableChannelCloser {
+        int closeCount;
+        SelectableChannel invokedToClose;
+        int releaseCount;
+        SelectableChannel invokedToRelease;
+
+        @Override
+        public void implCloseChannel(SelectableChannel sc) {
+            closeCount++;
+            invokedToClose = sc;
+        }
+
+        @Override
+        public void implReleaseChannel(SelectableChannel sc) {
+            releaseCount++;
+            invokedToRelease = sc;
+        }
+    }
+
+    /**
+     * Basic test of channel registered with Selector
+     */
+    public void testSelect() throws IOException {
+        Selector sel = Selector.open();
+        try (Connection connection = Connection.open()) {
+
+            // create channel with the file descriptor from one end of the connection
+            FileDescriptor fd = getFD(connection.channel1());
+            SelectableChannel ch = Channels.readWriteSelectableChannel(fd, new Closer());
+
+            // register for read events, channel should not be selected
+            ch.configureBlocking(false);
+            SelectionKey key = ch.register(sel, SelectionKey.OP_READ);
+            int n = sel.selectNow();
+            assertTrue(n == 0);
+
+            // write bytes to other end of connection
+            SocketChannel peer = connection.channel2();
+            ByteBuffer msg = ByteBuffer.wrap("hello".getBytes("UTF-8"));
+            int nwrote = peer.write(msg);
+            assertTrue(nwrote >= 0);
+
+            // channel should be selected
+            n = sel.select();
+            assertTrue(n == 1);
+            assertTrue(sel.selectedKeys().contains(key));
+            assertTrue(key.isReadable());
+            assertFalse(key.isWritable());
+            sel.selectedKeys().clear();
+
+            // change interest set for writing, channel should be selected
+            key.interestOps(SelectionKey.OP_WRITE);
+            n = sel.select();
+            assertTrue(n == 1);
+            assertTrue(sel.selectedKeys().contains(key));
+            assertTrue(key.isWritable());
+            assertFalse(key.isReadable());
+            sel.selectedKeys().clear();
+
+            // change interest set for reading + writing, channel should be selected
+            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
+            n = sel.select();
+            assertTrue(n == 1);
+            assertTrue(sel.selectedKeys().contains(key));
+            assertTrue(key.isWritable());
+            assertTrue(key.isReadable());
+            sel.selectedKeys().clear();
+
+            // change interest set to 0 to deregister, channel should not be selected
+            key.interestOps(0);
+            n = sel.selectNow();
+            assertTrue(n == 0);
+
+        } finally {
+            sel.close();
+        }
+    }
+
+    /**
+     * Test that the SelectableChannelCloser implCloseChannel method is invoked.
+     */
+    public void testImplCloseChannel() throws IOException {
+        try (Connection connection = Connection.open()) {
+            FileDescriptor fd = getFD(connection.channel1());
+            Closer closer = new Closer();
+            SelectableChannel ch = Channels.readWriteSelectableChannel(fd, closer);
+
+            // close channel twice, checking that the closer is invoked only once
+            for (int i=0; i<2; i++) {
+                ch.close();
+
+                // implCloseChannel should been invoked once
+                assertTrue(closer.closeCount == 1);
+                assertTrue(closer.invokedToClose == ch);
+
+                // implReleaseChannel should not have been invoked
+                assertTrue(closer.releaseCount == 0);
+            }
+        }
+    }
+
+    /**
+     * Test that the SelectableChannelCloser implReleaseChannel method is invoked.
+     */
+    public void testImplReleaseChannel() throws IOException {
+        Selector sel = Selector.open();
+        try (Connection connection = Connection.open()) {
+            FileDescriptor fd = getFD(connection.channel1());
+            Closer closer = new Closer();
+            SelectableChannel ch = Channels.readWriteSelectableChannel(fd, closer);
+
+            // register with Selector, invoking selectNow to ensure registered
+            ch.configureBlocking(false);
+            ch.register(sel, SelectionKey.OP_WRITE);
+            sel.selectNow();
+
+            // close channel
+            ch.close();
+
+            // implCloseChannel should have been invoked
+            assertTrue(closer.closeCount == 1);
+            assertTrue(closer.invokedToClose == ch);
+
+            // implReleaseChannel should not have been invoked
+            assertTrue(closer.releaseCount == 0);
+
+            // flush the selector
+            sel.selectNow();
+
+            // implReleaseChannel should have been invoked
+            assertTrue(closer.releaseCount == 1);
+            assertTrue(closer.invokedToRelease == ch);
+
+        } finally {
+            sel.close();
+        }
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testInvalidFileDescriptor() throws IOException {
+        FileDescriptor fd = IOUtil.newFD(-1);
+        Channels.readWriteSelectableChannel(fd, new SelectableChannelCloser() {
+            @Override
+            public void implCloseChannel(SelectableChannel sc) { }
+            @Override
+            public void implReleaseChannel(SelectableChannel sc) { }
+        });
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testNullFileDescriptor() throws IOException {
+        Channels.readWriteSelectableChannel(null, new SelectableChannelCloser() {
+            @Override
+            public void implCloseChannel(SelectableChannel sc) { }
+            @Override
+            public void implReleaseChannel(SelectableChannel sc) { }
+        });
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testNullCloser() throws IOException {
+        try (Connection connection = Connection.open()) {
+            FileDescriptor fd = getFD(connection.channel1());
+            Channels.readWriteSelectableChannel(fd, null);
+        }
+    }
+
+    private static FileDescriptor getFD(SocketChannel sc) {
+        try {
+            Class<?> clazz = sc.getClass();
+            Field f = clazz.getDeclaredField("fd");
+            f.setAccessible(true);
+            return (FileDescriptor) f.get(sc);
+        } catch (Exception e) {
+            throw new Error(e);
+        }
+    }
+}