文章目录
- NIO编程
- 1. 概述
- NIO和BIO的区别
- NIO三大核心
- 2. 文件IO
- 1. 概述和核心API
- 缓冲区Buffer
- 通道Channel
- 2. 案例
- 3. 网络IO
- 1. 概述和核心API
- 1. Selector(选择器/多路复用器)
- 2. SelectionKey
- 3. ServerSocketChannel
- 4. SocketChannel
- 2. 入门案例
- 4. NIO多人聊天案例
NIO编程
1. 概述
java.nio 全称 java non-blocking IO, 是指 JDK 提供的新 API。 从 JDK1.4 开始, Java 提供了
一系列改进的输入/输出的新特性, 被统称为 NIO(即 New IO)。 新增了许多用于处理输入输出
的类, 这些类都被放在 java.nio 包及子包下 。
NIO和BIO的区别
- BIO以流的方式处理数据,NIO以通道channel的方式处理数据
- BIO以自定义的byte类型数组充当缓冲区,NIO直接提供了buffer
- BIO阻塞式,NIO非阻塞式。
反转的思考:其实之前一直是往buffer中put数据的,现在想要get数据到通道,相当于指针还停留在put的数据的末尾,直接get得到空值,将指针重置到最初的位置,这样才能get所有数据。
NIO三大核心
Channel(通道)、Buffer(缓冲区)、Selector(选择器)。NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(连接请求、数据到达等),因此使用单个线程就可以监听多个客户端通道。
2. 文件IO
1. 概述和核心API
缓冲区Buffer
和BIO相比,抽象出buffer,读写都是到buffer。
是个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区状态变化情况。
buffer读写模式的转变
写:capacity为理论容量,底层可以扩,limit为实际容量
读:limit指向原来position的位置,position指向0,参见buffer的flip方法
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
读和写由channel来操作,channel.read(buffer);channel.write(buffer);
Channel提供了从文件、网络读取数据的渠道,但是读取或写入的数据必须经由Buffer。
在NIO中,Buffer是一个顶层父类,抽象类,常用的子类有:
- ByteBuffer,存储字节数据到缓冲区
- ShortBuffer,存储字符串数据到缓冲区
- CharBuffer,存储字符数据到缓冲区
- IntBuffer,存储整数数据到缓冲区
- LongBuffer,存储长整型数据到缓冲区
- DoubleBuffer,存储小数到缓冲区
- FloatBuffer,存储小数到缓冲区
对于Java中的基本数据类型,都有一个Buffer类型与之对应,最常用的是ByteBuffer类,其主要方法为
-
public abstract ByteBuffer put(byte[] b):
存储字节数据到缓冲区 -
public abstract byte[] get():
从缓冲区数据转换成字节数组 -
public final byte[] array():
把缓冲区数据转换为字节数组 -
public static ByteBuffer allocate(int capacity):
把一个现成的数组放到缓冲区中使用 -
public final Buffer flip():
翻转缓冲区,重置位置到初始位置
通道Channel
类似于BIO中的stream,例如FileInputStream对象,用来建立到目标(文件、网络套接字、硬件设备等)的一个连接,注意:BIO的stream是单向的,NIO的通道是双向的,可读可写操作.
客户端socketChannel先和服务端的ServerSocketChannel建立连接connect,之后ServerSocketChannel调用accept方法创建服务端的socketChannel。之后就是client和server的socketChannel之间进行数据的读写操作。
将这些交互抽象为事件,如server的ServerSocketChannel为accept事件,client的socketChannel为connect事件…
常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel
这里先讲解FileChannel类,主要用来对本地文件进行IO操作,主要方法如下
-
public int read(ByteBuffer dst):
从通道读取数据并放到缓冲区 -
public int write(ByteBuffer src):
把缓冲区的数据写到通道中 -
public long transferFrom(ReadableByteChannel src, long position, long count):
从目标通道中复制数据到当前通道 -
public long transferTo(long position, long count, WriteableByteChannel target):
把数据从当前通道复制给目标通道
2. 案例
往本地文件中写数据、读数据和复制操作
public class TestNio {
@Test //往本地文件中写数据
public void test01()throws Exception{
// 1.创建输出流
FileOutputStream fos = new FileOutputStream("basic.txt");
// 2. 从流中得到一个通道
FileChannel fc = fos.getChannel();
// 3. 提供一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4. 往缓冲区存入数据
String str = "hello,nio";
buffer.put(str.getBytes());
// 5. 翻转缓冲区
// 翻转的文档:After a sequence of channel-read or put operations,
// invoke this method to prepare for a sequence of channel-write or relative get operations.
buffer.flip();
// 6. 把缓冲区写入到通道
fc.write(buffer);
// 7. 关闭
fos.close();
}
@Test // 从本地文件读取数据
public void test02() throws Exception{
File file = new File("basic.txt");
// 1. 创建输入流
FileInputStream fis = new FileInputStream(file);
// 2. 得到一个通道
FileChannel fc = fis.getChannel();
// 3. 准备一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
// 4. 从通道中读取数据并存到缓冲区
fc.read(buffer);
// buffer.array() Returns the byte array数组 that backs this buffer
System.out.println(new String(buffer.array()));
// 5.关闭
fis.close();
}
@Test //使用NIO实现文件复制,特别适合复制大文件
public void test03() throws Exception{
// 1. 创建两个流
FileInputStream fis = new FileInputStream("basic.txt");
FileOutputStream fos = new FileOutputStream("target.txt");
// 2. 得到两个通道
FileChannel sourceFC = fis.getChannel();
FileChannel destFC = fos.getChannel();
// 3. 复制
destFC.transferFrom(sourceFC,0,sourceFC.size());
// 4. 关闭
fis.close();
fos.close();
}
}
3. 网络IO
1. 概述和核心API
FileChannel并不支持非阻塞操作,学习NIO主要就是进行网络IO,java NIO的网络通道是非阻塞IO的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,如一些即时通信的服务。
在java中编写Socket服务器,通常有以下几种模式:
- 一个客户端连接用一个线程。如果连接非常多,分配线程也非常多,服务器可能因为资源耗尽而崩溃。
- 每个客户端连接交给固定数量线程的连接池。可以处理大量的连接,但线程开销很大,连接如果非常多,排队现象比较严重
- 使用NIO,非阻塞方式处理,可以一个线程,处理大量客户端连接
1. Selector(选择器/多路复用器)
解决了服务端需要起多个独立的socketChannel线程跟client的socketChannel交互的问题。
selector不断轮询注册在它上面的Channel,如果serverSocketChannel有accept事件,就再注册新的socketChannel,注册到selector;如果某个socketChannel发生读写事件,就处于就绪状态,被轮询出来,通过selectedKeys获取就绪channel的集合,进行后续IO操作。
这是IO模型,还有线程模型,是一个线程处理,还是怎么分配,为reactor 线程模型。
能够检测多个注册的通道上是否有事件发生,有,获取事件相应处理。这样就可以一个单线程管理多个通道,也就是多个连接。这样,只有真正有读写事件发生时,调用函数读写,减少系统开销,不用维护多个线程,避免线程间上下文切换的开销。
常用方法如下:
-
public static Selector open():
得到一个选择器对象 -
public int select(long timeout):
监控所有注册的通道,当其中有IO操作时,将对应的SelectionKey加入到内部集合中并返回,参数超时时间 -
public Set<SelectionKey> selectionKeys():
从内部集合中得到所有的SelectionKey
2. SelectionKey
代表Selector和网络通道的注册关系,有四种
-
int OP_ACCEPT:
有新的网络连接可以accept,值为16 -
int OP_CONNECT:
代表连接已经建立,值为8 -
int OP_READ
和int OP_WRITE
:代表读写操作,值为1和4
其常用方法如下:
-
public abstract Selector selector():
得到与之关联的Selector对象 -
public abstract SelectableChannel channel():
得到与之关联的通道 -
public final Object attachment():
得到与之关联的共享数据 -
public abstract SelectionKey interestOps(int ops):
设置或改变监听事件 -
public final boolean isAcceptable():
是否可以accept -
public final boolean isReadable():
是否可以读 -
public final boolean isWritable():
是否可写
3. ServerSocketChannel
用来在服务器端监听新的客户端Socket连接,常用方法
-
public static ServerSocketChannel open():
得到一个ServerSocketChannel通道 -
public final ServerSocketChannel bind(SocketAddress local):
设置服务器端端口号 -
public final SelectableChannel configureBlocking(boolean block)
,设置阻塞或非阻塞模式,false表示非阻塞 -
public SocketChannel accept():
接收一个连接,返回代表该连接的通道对象 -
public final SelectionKey register(Selector sel,int ops):
注册一个连接器并设置监听事件
4. SocketChannel
网络IO通道,负责具体读写操作。NIO总是把缓冲区的数据写入到通道,或者把通道的数据读到缓冲区。常用方法
-
public static SocketChannel open():
得到一个SocketChannel通道 -
public final SelectableChannel configureBlocking(boolean block):
设置阻塞或非阻塞模式,false非阻塞 -
public boolean connect(SocketAddress remote):
连接服务器 -
public boolean finishConnect():
如果上面的方法连接失败,接下来就要通过该方法完成连接 -
public init write(ByteBuffer src):
往通道写数据 -
public int read(ByteBuffer dst):
从通道读数据 -
public final SelectionKey register(Selector sel,int ops,Object att):
注册一个选择器并设置监听事件,最后一个参数可设置共享数据 -
public final void close():
关闭通道
2. 入门案例
以服务器端和客户端的数据通信为例
客户端
public class NIOClient {
public static void main(String[] args) throws Exception{
// 1. 得到一个网络通道
SocketChannel channel = SocketChannel.open();
// 2. 设置非阻塞方式
channel.configureBlocking(false);
// 3. 提供服务器端的IP地址和端口号
InetSocketAddress address = new InetSocketAddress("127.0.0.1",9999);
// 4. 连接服务器端
if(!channel.connect(address)){
while(!channel.finishConnect()){//NIO作为非阻塞的优势
System.out.println("Client:连接服务器端的同时,我还可以做别的事情");
}
}
// 5. 得到一个缓冲区并存入数据
String msg = "hello,Server";
ByteBuffer writeBuf = ByteBuffer.wrap(msg.getBytes());
// 6. 发送数据
channel.write(writeBuf);
// 暂时不能关,否则服务器会报异常
System.in.read();//临时措施
}
}
服务器端
public class NIOServer {
public static void main(String[] args) throws Exception{
// 1.得到一个ServerSocketChannel对象,老大
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2. 得到一个Selector对象 ,间谍
Selector selector = Selector.open();
// 3. 绑定端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
// 4. 设置非阻塞方式
serverSocketChannel.configureBlocking(false);
// 5. 把serverSocketChannel对象注册给Selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6. 干活
while(true){
// 6.1 监控客户端
if(selector.select(2000)==0){//表示被监控的客户端通道有几个
System.out.println("Server:没有客户端搭理我,我干点别的事");
continue;
}
// 6.2 监控事件,得到SelectionKey,判断通道里的时间
// selectedKeys() returns this selector's selected-key set.
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while(keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
/*}
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {*/
if(key.isAcceptable()){// 客户端连接事件
System.out.println("OP_ACCEPT");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(key.isReadable()){//读取客户端数据事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("客户端发来数据:"+new String(buffer.array()));
}
// 6. 手动从集合中移除当前的key
keyIterator.remove();
}
}
}
}
4. NIO多人聊天案例
服务器端
public class ChatServer {
private ServerSocketChannel listenerChannel; //监听通道 老大
private Selector selector; //选择器对象 间谍
private static final int PORT = 9999; //服务器端口
public ChatServer(){
try {
// 1. 得到监听通道 老大
listenerChannel = ServerSocketChannel.open();
// 2.得到选择器 间谍
selector = Selector.open();
// 3. 绑定端口
listenerChannel.bind(new InetSocketAddress(PORT));
// 4. 设置非阻塞模式
listenerChannel.configureBlocking(false);
// 5. 将选择器绑定到监听通道监听accept事件
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Chat Server is ready....");
} catch (IOException e) {
e.printStackTrace();
}
}
// 6.干活
public void start() throws Exception{
try {
while(true){
if(selector.select(2000)==0){
System.out.println("Server:没有客户客户端找我,我就干别的事情");
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isAcceptable()){//连接请求事件
SocketChannel sc = listenerChannel.accept();//接收通道
sc.configureBlocking(false);//非阻塞
sc.register(selector,SelectionKey.OP_READ);//监听读事件
System.out.println(sc.getRemoteAddress().toString().substring(1)+"上线了");//打印客户端
}
if(key.isReadable()){
readMsg(key);
}
// 一定要把当前key删掉,防止重复处理
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 读取客户端发来的消息并广播出去
public void readMsg(SelectionKey key) throws Exception{
SocketChannel channel = (SocketChannel) key.channel();//获取通道
ByteBuffer buffer = ByteBuffer.allocate(1024); //获取缓冲区
int count = channel.read(buffer);//从缓冲区读取数据,count>0说明有数据
if(count>0){
String msg = new String(buffer.array()); // 缓冲区数据转换为字节数组,转换为字符串
printInfo(msg);
broadcast(channel,msg);//广播,排除发送者,发送msg
}
}
// 给所有的客户端发广播
public void broadcast(SocketChannel except,String msg) throws Exception{
System.out.println("服务器发送广播了。。。");
for (SelectionKey key : selector.keys()) {
// 获取每个selectionKey
SelectableChannel targetChannel = key.channel();//每个通道
// 通道是SocketChannel的实例并且不是except
if(targetChannel instanceof SocketChannel && targetChannel!=except){
SocketChannel destChannel = (SocketChannel)targetChannel;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());//wrap获取buffer
destChannel.write(buffer); //写数据
}
}
}
private void printInfo(String str){// 往控制台打印消息
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("["+sdf.format(new Date())+"] -> "+str);
}
public static void main(String[] args) throws Exception{
new ChatServer().start();
}
}
客户端
public class ChatClient {
private final String HOST = "127.0.0.1";//服务器地址
private int PORT = 9999;//服务器端口
private SocketChannel socketChannel;//网络通道
private String userName; // 聊天用户名
public ChatClient() throws IOException{
// 1. 得到一个网络通道
socketChannel = SocketChannel.open();
// 2. 设置非阻塞方式
socketChannel.configureBlocking(false);
// 3. 提供服务器端的IP地址和端口号
InetSocketAddress address = new InetSocketAddress(HOST,PORT);
// 4. 连接服务器端
if(!socketChannel.connect(address)){
while (!socketChannel.finishConnect()){//NIO的优势
System.out.println("Client:连接服务器的同时,我还可以干别的事情");
}
}
// 5. 得到客户端IP地址和端口信息,作为聊天用户名使用
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("----------Client(" +userName+" ) is ready---------------");
}
// 向服务器端发送数据
public void sendMsg(String msg) throws Exception{
if(msg.equalsIgnoreCase("bye")){
socketChannel.close();
return;
}
msg = userName + "说:"+msg;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
socketChannel.write(buffer);
}
// 从服务器端接收数据
public void receiveMsg() throws Exception{
ByteBuffer buffer = ByteBuffer.allocate(1024);
int size = socketChannel.read(buffer);
if(size>0){
String msg = new String(buffer.array());
System.out.println(msg.trim());//trim为了只显示简单的话,不带空格
}
}
}
客户端启动
public class TestChat {
public static void main(String[] args) throws Exception{
final ChatClient chatClient = new ChatClient();
new Thread(){ //单独开个线程,不停接收数据
@Override
public void run() {
while(true){
try {
chatClient.receiveMsg();
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNextLine()){
String msg = scanner.nextLine();
chatClient.sendMsg(msg);
}
}
}