BIO,NIO,AIO概念区别及使用场景
BIO
**.
传统的io,传统的服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O):
当客户端有请求到服务端的时候,服务端就会开启一个线程进行处理,当有多个请求进入时,就会开启多个线程分别处理对应的请求。
现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。
例如用java实现传统的BIO类型的TCP长链接,传输数据(聊天室),下面是示例代码,SpringBoot实现。
服务器端
@Component //此类注入Spring容器,其中run方法会随项目启动时执行
//这个地方我们开启了BIO类型的Socket服务端,监听本机的
//8003端口,等待客户端连接
public class SpringListener {
public void run(String... args) throws Exception {
try{
ServerSocket serverSocket = new ServerSocket(8003);
Socket client = null;
boolean flag = true;
//此段代码表示循环监听客户端连接此Socket,开启一个单独的线程,并一直阻塞,收到消息后才进行处理业务逻辑,在此期间,这个线程一直处于挂起状态。
while (flag){
System.out.println("服务器启动。。等待客户端连接");
client = serverSocket.accept();
new Thread(new ServerThread(client)).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
客户端端
@Component
public class TcpClient {
public static void main(String[] args) throws IOException {
Socket s = new Socket("0.0.0.0", 8003);
InputStream input = s.getInputStream();
OutputStream output = s.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(output));
bw.write("999\n"); //向服务器端发送一条消息
bw.flush();
BufferedReader br = new BufferedReader(new InputStreamReader(input)); //读取服务器返回的消息
String mess = br.readLine();
System.out.println("========服务器返回消息:" + mess);
if(mess.equals("999")){
Socket s1 = new Socket("0.0.0.0", 8003);
InputStream input1 = s1.getInputStream();
OutputStream output1 = s1.getOutputStream();
//继续发送消息
BufferedWriter bw1 = new BufferedWriter(new OutputStreamWriter(output1));
bw1.write("数据包!!!!");
bw1.flush();
BufferedReader br1= new BufferedReader(new InputStreamReader(input1)); //读取服务器返回的消息
String mess1 = br1.readLine();
System.out.println("========服务器返回消息:" + mess1);
}
}
}
可以看出以上方法浪费系统资源,客户端一直等待连接,资源处于挂起状态。而大量开启线程也是造成资源浪费的一点。
NIO
当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理模型。
大多数客户端BIO+连接池模型,可以建立n个连接,然后当某一个连接被I/O占用的时候,可以使用其他连接来提高性能。
但多线程的模型面临和服务端相同的问题:如果指望增加连接数来提高性能,则连接数又受制于线程数、线程很贵、无法建立很多线程,则性能遇到瓶.
NIO提供了ServerSocketChannel和SocketChannel。
普通Socket是客户端发出一次请求、服务端接收到后响应、客户端接收到服务端的响应才能再次请求。
NioSocket是引入了三个概念:Channel、Selector、Buffer。Buffer是将很多请求打包,一次性发出去,有Selector扮演选择器的角色,将请求分转给对应的通道(Channel)进行处理响应。
NIO之Buffer缓冲器
Buffer是java.nio包的一个类,主要用来存放数据。
4个重要的属性:
1、capacity——容量。表示Buffer最多可以保存元素的数量,在创建Buffer对象是设置,设置后不可改变。
2、limit——可以使用的最大数量。limit在创建对象时默认和capacity相等,若是有专门设置,其值不可超过capacity,表示可操作元素个数。capacity=100,表示最多可以保存100个,假设现在只存了20个就要读取,这是limit会被设为20.
3、position——当前所操作元素的位置索引,从0开始,随着get方法和put方法进行更新。
4、mark——用来暂存position。起中间变量的作用,使用mark保存当前position的值后,进行相关操作position会产生变化,若使用reset()方法,便会根据mark的值去恢复position操作前的索引。mark默认-1,取值不可超过position。
大小关系:mark<=position<=limit<=capacity
初始化方法:clear(),用于写数据前初始化相关属性值。
clear()初始化的是mark、position和limit的值,将mark=-1,position=0,limit=capacity。
位置置0方法:rewind(),读写模式都可以用。
将matk=-1,position = 0。limit和调用该方法前相同。
读模式转换方法:flip(),用于将写模式转换为读模式。写模式下position是随着数据的写入变化的,最后position会位于写入的最后一个元素的位置。flip方法转换读模式是将这个position的值用来设置limit,这样limit就成为了现在buffer中实际存放的元素个数,然后再将position置0,表示从头开始读,mark初始化为-1。
ServerSocketChannel
创建:通过ServerSocketChannel类的静态方法open()获得。
绑定端口:每个ServerSocketChannel都有一个对应的ServerSocket,通过其socket()方法获得。获得ServerSocket是为了使用其bind()方法绑定监听端口号。若是使用其accept()方法监听请求就和普通Socket的处理模式无异。
设置是否使用阻塞模式:true/false。configureBlocking(false)——不适用阻塞模式。阻塞模式不能使用Selector!
注册选择器以及选择器关心的操作类型:register(Selector,int) 第一个参数可以传入一个选择器对象,第二个可传入SelectionKey代表操作类型的四个静态整型常量中的一个,表示该选择器关心的操作类型。
Selector
创建:通过Selector的静态方法open()获得。
等待请求:select(long)——long代表最长等待时间,超过该时间程序继续向下执行。若设为0或者不传参数,表示一直阻塞,直到有请求。
获得选择结果集合:selectedKeys(),返回一个SelectionKey集合。SelectionKey对象保存处理当前请求的Channel、Selector、操作类型以及附加对象等等。
SelectionKey对象有四个静态常量代表四种操作类型以及对应的判断是否是该种操作的方法:
SelectionKey.OP_ACCEPT——代表接收请求操作 isAcceptable()
SelectionKey.OP_CONNECT——代表连接操作 isConnectable()
SelectionKey.OP_READ——代表读操作 isReadable()
SelectionKey.OP_WRITE——代表写操作 isWritable()
NioSocket中服务端的处理过程分为5步:
1、创建ServerScoketChannel对象并设置相关参数(绑定监听端口号,是否使用阻塞模式)
2、创建Selector并注册到服务端套接字信道(ServerScoketChannel)上
3、使用Selector的select方法等待请求
4、接收到请求后使用selectedKeys方法获得selectionKey集合
5、根据选择键获得Channel、Selector和操作类型进行具体处理。
服务器端
//这段代码中我有写一些业务逻辑,可以忽略不看,主要了解工作原理
@Slf4j
public class NioServer {
private InetAddress addr;
private int port;
private Selector selector;
private static int BUFF_SIZE = 2048;
public NioServer(InetAddress addr, int port) throws IOException {
this.addr = addr;
this.port = port;
startServer();
}
private void startServer() throws IOException {
// 获得selector及通道(socketChannel)
this.selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
// 绑定地址及端口
InetSocketAddress listenAddr = new InetSocketAddress(this.addr, this.port);
serverChannel.socket().bind(listenAddr);
serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);
while (true) {
log.info("服务器等待新的连接和selector选择…");
this.selector.select();
// 选择key工作
Iterator keys = this.selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = (SelectionKey) keys.next();
// 防止出现重复的key,处理完需及时移除
keys.remove();
//无效直接跳过
if (!key.isValid()) {
continue;
}
if (key.isAcceptable()) {
this.accept(key);
} else if (key.isReadable()) {
this.read(key);
} else if (key.isWritable()) {
this.write(key);
} else if (key.isConnectable()) {
this.connect(key);
}
}
}
}
private void connect(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
if (channel.finishConnect()) {
// 成功
log.info("成功连接了");
} else {
// 失败
log.info("失败连接");
}
}
private void accept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
channel.register(this.selector, SelectionKey.OP_READ);
Socket socket = channel.socket();
SocketAddress remoteAddr = socket.getRemoteSocketAddress();
log.info("连接到: " + remoteAddr);
}
private void read(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(BUFF_SIZE);
int numRead = channel.read(buffer);
if (numRead == -1) {
log.info("关闭客户端连接: " + channel.socket().getRemoteSocketAddress());
channel.close();
return;
}
String msg = new String(buffer.array()).trim();
log.info("得到了: " + msg);
// 回复客户端
String reMsg = msg + " 你好,这是服务器给你的回复消息:" + System.currentTimeMillis();
if("999".equals(msg)){
reMsg = "000";
}
channel.write(ByteBuffer.wrap(reMsg.getBytes()));
}
private void write(SelectionKey key) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(BUFF_SIZE);
byteBuffer.flip();
SocketChannel clientChannel = (SocketChannel) key.channel();
while (byteBuffer.hasRemaining()) {
clientChannel.write(byteBuffer);
}
byteBuffer.compact();
}
}
客户端端
@Slf4j
public class NioClient {
private static int BUFF_SIZE = 2048;
public static void main(String[] args) throws IOException, InterruptedException {
InetSocketAddress socketAddress = new InetSocketAddress("0.0.0.0", 10002);
SocketChannel socketChannel = SocketChannel.open(socketAddress);
log.info("连接客户端服务,端口:10002...");
socketChannel.write(ByteBuffer.wrap("999".getBytes()));
log.info("发送包头: " + 999);
ByteBuffer buffer1 = ByteBuffer.allocate(BUFF_SIZE);
buffer1.clear();
socketChannel.read(buffer1);
String result1 = new String(buffer1.array()).trim();
log.info("收到服务器回复的消息:" + result1);
if ("000".equals(result1)) {
File file = new File("C:Users" + File.separator + "86182" + File.separator + "Desktop" + File.separator + "settings.xml");
FileInputStream in = new FileInputStream(file);
int len = 0;
byte[] b2 = new byte[BUFF_SIZE];
while ((len = in.read(b2)) != -1) {
String value = new String(b2,0,len);
socketChannel.write(ByteBuffer.wrap(value.getBytes()));
ByteBuffer buffer = ByteBuffer.allocate(BUFF_SIZE);
buffer.clear();
socketChannel.read(buffer);
String result = new String(buffer.array()).trim();
log.info("收到服务器回复的消息:" + result);
}
in.close();
}
socketChannel.close();
}
}
总结
**传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了",在AIO模型里用户更需要关注的是“读完了”。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
**
NIO存在的问题
使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。
NIO并没有完全屏蔽平台差异,它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。使用NIO做网络编程构建事件驱动模型并不容易,陷阱重重。
推荐大家使用成熟的NIO框架:如Netty,MINA等,解决了很多NIO的陷阱,并屏蔽了操作系统的差异,有较好的性能和编程模型。**