Как правильно закрыть SocketChannel в Java NIO?

У меня есть простой неблокирующий сервер с основным циклом:

try {
    while (selector.select() > -1) {

        // Wait for an event one of the registered channels

        // Iterate over the set of keys for which events are available
        Iterator selectedKeys = selector.selectedKeys().iterator();
        while (selectedKeys.hasNext()) {
            SelectionKey key = (SelectionKey) selectedKeys.next();
            selectedKeys.remove();
            try {
                if (!key.isValid()) {
                    continue;
                }

                if (key.isConnectable()) {
                    connect(key);
                }

                // Check what event is available and deal with it
                if (key.isAcceptable()) {
                    accept(key);
                }

                if (key.isReadable()) {
                    read(key);
                }

                if (key.isWritable()) {
                    write(key);
                }
            } catch (Exception e) {
                e.printStackTrace();
                close(key);
            }
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

В разделе чтение/запись проверяю, есть ли что читать/писать, если нет - то пытаюсь закрыть канал:

if (channel.read(attachment.buffer) < 1) 
    close(key);

Метод закрытия:

private void close(SelectionKey key) throws IOException {
    key.cancel();
    key.channel().close();
}

Но во время обработки этого кода я получаю исключение в основном цикле (оно перехватывается, но я предположил, что что-то не так), я получаю эту трассировку стека:

java.nio.channels.CancelledKeyException
    at sun.nio.ch.SelectionKeyImpl.ensureValid(Unknown Source)
    at sun.nio.ch.SelectionKeyImpl.readyOps(Unknown Source)
    at java.nio.channels.SelectionKey.isWritable(Unknown Source)

Таким образом, он терпит неудачу в основном цикле, когда входит в раздел записи, закрывает канал и возвращается к основному циклу в «доступном для записи», если раздел и терпит неудачу с таким исключением. Какие-либо предложения?


person sphinks    schedule 07.08.2012    source источник
comment
SocketChannel.read может вернуть 0 и, кстати, не обязательно означает ошибку.   -  person obataku    schedule 08.08.2012
comment
Закрытие канала отменяет все его ключи. Вам не нужен csncel().   -  person user207421    schedule 08.08.2012


Ответы (1)


Ошибка очень проста.

if (!key.isValid()) {
    continue;
}

if (key.isConnectable()) {
    connect(key);
}

// Check what event is available and deal with it
if (key.isAcceptable()) {
    accept(key);
}

if (key.isReadable()) {
    read(key);
}

if (key.isWritable()) {
    write(key);
}

Ваш метод read отменяет SelectionKey. Однако после возврата из read вы снова проверяете ключ на предмет того, доступен ли канал для записи — возможно, после отмены того же самого ключа! Ваша первоначальная проверка здесь не поможет.


Одним из решений было бы проверить, действителен ли ключ, если он только что был отменен:

...
if (key.isValid() && key.isWritable()) {
  write(key);
}
...

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

if (!key.isValid()) {
  continue;
}

if (key.isConnectable()) {
  connect(key);
} else if (key.isAcceptable()) {
  accept(key);
} else if (key.isReadable()) {
  read(key);
} else if (key.isWritable()) {
  write(key);
}

Это может быть полезно в некоторых ситуациях; как правило, канал почти всегда будет готов к записи, сохранение заинтересованности в готовности к записи наряду с готовностью к чтению может привести к тому, что цикл Selector будет вращаться, что, скорее всего, нежелательно. По большей части обычно регистрируют интерес к готовности к записи только тогда, когда базовый выходной буфер сокета заполнен.


В качестве примечания знайте, что SocketChannel.read может вернуть значение < 1, и это не будет ошибкой.

Операция чтения может не заполнить буфер и вообще не прочитать ни одного байта. Так это или нет, зависит от природы и состояния канала. Канал сокета в неблокирующем режиме, например, не может прочитать больше байтов, чем доступно непосредственно из входного буфера сокета;

Кроме того, Selector.select ничего не говорится о возврате < -1, чтобы указать, что он закрыт.

Возвраты: количество ключей (возможно, ноль), наборы готовых операций которых были обновлены.

person obataku    schedule 07.08.2012
comment
Спасибо. Так как же правильно остановить серверный цикл? При каком условии? - person sphinks; 08.08.2012
comment
И сделайте основной цикл таким образом: if (key.isConnectable()) { connect(key); } else if (key.isAcceptable()) { accept(key); } else if (key.isReadable()) { read(key); } else if (key.isWritable()) { write(key); } - person sphinks; 08.08.2012
comment
@sphinks, вы должны использовать Selector.close ; это внутренне прервет поток цикла событий, если он блокируется на select. Вы можете проверить, открыт ли Selector, используя Selector.isOpen. Если вы попытаетесь select использовать закрытый Selector, он выдаст ClosedSelectorException. - person obataku; 08.08.2012
comment
@sphinks Возможно, имеет смысл зациклиться на while (selector.keys().size() > 0), который завершится, когда все управляемые каналы будут закрыты. Затем закройте селектор после цикла. - person user207421; 12.03.2015