NIO(Non-blocking无阻塞 I/O)
一个连接一个线程的经典模型中,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让第二个线程去处理第二个连接。其实这也是所有使用多线程的本质。包括servlet也是如此,一个连接被servlet容器分配一个线程去处理。(servlet多线程单实例依旧能保证线程安全的原因是servlet是无状态的,没有属性/实例变量的。)
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个线程专注于自己的连接的I/O,不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很"贵"的资源。当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理模型。
https://zhuanlan.zhihu.com/p/23488863
常见I/O模型对比:
NIO产生的原因:
所有的系统I/O分为两个阶段,等待就绪(等待可读/可写),操作(读/写)。读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞也基本不使用CPU的"只进行I/O的命令调度,I/O的读写过程不占用CPU",而且这个过程非常快,属于memory copy,基本不耗时。
1.传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。(当前事情必须有结果才能处理别的事情,且当前事情不一定有结果)
2.对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。(当前事情必须有结果才能处理别的事情,且当前事情一定有结果比如返回0)
3.最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的(当前事情没有结果也能处理别的事情)
回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能"傻等",即使通过各种估算,算出来操作系统没有能力进行读写,也没法在socket.read()和socket.write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程另起炉灶,没有好的办法利用CPU。
NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。
NIO的主要事件有:读就绪,写就绪,接收连接就绪,连接就绪。我们首先需要注册这几个事件到来时对应的处理器,然后在合适的时机告诉事件选择器所感兴趣的事件。对于读就绪,就是系统完成连接并且系统已经读满无法承载新读入的数据的时刻,对于写就绪,就是已经写满写不出去的时刻。。其次用一个死循环选择就绪的事件。新事件到来的时候会在selector上注册标记位,标示可读,可写,或新连接到来。
- SelectionKey.OP_ACCEPT —— 接收连接就续事件,表示服务器监听到了客户连接,并且服务器可以接收这个连接了
- SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功
- SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
- SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
也就是说理论上NIO实现的逻辑是当前事情没有结果/不能做的时候转而选择别的事情,而java实现的逻辑是所有事情中哪些事情可做就去做哪些事情。事件的就绪状态应该是随时间随机变化的,人为不可控,应该可以直接调用判断函数。这样的话应该有两个循环,先对当前信道对应的缓冲循环判断可读和可写事件和新连接到来事件,可读时去读,可写时去写。
Java NIO提供了与标准IO不同的IO工作方式:
Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
所有的 IO 在NIO 中都从一个Channel 开始。Channel 通道有点像流(因为像IO流一样有方向)。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。这里有个图示:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
使用selector监听channel通道前要先向selector注册channel,然后调用selector的select()选择事件就绪的管道, 选择方法会一直阻塞直到某个通道有事件就绪,如新连接进来,数据接收等。
Selector几个重载的select()方法:
select():阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout):和select()一样,但最长阻塞事件为timeout毫秒。
selectNow():非阻塞,只要有通道就绪就立刻返回。
select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。
如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
一旦调用select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键集合。如下: Set selectedKeys=selector.selectedKeys();
CSDN中的注册次序问题?到底是注册通道还是注册事件???
只监听通道是没有意义的,一定是监听通道的具体事件。
http://blog.csdn.net/mars5337/article/details/6576417【buffer】
创建一个容量为256字节的ByteBuffer,如果发现创建的缓冲区容量太小,唯一的选择就是重新创建一个大小合适的缓冲区.
ByteBuffer.allocate(256);
从套接字通道(信道)读取数据
int bytesReaded=socketChannel.read(buffer);
执行以上方法后,通道会从socket读取的数据填充此缓冲区,它返回成功读取并存储在缓冲区的字节数.在默认情况下,这至少会读取一个字节,或者返回-1指示数据结束.
向套接字通道(信道)写入数据
socketChannel.write(buffer);
此方法以一个ByteBuffer为参数,试图将该缓冲区中剩余的字节写入信道.
将缓冲区准备为数据传出状态,输出通道会从数据的开头而不是末尾开始
buffer.flip();
读取buffer中数据:
while (buffer.hasRemaining()) {
System.out.print((char)buffer.get());
}
serverSocket/serverSocketChannel执行了accept()方法后返回的socket/socketchannel肯定还是服务端的,只是包含了客户端数据而已。这样也说明客户端和服务端socket/socketChannel对等,区别在于有无客户端数据而已。这样才能理解为什么在服务端给服务端通道注册读就绪事件是用客户端通道注册的(其实还是用服务端通道注册的,只是这个服务端通道包含了客户端数据而已写成了客户端通道)
实例:
服务端:
package testSocket;
public class NIOServerCopy {
/*标识数字*/
private int flag = 0;
/*缓冲区大小*/
private int BLOCK = 4096;
/*接受数据缓冲区*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
private Selector selector;
public NIOServerCopy(int port) throws IOException {
//通道的服务端开启
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//配置为非阻塞
serverSocketChannel.configureBlocking(false);
//还是利用服务端socket接口绑定端口
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(port));
//通过open()方法找到通道事件选择器selector
selector = Selector.open();
//在构造函数中先将服务端通道接收连接事件注册进选择器(监听通道一定是监听到具体的事件才有意义,服务器是一定会接收连接的)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//服务端通道读就绪事件的注册必须等通道执行了接收(执行accept)才能注册
//serverSocketChannel.register(selector, SelectionKey.OP_READ);
}
// 监听
private void listen() throws IOException {
while (true) {
//选择事件已经就绪的通道,方法的返回值为事件已就绪通道的个数
selector.select();
//返回此选择器的已就绪键集
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectedKey = iterator.next();
iterator.remove();
handleKey(selectedKey);
}
}
}
// 处理请求
private void handleKey(SelectionKey selectedKey) throws IOException {
// 接受请求
ServerSocketChannel serverChannel = null;
SocketChannel clientChannel = null;
String receiveText;
String sendText;
int count=0;
// 测试此键的通道是否已准备好接受新的套接字连接。
if (selectedKey.isAcceptable()) {
//返回此键的通道
serverChannel = (ServerSocketChannel) selectedKey.channel();
// 接受到此通道套接字的连接。
// 服务端通道执行接收方法后当然还是服务端通道,只是因为包含了客户端数据在形式上写成客户端通道。 这样看来服务端通道和客户端通道是一样的,只是有没有客户端数据的区别而已。
clientChannel = serverChannel.accept();
// 配置为非阻塞
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
// 注册到selector,等待连接
//郭晋华注掉:(一开始在选择器中把服务器端管道的接受就绪和读就绪一起注册好,其次是客户端通常不用注册读就绪,有写就绪事件就可以了)client.register(selector, SelectionKey.OP_READ);
} else if (selectedKey.isReadable()) {
// 返回为之创建此键的通道。
clientChannel = (SocketChannel) selectedKey.channel();
//读取服务器发送来的数据到缓冲区中
count = clientChannel.read(receivebuffer);
if (count > 0) {
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("服务器端接受客户端数据--:"+receiveText);
clientChannel.register(selector, SelectionKey.OP_WRITE);
}
//将缓冲区清空以备下次读取
receivebuffer.clear();
} else if (selectedKey.isWritable()) {
//将缓冲区清空以备下次写入
sendbuffer.clear();
// 返回为之创建此键的通道。
clientChannel = (SocketChannel) selectedKey.channel();
sendText="message from server--" + flag++;
//向缓冲区中输入数据
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
//输出到通道
clientChannel.write(sendbuffer);
}
}
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
NIOServerCopy server = new NIOServerCopy(8888);
server.listen();
}
}
客户端:
package testSocket;
public class NIOClientCopy {
/*标识数字*/
private static int flag = 0;
/*缓冲区大小*/
private static int BLOCK = 4096;
/*接受数据缓冲区*/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/*服务器端地址*/
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"localhost", 8888);
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
// 打开SocketChannel通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞方式
socketChannel.configureBlocking(false);
// 打开选择器
Selector selector = Selector.open();
// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 连接
socketChannel.connect(SERVER_ADDRESS);
// 分配缓冲区大小内存
Set<SelectionKey> selectedKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectedKey;
SocketChannel clientChannel;
String receiveText;
String sendText;
int count=0;
while (true) {
//选择一组键,其相应的通道已为 I/O 操作准备就绪。
//此方法执行处于阻塞模式的选择操作。
selector.select();
//返回此选择器的已选择键集。
selectedKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
selectedKey = iterator.next();
if (selectedKey.isConnectable()) {
System.out.println("connect success");
clientChannel = (SocketChannel) selectedKey.channel();
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (clientChannel.isConnectionPending()) {
clientChannel.finishConnect();
System.out.println("完成连接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server.".getBytes());
sendbuffer.flip();
clientChannel.write(sendbuffer);
}
clientChannel.register(selector, SelectionKey.OP_READ);
} else if (selectedKey.isReadable()) {
clientChannel = (SocketChannel) selectedKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count=clientChannel.read(receivebuffer);
if(count>0){
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("客户端接受服务器端数据--:"+receiveText);
clientChannel.register(selector, SelectionKey.OP_WRITE);
}
} /*else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--" + (flag++);
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客户端向服务器端发送数据--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
} */
}
selectedKeys.clear();
}
}
}
http://blog.csdn.net/java2000_net/article/details/3102228 多个SocketChannel注册Selector统一管理