I/O模型简单来说:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能,java共支持三种网络编程模型:bio,nio,aio
区别
- BIO:同步阻塞IO(传统阻塞型):服务器实现模式为一个连接一个线程,客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
适用于连接数目比较小且固定架构,并发局限于应用中,jdk1.4以前的唯一选择 - NIO:(non-blocking IO)同步非阻塞IO,从jdk1.4开始,java提供了一系列改进输入输出的新特性:服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理
适用于连接数目多,且连接比较时间短的架构,如聊天服务器,并发局限于应用中,jdk1.4开始支持,netty基于nio,但是支持长连接,即对nio封装,更加强大
bio和nio图片对比
- AIO:(NIO2.0,Asynchronous IO)异步非阻塞IO:服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是操作系统完成了再通知服务器应用去启动线程进行处理
适用于连接数目多,且连接比较时间长的架构,如相册服务器,充分调用操作系统参与并发操作,jdk1.7开始支持
传统BIO
流程
- 服务端启动一个serverSocket
- 客户端启动socket对服务器进行通信,通常情况下服务器端需要对每个线程建立一个线程与之通信
- 客户端发出请求后,先咨询服务器是否有线程响应,没有则会等待或者被拒绝
- 如果有响应,客户端线程会等待请求
实例
public class BioTest1 {
public static void main(String[] args) throws Exception {
//创建服务端,监听6666端口
ServerSocket serverSocket = new ServerSocket(6666);
System.err.println("服务端启动完毕");
//创建线程池来接收客户端请求,模拟BIO,多个线程,每个线程处理一个客户端请求
ExecutorService pool = Executors.newFixedThreadPool(3);
while (true){
System.out.println("等待获取客户端连接");
//服务端等待连接,没有连接会一直阻塞在这里
Socket socket = serverSocket.accept();
System.out.println("获取到客户端连接");
//服务端使用线程池来分配一个线程处理客户端请求
pool.submit(() -> handler(socket));
}
}
private static void handler(Socket socket) {
/*
* 循环读取,如果一共15个字节,一次读取10个,打印出这10个文字,第二次循环读取剩余5个
*/
while (true) {
try {
System.out.println("当前线程"+Thread.currentThread().getName());
InputStream inputStream = socket.getInputStream();
System.out.println("获取客户端输入流结束");
byte[] bytes = new byte[10];
// 此处同样如果客户端没有数据发送过来,就会阻塞,直到接收到客户端数据或者客户端主动关闭连接(length=-1)
int length = inputStream.read(bytes);
System.out.println(length);
if (length != -1) {
System.out.println(new String(bytes, StandardCharsets.UTF_8));
} else {
System.out.println("客户端已经退出,连接关闭");
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
NIO
是事件驱动的
三大组件:selector,buffer,channel
下图为selector,channel,buffer和客户端之间的关系
从图上可以看出
- 每个channel都会对应一个buffer
- 一个线程对应一个selector,对应多个channel(可理解为一个连接)
- 该图反映了有3个channel注册到了这个selector
- 程序切换到哪个channel是由事件决定的
- selector会根据不同的事件在各个通道上切换
- buffer就是一个内存块,底层是有一个数组的
- 数据的读取写入都是需要通过buffer,这点和bio是有区别的,bio要么是输入流,要么是输出流,不能是双向流动的,但是nio的buffer是可以读也可以写的,但是需要flip切换
- channel是双向的,可以返回底层操作系统的情况,比如linux底层的操作系统通道就是双向的
http2.0使用了多路复用的技术,实现了同一个连接并发处理多个请求,并且并发请求的数量比http1.1大了好几个数量级
组件一:Buffer
概念
本质上是一个可以读写数据的内存块,可理解为一个容器对象(含数组)该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制能够跟踪和记录缓冲区的状态变化情况,channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经过buffer
重要属性
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
- capacity:该缓冲区最大容量,在该缓冲区创建时设定,并且不可改变,到达最大容量的时候,清空buffer才能继续写入
- position指向下一次写入或者读取的位置(索引),每次读取或写入都会改变这个值
- mark类似于书签,用于临时保存positon的值,每次调用mark()会将mark设置为当前position
-
limit写操作模式,代表最大能写入数据,此时limit=capacity
limit读操作模式,此时limit=buffer中实际数据大小
常用方法
例如limit(3),capacity为5,我只读取到10 11 12
flip
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
很明确,做了重置position在0索引和limit位置切换为实际数据个数(position指向4,数据从0开始,0-3位4)的操作
rewind
重置position,用于重新从头读写buffer
clear
当buffer数据满了以后,在重新填充前调用clear()
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
可以看到只是重新重置了一下位置等,并没有删除之前数据,等于用后续写入的数据覆盖原来的数据,等同于清空之前数据
案例代码
public class BasicBuffer {
public static void main(String[] args) {
//创建一个buffer,能够存储5个int
IntBuffer intBuffer = IntBuffer.allocate(5);
//使用:向buffer中存放数据
intBuffer.put(10);
intBuffer.put(11);
intBuffer.put(12);
intBuffer.put(13);
intBuffer.put(14);
//从buffer中读取数据
//将buffer转换:读写转换
intBuffer.flip();
//读取
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
注意:NIO的读操作(从channel中读取数据到buffer)对应buffer的写操作!
int size=channel.read(buffer);
返回从channel中读入到buffer数据大小
NIO的写操作也很常见(从buffer中读取数据到channel中),对应buffer的读操作
通过FileChannel将数据写入文件中,通过SocketChannel将数据写入网络发送到远程机器等
int size=channel.write(buffer)
最常用子类 ByteBuffer
静态方法实例化一个byteBuffe,指定capacityr
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
buffer使用注意事项:
- 按什么类型存入,就需要按什么类型获取
- 可以将buffer通过buffer.readOnlyBuffer转为只读buffer
- nio还提供了mappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由nio来完成
- nio支持多个buffer,即buffer数组来完成读写操作,即scattering和gathering
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口监听
serverSocketChannel.bind(inetSocketAddress);
//是可以正确的将数据分别放在第0个和第一个buffer的
ByteBuffer[] bufferArr=new ByteBuffer[2];
bufferArr[0]=ByteBuffer.allocate(5);
bufferArr[1]=ByteBuffer.allocate(3);
//等待客户端连接 telnet测试
SocketChannel socketChannel = serverSocketChannel.accept();
final int messageLength=8; //假设从客户端接收8个字节
//不清楚客户端会发送多少数据过来,因此我们采用循环读取
while (true){
int byteRead = 0;
while (byteReadSystem.out.println("position="+buffer.position()+",limit="+buffer.limit()));
//将所有buffer进行反转
Stream.of(bufferArr).forEach(Buffer::flip);
//将数据读出显示回客户端
long byteWrite =0;
while (byteWrite
组件二:Channel
概念
通道,类似 IO 中的流,用于读取和写入。但是区别如下:
- 通道可以同时进行读写,而流只能读或者只能写(输入流或者输出流)
- 通道可以实现异步读写数据(流在读或者写的时候是阻塞的)
- 通道可以从缓冲区读数据,也可以写数据到缓冲区;
标颜色的SocketChannel和ServerSocketChannel是最为核心的内容(当客户端连接server的时候,server端会由serverSocketChannel抽象类的实例(serverSocketChannleImpl)产生一个与这个客户端对应的socketChannel抽象类的实例(socketChannleImpl),进而与服务器进行通讯)
serversocketchannle功能类似于serversocket,socketchannel功能类似于socket
[图片上传中...(image.png-755aaf-1593847800288-0)]
几个子类的使用
(1)FileChannel
非重点,不支持非阻塞,使用例子:本地文件读取
@Test
void test() throws IOException {
//创建一个输入流
FileInputStream inputStream=new FileInputStream(new File("C:\\Users\\a\\Desktop\\1.txt"));
//通过这个输入流获取对应的channel(即对这个输入流进行包装)
//filechannel真实类型是filechannelImpl
FileChannel fileChannel = inputStream.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int read = fileChannel.read(buf);
System.out.println(read);
//bytebuffer.array意思是返回byteBuffer中的hb数组
System.out.println(new String(byteBuffer.array()));
inputStream.close();
}
案例代码2:从一个文件读取到另一个文件,nio的channel的读,对应写入buffer,写入的时候需要调用clear,保证position下一个写入位置不能超过limit,否则导致无尽的读到0
/**
* 用一个buffer完成文件的读取
*/
public class FileChannleTest2 {
public static void main(String[] args) throws IOException {
FileInputStream inputStream = new FileInputStream(new File("11111.txt"));
FileChannel channel01 = inputStream.getChannel();
FileOutputStream outputStream = new FileOutputStream(new File("22222.txt"));
FileChannel channel02 = outputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(3);
while (true){
byteBuffer.clear();
int read = channel01.read(byteBuffer);
if(read==-1){
break;
}
byteBuffer.flip();
//调用下面的代码将会让position和limit相等,如果不在下次循环的时候clear,两者相等就会导致channel从buffer读到的内容为空,即buffer读到了尽头的假象
channel02.write(byteBuffer);
}
}
}
我们可以使用transferFrom完成文件的快速拷贝
组件三:Selector(最为重要的核心组件)
概念
- 用于实现一个线程管理多个channel。又称多路复用,为非阻塞。
- selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行对应的处理,这样就可以只用一个单线程去管理多个通道即管理多个连接和请求
- 只有在连接/通道真正有读写事件发生的时候,才会进行读写,这样大大减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之前的上下文切换导致的开销
原理图
- 当客户端连接时,会通过serverSocketChannel得到socketChannel
- 将socketChannel注册到Selector上
AbstractSelectableChannel中的
public final SelectionKey register(Selector sel, int ops,Object att),一个selector上可以注册多个socketChannel,ops可以传入的是selectionKey的事件,比如op_read即有读事件发生了,即注册通道的时候,这个时候已经可以告诉selector,我这个通道关注的是什么事情
包含以下四种事件
(1). SelectionKey.OP_READ =1
对应通道中有数据可以进行读取
(2). SelectionKey.OP_WRITE =4
可以往通道中写入数据
(3). SelectionKey.OP_CONNECT =8
成功建立TCP连接
(4). SelectionKey.OP_ACCEPT =16
接受TCP连接 - 注册后返回一个SelectionKey,会被selector以集合的方式关联
- Selector进行监听,select方法,返回有事件发生的channel通道 的个数
- 当select方法返回的结果>0的时候,可以进一步得到各个有事件发生的selectionKey
- 再通过selectionKey反向获取到我们注册的socketChannel
public asbstract SelectableChannel channel() - 可以通过得到的channel,完成业务处理
selectionKey就像一个纽带,连接了selector和channel
常用方法
Selector类是一个抽象类
static Selector open()
得到一个选择器对象
int select(long timeout)
最多阻塞timeout时间,监控所有注册的通道,当其中有io操作可以进行时,将对应的selectionKey加入到内部集合中Set
int select()
该方法会一直阻塞,知道至少有一个通道准备好
int selectNow()
非阻塞,功能和select一样,区别是如果没有通道准备好,此方法会立即返回0
Selector wakeup()
这个方法用来唤醒等待在select()和select(time out)上的线程,如果wakeup先被调用,此时没有线程在select上阻塞,那么之后的一个select()或者select(timeout)会立即返回,不会阻塞,影响一次
Set
从内部集合中得到所有的SelectionKey,不管上面有没有事件发生
selectionKey
selector.selectedKeys()返回有事件发生的selectionKey
selector.keys()返回所有的注册的selectionKey
如果有一个serverSocketChannel,两个客户端连接,调用keys获得的数量是3,调用selectionKey获得的数量是1,因为第二个客户端连接的时候,只有第二个通道产生了事件
常用方法
NIO实例,实现服务器端和客户端之间通讯
服务端:
public class NIOServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//获取selector选择器
Selector selector = Selector.open();
//将serverSocketChannel注册到selector上,关心是否有连接事件发生
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
//循环获取是否有事件发生
while (true){
if(selector.select(1000)==0){
System.out.println("通道没有事件发生");
continue;
}
//到这里说明已经有事件发生了
Set selectionKeys = selector.selectedKeys();
// 遍历所有事件
Iterator keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey selectionKey = keyIterator.next();
if(selectionKey.isAcceptable()){
//如果此时有客户端连接,注意,下面的accept不同于bio不会阻塞,因为这个if已经判断了
//serverSocketChannel就新建一个socketchannel来读取这个客户端的信息
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
//注册到selector上 ,关心客户端的读事件 ,并配置一个buffer用于读取这个socketChannel的事件
socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));
}
if(selectionKey.isReadable()){
//客户端有消息发送过来了,通过key获取到他对应的channel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//将socketChannel中内容读取到buffer中
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
socketChannel.read(byteBuffer);
System.out.println("客户端发送过来的数据是: "+new String(byteBuffer.array()));
}
//为了防止多线程下的重复操作,移除当前key
keyIterator.remove();
}
}
}
}
客户端:
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
if(!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("还没有成功连接服务端");
}
}
//连接成功,就发送数据
String message = "你好,我来访问了";
socketChannel.write(ByteBuffer.wrap(message.getBytes()));
System.in.read(); //让代码停止在这里
}
}
和直接一个 SocketChannel 进来就开启一个线程不同的是,不需要在客户端没有数据过来的时候循环等死在这里了,即可以节省客户端5秒后才发送数据的这5秒的时间
实际情况不能用上面代码,因为如果key.isReadable可以进行读取请求数据了,可是后面的处理链路可能很长,耗时很久,这个时候应该交给新的线程来执行,或者提交到线程池中
可以设置 SocketChannel 为非阻塞模式(non-blocking mode).设置之后,就可以在异步模式下调用connect(), read() 和write()了
如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。
AIO
与NIO区别
- nio:同步非阻塞通信
老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有 - aio:彻底的异步通信
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~的噪音。老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。
我们经常使用线程池执行异步任务,提交任务的主线程将任务提交到线程池就可以马上返回,不必等到任务完成,要向知道任务执行结果,通常是通过传递一个回调函数的方式,任务执行结束去调用这个函数。
可运行实例
服务端
@Slf4j
public class AioTcpServer {
public static void main(String[] args) throws IOException, InterruptedException {
AsynchronousServerSocketChannel serverSocketChannel=
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
Attachment attachment=new Attachment();
attachment.setServerSocketChannel(serverSocketChannel);
serverSocketChannel.accept(attachment, new CompletionHandler() {
@Override
public void completed(AsynchronousSocketChannel client, Attachment attachment) {
//成功接收到新的连接
try {
SocketAddress clientAddress = client.getRemoteAddress();
log.info("收到新的客户端 [ "+clientAddress+" ] 连接请求,连接成功!");
//收到连接后,进入下次收集连接
attachment.getServerSocketChannel().accept(attachment,this);
Attachment newAtt=new Attachment();
newAtt.setServerSocketChannel(serverSocketChannel);
newAtt.setSocketChannel(client);
newAtt.setReadMode(true);
newAtt.setByteBuffer(ByteBuffer.allocate(2048));
client.read(newAtt.getByteBuffer(), newAtt, new CompletionHandler() {
@Override
public void completed(Integer result, Attachment att) {
log.info("已经接收到客户端发送过来的数据,下面将打印.....");
if(att.isReadMode()){
ByteBuffer buffer=att.getByteBuffer();
buffer.flip();
byte[] bytes=new byte[buffer.limit()];
buffer.get(bytes);
String msg= new String(buffer.array()).trim();
log.info("来自客户端的数据:"+msg);
//响应客户端
buffer.clear();
buffer.put("HELLO,THIS IS SERVER!".getBytes(Charset.forName("UTF-8")));
att.setReadMode(false);
buffer.flip();
att.getSocketChannel().write(buffer,att,this);
}else{
try {
att.getSocketChannel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void failed(Throwable exc, Attachment attachment) {
System.out.println("读取失败,原因是"+exc.getMessage());
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Attachment attachment) {
System.out.println("接受失败");
}
});
log.info("服务端异步操作注册完毕,accept以及read操作的回调函数待命中...........");
//让main线程不直接结束
Thread.currentThread().join();
}
}
客户端
@Slf4j
public class AioTcpClient {
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future> future=client.connect(new InetSocketAddress("localhost",8080));
//阻塞,等待连接成功
future.get();
log.info("连接服务端成功!");
Attachment att = new Attachment();
att.setSocketChannel(client);
att.setReadMode(false);
att.setByteBuffer(ByteBuffer.allocate(2048));
byte[] data="你好啊,nice to meet you".getBytes();
att.getByteBuffer().put(data);
att.getByteBuffer().flip();
log.info("准备发送数据");
client.write(att.getByteBuffer(), att, new CompletionHandler() {
@Override
public void completed(Integer result, Attachment attachment) {
ByteBuffer byteBuffer = attachment.getByteBuffer();
if(attachment.isReadMode()){
//读服务端数据
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
String msg = new String(bytes, Charset.forName("UTF-8"));
log.info("收到来自服务端的响应数据: " + msg);
//关闭连接
try {
attachment.getSocketChannel().close();
} catch (IOException e) {
e.printStackTrace();
}
}else{
//写操作完成后,进入这里
attachment.setReadMode(true);
byteBuffer.clear();
attachment.getSocketChannel().read(byteBuffer,attachment,this);
}
}
@Override
public void failed(Throwable exc, Attachment attachment) {
System.out.println("服务端无响应");
}
});
Thread.sleep(2000);
}
}