java Netty 之消息收发次数不匹配额问题
问题
在前面代码中,分三次发送信息:
private void sendMessageByFrame(ChannelStateEvent e) {
String msgOne = "Hello, ";
String msgTwo = "I'm ";
String msgThree = "client.";
e.getChannel().write(tranStr2Buffer(msgOne));
e.getChannel().write(tranStr2Buffer(msgTwo));
e.getChannel().write(tranStr2Buffer(msgThree));
}
这样的方式,连续返送三次消息。但是如果你在服务端进行接收计数的话,你会发现,大部分时候都是接收到两次的事件请求。不过消息都是完整的。
检查方法二
笔者从开始就怀疑是连续写入过快导致的问题,所以测试过每次write后停顿1秒。再write下一次。结果一切正常。
猜想原因
出现这样的现象,最大的原因就是缓存,连续发送太快,数据会进入到缓存,然后再定时在缓存中将数据取出来。
注意
1. 有一个messageReceived事件,事件的触发,是在读取完当前缓冲池中所有的信息之后在触发的。这倒是可以解释,为什么即使我们收到事件的次数少,但是消息是完整的。
2. 从目前来看,Netty通过Java 的NIO机制传递数据,数据读写跟事件没有严格的绑定机制。数据是以流的形式独立存在,读写都有一个缓冲池。
Selector模式
NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。
Selector服务端样例代码:
/**
* Java NIO Select模式服务端样例代码
*
*/
public class NioSelectorServer {
public static void main(String[] args) throws IOException {
// 创建一个selector选择器
Selector selector = Selector.open();
// 打开一个通道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 绑定到9000端口
socketChannel.socket().bind(new InetSocketAddress(8000));
// 使设定non-blocking的方式。
socketChannel.configureBlocking(false);
// 向Selector注册Channel及我们有兴趣的事件
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
for (;;) {
// 选择事件
selector.select();
// 当有客户端准备连接到服务端时,便会出发请求
for (Iterator keyIter = selector.selectedKeys()
.iterator(); keyIter.hasNext();) {
SelectionKey key = keyIter.next();
keyIter.remove();
System.out.println(key.readyOps());
if (key.isAcceptable()) {
System.out.println("Accept");
// 接受连接到此Channel的连接
socketChannel.accept();
}
}
}
}
}
Selector客户端样例代码:
/**
* Java NIO Selector模式,客户端代码
*
*/
public class NioSelectorClient {
public static void main(String[] args) throws IOException {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("127.0.0.1", 8000));
}
}
结果:
服务端接受到客户端的连接请求后,会打印出"Accept"信息。
简单概括就是,整一个通道,通道加个选择过滤器,看来的事件是不是我想要的,不想要的干脆不管,想要的,我就存起来,留着慢慢处理。Netty的实现机制也是按照这种原理。
最后总结
首先,Selector机制让我们注册一个感兴趣的事件,然后只要有该事件发生,就会传递给接收端。我们写了三次,接收端一定会出发三次的。然后,Netty实现机制里,有个Buffer缓冲池,把收到的信息都缓存在里面,通过一个线程统一处理。也就是我们看到的那个buffer的处理过程。
Netty的设置中,有一个一次性最多读取字节大小的设定。并且,事件的触发是在处理过缓冲池中的消息之后。我们再来回顾一下Netty中读取信息的那段代码:
ByteBuffer bb = recvBufferPool.acquire(predictedRecvBufSize);
try {
while ((ret = ch.read(bb)) > 0) {
readBytes += ret;
if (!bb.hasRemaining()) {
break;
}
}
failure = false;
} catch (ClosedChannelException e) {
// Can happen, and does not need a user attention.
} catch (Throwable t) {
fireExceptionCaught(channel, t);
}
if (readBytes > 0) {
bb.flip();
final ChannelBufferFactory bufferFactory =
channel.getConfig().getBufferFactory();
final ChannelBuffer buffer = bufferFactory.getBuffer(readBytes);
buffer.setBytes(0, bb);
buffer.writerIndex(readBytes);
recvBufferPool.release(bb);
// Update the predictor.
predictor.previousReceiveBufferSize(readBytes);
// Fire the event.
fireMessageReceived(channel, buffer);
} else {
recvBufferPool.release(bb);
}
可以看到,如果没有读取到字节是不会触发事件的,所以我们可能会收到2次或者3次信息。(如果发的快,解析的慢,后两次信息,一次性读取了,就2次,如果发送间隔长,分次解析,就收到3次。)原因应该就是如此。跟我们开始猜的差不多。
参考:
http://www.it165.net/pro/html/201207/3219.html