我们知道,Java服务端编程,很重要的一块就是IO,而我们的Java IO,经历了由BIO到NIO再到AIO的过程。
首先,我们来看一下什么是BIO:
1.普通BIO
我们刚开始学Java Socket的时候,可能都写过这么一段代码:
ServerSocket serverSocket = new ServerSocket(8000);
while (true){
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
byte [] buff = new byte[1024];
int len = is.read(buff);
while (len > 0){
String msg = new String(buff,0,len);
System.out.println("收到" + msg);
len = is.read(buff);
}
}
显然,这段代码把对客户端接入后的处理都放在了while循环中,也就是说,这段代码一段时间只能处理一个客户端连接,那么问题来了,我们想一想,能作为服务端的服务器性能应该是不错的,所以这种方式浪费了我们的性能,而且,由于一次处理一个客户端连接,并发量自然而然地上不去,所以,这个方法不会在生产环境中使用。
2.多线程式BIO
为了提高并发量,我们可以将代码改成这个样子:
ServerSocket serverSocket = new ServerSocket(8000);
while (true){
Socket socket = serverSocket.accept();
new Thread(new Handler(socket)).start();
}
public class Handler implements Runnable {
private Socket socket;
public Handler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream is = null;
try {
is = socket.getInputStream();
byte[] buff = new byte[1024];
int len = is.read(buff);
while (len > 0) {
String msg = new String(buff, 0, len);
System.out.println("收到" + msg);
len = is.read(buff);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
但是这样会有一个问题,我们来一个客户端连接不管三七二十一就会new一个线程,这样线程数和客户端连接数是1:1的关系,我们知道,线程的创建和销毁是很耗资源的一件事情,同样地,这样也会增加线程上下文切换的开销。
为了降低线程创建和销毁的开销,我们可以引入线程池(代码先不贴了),但是这样并没有从根本上解决问题。
为了从根本上解决问题,Java引入了NIO:
3.NIO
我们可以这么来理解,我们把web服务器比作银行,客户端发起的连接比作要办理业务的客户,银行处理业务的工作人员比作处理客户端连接的线程,由于银行的资源(假如说只有一台计算机能够处理业务)有限,那么同一时刻只有一个人可以办理业务。
那么我们的BIO就相当于:进来一个客户,就给它分配一个业务员,但是业务员只有得到资源(计算机)才能处理业务,所以客户一进入银行就给他分配业务员是没什么卵用的,还浪费资源(业务员是要领工资的)
而我们的NIO呢,就相当于只有一个业务员,但是有一个排号系统,客户进入银行先在排号系统登记,领一个号码,等到轮到某个客户处理业务了,才给他分配一个业务员,处理他的业务。
public class NIOServer {
private int port = 8000;
private Selector selector = null;
private Charset charset = Charset.forName("UTF-8");
public NIOServer(int port) throws IOException {
this.port = port;
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(this.port));
server.configureBlocking(false);
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
}
public void listener() throws IOException {
while (true) {
int wait = selector.select();
if (wait == 0) continue;
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
process(key);
}
}
}
public void process(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
StringBuilder content = new StringBuilder();
try {
while (client.read(buff) > 0) {
buff.flip();
content.append(charset.decode(buff));
}
System.out.println(content);
key.interestOps(SelectionKey.OP_READ);
} catch (IOException io) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
public static void main(String[] args) throws IOException {
new NIOServer(8000).listener();
}
}
NIO呢,就是我们来一个客户端连接,先不处理,先在Selector上注册,然后不断地轮询注册在Selector上面的channel,当准备好读或者写的时候,再对客户端连接进行操作,这样呢,就大大节省了服务端的资源,因此十分适合于大量长连接的场景。