目录
同步?异步?阻塞?非阻塞?
同步:用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪。
异步:用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)
阻塞:当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止
非阻塞: 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待
综上所述:
同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。
阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
同步和异步是目的,阻塞和非阻塞是实现方式。
=======================================================================
同步IO和异步IO,阻塞IO和非阻塞IO 区别
一个IO操作其实分成了两个步骤:发起IO请求 和 实际的IO操作。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
=======================================================================
概念
一、同步阻塞I/O(BIO):jdk1.4以前 面向流
用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。
服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。
BIO方式架适用于连接数目比较小且固定的构,这种方式对服务端资源要求比较高,并发局限于应用中。
二、同步非阻塞I/O(NIO):jdk1.4开始支持 面向缓冲区
用户进程发起一个IO操作以后便可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。
服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
三、异步非阻塞I/O(AIO):jdk1.7开始支持
应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序。
服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。
AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。
喻:
目标:烧十壶开水。
IO:需要10个人,一个人盯一壶。
BIO:需要1个人,轮流查看几个壶。
AIO:需要1个人兼职,给壶装个开关,水开后提示,水未开时人可以去做任何事。
=======================================================================
BIO(IO)
Socket又称“套接字”,应用程序通常通过“套接字”向网络发出请求或者应答网络请求。
Socket和ServerSocket类库位于java.net包中,serverSocket用于服务器端,Socket是建立网络连接时使用的。
在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。
对于一个网络连接来说,套接字是平等的,不因为在服务器端或在客户端而产生不同的级别,
Socket和ServerSocket都是通过SocketImpl类及其子类完成的
缺点:该模型最大的问题就是,当客户端数量增加时,服务端的线程数与客户端并发的数量是 1:1 关系,我们知道在jvm 中线程是非常宝贵的资源,当线程数不断上升时,系统性能将不断下降,可能会出现堆栈溢出,创建线程失败等问题,并最终导致宕机或者僵死,不对外提供服务。
网络编程的基本模型Client/Server模型,也就是两个进程直接进行相互通信,其中服务端提供配置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接成功,则双方即可进行通信(网络套接字socket)
优化:
采用线程池和任务队列可以实现一种伪异步的IO通信框架。
我们学过连接池的使用和队列的使用,其实就是将客户端的socket封装成一个task任务(实现runnable接口的类)然后投递到线程池中去,配置相应的队列进行实现。
代码演示
package com.io.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerHandler implements Runnable {
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
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 body = null;
while (true) {
body = in.readLine();
if (body == null) {
break;
}
System.out.println("Server :"+ body);
out.println("服务器端回送响的应数据.");
}
} catch (IOException 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;
}
}
}
}
package com.io.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
final int port = 8088;
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("服务器端口号:" + port);
Socket socket;
while (true){
//进行阻塞
socket = server.accept();
//新建一个线程执行客户端的任务
new Thread(new ServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(server != null){
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
package com.io.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
final int port = 8088;
Socket socket = null;
BufferedReader reader = null;
PrintWriter writer = null;
try {
socket = new Socket("127.0.0.1", port);
} catch (IOException e) {
System.out.println("客户端初始化失败");
}
try {
writer = new PrintWriter(socket.getOutputStream(),true);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
} catch (IOException e) {
System.out.println("输入输出流获取失败");
}
writer.println("客户端向服务器端发送的请求");
try {
String readLine = reader.readLine();
System.out.println("Client: " + readLine);
} catch (IOException e) {
System.out.println("读取失败");
}finally {
if(writer != null){
writer.close();
writer = null;
}
if(reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
reader = null;
}
}
}
}
=======================================================================
NIO
NIO 叫做 new IO,也叫做 Non-block IO , 即非阻塞IO
传统的IO流(inputstream,outputstream)都是单向的管道,要么去读,要么去写。
有一个server端,有n个client端。古老的socket编程,是每个客户端直接向server端发起一个套接字,建立一个tcp连接。
NIO是在传统的tcp之上进行一个抽象。不是client端和server端直接进行连接,而是,把client的通道注册到server端。
在NIO中没有Socket和ServerSocket的概念。
在NIO中服务端要实例化出一个ServerSocketChannel(把ServerSocket进行了一个抽象),客户端使用SocketChannel。
在Server端会创建一个Selector(多路复用器)。所有客户端的SocketChannel都要注册到Selector。
Selector的机制是使用一个线程,去轮询所有注册到这个服务器端的SocketChannel,根据通道的状态,执行相关的操作。
概念
(1)Buffer-缓冲区
(2)Channel(管道。通道)
(3)Selector(选择器,多路复用器)
Buffer
是一个对象,它包含一些要写入或者要读取的数据。在NIO类库中加入Buffer对象,体现了新库与原IO的一个重要的区别。在面向流的IO中,可以将数据直接写入或读取到Stream对象中。在NIO库中,所有的数据都是用缓冲区处理的(读写)。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组,这个数组为缓冲区提供了数据的访问读写等操作属性,如位置,容量,上限等概念,参考API文档。Buffer类型:我们最常用的就是ByteBuffer,实际上每一种java基本类型都对应了一种缓冲区(除了Boolean类型)
ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBUffer、FloatBuffer、DoubleBUffer
使用:
flip() 将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。
get() 读取数据
rewind() 将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素
mark() 可以标记Buffer中的一个特定position。之后可以通过调用。
reset() 恢复到Buffer.mark()标记时的position。
clear() 清空整个缓冲区。position将被设回0,limit被设置成 capacity的值
compact() 只会清除已经读过的数据;任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
将position设到最后一个未读元素正后面,limit被设置成 capacity的值。
通道(Channel)
它就像自来水管道一样,网络数据通过Channel读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上移动(一个流必须是inputStream或者outputStream的子类),而通道可以用于读,写或者二者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别。
事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类是用于文件操作的(FileChannel),我们使用的SocketChannel和ServerSockerChannel都是SelectableChannel的子类
使用
FileChannel:用于读取、写入、映射和操作文件的通道。
DatagramChannel:通过 UDP 读写网络中的数据通道。
SocketChannel:通过 TCP 读写网络中的数据。
ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
AbstractInterruptibleChannel
AbstractSelectableChannel
Pipe.SinkChannel
Pipe.SourceChannel
SelectableChannel
多路复用器(seletor)
他是NIO编程的基础,非常重要,多路复用器提供选择已经就绪的任务的能力。
Selector线程就类似一个管理者(Master),管理了成千上万个管道,然后轮询哪个管道的数据已经准备好,通知CPU执行IO的读取或写入操作。
Selector模式:当IO事件(管理)注册到选择器以后,selector会分配给每个管道一个key值,相当于标签。selector选择器是以轮询的方式进行查找注册的所有IO事件(管道)
如果某个通道发生了读写操作,这个通道就处于就绪状态,select就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道里读或写数据,写道我们的数据缓冲区中)。
每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。
SelectionKey.OP_CONNECT 连接状态
SelectionKey.OP_ACCEPT 阻塞状态
SelectionKey.OP_READ 可读状态
SelectionKey.OP_WRITE 可写状态
创建NIO服务器的主要步骤
(1)打开ServerSocketChannel,监听客户端连接
(2)绑定监听端口,设置连接为非阻塞模式
(3)创建Reactor线程,创建多路复用器并启动线程
(4)将ServerSocketChannel注册到Reactor线程中的Selector上,监听ACCEPT事件
(5)Selector轮询准备就绪的key
(6)Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,简历物理链路
(7)设置客户端链路为非阻塞模式
(8)将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息
(9)异步读取客户端消息到缓冲区
(10)对Buffer编解码,处理半包消息,将解码成功的消息封装成Task
(11)将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端
因为应答消息的发送,SocketChannel也是异步非阻塞的,所以不能保证一次能吧需要发送的数据发送完,此时就会出现写半包的问题。我们需要注册写操作,不断轮询Selector将没有发送完的消息发送完毕,然后通过Buffer的hasRemain()方法判断消息是否发送完成。
代码演示
package com.io.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class Server implements Runnable {
//多路复用器(管理所有的通道)
private Selector seletor;
//2 建立缓冲区
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//3
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
public Server(int port) {
try {
//步骤一: 打开服务器通道(ServerSocketChannel),用于监听客户端的连接,它是所有客户端连接的副管道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
//步骤二: 绑定监听端口,设置连接为非阻塞模式
socketChannel.bind(new InetSocketAddress(port));
socketChannel.configureBlocking(false);
//步骤三: 创建多路复用器(Selector)
this.seletor = Selector.open();
//步骤四: 把服务器通道(ServerSocketChannel)注册到多路复用器(Selector)上,并且监听阻塞(ACCEPT)事件
socketChannel.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//步骤五:多路复用器在线程run方法的无线循环体内轮询准备就绪的key
while (true) {
try {
//1 必须要让多路复用器开始监听
this.seletor.select();
//2 返回多路复用器已经选择的结果集
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
//3 进行遍历
while (keys.hasNext()) {
//4 获取一个选择的元素
SelectionKey key = keys.next();
//5 直接从容器中移除就可以了
keys.remove();
//6 如果是有效的
if (key.isValid()) {
//7 如果为阻塞状态
if (key.isAcceptable()) {
//步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理连接
//步骤七:设置客户端链路为非阻塞模式 3
//步骤八:将新接入的客户端连接注册到的多路复用器(Selector)上,监听读操作,读取客户端发送的网络消息 4
this.accept(key);
}
//8 如果为可读状态
if (key.isReadable()) {
//步骤九:异步读取客户端请求消息到缓冲区
this.read(key);
}
//9 写数据
if (key.isWritable()) {
this.write(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
try {
ssc.register(this.seletor, SelectionKey.OP_WRITE);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
private void read(SelectionKey key) {
try {
//1 清空缓冲区旧的数据
this.readBuf.clear();
//2 获取之前注册的socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
//3 读取数据
int count = sc.read(this.readBuf);
//4 如果没有数据
if (count == -1) {
key.channel().close();
key.cancel();
return;
}
//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
this.readBuf.flip();
//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
byte[] bytes = new byte[this.readBuf.remaining()];
//7 接收缓冲区数据
this.readBuf.get(bytes);
//8 打印结果
String body = new String(bytes).trim();
// 9.可以写回给客户端数据
System.out.println("Server : " + body);
} catch (IOException e) {
e.printStackTrace();
}
}
private void accept(SelectionKey key) {
try {
//1 获取服务通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 执行阻塞方法
SocketChannel sc = ssc.accept();
//3 设置为非阻塞模式
sc.configureBlocking(false);
sc.socket().setReuseAddress(true);
//4 注册到多路复用器上,并设置读取标识
sc.register(this.seletor, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(8081)).start();
}
}
package com.io.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
//需要一个Selector
public static void main(String[] args) {
//创建连接的地址
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8081);
//声明连接通道
SocketChannel sc = null;
//建立缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
//打开通道
sc = SocketChannel.open();
//进行连接
sc.connect(address);
while(true){
//定义一个字节数组,然后使用系统录入功能:
byte[] bytes = new byte[1024];
System.in.read(bytes);
buf.put(bytes);//把数据放到缓冲区中
buf.flip();//对缓冲区进行复位
sc.write(buf);//写出数据
buf.clear();//清空缓冲区数据
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(sc != null){
try {
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
推荐NIO底层讲解网址:
=======================================================================
AIO
package com.io.aio;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
public class ServerHandler implements CompletionHandler<AsynchronousSocketChannel, Server> {
@Override
public void completed(AsynchronousSocketChannel asc, Server attachment) {
//当有下一个客户端接入的时候 直接调用Server的accept方法,这样反复执行下去,保证多个客户端都可以阻塞(没有递归上限)
// 1.7以后AIO才实现了异步非阻塞
attachment.assc.accept(attachment, this);
read(asc);
}
private void read(final AsynchronousSocketChannel asc) {
//读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
asc.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer resultSize, ByteBuffer attachment) {
//进行读取之后,重置标识位
attachment.flip();
//获得读取的字节数
System.out.println("Server -> " + "收到客户端的数据长度为:" + resultSize);
//获取读取的数据
String resultData = new String(attachment.array()).trim();
System.out.println("Server -> " + "收到客户端的数据信息为:" + resultData);
String response = "服务器响应, 收到了客户端发来的数据: " + resultData;
write(asc, response);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
private void write(AsynchronousSocketChannel asc, String response) {
try {
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(response.getBytes());
buf.flip();
asc.write(buf).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Server attachment) {
exc.printStackTrace();
}
}
package com.io.aio;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server{
//线程池
private ExecutorService executorService;
//线程组
private AsynchronousChannelGroup threadGroup;
//服务器通道
public AsynchronousServerSocketChannel assc;
public Server(int port){
try {
//创建一个缓存池
executorService = Executors.newCachedThreadPool();
//创建线程组
threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
//创建服务器通道
assc = AsynchronousServerSocketChannel.open(threadGroup);
//进行绑定
assc.bind(new InetSocketAddress(port));
System.out.println("server start , port : " + port);
//进行阻塞
assc.accept(this, new ServerHandler());
//一直阻塞 不让服务器停止
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server(8082);
}
}
package com.io.aio;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;
public class Client implements Runnable{
private AsynchronousSocketChannel asc ;
public Client() throws Exception {
asc = AsynchronousSocketChannel.open();
}
public void connect(){
asc.connect(new InetSocketAddress("127.0.0.1", 8082));
}
public void write(String request){
try {
asc.write(ByteBuffer.wrap(request.getBytes())).get();
read();
} catch (Exception e) {
e.printStackTrace();
}
}
private void read() {
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
asc.read(buf).get();
buf.flip();
byte[] respByte = new byte[buf.remaining()];
buf.get(respByte);
System.out.println(new String(respByte,"utf-8").trim());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
}
}
public static void main(String[] args) throws Exception {
Client c1 = new Client();
c1.connect();
Client c2 = new Client();
c2.connect();
Client c3 = new Client();
c3.connect();
new Thread(c1, "c1").start();
new Thread(c2, "c2").start();
new Thread(c3, "c3").start();
Thread.sleep(1000);
c1.write("c1 aaa");
c2.write("c2 bbbb");
c3.write("c3 ccccc");
}
}
=======================================================================
参考地址