NIO介绍
基本Java套接字对于小规模系统可以很好运行,涉及同时有上千个客户端,就会出现问题,其中一客户一线程的方式在线程的创建,维护和切换需要系统开销较大,而使用线程池的方式虽然节省了一定的系统开销,但是对于连接生存期比较长的协议,线程池的大小限制了系统可以同时服务的客户端总数。随着线程池数量增加,带来了更多的线程处理开销,而不能提升系统的性能。此外线程很难做到以下方面:
1)要保证某些连接优先获取服务,或想要指定一定的服务顺序,线程可能就很难做到。
2)不同客户端对服务器端的共享信息(状态)同步访问和修改,需要使用锁机制或者其他互斥机制对依次访问的状态进行严格的同步。同时需要考虑多线程服务器的正确性和高效性就变得非常困难。
NIO就可以解决上面的问题,并且通过轮询一组客户端的方法,来查找需要服务的客户端。NIO主要包括两个部分:java.nio.channels包下面的Selector(选择器)和Channel(通道),java.nio包下面的Buffer(缓冲区)抽象。
1)Channel类似于流,一个Channel实例代表了一个可轮询的IO目标,如套接字(或者文件,设备),需要将要监控的通道Channel注册到Selector实例上,然后调用选择器的select()方法,该方法会一直阻塞,直到一个或者多个通道准备好了IO操作(接收,读,写)或者等待超时。
2)Buffer是一个有限容量的数据容器,主要用来从Channel中读取数据或者发送数据到Channel中,其本质是一个数组,使用Buffer的好处。第一,与读写缓冲区数据相关联的系统开销暴露了出来,如缓冲区空间不足,但需要存入数据时,需要操作获取空间,你可以控制发生的时间,如何发生以及是否发生。第二,一些对Java对象的特殊Buffer映射操作能够直接操作底层平台的资源,这些操作节省了再不同地址空间中复制数据的开销。
使用选择器具体步骤如下:
1.创建一个Selector实例
2.将要检测的通道注册到选择上面,并同时制定通道上感兴趣的IO操作。
3.重复执行。
1)调用一种select()方法。
2)获取选择的键的列表。
3)对于已选择键集中的每个键。
a.获取通道,并从键中获取附件(如果合适的话)
b.确定准备就绪的操作并执行。如果是accept操作,将接受的通道设置为非阻塞模式,并将其注册到选择器上。
c.如果需要,修改键的兴趣操作集。
d.从已经选择键中移除键。
NIO注意点
1.在NIO中数据的读写操作始终与缓冲区关联,Channel将数据读入缓冲区,然后我们从缓冲区访问数据。写数据也是将要发送的数据按顺序填入缓冲区。
2.缓冲区提供了Java基本数据类型中除布尔类型以外的视图,数据真正存储地方是ByteBuffer,其他如CharBuffer只是将ByteBuffer解释成其他基本类型。
3.Buffer比较常见的方法flip()和hasRemaining(),其中flip()是用于缓冲区写入模式切换到读取模式(将缓冲区当前置为0,从头开始读取数据),hasRemaining()方法表示的是缓冲区是否还有数据。
4.Channel和Selector配置使用时,必须先将Channel注册到Selector上面,且要将Channel设置成非阻塞模式。Selector可以对通道,Selector可以监听通道四种不同的事件:Connect,Accept,Read,Write;
案例
public class TCPEchoClientNonBlocking {
private static final String SERVER = "127.0.0.1";
private static final int PORT = 1001;
public static void main(String[] args) throws IOException {
byte[] argument = "Hello ServerSocketChannel".getBytes();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
//判断是否已经完成连接
if (!socketChannel.connect(new InetSocketAddress(SERVER, PORT))) {
while (!socketChannel.finishConnect()) {
System.out.println(".");
}
}
ByteBuffer writeBuffer = ByteBuffer.wrap(argument);
ByteBuffer readBuffer = ByteBuffer.allocate(argument.length);
//接收到的总的字节数
int totalBytesRcvd = 0;
//调用一次read()方法返回的字节数
int byteRcvd;
while (totalBytesRcvd < argument.length) {
if (writeBuffer.hasRemaining()) {
socketChannel.write(writeBuffer);
}
if ((byteRcvd = socketChannel.read(readBuffer)) == -1) {
throw new SocketException("Connetion closed prematurely");
}
totalBytesRcvd += byteRcvd;
System.out.println(".");
}
//打印接收到的字节数
System.out.println("Received :" + new String(readBuffer.array(), 0, totalBytesRcvd));
socketChannel.close();
}
}
public class TCPServerSelector {
private static final int BUFSIZE = 256;
private static final int TIMEOUT = 300;
public static void main(String[] args) throws IOException {
String[] ports = "1001,1002,1003,1004".split(",");
//创建选择器实例
Selector selector = Selector.open();
for (String port : ports) {
//创建通道,并将通道设置为非阻塞状态,注册到选择器上面
ServerSocketChannel listenChannel = ServerSocketChannel.open();
listenChannel.bind(new InetSocketAddress(Integer.parseInt(port)));
listenChannel.configureBlocking(false);
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
}
TCPProtocol protocol = new EchoSelectorProtocol(BUFSIZE);
while (true) {
//等待通道准备好
if (selector.select(TIMEOUT) == 0) {
System.out.println(".");
continue;
}
//selectionKeys返回的集合中包含每个准备好某一IO操作的信道SelectionKey(注册时创建)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//通道感兴趣操作是接收操作
if (key.isAcceptable()) {
protocol.handleAccept(key);
}
//通道感兴趣的操作是读取操作
if (key.isReadable()) {
protocol.handleRead(key);
}
//通道感兴趣的操作是写入操作(向客户端)
if (key.isValid() && key.isWritable()) {
protocol.handleWrite(key);
}
//从键集合中移除已经处理后的键
iterator.remove();
}
}
}
}
public interface TCPProtocol {
void handleAccept(SelectionKey key) throws IOException;
void handleRead(SelectionKey key) throws IOException;
void handleWrite(SelectionKey key) throws IOException;
}
public class EchoSelectorProtocol implements TCPProtocol {
private int bufSize;
public EchoSelectorProtocol(int bufSize) {
this.bufSize = bufSize;
}
@Override
//服务端处理客户端的连接
public void handleAccept(SelectionKey key) throws IOException {
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
//通道注册到选择器上,附件是缓存区
clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
}
@Override
//服务器端从通道中读取数据
public void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
long bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
clientChannel.close();
} else if (bytesRead > 0) {
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
}
@Override
//将数据写到客户端中
public void handleWrite(SelectionKey key) throws IOException {
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
SocketChannel clientChannel = (SocketChannel) key.channel();
clientChannel.write(buffer);
if (!buffer.hasRemaining()) {
key.interestOps(SelectionKey.OP_READ);
}
buffer.compact();
}
}