NIO网络编程学习笔记
文章目录
一、BIO
其实你之前学过那些java.io包下面的输入流和输出流就是BIO中的文件IO部分。例如InputStream、OutputStream。还有一部分就是网络IO,大家学过的在java.net包下面的提供了部分网络API,例如Socket, ServerSocket。
1、概念
Java BIO就是传统的java io编程,在java.io包下有相关的类和接口。
BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。如果这个连接不做任何事情就会造成不必要的线程开销(当然可以通过线程池机制改善)。
同步和异步是针对应用程序和内核的交互而言的。
同步:指的是用户进程触发IO操作并等待或轮询的去查看IO操作是否就绪。
案例:自己去商店买东西(使用同步IO时,Java自己处理IO读写)
异步:指的是用户进程触发IO操作以后便开始做其他的事情,而当IO操作已经完成的时候会得到IO完成的通知。
案例:托朋友买东西,告诉朋友要买的东西,自己去办其他事情,同时,你还要告诉朋友买完东西后给你送到哪里。(使用异步I/O时, Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS)。
阻塞和非阻塞是针对进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式。
阻塞:指的是准备对文件进行读写时,如果当时没有东西可读,或暂时不可写,程序就进入等待状态,直到有东西可读或可写为止。
案例:排队上厕所,厕所有人,你只能在那等着(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)。
非阻塞:指的是如果没有东西可读,或不可写,读写函数马上返回,而不会等待。
案例:排队上厕所,厕所有人,你可以选择做其他事(使用非阻塞IO时,如果不能读写,Java调用会马上返回)。
2、工作原理
- 启动一个服务器(serverSocket),然后等待客户端的连接。
- 启动一个客户端(Socket),然后与服务器进行通信(默认情况下服务器端需要给每个客户端建立一个线程与其通信)。
- 客户端发出请求,询问服务器是否有线程响应,如果没有就会等待或被拒绝。
- 如果有响应,客户端线程会等待请求结束后再继续执行。
3、案例
使用BIO模型编写一个服务器(服务器端口为10086),客户端可以发消息给服务器。
3.1 服务器端
package com.dapan.bio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* BIO模式下的服务器
*/
public class BioServer {
public static void main(String[] args) {
//创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
ServerSocket serverSocket = null;
{
try {
serverSocket = new ServerSocket(10086);
while (true) {
//连接服务器的客户端
Socket client = serverSocket.accept();
System.out.println("有客户端连接成功");
//为每个客户端都创建一个新的线程与之通信
executorService.execute(()->{
System.out.println("线程id:"+Thread.currentThread().getId());
System.out.println("线程名称:"+Thread.currentThread().getName());
InputStream inputStream = null;
try {
int len = -1;
byte[] byteArr = new byte[1024];
//读取来自客户端的数据
inputStream = client.getInputStream();
while ((len = inputStream.read(byteArr)) != -1) {
String msg = new String(byteArr,0,len);
System.out.println("来自客户端的消息:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
3.2 客户端
package com.dapan.bio;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class BioClient {
public static void main(String[] args) {
//创建客户端
try {
Socket client = new Socket("127.0.0.1", 10086);
String msg = "hello world";
OutputStream outputStream = client.getOutputStream();
outputStream.write(msg.getBytes(),0,msg.length());
outputStream.close();
//为了让客户端与服务器保持连接
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3 测试
允许main方法多次运行
或者
结果:
4、总结
BIO的缺点:客户端越来越来越多,服务器就要开启越来越多的线程,对服务器的压力就会越大;而且客户端发起一个连接之后不一定都在做事情,这个时候服务器也要维护,造成不必要的压力。
使用场景:BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,该方式是JDK1.4以前的唯一选择,但程序直观简单易理解。
二、NIO
1、概念
Java NIO(全称java non-blockingIO): 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
NIO和BIO的作用和目的相同,但是实现方式不同。
- BIO以流的方式处理数据,而NIO以块的方式处理数据,因此效率要高很多。
- NIO是既可以是非阻塞式的,也可以是阻塞式的,而BIO是阻塞式的
NIO是在Java 1.4开始引入了NIO框架(java.nio包) ,java提供了一系列改进的输入输出的新特性,这些统称NIO,也有人成为New IO.
NIO主要有三大核心:Channe(通道)、Buffer(缓冲区)、Selector(选择器)
NIO提供了Channel、Selector、 Buffer等新的抽象 ,可以构建多路复用IO程序,同时提供更接近操作系统底层的高性能数据操作方式。传统BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件,因此使用单个线程就可以监听多个数据通道。
2、工作原理
- 一个线程一个selector,一个线程对应多个channel(连接),每个Channel对应一个Buffer。
- 多个channel可以注册到一个selector,事件决定selector切换到哪一个channel。
- 数据的读写通过Buffer,BIO中的流是单向的,要么输入流要么输出流,NIO的Buffer是可双向读写,通过flip方法切换即可。
- channel也是双向的,可以返回底层操作系统的情况,例如Linux,底层的操作系统通道就是双向的。
3、NIO核心
3.1 缓冲区 Buffer
3.1.1 概念
缓冲区(Buffer) :缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包装成NIO Buffer对象,可以理解成是一个容器,是一个特殊的数组,该对象提供了一组方法,用来方便的访问该块内存。
Channel提供从文件或网络读取数据的渠道,但是读取或者写入的数据都是经过Buffer。
3.1.2 Buffer类及其子类
在NIO中,Buffer是一个顶级父类,也是一个抽象类,有很多的子类。
3.1.3 Buffer中的属性
属性 | 描述 |
---|---|
capacity | 容量;即可以容纳的最大数据量,在缓冲区创建时被设定并且不能改变。 |
position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变该值,为下次读写准备 |
limit | 表示缓冲区的当前终点 ,不能对缓冲区的超过极限的位置进行读写操作。写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。简而言之,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position) |
3.1.4 Buffer中的方法
3.1.5 Buffer的基本使用
使用Buffer读写数据一般遵循以下四个步骤:
- 创建缓冲区,写入数据到Buffer
- 调用flip()方法将缓冲区改成读模式
- 从Buffer中读取数据
- 调用clear()方法或者compact()方法
虽然java中的基本数据类型都有对应的Buffer类型与之对应(Boolean除外),但是使用频率最高的是ByteBuffer类。所以先介绍一下ByteBuffer中的常用方法。
3.1.6 ByteBuffer中常用方法
allocate(int):创建间接缓冲区:在堆中开辟,易于管理,垃圾回收器可以回收,空间有限,读写文件速度较慢。
allocateDirect(int):创建直接缓冲区:不在堆中,物理内存中开辟空间,空间比较大,读写文件速度快,缺点:不受垃圾回收器控制,创建和销毁耗性能。
3.1.7 案例一
package com.dapan.nio.buffer;
import java.nio.ByteBuffer;
public class ByteBuffer01 {
public static void main(String[] args) {
//1、创建缓冲区,写入数据到Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);//创建指定容量的间接缓冲区
// ByteBuffer buffer1 = ByteBuffer.allocateDirect(1024); //创建指定容量的直接缓冲区
//写入数据的方式一
// buffer.put("hello,world".getBytes());
//写入数据的方式二
buffer.put((byte) 'h');
buffer.put((byte) 'e');
buffer.put((byte) 'l');
buffer.put((byte) 'l');
buffer.put((byte) 'o');
buffer.put((byte) ',');
buffer.put((byte) 'w');
buffer.put((byte) 'o');
buffer.put((byte) 'r');
buffer.put((byte) 'l');
buffer.put((byte) 'd');
//2、调用flip方法将缓冲区改为读模式
buffer.flip();
//3、从Buffer中读取数据:方式一、单个自己读取
// while (buffer.hasRemaining()) {
// byte b = buffer.get();
// System.out.println((char)b);
// }
//方式二、
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
//4、调用clear()方法或compact()方法
buffer.clear();
// buffer.compact();
}
}
/**
* clear(): position将被置为0,limit被设置成capacity的值。可以理解为Buffer被清空了,
* 但是Buffer中的数据并未清除,
* 只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
* 如果Buffer中 有一些未读的数据,调用clear()方法,未读数据将“被遗忘”
* 意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有.
*
* compact(): 将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。
* limit属性依然像clear()方法一样,设置成capacity。
* 现在Buffer准备好写数据了,但是不会覆盖未读的数据。
*/
结果:
3.1.8 案例二
package com.dapan.nio.buffer;
import java.nio.ByteBuffer;
public class ByteBuffer02 {
public static void main(String[] args) {
//1、创建缓冲区,写入数据到Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);//创建指定容量的间接缓冲区
//写入数据,按照类型化的方式
buffer.putChar('h');
buffer.putLong(1024L);
buffer.putInt(10);
buffer.putShort((short) 0);
//2、调用flip方法将缓冲区改为读模式
buffer.flip();
//3、从Buffer中读取数据:
System.out.println(buffer.getChar());
System.out.println(buffer.getLong());
System.out.println(buffer.getInt());
System.out.println(buffer.getShort());
//4、调用clear()方法
buffer.clear();
}
}
结果:
注意:当读数据与写数据的顺序不一致时,会出现问题
3.1.9 案例三
package com.dapan.nio.buffer;
import java.nio.ByteBuffer;
public class ByteBuffer03 {
public static void main(String[] args) {
//创建缓冲区,写入数据到Buffer
ByteBuffer buffer = ByteBuffer.allocate(64);//创建指定容量的间接缓冲区
//循环放入数据
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte) i);
}
//调用flip方法将缓冲区改为读模式
buffer.flip();
//得到一个只读buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println("readOnlyBuffer类型:"+readOnlyBuffer.getClass());
//读取数据
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
//写入数据会抛出--ReadOnlyBufferException异常
readOnlyBuffer.put((byte) 66);
//调用clear()方法
buffer.clear();
}
}
结果:
3.2 通道 Channel
3.2.1 概念
通道(Channel) :类似于BIO中的stream,例如FileInputStream对象,用来建立到目标(文件,网络套接字,硬件设备等)的一个连接。
但是也有区别:
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入
3.2.2 Channel的实现
常用的Channel类有: FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel.
FileChannel:从文件中读写数据。
DatagramChannel:能通过UDP读写网络中的数据。
SocketChannel:能通过TCP读写网络中的数据。
ServerSocketChannel :可以监听新连接的TCP连接,像web服务器那样,对每一个新的连接都会创建一个SocketChannel。
FileChannel主要用来对本地文件进行读写操作,但是FileChannel是一个抽象类,所以我们实际用的更多的是其子类FileChannelImpl。
3.2.3 案例一
package com.dapan.nio.channel;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 写出数据到本地文件中
*/
public class FileChannel01 {
public static void main(String[] args) throws IOException {
String msg = "hello,world";
String fileName = "channel01.txt";
//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将信息写入缓冲区
buffer.put(msg.getBytes());
//对缓冲区进行读写切换
buffer.flip();
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream(fileName);
//获取同一个通道--channel的实际类型是FileChannelImpl
FileChannel channel = fileOutputStream.getChannel();
//将缓冲区中的数据写到通道中
int num = channel.write(buffer);
System.out.println("写入完毕!"+num);
//关闭流
fileOutputStream.close();
}
}
结果:
3.2.3 案例二
package com.dapan.nio.channel;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 从本地文件中读取数据
*/
public class FileChannel02 {
public static void main(String[] args) throws IOException {
File file = new File("channel01.txt");
//创建输入流
FileInputStream fileInputStream = new FileInputStream(file);
//获取通道
FileChannel channel = fileInputStream.getChannel();
//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
//将通道中的数据读取到buffer中
channel.read(buffer);
//将buffer中的字节数组转化为字符串输出
System.out.println(new String(buffer.array()));
//关闭流
fileInputStream.close();
}
}
结果:
3.2.3 案例三
package com.dapan.nio.channel;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 实现文件的复制
*/
public class FileChannel03 {
public static void main(String[] args) throws IOException {
//准备好要复制的源文件和目标文件
File file = new File("16.png");
File fileCopy = new File("1.png");
//创建输入流和输出流
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream(fileCopy);
//获取两个通道
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = -1;
while (true) {
len = inChannel.read(buffer);
if (len == -1) {
break;
}
//读写切换
buffer.flip();
outChannel.write(buffer);
//将标志位重置
buffer.clear();
}
System.out.println("复制完毕!");
//关闭流
inChannel.close();
outChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
}
准备好一张图片在根目录下
结果:
3.2.4 案例四
package com.dapan.nio.channel;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
/**
* 实现文件的复制 方式二:transferTo/transferFrom
*/
public class FileChannel04 {
public static void main(String[] args) throws IOException {
//准备好要复制的源文件和目标文件
File file = new File("16.png");
File fileCopy = new File("1.png");
//创建输入流和输出流
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream(fileCopy);
//获取两个通道
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//使用transferFrom复制---适合大文件的复制
outChannel.transferFrom(inChannel, 0, inChannel.size());
//使用transferTo复制---适合大文件的复制 注意方向
// inChannel.transferTo(0, inChannel.size(), outChannel);
System.out.println("复制完毕!");
//关闭流
inChannel.close();
outChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
}
运行结果和案例三一样
3.3 选择器 Selector
3.3.1 概念
Selector 一般称为选择器 ,当然你也可以翻译为多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否有事件发生(读、写、连接)。如果有事件发生,便获取事件然后针对每个事件进行相应的处理,如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。
刚刚进行文件IO时用到的FileChannel并不支持非阻塞操作,咱们学习NIO主要就是进行网络IO的学习, Java NIO中的网络通道是非阻塞IO的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但数据交换量不大的情况,例如一些即时通信的服务等等。
在之前的BIO中已经编写过Socket服务器:
ServerSocket–BIO模式的服务器,一个客户端一个线程,虽然写起来简单,但是如果连接越多,线程就会越多,容易耗尽服务器资源而使其宕机。
使用线程池优化–让每个客户端的连接请求交给固定数量线程的连接池处理,写起来简单还能处理大量连接。但是线程的开销依然不低,如果请求过多,会出现排队现象。
如果使用java中的NIO,就可以用非堵塞的IO模式处理,这种模式下可以使用一个线程,处理大量的客户端连接请求。
只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。避免了多线程之间的上下文切换导致的开销。
Selector:选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。
Selector是一个抽象类,实际使用的时候用的是SelectorImpl。
Selector类的select的三种不同形式:
1、无参的select():
Selector类的select()方法会无限阻塞等待,直到有信道准备好了IO操作,或另一个线程唤醒了它(调用了该选择器的wakeup())返回SelectionKey
2、带有超时参数的select(long time):
当需要限制线程等待通道就绪的时间时使用,
如果在指定的超时时间(以毫秒计算)内没有通道就绪时,它将返回0。
将超时参数设为0表示将无限期等待,那么它就等价于select( )方法了。
3、selectNow()是完全非阻塞的:
该方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回0
3.3.2 SelectionKey介绍
SelectionKey:一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。
3.3.3 SelectableChannel介绍
类关系:
两个重要方法:
SelectableChannel抽象类的confifigureBlocking()方法是由 AbstractSelectableChannel抽象类实现的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了AbstractSelectableChannel抽象类 (上图明确展示了这些类关系)。
register() 方法的第二个参数是一个interset集合 ,指通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:
- Connect
- Accept
- Read
- Write
通道触发了一个事件意思是该事件已经就绪。比如某个Channel成功连接到另一个服务器称为“ 连接就绪 ”。一个ServerSockeChannel准备好接收新进入的连接称为“ 接收就绪 ”。一个有数据可读的通道可以说是“ 读就绪 ”。等待写数据的通道可以说是“ 写就绪 ”。
这四种事件用SelectionKey的四个常量来表示:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
如果你对不止一种事件感兴趣,使用或运算符即可,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
3.3.4 ServerSocketChannel
ServerSocketChannel用来在服务器端监听新的客户端Socket连接
常用的方法除了两个从SelectableChannel类中继承而来的两个方法confifigureBlocking()和register()
方法外,还有以下要记住的方法:
3.3.5 SocketChannel
SocketChannel,网络IO通道,具体负责读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区(buffer) 。
常用的方法除了两个从SelectableChannel类中继承而来的两个方法confifigureBlocking()和register()
方法外,还有以下要记住的方法:
3.3.6 案例一
NIO服务器端
package com.dapan.nio.example;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* NIO模式的服务器端
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建服务器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定服务器端口
serverSocketChannel.bind(new InetSocketAddress(10086));
//设置为非阻塞通道---【重要】
serverSocketChannel.configureBlocking(false);
//创建Selector对象
Selector selector = Selector.open();
//将serverSocketChannel通道注册到selector中,并监听请求连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端的连接---服务器一直在线
while (true) {
//selector.select(0)表示将无限期等待
//如果在指定的超时时间(以毫秒计算)内没有通道就绪时,它将返回0。
if (selector.select(3000) == 0) {
//服务器没有阻塞,可以做其他的事
System.out.println("server:快点啊,等到花儿都谢了...");
continue;
}
//获取SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//如果是连接时间
if (key.isAcceptable()) {
//获取连接的客户端
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
System.out.println("有新的客户端连接!!!产生的socketChannel:" + socketChannel.hashCode());
//socketChannel也是通道,也需要注册到Selector中
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
//如果是通信事件
if (key.isReadable()) {
//获取关联的通道
SocketChannel socketChannel = (SocketChannel) key.channel();
//获取相关联的内容
ByteBuffer buffer = (ByteBuffer) key.attachment();
//读取数据
socketChannel.read(buffer);
System.out.println("来自客户端的消息:"+new String(buffer.array()));
}
//还可以处理其他事件....
//避免重复处理
iterator.remove();
}
}
}
}
NIO客户端
package com.dapan.nio.example;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* NIO模式的客户端
*/
public class NIOClient {
public static void main(String[] args) throws IOException {
//创建客户端
SocketChannel socketChannel = SocketChannel.open();
//通道都要设置为非阻塞
socketChannel.configureBlocking(false);
//准备要连接的服务器的地址和端口号
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 10086);
//准备连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
//如果connect()连接失败,则使用finishConnect()去保持连接
while (!socketChannel.finishConnect()) {
System.out.println("Client:努力连接中....可以先做点其他事");
}
}
//连接成功之后,发消息给服务器
String msg = "hello world";
//将msg的信息包装到Buffer中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
socketChannel.write(buffer);
//保持客户端的连接
System.in.read();
}
}
结果:
ServerSocketChannel没有阻塞,两个客户端连接
3.3.7 案例二
服务器端
package com.dapan.nio.example02;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* 聊天室的服务器端
*/
public class ChatServer {
//定义服务器
private ServerSocketChannel serverSocketChannel;
//定义选择器
private Selector selector;
//定义端口
private static final int PORT = 10086;
public ChatServer() {
//完成属性初始化
try {
serverSocketChannel = ServerSocketChannel.open(); //创建服务器
serverSocketChannel.bind(new InetSocketAddress(PORT)); //绑定端口
serverSocketChannel.configureBlocking(false); //所有通道都需要设置为非阻塞
selector = Selector.open(); //创建选择器
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //将serverSocketChannel注册到selector中
} catch (IOException e) {
e.printStackTrace();
}
}
//监听事件
public void listening() {
try {
//服务器一直监听
while (true) {
//如果在指定的超时时间(以毫秒计算)内没有通道就绪时,它将返回0。
int num = selector.select(3000);
if (num > 0) {
//获取SelectionKey集合进行遍历
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//处理客户端的连接请求
if (key.isAcceptable()) {
//连接到服务器的客户端
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);//设置为非阻塞
socketChannel.register(selector, SelectionKey.OP_READ); //注册到selector中
System.out.println(socketChannel.getRemoteAddress()+"连接成功!进入聊天室...");
}
//处理客户端的通信请求
if (key.isReadable()) {
//处理数据的读取和转发给除了自己之外的客户端
handleReadData(key);
}
//避免处理同一个通道事件
iterator.remove();
}
}else {
//没有客户端的连接,等待...
System.out.println("server:快点啊,等到花儿都谢了...");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 处理数据的读取
* @param key
*/
private void handleReadData(SelectionKey key) {
SocketChannel socketChannel = null;
try {
//服务器接收来自客户端的消息
socketChannel = (SocketChannel) key.channel();
//创建指定容量的间接缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(buffer);
if (len > 0) {
String msg = new String(buffer.array());
System.out.println("来自客户端的消息是:"+msg);
//转发消息给其他客户端
transferMessage(msg,socketChannel);
}
} catch (IOException e) {
try {
System.out.println(socketChannel.getRemoteAddress()+"离开的聊天室");
key.cancel();//取消注册
socketChannel.close();//关闭通道
} catch (IOException ioException) {
ioException.printStackTrace();
}
//e.printStackTrace();
}
}
/**
* 转发
* @param msg
* @param socketChannel
* @throws IOException
*/
private void transferMessage(String msg, SocketChannel socketChannel) throws IOException {
System.out.println("server转发消息....");
//遍历所有已注册的关系
for (SelectionKey selectionKey : selector.keys()) {
//获取关联的通道
SelectableChannel channel = selectionKey.channel();
//其他的客户端
if (channel instanceof SocketChannel && channel != socketChannel) {
SocketChannel client = (SocketChannel) channel;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
client.write(buffer);
}
}
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
chatServer.listening();
}
}
客户端
package com.dapan.nio.example02;
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.Scanner;
/**
* 聊天室的客户端
*/
public class ChatClient {
private final String HOSTNAME = "127.0.0.1";
private final int PORT = 10086;
private SocketChannel socketChannel;
private Selector selector;
private String username;
public ChatClient() {
try {
socketChannel = SocketChannel.open(new InetSocketAddress(HOSTNAME, PORT));
socketChannel.configureBlocking(false);
selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_READ);
username = socketChannel.getLocalAddress().toString();
System.out.println(username + "is ready...");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发送消息
* @param info
*/
private void sendInfo(String info) {
try {
info = username+" : "+info;
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取消息
*/
private void readInfo() {
try {
int readyChannel = selector.select();
//如果有通道准备就绪
if (readyChannel > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel socketChannel1 = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel1.read(buffer);
String msg = new String(buffer.array());
System.out.println(msg);
}
iterator.remove();
}
}else {
System.out.println("没有准备就绪的通道!");
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ChatClient chatClient = new ChatClient();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
chatClient.readInfo();
}
}
}).start();
Scanner input = new Scanner(System.in);
while (input.hasNextLine()) {
String msg = input.nextLine();
chatClient.sendInfo(msg);
}
}
}
结果:
三、AIO
1、概念
JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式: Reactor 和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式, 简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
四、总结
1、IO对比总结
IO 的方式通常分为几种:同步阻塞的 BIO、同步非阻塞的 NIO、异步非阻塞的 AIO。
- BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
- NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器, 并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
- AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器, 充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
举个栗子:
同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,饭馆没做好,你就必须等着
同步非阻塞:你在饭馆点完餐,就去玩儿了。不过玩一会儿,就回饭馆问一声:好了没啊!
异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩就可以了,类似于现在的外卖。
对比总结 | BIO | NIO | AIO |
---|---|---|---|
IO方式 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
API使用难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |
再举个栗子:
BIO:上厕所,坑位满了,只能等待,不可以干别的事。
NIO:上厕所,坑位满了,可以在等待时干别的事,抽烟,打游戏。
AIO:上厕所,坑位满了,可以在等待时干别的事,抽烟,打游戏。当有位子了会发一条信息通知你。
以下内容参考文章:100%弄明白5种IO模型 - 知乎 (zhihu.com)
2、五种IO模型
1、阻塞IO模型
所谓阻塞IO就是当应用B发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。
描述
在应用调用recvfrom读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,在此期间一直会等待,进程从调用到返回这段时间内都是被阻塞的称为阻塞IO;
2、非阻塞IO模型
描述:
非阻塞IO是在应用调用recvfrom读取数据时,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。在没有数据的时候会即刻返回错误标识,那也意味着如果应用要读取数据就需要不断的调用recvfrom请求,直到读取到它数据要的数据为止。
3、IO复用模型(I/O multiplexing)
有人就提出了一个思路,能不能提供一种方式,可以由一个线程监控多个网络请求(我们后面将称为fd文件描述符,linux系统把所有网络请求以一个fd来标识),这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路。
IO复用模型的思路就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,select函数监控的fd中只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据。
复用IO的基本思路就是通过slect或poll、epoll 来监控多fd ,来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。
描述
进程通过将一个或多个fd传递给select,阻塞在select操作上,select帮我们侦测多个fd是否准备就绪,当有fd准备就绪时,select返回数据可读状态,应用程序再调用recvfrom读取数据。
4、信号驱动IO模型(signal blocking I/O)
复用IO模型解决了一个线程可以监控多个fd的问题,但是select是采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否就可读的数据,而无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,能不能不要我总是去问你是否数据准备就绪,能不能我发出请求后等你数据准备好了就通知我,所以就衍生了信号驱动IO模型。
信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个fd。
避免了大量无效的数据状态轮询操作。
描述
首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此时请求即刻返回,当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知应用线程调用recvfrom来读取数据。
select、poll、epoll 区别
支持一个进程可以打开的最大连接数。
Select:
单个进程所能打开的最大连接数由 FD_SETSIZE 宏定义,其大小是 32 个整数的大小(在 32 位机器上,大小是 32*
32,在 64 位机器上 FD_SETSIZE 为 32*
64)。 消息传递时,内核需要将消息传递到用户空间,需要内核的拷贝动作。
Poll:
本质上 select 没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。消息传递时,内核需要将消息传递到用户空间,需要内核的拷贝动作。
Epoll:
有连接数上限,但是很大,1G 内存的机器可以打开10万连接。消息传递时,通过内核和用户空间共享一块内存来实现,性能较高。
6、异步IO模型
Asynchronous IO:基于事件和回调机制
其实经过了上面两个模型的优化,我们的效率有了很大的提升,但是我们当然不会就这样满足了,有没有更好的办法,通过观察我们发现,不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。
有人设计了一种方案,应用只需要向内核发送一个read 请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种一劳永逸的模式为异步IO模型。
描述
应用告知内核启动某个操作,并让内核在整个操作完成之后,通知应用,这种模型与信号驱动模型的主要区别在于,信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核通知我们操作什么时候完成。
AIO 如何处理结果?
1、基于回调:实现 CompletionHandler 接口,调用时触发回调函数
2、返回 Future:通过 isDone()查看是否准备好,通过 get()等待返回数据
3、Java中几种类型的流
4、Java的序列化
可序列化 Serializalbe 接口存在于 java.io 包中,构成了 Java 序列化机制的核心,它没有任何方法,它的用途是标记某对象为可序列化对象,指示编译器使用 Java 序列化机制序列化此对象。
对象序列化的用途:
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中。
- 在网络上传送对象的字节序列。(RPC)