public class Client {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
// 模拟三个发端
new Thread() {
public void run() {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.socket().connect(new InetSocketAddress("127.0.0.1", 8888));
File file = new File("E:\\" + 11 + ".txt");
FileChannel fileChannel = new FileInputStream(file).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(100);
socketChannel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit(), Charset.forName("utf-8")));
buffer.clear();
int num = 0;
while ((num=fileChannel.read(buffer)) > 0) {
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
if (num == -1) {
fileChannel.close();
socketChannel.shutdownOutput();
}
// 接受服务器
socketChannel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit(), Charset.forName("utf-8")));
buffer.clear();
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
};
}.start();
}
Thread.yield();
}
}
可见这里我们仅仅使用了一个线程就管理了三个连接,相比以前使用阻塞的Socket要在accept函数返回后开启线程来管理这个连接,而使用NIO我们在accept返回后,仅仅将其注册到选择器上,读操作在下次检测到有可读的键的集合时就会去处理。
NIO+线程池改进
当然使用单线程并不都是一个好主意,使用单线程的好处在于任何情况下都只有一个线程能够运行。通过消除在线程之间进行上下文切换带来的额外开销,总吞吐量可以得到提高。然而在其他情况下,尤其是对于多核CPU来说,使用单线程是对CPU的浪费,更好的策略是使用一个线程来管理选择器,监控通道的就绪状态,对于数据的处理可以使用线程池处理。
这样上面的例子改为
public class ThreadPoolServer extends Server{
private ExecutorService exec = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws IOException {
ThreadPoolServer server = new ThreadPoolServer();
server.startServer();
}
@Override
protected void readData(final SelectionKey key) throws IOException {
// 移除掉这个key的可读事件,已经在线程池里面处理
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
exec.execute(new Runnable() {
@Override
public void run() {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
FileChannel fileChannel = fileMap.get(key);
buffer.clear();
SocketChannel socketChannel = (SocketChannel) key.channel();
int num = 0;
try {
while ((num = socketChannel.read(buffer)) > 0) {
buffer.flip();
// 写入文件
fileChannel.write(buffer);
buffer.clear();
}
} catch (IOException e) {
key.cancel();
e.printStackTrace();
return;
}
// 调用close为-1 到达末尾
if (num == -1) {
try {
fileChannel.close();
System.out.println("上传完毕");
buffer.put((socketChannel.getRemoteAddress() + "上传成功").getBytes());
buffer.clear();
socketChannel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
// 只有调用cancel才会真正从已选择的键的集合里面移除,否则下次select的时候又能得到
// 一端close掉了,其对端仍然是可读的,读取得到EOF,返回-1
key.cancel();
return;
}
// Channel的read方法可能返回0,返回0并不一定代表读取完了。
// 工作线程结束对通道的读取,需要再次更新键的ready集合,将感兴趣的集合重新放在里面
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
// 调用wakeup,使得选择器上的第一个还没有返回的选择操作立即返回即重新select
key.selector().wakeup();
}
});
}
}
几点说明
1.将通道注册到一个选择其中,会返回一个键,这个键代表了通道和选择器之间的注册关系
2.SelectionKey键包含两个集合
interest集合:指示每个通道所关心的操作,这时在注册通道时确定的,也可以使用键的带参数的interestOps方法修改。
ready集合:表示通道已经在该操作上准备好,是interest集合的子集,表示了interest集合中从上次调用select以来已经就绪的那些操作。该集合不能修改,由操作系统告诉我们。
Selector包含三个集合
已注册的键的集合(Registered key set):调用Channel的register方法
已选择的键的集合(Selected key set)
已取消的键的集合(Canceled key set):调用key的cancel方法,select的cancel方法等都会将该key放在已取消的键的集合,通道关闭时,相关的键也会自动取消
3.当调用select方法时,会执行以下操作,这是理解NIO中调用各操作的关键。
1).检查已取消的键的集合,非空,将该键从另外两个集合移除,这步完成后,已取消的键的集合为空
2).检查已注册的键的集合中的键的interest集合,对于那些操作系统指示至少已经准备好interest集合中的一种操作的通道,将执行以下两种操作中的一种:
a.该键没有出在已选择的键的集合里面:该键的ready集合被清空,当前已经就绪的操作将会被添加到ready集合中,然后将该键放入已选择的键的集合中
b.否则,键存在于选择器的已选择的键的集合中,它的ready 集合将是累积的,只会设置这次操作系统决定的操作的位,之前的设置不会变。
3).重复执行1
4).select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是已准备好的所有键的个数。
从上面的选择过程可以知道一旦一个选择器将一个键添加到它的已选择的键的集合中,它就不会移除这个键,并且,一旦一个键处于已选择的键的集合中,这个键的ready 集合将只会被设置,而不会被清理。于是管理键以确保它们正确的状态信息的任务要由程序员来做。
程序中的remove方法将一个键从已选择的集合中删除,否则不会自动删除,表示已经对这个键所代表的的通道进行了处理。
当一个通道上的操作执行完想要删除该通道时,调用key的cancel方法,它将被加入到取消集合中,注册的关系不会立刻取消,键会立即失效,下一次调用select方法,就会从另两个集合中删除,通道也会被注销。
使用NIO+线程池时,在上面的程序中当有可读事件时,交由线程池去处理,这个期间要忽略该通道的read操作,即将其从interest集合删除。当线程结束给该通道的操作时,需要更新键的ready集合,将感兴趣的集合重新放在里面。调用selector的wakeup方法是为了防止如果此时阻塞在select上,他会立刻返回。
4.tcp中仍然需要告诉对端结束标志,这里调用了shutDownOutput方法(NIO的API 1.7以后才有,可以使用结束标志),这样另一端的read方法就会读到EOF,返回-1
参考链接