JAVA BIO和NIO
同步、异步、阻塞、非阻塞
- 同步: 小明上传一个500m的视频,等待这个视频上传成功,返回结果,这个等待成功的通知就是同步
- 阻塞: 小明上传一个500m的视频,等待这个视频上传成功,返回结果,在上传视频的过程就只有在等待成功的通知,这个等待的过程就是阻塞
- 异步: 小明上传一个500m的视频,但是设置了一个上传成功提醒的功能,等到视频上传成功,系统会自动提醒,这样的就是异步
- 非阻塞: 小明上传一个500m的视频,等待这个视频上传成功,返回结果,但是在等待的过程过在做其他事情,这样的就是非阻塞
从上面的这四个例子,说明了同步异步是相对于消息通知机制的,当小明在等待消息通知的时候就是一个同步的过程,当系统自动提醒的时候就是一个异步的过程;阻塞和非阻塞是相对于消息通知的过程,在等待消息通知的过程,你没有做其它事情就是阻塞的,在消息等待过程,边等消息边做其它事情就代表这是非阻塞的。
IO模型
linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)
-
阻塞IO: 最常用的I/O模型就是阻塞I/O模型,缺省情况下,所有文件操作都是阻塞的。在进程空间recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间会一直等待,进程从调用recvfrom开始到它返回的整段时间都是被阻塞,因此被称为阻塞I/O模型。
-
非阻塞IO: recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。
-
IO复用: linux提供select/poll,进程通过将一个或多个fd传递给select或者poll系统调用,阻塞在select操作上,这样select/poll可以帮我们监测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。linux还提供了一个epoll系统调用,epoll用于基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪的时候,立即回调rollback
-
IO信号驱动: 首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。
5.异步IO: 告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作;异步I/O模型由内核告诉我们I/O操作何时已经完成。
JAVA BIO通信
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答的通信模型。
同步阻塞I/O创建的BioSocketServer:
public class BioSocketServer {
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket serverSocket = null;
try{
serverSocket = new ServerSocket(port);
System.out.println("The server is satrt in port: " + port);
Socket socket = null;
while (true){
socket = serverSocket.accept();
new Thread(new BioSocketServerHandler(socket)).start();
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (serverSocket != null){
System.out.println("The server close");
serverSocket.close();
serverSocket = null;
}
}
}
}
绑定8080端口,默认主机localhost,通过while循环来监听客户端的连接,如果没有客户端连接,阻塞在accept操作上。
客户端连接,服务端读写处理类BioSocketServerHandler:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;
public class BioSocketServerHandler implements Runnable {
private Socket socket;
public BioSocketServerHandler (Socket socket){
this.socket = socket;
}
@Override
public void run() {
//创建BufferReader对象读取客户端发送的数据
BufferedReader in = null;
//创建PrintWriter返回响应信息给客户端
PrintWriter out = null;
try{
//客户端和服务端数据交互是通过流的方式进行传输交互,所以获取流转换成BufferReader对象
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
//这里要设置autoFlush为true,实现自动刷新到缓冲区,否则数据是无法进行发送的,除非手动调用flush刷新到缓冲区
out = new PrintWriter(this.socket.getOutputStream(), true);
String currentTime = null;
String body = null;
//循环从BufferReader中读取客户端发送的数据,当读到了输入流的尾部,就退出循环
while (true){
body = in.readLine();
if (body == null){
break;
}
System.out.println("The server receive msg:" + body);
currentTime = new Date(System.currentTimeMillis()).toString();
//返回响应信息给客户端
out.println(currentTime);
}
}catch (Exception e){
if (in != null){
try {
in.close();
}catch (IOException e1){
e1.printStackTrace();
}
}
if (out != null){
out.close();
out = null;
}
if (this.socket != null){
try {
this.socket.close();
}catch (IOException e1){
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
当有新客户端连接的时候,调用BioSocketServerHandler进行连接,读写事件处理。
同步阻塞I/O创建的BioSocketClient:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class BioSocketClient{
public static void main(String[] args) {
int port = 8080;
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
//指定连接的host和端口
socket = new Socket("127.0.0.1", port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//发送数据给服务端
out.println("hello, I am client");
//接收服务端返回响应信息
String resp = in.readLine();
System.out.println("Now is : "+resp);
}catch (Exception e){
e.printStackTrace();
}finally {
if (in != null){
try {
in.close();
}catch (IOException e1){
e1.printStackTrace();
}
}
if (out != null){
out.close();
out = null;
}
if (socket != null){
try {
socket.close();
}catch (IOException e1){
e1.printStackTrace();
}
socket = null;
}
}
}
}
通过上面的BIO开发,我们发现了每当有一个新客户连接进来的时候,服务端都必须创建一个线程处理新的客户端连接,一个线程只能处理一个客户端连接,在高性能高并发的服务器应用领域,往往可能都是需要能够承受成千上万个连接处理,这样的模型显然不能满足需求。
伪异步I/O编程
优化上面的同步阻塞I/O一个连接需要一个线程处理带来的巨额线程开销,下面使用线程池来进行处理。
伪异步I/O创建的AsynBioSocketServer:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class AsynBioSocketServer{
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket serverSocket = null;
try{
serverSocket = new ServerSocket(port);
System.out.println("The server is satrt in port: " + port);
Socket socket = null;
//自定义一个线程池
AsynBioSocketServerHandlerExecutePool singleExecutor = new AsynBioSocketServerHandlerExecutePool(50, 10000);
while (true){
socket = serverSocket.accept();
//把每次请求封装成一个task放到线程池里面去进行处理
singleExecutor.execute(new com.yate.asynbio.AsynBioSocketServerHandler(socket));
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (serverSocket != null){
System.out.println("The time server close");
serverSocket.close();
serverSocket = null;
}
}
}
}
AsynBioSocketServerHandlerExecutePool :
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class AsynBioSocketServerHandlerExecutePool {
private ExecutorService executor;
public AsynBioSocketServerHandlerExecutePool (int maxPoolSize, int queueSize){
this.executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
}
public void execute(Runnable task){
executor.execute(task);
}
伪异步I/O创建的AsynBioSocketServerHandler:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;
public class AsynBioSocketServerHandler implements Runnable {
private Socket socket;
public AsynBioSocketServerHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try{
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String currentTime = null;
String body = null;
while (true){
body = in.readLine();
if (body == null){
break;
}
System.out.println("The server receive msg:" + body);
currentTime = new Date(System.currentTimeMillis()).toString();
out.println(currentTime);
}
}catch (Exception e){
if (in != null){
try {
in.close();
}catch (IOException e1){
e1.printStackTrace();
}
}
if (out != null){
out.close();
out = null;
}
if (this.socket != null){
try {
this.socket.close();
}catch (IOException e1){
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
伪异步I/O创建的AsynBioSocketClient:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class AsynBioSocketClient{
public static void main(String[] args) {
int port = 8080;
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println("hello, i am client");
String resp = in.readLine();
System.out.println("Now is : "+resp);
}catch (Exception e){
}finally {
if (in != null){
try {
in.close();
}catch (IOException e1){
e1.printStackTrace();
}
}
if (out != null){
out.close();
out = null;
}
if (socket != null){
try {
socket.close();
}catch (IOException e1){
e1.printStackTrace();
}
socket = null;
}
}
}
}
使用了线程池来进行优化了,由于线程池和消息队列都是有界的,因此,无论并发量多大,都不会导致线程个数膨胀或者溢出,相比于原本的BIO,是一种改良,但是由于它底层采用的还是同步阻塞模型,因此还是无法从根本上解决问题。在进行read和write操作都是同步阻塞的,阻塞的时间取决于对方的I/O线程的处理速度和网络I/O的传输速度,如果我们的应用程序依赖用户的处理速度,那么可靠性就变得特别差。
总结一下BIO或者伪异步IO存在的问题:
1.服务端处理缓慢,返回应答消息耗费60s,平时只需要10ms
2.采用伪异步I/O的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,它将会被阻塞60s
3.假如所有的可用线程都被故障服务器阻塞,那后续所有的I/O消息都将在队列中排队
4.由于线程池采用的阻塞队列实现,当队列满了以后,后续入队列的操作将被阻塞
5.由于前端只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时
6.由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收到新的请求消息。
那么如何可以解决这些问题呢,没错,NIO就可以解决这些问题。
JAVA NIO编程
下面介绍一个NIO重要的三个组件
1.缓冲区Buffer
在java的NIO类库中,所有的数据都是用Buffer缓冲区来进行处理的。在读取数据的时候,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区。Buffer实质上就是一个数组,但是它提供了对数据的结构化访问以及维护读写位置等信息
2.通道Channel
Channel是一个管道,NIO的网络数据的读写都是通过Channel来读取和写入的。Channel和Stream的区别就是Channel是双向的,流只是在一个方向上移动,Channel可以用于读、写或者二者同时进行。
3.多路复用器selector
selector会不断轮询注册在其上的Channel,如果某个Channel发生读或者写事件,这个Channel就处于就绪状态,会被selector轮询出来,然后通过SelectionKey可以获取就绪的Channel集合,进行后续的I/O操作。
介绍完NIO基本的概念后,下面用代码来说明NIO的强大
NIO创建的NIOSocketServer:
public class NIOSocketServer{
public static void main(String[] args) {
int port = 8080;
NIOSocketServerHandler serverHandler= new NIOSocketServerHandler(port);
new Thread(timeServer).start();
}
}
NIO创建的NIOSocketServerHandler:
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.util.Date;
import java.util.Iterator;
import java.util.Set;
public class NIOSocketServerHandler implements Runnable {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean stop;
public NIOSocketServerHandler (int port){
try {
//打开selector监控channel
selector = Selector.open();
//打开服务器管道
serverSocketChannel = ServerSocketChannel.open();
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//绑定监听地址端口,不指定ip默认为本机,设置backlog1024
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
//往selector上面注册ACCPET事件,serverSocketChannel只有accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The Time server is start in port : "+port);
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
this.stop = true;
}
@Override
public void run() {
while (!stop){
try{
//无论是否有读写事件,都会隔1s唤醒selector
selector.select(1000);
//获取所有注册的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch (Exception e){
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public void handleInput(SelectionKey key) throws Exception{
if (key.isValid()){
//进行accpet事件的处理,完成以下事情相当于tcp三次握手,TCP物理链路正式建立,注意一定要将新创建的SocketChannel设置为非阻塞。
if (key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc =ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
//进行read事件处理
if (key.isReadable()){
//获取创建socketChannel用来处理read
SocketChannel sc = (SocketChannel) key.channel();
//NIO是基于buffer来读取处理数据的,创建一个1M大小的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//由于我们已经设置socketChannel为非阻塞的,所以这里的read操作不会阻塞,
//调用read方法传入buffer,用返回值进行判断
//1.返回值大于0:读到了字节,对字节码进行编解码
//2.返回值等于0:没有读到字节
//3.返回值为-1:链路已经关闭,需要关闭SocketChannel,释放资源
int readBytes = sc.read(buffer);
if (readBytes > 0){
//将buffer当前的limit设置为position,position设置为0
buffer.flip();
//根据缓冲区可读数据进行创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//调用ByteBuffer的get方法将缓冲区可读的字节数组复制到新创建的字节数组中
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The time server receive order : "+body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(sc, currentTime);
}else if (readBytes < 0){
//注销key事件
key.cancel();
//关闭socketChannel
sc.close();
}
}
}
}
public void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0){
//将字符串转成字节数组
byte[] bytes =response.getBytes();
//根据字节数组创建指定长度的缓冲区
ByteBuffer byteBuffer =ByteBuffer.allocate(bytes.length);
//调用缓冲区的put方法把字节数组的数据复制到缓冲区中来
byteBuffer.put(bytes);
//进行复位操作
byteBuffer.flip();
//调用channel进行write操作
channel.write(byteBuffer);
}
}
}
NIO创建的NioSocketClient:
public class NioSocketClient {
public static void main(String[] args) {
int port = 8080;
new Thread(new NioSocketClientHandler("127.0.0.1", port)).start();
}
}
NIO创建的NioSocketClientHandler:
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.Set;
public class NioSocketClientHandler implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public NioSocketClientHandler(String host, int port){
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run() {
try {
doConnect();
}catch (IOException e){
e.printStackTrace();
}
while (!stop){
try {
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()){
key = it.next();
it.remove();
try {
handleInput(key);
}catch (Exception e){
if (key != null){
key.cancel();
if (key.channel() != null){
key.channel().close();
}
}
}
}
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
}
if (selector != null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void doConnect() throws IOException{
//对SocketChannel的connect操作进行判断,如果连接成功,就注册read到selector去,
//否则就注册连接事件到selector去,
if (socketChannel.connect(new InetSocketAddress(host, port))){
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
}else{
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
public void handleInput(SelectionKey key) throws IOException{
if (key.isValid()){
//打开socketChannel通道
SocketChannel sc = (SocketChannel) key.channel();
//进行连接事件处理
if (key.isConnectable()){
//调用finishConnect方法,如果返回值为true代表连接成功了,否则就直接退出程序
if (sc.finishConnect()){
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
}else {
System.exit(1);
}
}
if (key.isReadable()){
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(buffer);
if (readBytes > 0){
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is : "+body);
this.stop = true;
}else if (readBytes < 0){
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel sc) throws IOException{
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer buffer = ByteBuffer.allocate(req.length);
buffer.put(req);
buffer.flip();
sc.write(buffer);
//write是异步非阻塞的操作,所以会产生写半包的情况,通过hasRemaining方法进行判断,如果缓冲区中的消息全部发送完成,
//打印"Send order 2 server succeed"
if (!buffer.hasRemaining()){
System.out.println("Send order 2 server succeed");
}
}
}
通过代码的编写,发现NIO编程难度比BIO的大很多,NIO代码这么复杂,为什么应用这么广泛呢,总结一下NIO编程优点:
1.客户端发起的连接操作都是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞
2.SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他链路,不需要同步等待这个链路可用
3.java的NIO采用的是epoll实现,没有连接句柄数的限制(select/poll在一个进程内默认的fd是1024),这意味着selector线程可以处理成千上万个客户端连接,而且性能不会随着客户端的增加而下降,非常适合做高性能、高负载的网络服务器。