当通道准备好读取时,即
key.isReadable()
返回true时,将调用read(key)
来读取数据。目前,我正在为所有通道使用单个读取缓冲区,并在
read()
方法中清除缓冲区并读入数据,最后将其放入字节数组中,假设我会一次性获得所有数据。但是,假设我没有一次性获取完整的数据(我在数据结尾处有特殊字符进行检测)。
问题:
现在如何将这些部分数据保留在通道中,或如何处理部分读取问题?还是全局的?
我在某个地方读到附件不好。
key.isReadable()
返回true时,将调用read(key)
来读取数据。read()
方法中清除缓冲区并读入数据,最后将其放入字节数组中,假设我会一次性获得所有数据。public class Reactor implements Runnable{
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
public Reactor(int port) throws IOException {
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
// let Reactor handle new connection events
registerAcceptor();
}
/**
* Registers Acceptor as handler for new client connections.
*
* @throws ClosedChannelException
*/
private void registerAcceptor() throws ClosedChannelException {
SelectionKey selectionKey0 = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
selectionKey0.attach(new Acceptor());
}
@Override
public void run(){
while(!Thread.interrupted()){
startReactorLoop();
}
}
private void startReactorLoop() {
try {
// wait for new events for each registered or new clients
selector.select();
// get selection keys for pending events
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> selectedKeysIterator = selectedKeys.iterator();
while (selectedKeysIterator.hasNext()) {
// dispatch even to handler for the given key
dispatch(selectedKeysIterator.next());
// remove dispatched key from the collection
selectedKeysIterator.remove();
}
} catch (IOException e) {
// TODO add handling of this exception
e.printStackTrace();
}
}
private void dispatch(SelectionKey interestedEvent) {
if (interestedEvent.attachment() != null) {
EventHandler handler = (EventHandler) interestedEvent.attachment();
handler.processEvent();
}
}
private class Acceptor implements EventHandler {
@Override
public void processEvent() {
try {
SocketChannel clientConnection = serverSocketChannel.accept();
if (clientConnection != null) {
registerChannel(clientConnection);
}
} catch (IOException e) {e.printStackTrace();}
}
/**
* Save Channel - key association - in Map perhaps.
* This is required for subsequent/partial reads/writes
*/
private void registerChannel(SocketChannel clientChannel) {
// notify injection mechanism of new connection (so it can activate Read Handler)
}
一旦读取事件被处理,通知注入机制可以注入写入处理程序。
当有新的连接可用时,注入机制会创建读取和写入处理程序的新实例。此注入机制根据需要切换处理程序。每个通道的处理程序查找是通过在连接接受时由方法 `registerChannel()` 填充的 Map 进行的。
读取和写入处理程序具有 ByteBuffer
实例,并且由于每个 Socket 通道都有自己的一对处理程序,因此现在可以在部分读取和写入之间维护状态。
提高性能的两个提示:
在连接被接受时立即尝试进行第一次读取。仅当您的自定义协议中的头部定义的数据不足时,才为读取事件注册通道兴趣。
尝试先写入而不注册写入事件的兴趣,只有当您没有写入所有数据时,才注册写入兴趣。
这将减少选择器唤醒次数。
类似于这样:
SocketChannel socketChannel;
byte[] outData;
final static int MAX_OUTPUT = 1024;
ByteBuffer output = ByteBuffer.allocate(MAX_OUTPUT);
// if message was not written fully
if (socketChannel.write(output) < messageSize()) {
// register interest for write event
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_WRITE);
selectionKey.attach(writeHandler);
selector.wakeup();
最后,应该设置定时任务来检查连接是否仍然存活/选择键是否被取消。如果客户端断开TCP连接,服务器通常不会知道这一点。因此,将有许多事件处理程序绑定为过期的连接附件,这将导致内存泄漏。
这就是为什么你可能会说附件不好,但是这个问题可以处理。
要解决这个问题,有两种简单的方法:
启用TCP保持活动功能
定期任务可以检查给定通道上最后一次活动的时间戳。如果它闲置太长时间,服务器应该终止连接。
有人在亚马逊的一篇古老而且非常不准确的NIO博客中错误地声称关键附件是内存泄漏,这完全是胡说八道,甚至不合逻辑。这也是他断言你需要各种补充队列的博客之一。在大约13年的NIO使用中从未遇到过这种情况。
你所需要的是每个通道一个ByteBuffer
,或者可能是两个,一个用于读取,一个用于写入。你可以将单个缓冲区作为附件本身存储:如果您想要两个,或者有其他数据需要存储,则需要定义一个包含两个缓冲区和任何您想要与通道关联的其他数据(例如客户端凭据)的Session
类,并使用Session
对象作为附件。
你真的无法通过一个单一的缓冲区为所有通道提供很多帮助。