第五章 NIO
5.1我们为什么需要NIO
多线程环境下对共享状态进行同步访问,增加了系统调度和切换上下文的开销,程序员对这些开销无法控制。
阻塞等待。
我们需要一种可以一次轮询一组客户端,以查找那个客户端需要服务。在NIO中,一个Channel代表一个可以轮询的I/O目标,Channel能够注册一个Selector实例。Selector的select可以查找“在当前一组信道中,哪一个需要服务”。
Buffer提供了比Stream抽象更高效和可预测的I/O。Stream抽象好的方面是隐藏了底层缓冲区的有限性,提供了一个能够容纳任意长度数据的容器的假象,要么会产生大量的内存开销,要么会引入大量的上下文切换。使用线程的过程中,这些开销都隐藏在具体实现中,也失去了对其的可控性和可预测性。这种方法使得编写程序变得容易,但调整他们的性能则变得困难。不幸的是,使用Java的Socket抽象,流是唯一的选择。
Buffer抽象代表了一个有限容量的数据容器——其本质是一个数组,由指针指示了在哪存放数据和在哪读取数据。使用Buffer有两个好处,第一、与读写缓冲区数据相关联的系统开销暴露给了程序员,第二、一些对Java对象特殊的Buffer映射能够直接操作底层的平台的资源,这样操作节省了在不同的地址空间复制数据的开销。
5.2 与Buffer一起使用Channel
Channel使用的不是流而是缓冲区来发送或者读取数据。Buffer类或者其任何子类的实例都可以看做是一个定长的Java基本数据类型元素序列。与流不同,缓冲区由固定的、有限的容量,并由内部状态记录了由多少数据放入或者取出,就像是有限容量的队列一样。在Channel中使用的Buffer通常不是构造函数创建的,而是通过调用allocate()方法创建指定容量的Buffer实例:
ByteBuffer buffer = ByteBuffer.allocate(CAPACITY)
或者使用包装一个已有数据来实现
ByteBuffer buffer = ByteBuffer.wrap(byteArray)
NIO的强大来自于channel的非阻塞特性。
下面是一个字符回显的非阻塞客户端
package com.suifeng.tcpip.chapter5;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* 非阻塞客户端
*
* @author Suifeng
*
*/
public class TCPEchoClientNonBlocking
{
public static void main(String[] args) throws IOException
{
if (args.length < 2 || args.length > 3)
{
throw new IllegalArgumentException("Parameters:<Server> <Word> [<Port>]");
}
String server = args[0];
byte[] msg = args[1].getBytes();
int serverPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;
SocketChannel channel = SocketChannel.open();
// 将信道置为非阻塞方式
channel.configureBlocking(false);
if(!channel.connect(new InetSocketAddress(server,serverPort)))
{
System.out.print("Trying connected server");
// 轮询连接的状态,知道建立连接,这样忙等比较浪费资源,
while(!channel.finishConnect())
{
System.out.print(".");
}
}
System.out.println("\nClient has connected to server successfully");
// 写缓冲区
ByteBuffer writeBuffer = ByteBuffer.wrap(msg);
// 读缓冲区
ByteBuffer readBuffer = ByteBuffer.allocate(msg.length);
int totalBytesReceived = 0;
int bytesReceived = -1;
System.out.print("Waiting for server Response");
while(totalBytesReceived < msg.length)
{
// 向服务器发送数据
if(writeBuffer.hasRemaining())
{
channel.write(writeBuffer);
}
// 等待服务器返回数据
if((bytesReceived = channel.read(readBuffer)) == -1)
{
throw new SocketException("Connection closed prematurely");
}
totalBytesReceived += bytesReceived;
System.out.print(".");
}
System.out.println("");
System.out.println("Received: "+new String(readBuffer.array(),0,totalBytesReceived));
channel.close();
}
}
启动服务器端,监听39393端口
启动客户端
再次查看服务器端
5.3 Selector
一个Selector实例可以检查一组信道的I/O状态。
下面使用信道和选择器实现一个回显服务器,并且不适用多线程和忙等。
协议接口
package com.suifeng.tcpip.chapter5;
import java.io.IOException;
import java.nio.channels.SelectionKey;
/**
* 回显服务器协议接口
* @author Suifeng
*
*/
public interface TCPProtocol
{
/**
* 接收请求
* @param key
* @throws IOException
*/
void handleAccept(SelectionKey key) throws IOException;
/**
* 读取数据
* @param key
* @throws IOException
*/
void handleRead(SelectionKey key) throws IOException;
/**
* 接收数据
* @param key
* @throws IOException
*/
void handleWrite(SelectionKey key) throws IOException;
}
回显协议的实现
package com.suifeng.tcpip.chapter5;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* 使用选择器和信道实现的回显协议
* @author Administrator
*
*/
public class EchoSelectorProtocol implements TCPProtocol
{
private int bufferSize;
public EchoSelectorProtocol(int bufferSize) {
super();
this.bufferSize = bufferSize;
}
@Override
public void handleAccept(SelectionKey key) throws IOException
{
System.out.println("Handle Accepting Now...");
SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
// 设置为阻塞方式
channel.configureBlocking(false);
// 信道可读
channel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
@Override
public void handleRead(SelectionKey key) throws IOException
{
System.out.println("Handle Reading Now...");
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = channel.read(buf);
System.out.println("Receiving from client:" + channel.socket().getRemoteSocketAddress()+"\nReceived:"+new String(buf.array()));
if (bytesRead == -1)
{
channel.close();
}
else if(bytesRead > 0)
{
// 信道可读、可写
key.interestOps(SelectionKey.OP_WRITE | SelectionKey.OP_READ);
}
}
@Override
public void handleWrite(SelectionKey key) throws IOException
{
System.out.println("Handling Writing Now....");
ByteBuffer buf = (ByteBuffer) key.attachment();
buf.flip();
SocketChannel channel = (SocketChannel) key.channel();
// 向客户端写入数据
channel.write(buf);
if (!buf.hasRemaining())
{
// 信道可读
key.interestOps(SelectionKey.OP_READ);
}
buf.compact();
}
}
服务器端
package com.suifeng.tcpip.chapter5;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
/**
* 非阻塞处理服务器端
*
* @author Suifeng
*
*/
public class TCPServerSelector
{
private static final int BUFFER_SIZE = 32;
private static final int TIMEOUT = 3000;
public static void main(String[] args) throws IOException
{
if (args.length < 1)
{
throw new IllegalArgumentException("Parameter(s):<Port> ...");
}
// 创建选择器实例
Selector selector = Selector.open();
// 可以同时监听来自多个信道的数据,使用不同的端口
for (String arg : args)
{
// 创建信道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 侦听指定的端口
serverChannel.socket().bind(new InetSocketAddress(Integer.parseInt(arg)));
// 将信道设置为非阻塞方式
serverChannel.configureBlocking(false);
// 该信道可以进行accept操作
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
TCPProtocol protocol = new EchoSelectorProtocol(BUFFER_SIZE);
System.out.println("Server is Running.");
while(true)
{
// 阻塞等待直到超时
if(selector.select(TIMEOUT) == 0)
{
System.out.println("Waiting data from client.");
continue;
}
// 获取选择器下的键集
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while(keys.hasNext())
{
SelectionKey key = keys.next();
if(key.isAcceptable()) // accept操作
{
protocol.handleAccept(key);
}
if(key.isReadable()) // 可读
{
protocol.handleRead(key);
}
if(key.isValid() && key.isWritable()) // 可写
{
protocol.handleWrite(key);
}
keys.remove();
}
}
}
}
启动服务器端,侦听39393和39395端口
启动客户端,一次使用39393端口和39395端口发送数据

再次查看服务器端