NIO的由来
为啥要有NIO呢,因为以前的IO用起来不爽呗。在JDK1.4之前的版本用的都是BIO,没错,就是阻塞的IO,阻塞的原因在于Java的InputStream,OutputStream本身就是阻塞的,为了解决这种缺陷,从JDK1.4开始引入Buffer解决了阻塞问题,多个输入请求还要通过Selector来注册管理,通过Channel来将请求数据写入到Buffer,有了这三样东西,NIO的雏形就形成了。
BIO的实现
BIO的实现是一个同步阻塞模型,一个客户端连接对应一个处理线程。
image.png
为了说明BIO的问题,我们先看他的一个基本实现。
BIO客户端实现:
package com.david.bio;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Scanner;
public class BIOClient implements Runnable {
private int port;
private Charset charset = Charset.forName("UTF-8");
private String host;
public BIOClient(String host, int port) {
super();
this.host = host;
this.port = port;
}
@Override
public void run() {
//建立Socket连接,获取输出流,完成后自动释放
try (Socket socket = new Socket(host, port);
OutputStream out = socket.getOutputStream();){
//从控制台获取输入的消息
Scanner scanner = new Scanner(System.in);
System.out.println("Please Input Message");
String strMsg = scanner.nextLine();
out.write(strMsg.getBytes(charset));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BIOClient client = new BIOClient("localhost", 8090);
client.run();
}
}
BIO服务器端实现:
package com.david.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
public class BIOServer {
private static Charset charset = Charset.forName("UTF-8");
private static final int port = 8090;
public static void main(String[] args) {
//建立一个port上的Sokcet连接,结束后自动释放ServerSocket
try (ServerSocket serverSocket = new ServerSocket(port);) {
while (true) {
Socket socket = serverSocket.accept(); //监听socket连接,这个方法是阻塞的,直到连接建立
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), charset));
//这里也是阻塞的,没有读到客户端输入就一直阻塞着
String receiveMsg = null;
while ((receiveMsg = bufferedReader.readLine()) != null) {
System.out.println(receiveMsg);
}
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们看到了BIO服务端里面accept方法和read方法都是阻塞的,就是如果没有连接请求,accept就一直阻塞着,如果没有数据可读取,read方法阻塞。
我们启动下服务器端,再启动客户端,结果看到:
image.png
这个时候说明客户端和服务器端的连接已经建立了。
这里有一个问题,多个Client启动的时候,后面启动的一定要等前面的Client输入完成Server读取到数据后才能处理下一个Client的输入,像下面这种效果。
image.png
image.png
image.png
这个问题好解决,我们多起来几个线程接受Socket连接就可以了,这样我们的服务器代码改造成这样了:
package com.david.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServerMultiThread {
private static Charset charset = Charset.forName("UTF-8");
private static ExecutorService executorService = Executors.newFixedThreadPool(3);
public static void main(String[] args) {
int port = 8090;
try (ServerSocket serverSocket = new ServerSocket(port);) {
while (true) {
Socket s = serverSocket.accept();
executorService.submit(new SocketProcess(s));
//new Thread(new SocketProcess(s));
}
} catch (IOException e) {
e.printStackTrace();
}
}
static class SocketProcess implements Runnable {
Socket s;
public SocketProcess(Socket socket) {
this.s = socket;
}
@Override
public void run() {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(s.getInputStream(), charset));)
{
//这里也是阻塞的,没有读到客户端输入就一直阻塞着
String receiveMsg = null;
while ((receiveMsg = bufferedReader.readLine()) != null) {
System.out.println(receiveMsg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这个时候我们再分别启动服务器和客户端,输入后,结果是这样:
image.png
image.png
image.png
image.png
到这里,阻塞的问题似乎解决了。可是如果来的输入特别多的话,仍然会有资源竞争,这个时候会出现排队太久的问题,于是我们的NIO就诞生了。
NIO的实现
NIO和上面的实现最大的改进在于去掉了InputStream的阻塞,它的设计大致是这样的:
image.png
我们可以看到NIO主要由三大组件组成:
Buffer:用于存储数据,底层基于数组实现,针对8种基本类型提供了缓冲区。
Channel:用于进行数据传输,面向缓冲区进行操作,支持双向传输,数据可以从Channel读取到Buffer中,也可以从
Selector:选择器,主要是用来注册Channel和记录(监听)Channel状态的。当某个Channel上面发生了读或者写事件,Channel就处于就绪状态,会被Selector监听到,然后通过SelectionKeys就可以获取就绪状态的Channel集合,进行后续的操作了。
NIO的服务器端实现:
package com.david.nio;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NIOServer {
private static Charset charset = Charset.forName("UTF-8");
private static CharsetDecoder cd = charset.newDecoder();
private static final int port = 8090;
private static ExecutorService executorService = Executors.newFixedThreadPool(3);
public static void main(String[] args) throws IOException{
//创建一个slector
Selector selector = Selector.open();
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(port));
//需要设置成非阻塞
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);
int connectCount = 0;
//少量的线程
int threads = 3;
while (true) {
int readyChannelCount = selector.select();
if(readyChannelCount == 0) continue;
Set selectionKeys = selector.selectedKeys();
Iterator keyInterator = selectionKeys.iterator();
while (keyInterator.hasNext()) {
SelectionKey key = keyInterator.next();
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
SocketChannel sc = serverSocketChannel.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
executorService.execute(new SocketProcess(key));
key.channel();
} else if (key.isWritable()) {
} else if (key.isConnectable()) {
}
keyInterator.remove();
}
}
}
static class SocketProcess implements Runnable {
SelectionKey key;
public SocketProcess(SelectionKey key) {
this.key = key;
}
@Override
public void run() {
try
{
System.out.println("connet" + key.attachment() + "send msg:" + readFromChannel());
key.channel().close(); //不需要的时候要关闭channel
} catch (IOException e) {
e.printStackTrace();
}
}
private String readFromChannel() throws IOException {
SocketChannel sc = (SocketChannel)key.channel();
int bufferSize = 1024;
ByteBuffer bf = ByteBuffer.allocateDirect(bufferSize);
//定义一个更大的buffer
ByteBuffer biggerBf = null;
int count = 0; //读的次数统计
while ((sc.read(bf)) != -1) {
count++;
ByteBuffer temp = ByteBuffer.allocateDirect(bufferSize * (count + 1));
//这里将Buffer由写转为读模式
if (biggerBf != null) {
biggerBf.flip();
temp.put(biggerBf);
}
biggerBf = temp;
//将这次独到的放入大的buffer里面
bf.flip();
biggerBf.put(bf);
bf.clear(); //为了下次读需要清理buffer
}
if(biggerBf != null) {
biggerBf.flip();
try {
return cd.decode(biggerBf).toString();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
}