Проблема Java Solaris NIO OP_CONNECT - PullRequest
5 голосов
/ 30 июня 2011

У меня есть клиент Java, который подключается к серверу C ++ с помощью сокетов TCP с использованием Java NIO. Это работает в Linux, AIX и HP / UX, но в Solaris событие OP_CONNECT никогда не срабатывает.

Более подробная информация:

  • Selector.select() возвращает 0, и «выбранный набор ключей» пуст.
  • Эта проблема возникает только при подключении к локальной машине (через шлейф или через интерфейс Ethernet), но работает при подключении к удаленной машине.
  • Я подтвердил проблему с двумя разными машинами Solaris 10; физический SPARC и виртуальный x64 (VMWare), использующие обе версии JDK 1.6.0_21 и _26.

Вот некоторый тестовый код, который демонстрирует проблему:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class NioTest3
{
    public static void main(String[] args)
    {
        int i, tcount = 1, open = 0;
        String[] addr = args[0].split(":");
        int port = Integer.parseInt(addr[1]);
        if (args.length == 2)
            tcount = Integer.parseInt(args[1]);
        InetSocketAddress inetaddr = new InetSocketAddress(addr[0], port);
        try
        {
            Selector selector = Selector.open();
            SocketChannel channel;
            for (i = 0; i < tcount; i++)
            {
                channel = SocketChannel.open();
                channel.configureBlocking(false);
                channel.register(selector, SelectionKey.OP_CONNECT);
                channel.connect(inetaddr);
            }
            open = tcount;
            while (open > 0)
            {
                int selected = selector.select();
                System.out.println("Selected=" + selected);
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext())
                {
                    SelectionKey key = it.next();
                    it.remove();
                    channel = (SocketChannel)key.channel();
                    if (key.isConnectable())
                    {
                        System.out.println("isConnectable");
                        if (channel.finishConnect())
                        {
                            System.out.println(formatAddr(channel) + " connected");
                            key.interestOps(SelectionKey.OP_WRITE);
                        }
                    }
                    else if (key.isWritable())
                    {
                        System.out.println(formatAddr(channel) + " isWritable");
                        String message = formatAddr(channel) + " the quick brown fox jumps over the lazy dog";
                        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
                        channel.write(buffer);
                        key.interestOps(SelectionKey.OP_READ);
                    }
                    else if (key.isReadable())
                    {
                        System.out.println(formatAddr(channel) + " isReadable");
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        channel.read(buffer);
                        buffer.flip();
                        byte[] bytes = new byte[buffer.remaining()];
                        buffer.get(bytes);
                        String message = new String(bytes);
                        System.out.println(formatAddr(channel) + " read: '" + message + "'");
                        channel.close();
                        open--;
                    }
                }
            }

        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    static String formatAddr(SocketChannel channel)
    {
        return Integer.toString(channel.socket().getLocalPort());
    }
}

Вы можете запустить это с помощью командной строки:

java -cp . NioTest3 <ipaddr>:<port> <num-connections>

Где порт должен быть 7, если вы работаете с реальным эхо-сервисом; i.e.:

java -cp . NioTest3 127.0.0.1:7 5

Если вы не можете запустить реальную службу эха, тогда источником для нее является здесь . Скомпилируйте эхо-сервер под Solaris с помощью:

$ cc -o echoserver echoserver.c -lsocket -lnsl

и запустите его так:

$ ./echoserver 8007 > out 2>&1 &

Об этом было сообщено Sun как ошибка .

Ответы [ 2 ]

9 голосов
/ 01 июля 2011

Ваш отчет об ошибке закрыт как «не ошибка» с объяснением.Вы игнорируете результат connect(), который при значении true означает, что OP_CONNECT никогда не сработает, потому что канал уже подключен.Вам понадобится только целая OP_CONNECT/finishConnect() мегилла, если она вернет false.Поэтому вам даже не следует регистрировать OP_CONNECT, если connect() не возвращает false, не говоря уже о том, чтобы зарегистрировать его, прежде чем вы даже позвоните connect().

Дополнительные замечания:

Под капотом OP_CONNECTи OP_WRITE - это одно и то же, что объясняет его часть.

Поскольку для этого у вас есть один поток, обходным решением будет выполнить подключение в режиме блокировки, а затем переключиться на неблокирование для I /O.

Вы выполняете select () после регистрации канала с помощью селектора?

Правильный способ обработки неблокирующего соединения заключается в следующем:

channel.configureBlocking(false);
if (!channel.connect(...))
{
    channel.register(sel, SelectionKey.OP_CONNECT, ...); // ... is the attachment, or absent
}
// else channel is connected, maybe register for OP_READ ...
// select() loop runs ...
// Process the ready keys ...
if (key.isConnectable())
{
  if (channel.finishConnect())
  {
     key.interestOps(0); // or SelectionKey.OP_READ or OP_WRITE, whatever is appropriate
  }
}

Несколько неисчерпывающих комментариев после просмотра вашего расширенного кода:

  1. Закрытие канала отменяет ключ.Вам не нужно выполнять оба этих действия.

  2. Нестатический метод removeInterest () реализован неправильно.

  3. TYPE_DEREGISTER_OBJECT также закрывает канал,Не уверен, что это то, что вы действительно хотели.Я бы подумал, что он должен просто отменить ключ, и должна быть отдельная операция для закрытия канала.

  4. Вы перешли слишком далеко на маленьких методах и обработке исключений.addInterest () и removeInterest () являются хорошими примерами.Они перехватывают исключения, регистрируют их, затем поступают так, как если бы исключение не произошло, когда все, что они на самом деле делают, это устанавливает или сбрасывает немного: одна строка кода.Кроме того, многие из них имеют как статические, так и нестатические версии.То же самое относится ко всем маленьким методам, которые вызывают key.cancel (), channel.close () и т. Д. Нет никакого смысла во всем этом, это просто объединение строк кода.Это только добавляет неясности и усложняет понимание вашего кода.Просто выполните требуемую операцию в строке и поместите один перехватчик внизу цикла выбора.

  5. Если finishConnect () возвращает false, это не ошибка соединения, простозавершено еще.Если выдается исключение , , что означает сбой соединения.

  6. Вы регистрируетесь для OP_CONNECT и OP_READ одновременно.Это не имеет смысла, и это может вызвать проблемы.Нечего читать, пока не сработает OP_CONNECT.Сначала зарегистрируйтесь для OP_CONNECT.

  7. Вы выделяете ByteBuffer для чтения.Это очень расточительно.Используйте тот же самый для жизни соединения.

  8. Вы игнорируете результат read ().Это может быть ноль.Это может быть -1, указывая EOS, на котором вы должны закрыть канал.Вы также предполагаете, что получите единственное сообщение приложения за одно чтение.Вы не можете предполагать это.Это еще одна причина, почему вы должны использовать один ByteBuffer для жизни соединения.

  9. Вы игнорируете результат write ().Может быть меньше, чем buffer.remaining (), когда вы его вызывали.Это может быть ноль.

  10. Вы можете значительно упростить это, сделав NetSelectable ключевым вложением.Тогда вы можете покончить с несколькими вещами, включая, например, карту каналов и утверждение, потому что канал ключа всегда должен быть равен каналу вложения ключа.

  11. Я бы также определенно переместил код finishConnect () в NetSelector, а connectEvent () был бы просто уведомлением об успехе / неудаче.Вы не хотите распространять подобные вещи.Сделайте то же самое с readEvent (), т. Е. Выполните само чтение в NetSelector с буфером, предоставленным NetSelectable, и просто уведомите NetSelectable о результате чтения: count или -1 илиисключение. То же самое при записи: если канал доступен для записи, получите что-то для записи из NetSelectable, запишите это в NetSelector и сообщите результат. Вы можете сделать так, чтобы обратные вызовы уведомлений возвращали что-то, чтобы указать, что делать дальше, например, закрыть канал.

Но на самом деле все это в пять раз сложнее, чем нужно, и тот факт, что у вас есть эта ошибка, доказывает это. Упростите свою голову.

1 голос
/ 06 июля 2011

Я обошел эту ошибку, используя следующее:

Если Selector.select() возвращает 0 (и не сделал тайм-аут, если использовалась версия тайм-аута), то:

  1. Перебирайте ключи, зарегистрированные селектором, с помощью selector.keys().iterator() (запоминание , а не для вызова iterator.remove()).
  2. Если OP_CONNECT проценты были установлены с ключом, то позвоните channel.finishConnect() и сделайте все, что было бы сделано, если бы isConnectable() вернул true.

Например:

if (selected == 0 && elapsed < timeout)
{
    keyIter = selector.keys().iterator();
    while (keyIter.hasNext())
    {
        key = keyIter.next();
        if (key.isValid())
        {
            channel = (SocketChannel)key.channel();
            if (channel != null)
            {
                if ((key.interestOps() & SelectionKey.OP_CONNECT) != 0)
                {
                    if (channel.finishConnect())
                    {
                        key.interestOps(0);
                    }
                }
            }
        }
    }
}        

Это было сообщено Солнцу как ошибка .

...