关闭套接字唤醒选择器

时间:2016-08-08 06:02:23

标签: java nio

我们写了一个以这种方式工作的传入反应堆:

  1. 打开选择器
  2. 打开服务器套接字通道
  3. 启动一个选择循环,其中:ServerSocketChannel接受新的SocketChannel进入循环,每个SocketChannel读取数据并将其传输给工作人员。
  4. 反应堆的关闭程序正在迭代selector.keys(),并且每个关闭相应的通道并取消关键。

    我们为关机程序编写了以下单元测试:

    1. 打开运行选择循环的反应器线程。
    2. 打开几个Sender线程。每个都打开一个插座到反应堆并读取。
    3. 读取阻塞直到它变为-1(表示反应器关闭了套接字)。
    4. 读取返回-1后,发件人关闭套接字并完成。
    5. 测试导致ConcurrentModificationException指向循环迭代套接字并关闭它们(在主线程上下文中)。

      我们的假设是当一个Sender读取方法得到-1时,它关闭了套接字并以某种方式唤醒了选择器选择方法,然后选择器访问它的密钥集,该密钥集由关闭循环迭代,因此是异常。 / p>

      我们通过使用选择器的所有键创建新列表来解决此问题。通过迭代此列表来取消这些键可防止两个对象修改相同的键集。

      我们的问题是:

      1. 我们的假设是否正确?当客户端套接字调用close方法时 - 它是否真的唤醒了选择器?
      2. 创建新列表是否是合适的解决方案,还是仅仅是一种解决方法?
      3. 编辑:添加了一些代码片段以供澄清 (我们尽可能缩小代码范围)

        IncomingReactor:

        public boolean startAcceptingIncomingData() {
            Selector selector = Selector.open();
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open());
            serverSocketChannel.bind(new InetSocketAddress(incomingConnectionsPort));
            serverSocketChannel.configureBlocking(false);
            SelectionKey acceptorSelectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            acceptorSelectionKey.attach((Worker) this::acceptIncomingSocket);
            startSelectionLoop(selector);
            return true;
          }
        
        private boolean acceptIncomingSocket() {
            try {
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
                selectionKey.attach(new WorkerImpl() /*Responsible for reading data and tranferring it into a parsing thread*/);
                return true;
            } catch (IOException e) {
              return false;
            }
          }
        
        private void startSelectionLoop(Selector selector) {
            shouldLoop = true;
            while (shouldLoop) {
              try {
                selector.select();
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                if (!shouldLoop) {
                  break;
                }
                selectedKeys.forEach((key) -> {
                  boolean workSuccess = ((Worker) key.attachment()).work();
                  if (!workSuccess) {
                      key.channel().close();
                      key.cancel();
                  }
                });
                selectedKeys.clear();
              } catch (ClosedSelectorException ignore) {
              }
            }
          }
        
        public void shutDown() {
            shouldLoop = false;
            selector.keys().forEach(key -> { /***EXCEPTION - This is where the exception points to (this is line 129) ***/
                key.channel().close();
                key.cancel();
            });
            try {
              selector.close();
            } catch (IOException e) {
            }
          }
        

        单元测试:

           @Test
          public void testMaximumConnectionsWithMultipleThreads() {
            final int PORT = 24785;
            final int MAXINUM_CONNECTIONS = 10;
        
            IncomingReactor incomingReactor = new IncomingReactor(PORT);
            Callable<Boolean> acceptorThread = () -> {
              incomingReactor.startAcceptingIncomingData();
              return true;
            };
        
            ExecutorService threadPool = Executors.newFixedThreadPool(MAXIMUM_CONNECTIONS + 1);
            Future<Boolean> acceptorFuture = threadPool.submit(acceptorThread);
        
            List<Future<Boolean>> futureList = new ArrayList<>(MAXIMUM_CONNECTIONS);
            for (int currentSenderThread = 0; currentSenderThread < MAXIMUM_CONNECTIONS; currentSenderThread++) {
              Future<Boolean> senderFuture = threadPool.submit(() -> {
                Socket socket = new Socket(LOCALHOST, PORT);
                int bytesRead = socket.getInputStream().read();
                if (bytesRead == -1) { //The server has closed us
                  socket.close();
                  return true;
                } else {
                  throw new RuntimeException("Got real bytes from socket.");
                }
              });
              futureList.add((senderFuture));
            }
        
            Thread.sleep(1000); //We should wait to ensure that the evil socket is indeed the last one that connects and the one that will be closed
            Socket shouldCloseSocket = new Socket(LOCALHOST, PORT);
            Assert.assertEquals(shouldCloseSocket.getInputStream().read(), -1);
            shouldCloseSocket.close();
            incomingReactor.shutDown();
            for (Future<Boolean> senderFuture : futureList) {
              senderFuture.get();
            }
            acceptorFuture.get();
            threadPool.shutdown();
          }
        

        例外:

        java.util.ConcurrentModificationException
            at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
            at java.util.HashMap$KeyIterator.next(HashMap.java:1461)
            at java.lang.Iterable.forEach(Iterable.java:74)
            at java.util.Collections$UnmodifiableCollection.forEach(Collections.java:1080)
            at mypackage.IncomingReactor.shutDown(IncomingReactor.java:129)
            at mypackage.tests.TestIncomingReactor.testMaximumConnectionsWithMultipleThreads(TestIncomingReactor.java:177)
            at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
            at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
            at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            at java.lang.reflect.Method.invoke(Method.java:498)
            at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:85)
            at org.testng.internal.Invoker.invokeMethod(Invoker.java:659)
            at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:845)
            at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1153)
            at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:125)
            at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:108)
            at org.testng.TestRunner.privateRun(TestRunner.java:771)
            at org.testng.TestRunner.run(TestRunner.java:621)
            at org.testng.SuiteRunner.runTest(SuiteRunner.java:357)
            at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:352)
            at org.testng.SuiteRunner.privateRun(SuiteRunner.java:310)
            at org.testng.SuiteRunner.run(SuiteRunner.java:259)
            at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
            at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
            at org.testng.TestNG.runSuitesSequentially(TestNG.java:1199)
            at org.testng.TestNG.runSuitesLocally(TestNG.java:1124)
            at org.testng.TestNG.run(TestNG.java:1032)
            at org.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:74)
            at org.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:124)
            at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
            at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
            at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            at java.lang.reflect.Method.invoke(Method.java:498)
            at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
        

2 个答案:

答案 0 :(得分:2)

  

反应堆的关闭程序正在遍历selector.keys(),并且每个关闭相应的通道并取消该键。

它应该从停止选择器循环开始。注意关闭频道会取消该键。你不必自己取消它。

  

我们为关机程序编写了以下单元测试:

     

打开运行选择循环的反应器线程。   打开几个Sender线程。每个都打开一个到反应器的插座并读取。   读取阻塞直到它变为-1(意味着反应器关闭了套接字)。

反应堆关闭接受的套接字。您的客户端套接字保持打开状态。

  

读取返回-1后,发件人关闭套接字并完成。

我希望这意味着发件人关闭了它的客户端套接字。

  

测试导致ConcurrentModificationException指向循环迭代套接字并关闭它们(在主线程上下文中)。

真的?我的问题中没有看到任何堆栈跟踪。

  

我们的假设是当一个Sender读取方法得到-1时,它关闭了套接字并以某种方式唤醒了选择器选择方法

除非反应堆没有关闭通道,否则不可能,在这种情况下你不会从读取等中得到-1。

  

然后,选择器访问其密钥集,该密钥集由关闭循环迭代,因此异常。

异常是在迭代期间修改密钥集引起的。服务器代码中的错误。

  

我们通过使用选择器的所有键创建新列表来解决此问题。通过迭代此列表来取消这些键可防止两个对象修改相同的键集。

您需要修复实际问题,为此您需要发布实际代码。

  

我们的问题是:

     

我们的假设是否正确?当客户端套接字调用close方法时 - 它是否真的唤醒了选择器?

除非选择器端通道仍处于打开状态。

  

创建新列表是否是合适的解决方案,还是只是一种解决方法?

对于尚未确定的问题,这只是一个讨厌的解决方法。

答案 1 :(得分:1)

您无法从for循环内部修改selector.keys() Set<SelectionKey>,因为Set无法进行并发修改。 (调用channel.close()会修改Set内的Set {/ 1}}

https://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html

  

此类的迭代器方法返回的迭代器是快速失败的:   如果在创建迭代器后的任何时间修改了该集,则   除了通过迭代器自己的删除方法,Iterator之外的任何方式   抛出ConcurrentModificationException。因此,面对   并发修改,迭代器快速干净地失败,   而不是冒着任意的,非确定性的行为冒险   未来不确定的时间。

SelectionKey[] keys = selector.keys().toArray(new SelectionKey[0]);

for( SelectionKey k : keys )
{
    try
    {
        k.channel().close();
    }
    catch(Throwable x )
    {
        // print
    }
}

try
{
    selector.close();
}
catch(IoException e )
{
    // print
}