BIO即阻塞IO,NIO是非阻塞IO。NIO 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 BIO 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。NIO被称为 no-blocking io 或者 new io都说得通。如果使用BIO作为服务端,多个请求到来时由于是阻塞的,那么处理是串行的。用一个BIO代码例子来帮助理解
BIO的网络编程
本例子实现简单的web服务器(简单模仿Tomcat的请求和响应),下面将从最原始的版本逐渐改进BIO服务,来帮助理解BIO的工作过程
BIO服务-版本1
服务器端的实现:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.创建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.设置端口号
serverSocket.bind(new InetSocketAddress(8989));
//3.获取报文,阻塞直到有连接进来
Socket socket = serverSocket.accept();
//a.获得客户端的意图 request
InputStream is = socket.getInputStream();
//字符流和字节流的转换
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据,没有数据也会阻塞
while((line = br.readLine()) != null){
sb.append(line);
}
System.out.println("服务器收到的数据:"+sb.toString());
//b.返回客户端数据
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的数据:"+sb.toString()+" 服务器已经接收到数据");
//将数据发送
pw.flush();
br.close();
pw.close();
socket.close();
}
}
客户端的实现:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 客户端
* @author MOTUI
*
*/
public class BIOClientSocket {
public static void main(String[] args) throws Exception {
//1.创建Socket
Socket socket = new Socket();
//2.连接服务器
socket.connect(new InetSocketAddress("192.168.0.117",8989));
//a.发送数据到服务器
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("这是客户端数据");
//将数据发送
pw.flush();
//告知服务器已经到了流的结尾
socket.shutdownOutput();
//b.获得服务器的回应
InputStream is = socket.getInputStream();
//字符流和字节流的转换
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据
while((line = br.readLine()) != null){
sb.append(line);
}
//打印服务器回应数据
System.out.println(sb.toString());
br.close();
pw.close();
socket.close();
}
}
服务器端和客户端运行结束。这样的结果并和我们想要的结果不同,我们需要的是服务器端一直监听客户端发送的请求并作出响应。我们【服务器端】做如下的修改,客户端代码不做任何修改。
BIO服务-版本2
服务器端的实现:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.创建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.设置端口号
serverSocket.bind(new InetSocketAddress(8989));
while(true){
//3.阻塞直到有连接进来
Socket socket = serverSocket.accept();
//a.获得客户端的意图 request
InputStream is = socket.getInputStream();
//字符流和字节流的转换
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据,没有数据也会阻塞
while((line = br.readLine()) != null){
sb.append(line);
}
System.out.println("服务器收到的数据:"+sb.toString());
//b.返回客户端数据
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的数据:"+sb.toString()+" 服务器已经接收到数据");
//将数据发送
pw.flush();
br.close();
pw.close();
socket.close();
}
}
}
在执行的代码中加上while(true) ,保证执行代码一直执行。这样就达到了一直监听客户端的请求的目的,但是还是有问题:我们处理客户端的请求的时候会存在一个请求没有处理完成,另一个请求不能处理。(在处理的过程中最耗时的操作就是I/O操作)。为了解决这个问题,我们进行如下修改。
BIO服务-版本3
服务器端的修改实现:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.创建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.设置端口号
serverSocket.bind(new InetSocketAddress(8989));
while(true){
//3.阻塞直到有连接进来
final Socket socket = serverSocket.accept();
//启动线程
new Thread(){
public void run() {
try {
//a.获得客户端的意图 request
InputStream is = socket.getInputStream();
//字符流和字节流的转换
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据,没有数据也会阻塞
while((line = br.readLine()) != null){
//将数据存储在StringBuilder中
sb.append(line);
}
System.out.println("服务器收到的数据:"+sb.toString());
//b.返回客户端数据
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的数据:"+sb.toString()+" 服务器已经接收到数据");
//将数据发送
pw.flush();
br.close();
pw.close();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};
}.start();
}
}
}
客户端代码修改为:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 客户端
* @author MOTUI
*
*/
public class BIOClientSocket {
public static void main(String[] args) throws Exception {
//使用线程模拟用户 并发访问
for (int i = 0; i < 20; i++) {
new Thread(){
public void run() {
try {
//1.创建Socket
Socket socket = new Socket();
//2.连接服务器
socket.connect(new InetSocketAddress("192.168.0.117",8989));
//a.发送数据到服务器
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("这是客户端数据");
//将数据发送
pw.flush();
//告知服务器已经到了流的结尾
socket.shutdownOutput();
//b.获得服务器的回应
InputStream is = socket.getInputStream();
//字符流和字节流的转换
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据
while((line = br.readLine()) != null){
sb.append(line);
}
System.out.println(sb.toString());
br.close();
pw.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
};
}.start();
}
}
}
到这里我们感觉这种方式已经很合理了。在了解高并发之前,这的确看着不错。但是我们可以思考一个问题:如果我们的并发打到万级或者百万级或者更高的时候,我们的程序有没有问题呢?我们每有一个请求就创建一个线程,而且我们创建线程之后并没有关注这个线程是否正确执行?是否执行结束?我们都没有关注,这时候就出现问题了,当高并发情况下有很多的线程处于阻塞状态,而我们的系统资源已经占用,系统对I/O的处理就会慢,对I/O的处理变慢就会导致我们的线程阻塞,恶性循环直到系统假死(宕机)。这样的处理是不是不合理呢。我们现在虽然无法做到线程不阻塞(不阻塞就是NIO的方式),但是我们可以进行其他的做法,我们可以控制线程的创建个数,这样就会相对打到一个平衡。
BIO服务-版本4
服务器端修改为:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服务器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.创建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.设置端口号
serverSocket.bind(new InetSocketAddress(8989));
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
while(true){
//3.阻塞直到有连接进来
final Socket socket = serverSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("当前线程ID:"+Thread.currentThread().getId());
//a.获得客户端的意图 request
InputStream is = socket.getInputStream();
//字符流和字节流的转换
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据,没有数据也会阻塞
while((line = br.readLine()) != null){
sb.append(line);
}
System.out.println("服务器收到的数据:"+sb.toString());
//b.返回客户端数据
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的数据:"+sb.toString()+" 服务器已经接收到数据");
//将数据发送
pw.flush();
br.close();
pw.close();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
}
}
到此我们的程序就算相对完善的。在服务器端我们设置了线程的最大数为10,所以我们的程序最大并发量为10,这样我们的程序就不需要一直新建线程浪费资源,只需要等待别人用完还回线程池,然后拿到继续使用即可。但是我们的程序依然是线程阻塞的。
NIO的网络编程
NIO是非阻塞IO,与BIO的区别就是在BIO中阻塞的地方都可以设置成非阻塞(监听连接的 accept() 方法,以及从socket中读取数据的方法)。
阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)
NIO与BIO的主要区别:面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
NIO三大核心组件
在java.io 中最为核心的一个概念是流(Stream),面向流的编程。一个流要么是输入流,要么是输出流,不可能同时是输入流又是输出流。java.nio是面向缓冲的,Buffer 本身就是一块内存,底层实现上,它实际上是个数组.数据的读写都是通过 Buffer 来实现的。