一、同步与异步、阻塞与非阻塞
1、同步与异步
同步与异步的区别在于,数据从内核空间拷贝到用户空间是否由用户线程完成。
– 对于同步来说,分阻塞和非阻塞两种。阻塞的情况,一个线程维护一个链接,该线程完成数据的读写与处理的全部过程,并且数据的读写是阻塞的。 对于非阻塞来说,虽然读写的过程不会阻塞当前线程,立即返回,但是用户线程(Selector选择器)仍然要不断主动去判断数据是否“就绪”(感兴趣的事件是否发生,具体可参考后文对NIO的描述),当出现可以操作的IO时,进行数据的读写并处理,此时还是会阻塞等待内核复制数据到用户进程。他与同步BIO的区别是后者使用一个连接全程等待;
可参考下图(同步非阻塞):
可以看到,在将数据从内核拷贝到用户空间这一过程,是由用户线程阻塞完成的。
–对于异步来说,用户进行读或者写后,将立刻返回,由内核去完成数据读取以及拷贝工作,完成后通知用户,并执行回调函数(用户提供的callback),此时数据已从内核拷贝到用户空间,用户线程只需要对数据进行处理即可,不需要关注读写,用户不需要等待内核对数据的复制操作,用户在得到通知时数据已经被复制到用户空间
可参考下图(异步非阻塞):
可发现,用户在调用之后,立即返回,由内核完成数据的拷贝工作,并通知用户线程,进行回调。
(IO“就绪”和“完成”的区别:就绪指的是还需要用户自己去处理,完成指的是内核帮助完成了,用户不用关心IO过程,只需要提供回调函数。)
(一般来说,IO操作都分为两个阶段,就拿套接口的输入操作来说,它的两个阶段主要是:1)等待网络数据到来,当分组到来时,将其拷贝到内核空间的临时缓冲区中2)将内核空间临时缓冲区中的数据拷贝到用户空间缓冲区中)
(网上还有一种对异步同步的解释也很形象:同步和异步关注的是消息通信机制synchronous communication/ asynchronous communication。所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。)
2、阻塞与非阻塞
阻塞与非阻塞IO,指的是在IO读写过程中有没有阻塞线程,即当没有可读数据时,方法是否即刻返回。
例如在BIO中,使用流的方式进行读写操作,而流的读写操作是阻塞的,例如inpustrem.readline()函数,当没有有效数据可读时,线程将阻塞在该语句处。写操作是同样的道理。如下图所示:
而在NIO中,使用channel与buffer的方式进行数据的读写,只有在有可读或者可写数据时,才会将数据从内核空间读/写入用户空间缓冲区,不会造成线程的阻塞。具体可参考后文对BIO、NIO的描述。如下图所示:
3、IO模式
在Java中,使用socket进行网络通信,IO有如下模式:BIO、NIO、AIO。
分别代表着:同步阻塞IO、同步非阻塞IO、异步非阻塞IO
二、BIO
1 概念描述
指阻塞式IO通信模式。如下图所示为BIO模式示意图:
每建立一个Socket连接时,同时创建一个新线程对该Socket进行单独通信(采用阻塞的方式通信)。这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况。
2 特点
他有以下两个特点:
(a)使用一个独立的线程维护一个socket连接,随着连接数量的增多,对虚拟机造成一定压力;
(b)使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会造成资源的浪费;
3 代码实现
我们使用Socket编程的方式,用代码来体会一下BIO模式通信方式
服务端监听线程:
package study20170324;
/**
* Created by apple on 17/3/24.
*/
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 开启服务监听线程,当收到连接请求后,开启新的线程进行处理
*/
public class ServerThread implements Runnable{
@Override
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(Constant.PORT);
while (true){
Socket socket = serverSocket.accept();
new Thread(new ServerProcessThread(socket)).start();//开启新的线程进行连接请求的处理
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端数据处理线程:
package study20170324;
/**
* Created by apple on 17/3/24.
*/
import java.io.*;
import java.net.Socket;
/**
* 服务端收到连接请求后,处理请求的线程,阻塞式IO
*/
public class ServerProcessThread implements Runnable {
private Socket socket;
public ServerProcessThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//获取客户端的数据,并写回
//等待响应
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = "";
String requestStr = "";
System.out.println("来自客户端的数据:");
while((line = bufferedReader.readLine()) != null){
requestStr += line;
System.out.println(line);
}
Writer writer = new OutputStreamWriter(socket.getOutputStream());
writer.write("data from server " + requestStr + "\r\n");
writer.flush();
writer.close();
bufferedReader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端处理线程
package study20170324;
import java.io.*;
import java.net.Socket;
/**
* Created by apple on 17/3/24.
*/
/**
* 维护客户端socket连接的线程,阻塞式IO
*/
public class ClientProcessThread implements Runnable {
private Socket socket;
public ClientProcessThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//写数据,等待响应,输出响应
String requestStr = "data from client \r\n";
try {
Writer writer = new OutputStreamWriter(socket.getOutputStream());
writer.write(requestStr);
writer.flush();
socket.shutdownOutput();
//等待响应
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
System.out.println("来自服务端的响应:");
while((line = bufferedReader.readLine()) != null){
System.out.println(line);
}
writer.close();
bufferedReader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
常量类:
package study20170324;
/**
* Created by apple on 17/3/24.
*/
public class Constant {
public static final String HOST = "127.0.0.1";
public static final int PORT = 8080;
}
主运行类:
package study20170324;
import java.io.IOException;
import java.net.Socket;
/**
* Created by apple on 17/3/24.
*/
public class ClientMain {
public static void main(String[] args) {
//开启服务
System.out.println("开启服务,监听端口:" + Constant.PORT);
new Thread(new ServerThread()).start();
//建立一个socket客户端,发起请求
System.out.println("客户端,请求连接,并发送数据");
try {
Socket socket = new Socket(Constant.HOST,Constant.PORT);
new Thread(new ClientProcessThread(socket)).start();//开启新的线程处理socket连接
} catch (IOException e) {
e.printStackTrace();
}
}
}
最终的运行结果:
三、NIO
1 概念描述
指的是非阻塞式IO通信模式
针对于BIO的两个特点,其实也是两个缺点,Java提供了NIO通信模式的实现。相对于BIO来说,NIO模式即非阻塞IO。服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时(读就绪),则调用该socket连接的相应读操作;如果发现某个 Socket端口上有数据可写时(写就绪),则调用该socket连接的相应写操作;如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。Java中使用Selector、Channel、Buffer来实现上述效果,如下图所示:
线程中包含一个Selector对象,他相当于一个通道管理器,可以实现在一个单独线程中处理多个通道的目的,减少线程的创建数量。远程连接对应一个channel,数据的读写通过buffer均在同一个channel中完成,并且数据的读习是非阻塞的。通道创建后需要注册在selector中,同时需要为该通道注册感兴趣事件(客户端连接服务端事件、服务端接收客户端连接事件、读事件、写事件),selector线程需要采用轮训的方式调用selector的select函数,直到所有注册通道中有兴趣的事件发生,则返回,否则一直阻塞。而后循环处理所有就绪的感兴趣事件。以上步骤解决BIO的两个瓶颈:(1)不必对每个连接分别创建线程;(2)数据读写非阻塞
下面对以下三个概念做一个简单介绍,Java NIO由以下三个核心部分组成:
-(a)selector
Selector允许单线程处理多个Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。要使用Selector,得向Selector注册Channel,然后调用他的select方法,这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子入有新连接接进来,数据接收等。
-(b)channel与buffer
基本上,所有的IO在NIO中都从一个Channel开始。Channel有点像流。数据可以从channel读到buffer,也可以从budder写到channel。
channel和buffer有好几种类型。下面是Java NIO中的一些主要channel的实现:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
正如你所看到的,这些通道涵盖了UDP和TCP网络IO,以及文件IO。
以下是Java NIO里关键的buffer实现:
ByteBuffer
CharBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
具体介绍可参考http://ifeve.com/overview/ 关于Java NIO的系列教程(上述a、b也摘抄于该教程)
2 特点
NIO的特点:
(a)一个线程可以处理多个通道,减少线程创建数量;
(b)读写非阻塞,节约资源:没有可读/可写数据时,不会发生阻塞导致线程资源的浪费
3 代码描述
使用Java代码实现NIO的通信方式
基础类:
package study20170325;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* Created by apple on 17/3/25.
*/
public class NIOBase {
// 线程中的通道管理器
public Selector selector;
public String from,to;//server or client
public NIOBase(String from,String to){
this.from = from;
this.to = to;
}
/**
* 初始化 该线程中的通道管理器Selector
*/
public void initSelector() throws IOException {
this.selector = Selector.open();
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则循环处理
* 这里主要监听连接事件以及读事件
*/
public void listen() throws IOException {
//轮询访问select
while(true){
//当注册的事件到达时,方法返回;否则将一直阻塞
selector.select();
//获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//循环处理注册事件
/**
* 一共有四种事件:
* 1. 服务端接收客户端连接事件: SelectionKey.OP_ACCEPT
* 2. 客户端连接服务端事件: SelectionKey.OP_CONNECT
* 3. 读事件: SelectionKey.OP_READ
* 4. 写事件: SelectionKey.OP_WRITE
*/
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//手动删除已选的key,以防重复处理
iterator.remove();
//判断事件性质
if (key.isAcceptable()){//服务端接收客户端连接事件
accept(key);
}else if (key.isReadable()){//读事件
read(key);
}else if (key.isConnectable()) {//客户端连接事件
connect(key);
}
}
}
}
/**
* 当监听到读事件后的处理函数
* @param key 事件key,可以从key中获取channel,完成事件的处理
*/
public void read(SelectionKey key) throws IOException {
//step1. 得到事件发生的通道
SocketChannel socketChannel = (SocketChannel) key.channel();
//step2. 创建读取的缓冲区.将数据读取到缓冲区中
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
int len = socketChannel.read(byteBuffer);
String msg = "";
byte[] arr = null;
while (len > 0){
byteBuffer.flip();
arr = new byte[len];
byteBuffer.get(arr,0,len);
msg += new String(arr);
byteBuffer.clear();
len = socketChannel.read(byteBuffer);
}
System.out.println(from + " received data from " + to + ":" + msg);
//step3. 再将消息回发给客户端
if (from.equals("server"))socketChannel.write(ByteBuffer.wrap(new String(" server send some data back to client").getBytes()));
}
/**
* 当监听到服务端接收客户端连接事件后的处理函数
* @param key 事件key,可以从key中获取channel,完成事件的处理
*/
public void accept(SelectionKey key) throws IOException{}
/**
* 当监听到客户端连接事件后的处理函数
* @param key 事件key,可以从key中获取channel,完成事件的处理
*/
public void connect(SelectionKey key) throws IOException{}
}
服务端线程类:
package study20170325;
/**
* Created by apple on 17/3/25.
*/
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* 采用NIO的方式,开启服务线程
* 该线程存在一个Selector,通道管理器,管理所有的channel
* step1.服务初始化时,会初始化一个ServerSocektChannel,并注册到Selector中,注册"服务端接收客户端连接"事件
*
* step2.之后开启监听,轮询判断Selector上是否有需要处理的事件,如果有则循环处理;
*
* step2.1 客户端连接事件:在处理的过程中,获取与客户端连接的通道 socketChannel,并注册到Selector中,通过该通道,与客户端进行读写操作
*
* step2.2 读事件,利用读取缓冲区与通道结合进行
*/
public class NIOServerThread extends NIOBase implements Runnable{
public NIOServerThread(String from, String to) {
super(from, to);
}
//服务端线程中的通道管理器,使用它,可以在同一个线程中管理多个通道
@Override
public void run() {
try {
initSelector();//初始化通道管理器Selector
initServer(Constant.IP,Constant.PORT);//初始化ServerSocketChannel,开启监听
listen();//轮询处理Selector选中的事件
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获得一个ServerSocket通道,并通过port对其进行初始化
* @param port 监听的端口号
*/
private void initServer(String ip,int port) throws IOException {
//step1. 获得一个ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//step2. 初始化工作
serverSocketChannel.configureBlocking(false);//设置通道为非阻塞
serverSocketChannel.socket().bind(new InetSocketAddress(ip,port));
//step3. 将该channel注册到Selector上,并为该通道注册SelectionKey.OP_ACCEPT事件
//这样一来,当有"服务端接收客户端连接"事件到达时,selector.select()方法会返回,否则将一直阻塞
serverSocketChannel.register(this.selector,SelectionKey.OP_ACCEPT);
}
/**
* 当监听到服务端接收客户端连接事件后的处理函数
* @param key 事件key,可以从key中获取channel,完成事件的处理
*/
@Override
public void accept(SelectionKey key) throws IOException {
//step1. 获取serverSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//step2. 获得和客户端连接的socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);//设置为非阻塞
//step3. 通过socketChannel给客户端发送信息
socketChannel.write(ByteBuffer.wrap(new String("server has a connection with client").getBytes()));
//step4. 注册该socketChannel
socketChannel.register(selector,SelectionKey.OP_READ);//为了接收客户端的消息,注册读事件
}
}
客户端线程类:
package study20170325;
/**
* Created by apple on 17/3/25.
*/
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
/**
* NIO 客户端线程
*/
public class NIOClientThread extends NIOBase implements Runnable{
public NIOClientThread(String from, String to) {
super(from, to);
}
@Override
public void run() {
try {
initSelector();//初始化通道管理器
initClient(Constant.IP,Constant.PORT);//初始化客户端连接scoketChannel
listen();//开始轮询处理事件
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获得一个SocketChannel,并对该channel做一些初始化工作,并注册到
* @param ip
* @param port
*/
private void initClient(String ip,int port) throws IOException {
//step1. 获得一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//step2. 初始化该channel
socketChannel.configureBlocking(false);//设置通道为非阻塞
//step3. 客户端连接服务器,其实方法执行并没有实现连接,需要再listen()方法中调用channel.finishConnect()方法才能完成连接
socketChannel.connect(new InetSocketAddress(ip,port));
//step4. 注册该channel到selector中,并为该通道注册SelectionKey.OP_CONNECT事件和SelectionKey.OP_READ事件
socketChannel.register(this.selector,SelectionKey.OP_CONNECT|SelectionKey.OP_READ);
}
/**
* 当监听到客户端连接事件后的处理函数
* @param key 事件key,可以从key中获取channel,完成事件的处理
*/
@Override
public void connect(SelectionKey key) throws IOException {
super.connect(key);
//step1. 获取事件中的channel
SocketChannel socketChannel = (SocketChannel) key.channel();
//step2. 如果正在连接,则完成连接
if (socketChannel.isConnectionPending()){
socketChannel.finishConnect();
}
socketChannel.configureBlocking(false);//将连接设置为非阻塞
//step3. 连接后,可以给服务端发送消息
socketChannel.write(ByteBuffer.wrap(new String("client send some data to server").getBytes()));
}
}
常量类:
package study20170325;
/**
* Created by apple on 17/3/25.
*/
public class Constant {
public static final int PORT = 8080;
public static final String IP = "127.0.0.1";
}
运行主类:
package study20170325;
/**
* Created by apple on 17/3/25.
*/
public class NIOMain {
public static void main(String[] args) {
new Thread(new NIOServerThread("server","client")).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new NIOClientThread("client","server")).start();
}
}
运行结果为:
四、AIO
异步非阻塞IO,与NIO的区别可参考 异步/同步介绍。
可以参考之前对vertx的学习笔记,vertx相当于node在java上的实现,采用的是AIO的模式,即异步非阻塞IO。