一、简介
1、基本概念区分
(1)同步:Java自己处理IO读写。
(2)异步:Java将IO读写委托给OS处理,将数据缓冲区地址和大小传给OS,完成后OS通知Java处理。
(3)阻塞:调用后会一直阻塞到读写完成后再返回。
(4)非阻塞:如果不能立马读写,调用后就会马上返回,当IO事件分发器通知可再次读写时再进行读写,不断循环直到读写完成。
2、关于BIO、NIO、AIO模型
(1)BIO:同步并阻塞
服务器的实现模式为:一个连接,一个线程。
缺点:客户端连接数与服务端连接数成正比,容易造成不必要的线程开销,甚至导致内存溢出。
(2)NIO:同步非阻塞
服务器的实现模式为:多个请求,一个线程。请求会注册在IO复用器selector上,多路复用器轮询,直到连接有IO请求时才启动一个线程进行处理。
(3)AIO:异步非阻塞
服务器的实现模式为:多个有效请求,一个线程。客户端的IO请求会由OS先完成再通知服务器应用去启动线程处理。
二、NIO编程
1、NIO与BIO的不同
NIO解决的是BIO的并发问题,在使用BIO时由于每创建一个线程,就要为这个线程分配一定的内存空间,而操作系统本身对线程的总数也会有一定的限制,如果客户端的请求过多,服务端程序可能因为不堪重负而拒绝客户端请求 ,甚至瘫痪。而 NIO中当一个连接创建后,不需要新创新一个线程,这个连接会被注册到复用器上,所有连接只需要一个线程即可 ,这个线程中的复用器会进行轮询,发现连接上有请求时才开启一个线程进行处理。
2、NIO中的三大组件
(1)通道Channel
①类型
FileChannel:从文件中读取数据。
DatagramChannel:通过UDP读取网络中的数据。
SocketChannel:通过TCP读写网络中数据,用来建立TCP连接。
ServerSocketChannel:一般在服务端使用,用于监听新的TCP连接,对每个新进来的连接创建SocketChannel。
②特点
既可以从通道中读取数据,又可以写数据到通道(不同于流,流的读写通常是单向的);通道可以异步地读写。
(2)缓冲区Buffer
缓冲区主要是跟通道Channel打交道,数据总是从Buffer写入到Channel中,或者从Channel读取数据到Buffer。
①类型:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer。
②状态变量
capacity:最大容量。
position:当前读写的位置。
limit:缓冲区现存元素的计数。
mark:标记位置,调用mark()来设定mark=postion,调用reset()设定position=mark(标记在未设定前是undefined)。
这四个属性的关系:0<=mark<=position<=limit<=capacity。
③缓冲区相等的必要条件:缓冲区的对象类型相同;对象都剩余同样数量的元素;缓冲区中被get()的剩余数据元素序列必须一致。
④buffer相关方法
allocate(capacity):在堆上创建指定大小的对象空间。
allocateDirect(capacity):在堆外空间创建指定大小的空间。有时buffer作用时间超长,在堆内创建会影响gc效率,在堆外创建的话,不受gc的影响。
wrap(byte()):通过存在的数组创建对象。
wrap(byte[] ,offerset,length):通过存在的数组创建对象。
put():往buffer中写入数据,position指针移动。
flip():读写模式切换,limit指向pos,pos指向mark。//重要
get():pos指针移动。
clear():清空buffer缓存 mark=undefined。 pos=0,limit=capacity。
(3)复用器Selector
能检测一到多个NIO通道,并知道通道是否做好准备。Selector的存在让一个单独的线程可以管理多个Channel,从而管理多个连接。
①监听事件
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
②方法
Selector相关方法:
select():选择器进行监听,有事件则返回,会一直阻塞直到至少有一个事件到达。
select(long timeout):和select()一样,只是设定了最长阻塞时长timeout,单位是毫秒。
SelectionKey相关方法:
channel():将注册到selector中的通道在SelectionKey中也维护一个。
selector():将当前选择器实例在SelectionKey中维护一个。
isValid():当前的selectionKey是否有效。
cancel() : 取消对当前key的关注。
isWritable():是否为可写事件。
isReadable():是否为可读事件。
isConnectable():是否为可连接事件。
isAcceptable():是否为可接受事件。
3、服务端
(1)具体流程
①创建ServerSocketChannel实例;
②绑定端口并将通道设置为非阻塞(如果通道在某个事件上被阻塞,那么服务器就必须等这个事件处理完才能去处理其它事件,选择器失去意义);
③实例化Selector复用器,Selector.open();
④将ServerSocketChannel实例注册到selector复用器上,关注accept事件;
⑤复用器进行监听,有事件则返回,selector.select()会一直阻塞直到至少有一个事件到达;
⑥获得感兴趣事件集合并遍历,判断是否有可接收事件,若有则调用accept(),实例化socketChannel,并设置为非阻塞后注册给复用器;
⑦通过Buffer从Channel中读取数据,最后关闭Selector、SocketChannel、ServerSocketChannel等资源。
(2)代码实现
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class NIOServerDemo {
public static void main(String[] args) {
try {
//创建ServerSocketChannel通道实例
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(8889));
System.out.println("服务端启动");
//将channel设置为非阻塞
serverSocketChannel.configureBlocking(false);
//实例化IO复用器selector
Selector selector = Selector.open();
//将serverSocketChannel注册到复用器上,并关注accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//监听复用器中是否有事件完成,select()会阻塞,直至有事件完成才返回
while (selector.select() > 0) {
//获取完成事件的结果集
//包含已完成事件的集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
//有可接受事件完成
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
//接收用户的连接
SocketChannel socketChannel = serverSocketChannel1.accept();
System.out.println("客户端" + socketChannel.getRemoteAddress()+"连接上");
//将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel通道注册到复用器上,并监听read事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
if (selectionKey.isReadable()) {
//有可读事件发生
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//进行读操作,对于Buffer是写操作
int read = channel.read(buffer);
if (read != -1) {
//进行读写模式的切换
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String msg = new String(bytes);
if (!"".equals(msg)) {
//关闭socketChanel实例
channel.close();
}
System.out.println("收到客户端发送的:" + new String(bytes));
}
}
}
}
//关闭资源
selector.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4、客户端
(1)具体流程
①实例化SocketChannel,并设置为非阻塞。
②实例化Selector;
③连接服务端,将socketchannel注册到selector,并关注可连接事件;
④监听复用器中已完成的事件,判断集合中的已完成事件是否为可连接事件,并通过finishConnect将可连接事件完成连接;
⑤发送、接收消息,最后关闭资源。
(2)代码实现
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.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class NIOClientDemo {
public static void main(String[] args) {
try {
//创建SocketChannel实例
SocketChannel socketChannel = SocketChannel.open();
//将SocketChannel实例设置为非阻塞
socketChannel.configureBlocking(false);
//创建selector实例
Selector selector = Selector.open();
//连接服务端,因为前面设置的是非阻塞,所以调用这个方法会立即返回.
//因此如果返回true就不需要放到select中
if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 8889))) {
//将SocketChannel实例注册到选择器中,并关注connect事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
//感兴趣操作是否完成
selector.select();
// System.out.println("有可接收事件发生");
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isValid() && key.isConnectable()) {
SocketChannel Channel = (SocketChannel) key.channel();
//如果连接成功则完成连接
if (Channel.isConnectionPending())
Channel.finishConnect();
System.out.println("客户端连接成功");
Channel.configureBlocking(false);
//监听SocketChannel读事件。
Channel.register(selector, SelectionKey.OP_READ);
}
}
}
//设置buffer缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//创建控制台实例
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("请输入:");
String msg = scanner.nextLine();
//客户端结束条件
if ("exit".equals(msg)){
break;
}
//向缓存写入数据
buffer.put(msg.getBytes());
buffer.flip();
//发送数据
socketChannel.write(buffer);
//清空缓存
buffer.clear();
//监听读事件,没有则阻塞
selector.select();
socketChannel.read(buffer);
//读写模式切换
buffer.flip();
//通过缓冲区有效元素大小确定接收数组大小
byte[] bytes = new byte[buffer.remaining()];
//从缓存读取数据
buffer.get(bytes);
System.out.println("recv:" + msg);
buffer.clear();
}
socketChannel.close();
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}