最近使用NIO在写client和server端的Demo的时候,发生一个问题,就是client一直触发写操作,这样就导致服务端可能接收到客户端写入的多个重复的信息,首先看一下客户端和服务端的代码。
服务端
package com.ly.search.nio.server;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
public class TimeServer {
private int port;
public TimeServer(int port) {
this.port = port;
}
public void service() {
ServerSocketChannel ssc = null;
try {
ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(port), 10);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("service run.....");
while (true) {
if (selector.select() == 0) {
continue;
}
Iterator<SelectionKey> i = selector.selectedKeys().iterator();
while (i.hasNext()) {
SelectionKey sk = i.next();
i.remove();
if (sk.isAcceptable()) {
SocketChannel sc = ((ServerSocketChannel) sk.channel())
.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer bb = ByteBuffer.allocate(1024);
int readBytes = sc.read(bb);
if (readBytes > 0) {
bb.flip();
byte[] req = new byte[bb.remaining()];
bb.get(req);
String reqMsg = new String(req, "utf-8");
System.out
.println("The time server receive order : "
+ reqMsg);
String respMsg = "QUERY TIME ORDER"
.equalsIgnoreCase(reqMsg) ? new Date(
System.currentTimeMillis())
.toString()
: "BAD ORDER";
byte[] resp = respMsg.getBytes("utf-8");
ByteBuffer respBb = ByteBuffer
.allocate(resp.length);
respBb.put(resp);
respBb.flip();
sc.register(sk.selector(), SelectionKey.OP_WRITE,
respBb);
}
} else if (sk.isWritable()) {
ByteBuffer respBb = (ByteBuffer) sk.attachment();
if (respBb.hasRemaining()) {
SocketChannel sc = (SocketChannel) sk.channel();
sc.write(respBb);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ssc != null) {
try {
ssc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new TimeServer(8888).service();
}
}
客户端代码:
package com.ly.search.nio.clinet;
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.Iterator;
public class TimeClient {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
Selector selector = Selector.open();
sc.configureBlocking(false);
boolean isConnect = sc.connect(new InetSocketAddress("localhost", 8888));
if (!isConnect) {
sc.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
}
boolean isStop = false;
while (true) {
int count = selector.select();
if (count == 0)
continue;
Iterator<SelectionKey> i = selector.selectedKeys().iterator();
while (i.hasNext()) {
SelectionKey sk = i.next();
i.remove();
if (sk.isValid()) {
if (sk.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_WRITE);
} else {
System.exit(1);
}
} else if (sk.isWritable()) {
System.out.println("writing");
byte[] req = "QUERY TIME ORDER".getBytes("utf-8");
ByteBuffer bb = ByteBuffer.allocate(req.length);
bb.put(req);
bb.flip();
sc.write(bb);
} else if (sk.isReadable()) {
System.out.println("reading");
ByteBuffer bb = ByteBuffer.allocate(1024);
int readBytes = sc.read(bb);
bb.flip();
byte[] resp = new byte[readBytes];
bb.get(resp);
String respMsg = new String(resp, "utf-8");
System.out.println("Now is " + respMsg);
isStop = true;
break;
}
}
}
if (isStop) {
sc.close();
break;
}
}
}
}
首先运行服务端,然后运行客户端,运行的结果如下:
服务端:
然后客户端执行了多次的写操作,截图如下:
可以看到服务端收到了两条信息。
这里有两个疑问?
1.为什么客户端没有执行读事件?
2.为什么客户端一直执行写事件?
这个需要查看下SelectableChannel的register方法的源码:
我们一直点下去,进入AbstractSelectableChannel的register方法
重点看一下k.interestOps(ops)方法,然后进入SelectionKeyImpl.interestOps方法
在进入nioInterestOps(ops)方法
可以看到直接覆盖了之前设置的interestOps,也就是说我们在客户端完成连接,设置SelectionKey.OP_WRITE事件,其实已经把之前的事件全部覆盖了。
这样客户端只要接收到操作系统的可写事件,那么一直触发可写操作。
其实我们可以在看下和操作系统相关的源码,也就是看一下channel.translateAndSetInterestOps(ops,this)方法,由于是客户端,那么直接看SocketChannelImpl类
所以也是重新覆盖。
通过源码的阅读,我们大概知道怎么解决客户端重复写的问题,也就是在执行完OP_WRITE操作之后,在重新设置下OP_READ事件,这样就能只对读事件感兴趣。
修改之后的客户端源码如下:
package com.ly.search.nio.clinet;
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.Iterator;
public class TimeClient {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
Selector selector = Selector.open();
sc.configureBlocking(false);
boolean isConnect = sc.connect(new InetSocketAddress("localhost", 8888));
if (!isConnect) {
sc.register(selector, SelectionKey.OP_CONNECT);
}
boolean isStop = false;
while (true) {
int count = selector.select();
if (count == 0)
continue;
Iterator<SelectionKey> i = selector.selectedKeys().iterator();
while (i.hasNext()) {
SelectionKey sk = i.next();
i.remove();
if (sk.isValid()) {
if (sk.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_WRITE);
} else {
System.exit(1);
}
} else if (sk.isWritable()) {
System.out.println("writing");
byte[] req = "QUERY TIME ORDER".getBytes("utf-8");
ByteBuffer bb = ByteBuffer.allocate(req.length);
bb.put(req);
bb.flip();
sc.write(bb);
sc.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
System.out.println("reading");
ByteBuffer bb = ByteBuffer.allocate(1024);
int readBytes = sc.read(bb);
bb.flip();
byte[] resp = new byte[readBytes];
bb.get(resp);
String respMsg = new String(resp, "utf-8");
System.out.println("Now is " + respMsg);
isStop = true;
break;
}
}
}
if (isStop) {
sc.close();
break;
}
}
}
}
执行之后,服务端打印结果如下:
客户端的打印结果如下:
总结一下:
SelectableChannel.register(selector, SelectionKey.OP_WRITE);的方法会直接对兴趣进行重新赋值,也就是会覆盖掉之前的兴趣设置。
可写事件的触发有操作系统决定,由于NIO是异步的,只要内核中的网卡的缓冲区可写就会触发可写事件。