一、概述
1、定义
java.nio全称java non-blocking IO,是指JDK1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络(来源于百度百科)。
2、为什么使用NIO
在上面的描述中提到,是在JDK1.4以上的版本才提供NIO,那在之前使用的是什么呢?答案很简单,就是BIO(阻塞式IO),也就是我们常用的IO流。
BIO的问题其实不用多说了,因为在使用BIO时,主线程会进入阻塞状态,这就非常影响程序的性能,不能充分利用机器资源。但是这样就会有人提出疑问了,那我使用多线程不就可以了吗?
但是在高并发的情况下,会创建很多线程,线程会占用内存,线程之间的切换也会浪费资源开销。
而NIO只有在连接/通道真正有读写事件发生时(事件驱动),才会进行读写,就大大地减少了系统的开销。不必为每一个连接都创建一个线程,也不必去维护多个线程。
避免了多个线程之间的上下文切换,导致资源的浪费。
二、Buffer缓冲区
Buffer是一个内存块。在NIO中,所有的数据都是用Buffer处理,有读写两种模式。所以NIO和传统的IO的区别就体现在这里。传统IO是面向Stream流,NIO而是面向缓冲区(Buffer)。
import java.nio.ByteBuffer;
public class BufferTest {
public static void main(String[] args) {
String msg="HelloWorld,我爱Java";
//创建一个大小为1024的byteBuffer
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
byte[] bytes=msg.getBytes();
//将数据写入byteBuffer中
byteBuffer.put(bytes);
//关键一步,将byteBuffer转化为读取模式
byteBuffer.flip();
byte[] temp=new byte[bytes.length];
int i=0;
//将byteBuffer中的数据放入temp
while (byteBuffer.hasRemaining()){
byte b=byteBuffer.get();
temp[i++]=b;
}
//打印结果
System.out.println(new String(temp));
}
}
这上面有一个flip()方法是很重要的。意思是切换到读模式。上面已经提到缓存区是双向的,既可以往缓冲区写入数据,也可以从缓冲区读取数据。但是不能同时进行,需要切换。
三、Channel管道
常用的Channel有这四种:
- FileChannel,读写文件中的数据。
- SocketChannel,通过TCP读写网络中的数据。
- ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
- DatagramChannel,通过UDP读写网络中的数据。
Channel本身并不存储数据,只是负责数据的运输。必须要和Buffer一起使用。
1、FileChannel
首先准备一个"1.txt"放在项目的根目录下,然后编写一个main方法:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest {
public static void main(String[] args) throws Exception {
//获取文件输入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//从文件输入流获取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//获取文件输出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//从文件输出流获取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//创建一个byteBuffer,小文件所以就直接一次读取,不分多次循环了
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
//把输入流通道的数据读取到缓冲区
inputStreamChannel.read(byteBuffer);
//切换成读模式
byteBuffer.flip();
//把数据从缓冲区写入到输出流通道
outputStreamChannel.write(byteBuffer);
//关闭通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
}
运行之后,根目录多了一个2.txt
2、SocketChannel
public static void main(String[] args) throws Exception {
//获取ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
//绑定地址,端口号
serverSocketChannel.bind(address);
//创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
//获取SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
while (socketChannel.read(byteBuffer) != -1){
//打印结果
System.out.println(new String(byteBuffer.array()));
//清空缓冲区
byteBuffer.clear();
}
}
}
然后运行main()方法,我们可以通过telnet命令进行连接测试:
3、Selector选择器
Selector翻译成选择器,有些人也会翻译成多路复用器,实际上指的是同一样东西。
四、通道间的数据传输
1、transferTo()
把源通道的数据传输到目的通道中。
public static void main(String[] args) throws Exception {
//获取文件输入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//从文件输入流获取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//获取文件输出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//从文件输出流获取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//创建一个byteBuffer,小文件所以就直接一次读取,不分多次循环了
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//把输入流通道的数据读取到输出流的通道
inputStreamChannel.transferTo(0, byteBuffer.limit(), outputStreamChannel);
//关闭通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
2、transferFrom()
把来自源通道的数据传输到目的通道。
public static void main(String[] args) throws Exception {
//获取文件输入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//从文件输入流获取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//获取文件输出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//从文件输出流获取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//创建一个byteBuffer,小文件所以就直接一次读取,不分多次循环了
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//把输入流通道的数据读取到输出流的通道
outputStreamChannel.transferFrom(inputStreamChannel,0,byteBuffer.limit());
//关闭通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
3、分散读取和聚合写入
1.txt内容
你好你好你好你好你好+-
JavaJava
abcdefg
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
public class WriteAndRead {
public static void main(String[] args) throws Exception {
//获取文件输入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//从文件输入流获取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//获取文件输出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//从文件输出流获取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//创建三个缓冲区,分别都是5
ByteBuffer byteBuffer1 = ByteBuffer.allocate(5);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(5);
ByteBuffer byteBuffer3 = ByteBuffer.allocate(5);
//创建一个缓冲区数组
ByteBuffer[] buffers = new ByteBuffer[]{byteBuffer1, byteBuffer2, byteBuffer3};
//循环写入到buffers缓冲区数组中,分散读取
long read;
long sumLength = 0;
while ((read = inputStreamChannel.read(buffers)) != -1) {
sumLength += read;
Arrays.stream(buffers)
.map(buffer -> "posstion=" + buffer.position() + ",limit=" + buffer.limit())
.forEach(System.out::println);
//切换模式
Arrays.stream(buffers).forEach(Buffer::flip);
//聚合写入到文件输出通道
outputStreamChannel.write(buffers);
//清空缓冲区
Arrays.stream(buffers).forEach(Buffer::clear);
}
System.out.println("总长度:" + sumLength);
//关闭通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
}
打印内容:
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5
posstion=1,limit=5
posstion=0,limit=5
总长度:51
可以看出,进行了四次循环,最后一次循环的第三个buffer没有存数据刚好读完
这就是分散读取,聚合写入的过程。
四、使用Selector的小例子
1、客户端连接服务器
客户端代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
socketChannel.configureBlocking(false);
//连接服务器
boolean connect = socketChannel.connect(address);
//判断是否连接成功
if(!connect){
//等待连接的过程中
while (!socketChannel.finishConnect()){
System.out.println("连接服务器需要时间,期间可以做其他事情...");
}
}
String msg = "hello 我爱Java!!!";
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
//把byteBuffer数据写入到通道中
socketChannel.write(byteBuffer);
//让程序卡在这个位置,不关闭连接
System.in.read();
}
}
服务端代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception {
//打开一个ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
//绑定地址
serverSocketChannel.bind(address);
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//打开一个选择器
Selector selector = Selector.open();
//serverSocketChannel注册到选择器中,监听连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端的连接
while (true) {
//等待3秒,(返回0相当于没有事件)如果没有事件,则跳过
if (selector.select(3000) == 0) {
System.out.println("服务器等待3秒,没有连接");
continue;
}
//如果有事件selector.select(3000)>0的情况,获取事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//获取迭代器遍历
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
//获取到事件
SelectionKey selectionKey = it.next();
//判断如果是连接事件
if (selectionKey.isAcceptable()) {
//服务器与客户端建立连接,获取socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//设置成非阻塞
socketChannel.configureBlocking(false);
//把socketChannel注册到selector中,监听读事件,并绑定一个缓冲区
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
//如果是读事件
if (selectionKey.isReadable()) {
//获取通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//获取关联的ByteBuffer
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
//打印从客户端获取到的数据
socketChannel.read(buffer);
System.out.println("from 客户端:" + new String(buffer.array()));
}
//从事件集合中删除已处理的事件,防止重复处理
it.remove();
}
}
}
}
控制台打印信息
2、SelectionKey
在SelectionKey类中有四个常量表示四种事件,来看源码:
public abstract class SelectionKey {
//读事件
public static final int OP_READ = 1 << 0; //2^0=1
//写事件
public static final int OP_WRITE = 1 << 2; // 2^2=4
//连接操作,Client端支持的一种操作
public static final int OP_CONNECT = 1 << 3; // 2^3=8
//连接可接受操作,仅ServerSocketChannel支持
public static final int OP_ACCEPT = 1 << 4; // 2^4=16
}
3、NIO实现聊天室
服务器端
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class Server {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
public static final int PORT = 6666;
//构造器初始化成员变量
public Server(){
try{
//打开一个选择器
this.selector = Selector.open();
//打开serverSocketChannel
this.serverSocketChannel = ServerSocketChannel.open();
//绑定地址,端口号
this.serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", PORT));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把通道注册到选择器中
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e){
e.printStackTrace();
}
}
/*
监听,接收客户端的信息,并转发到其他客户端
*/
public void listen(){
try{
while(true){
//获得监听的事件总数
int count=selector.select(2000); //2s
if(count>0){
Set<SelectionKey> selectionKeys=selector.selectedKeys();
//获取SelectorKey集合
Iterator<SelectionKey> it=selectionKeys.iterator();
while(it.hasNext()){
SelectionKey sk=it.next();
//如果是获取连接事件
if(sk.isAcceptable()){
SocketChannel socketChannel=serverSocketChannel.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
//注册到selector中
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress()+"上线了");
}
//如果是读取事件
if(sk.isReadable()){
//读取消息,并且转发到其他客户端
readData(sk);
}
it.remove();
}
}else{
System.out.println("等待。。。");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
//获取客户端来的消息
private void readData(SelectionKey selectionKey){
SocketChannel socketChannel=null;
try{
//从selectionKey中获取Channel
socketChannel=(SocketChannel)selectionKey.channel();
//创建一个缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//把通道的数据写入缓冲区
int count=socketChannel.read(byteBuffer);
//判断返回是否大于0,大于0表示读到了数据
if(count>0){
//把缓冲区的byte[]转成字符串
String msg=new String(byteBuffer.array());
//输出到控制台
System.out.println("From 客户端:"+msg);
//转发到其它客户端
notifyAllClient(msg,socketChannel);
}
} catch (Exception e){
try{
//打印离线通知
System.out.println(socketChannel.getRemoteAddress()+"离线了~");
//取消注册
selectionKey.cancel();
//关闭流
socketChannel.close();
}catch (Exception e1){
e1.printStackTrace();
}
e.printStackTrace();
}
}
/*
转发到其他客户端
msg 消息
noNotifyChannel 不需要通知的Channel
*/
private void notifyAllClient(String msg,SocketChannel noNotifyChannel) throws Exception{
System.out.println("服务器转发~");
for(SelectionKey selectionKey:selector.keys()){
Channel channel=selectionKey.channel();
//channel的类型是SocketChannel并且不是noNotifyChannel
if(channel instanceof SocketChannel && channel!=noNotifyChannel){
//强转成SocketChannel类型
SocketChannel socketChannel=(SocketChannel)channel;
//通过消息,包裹获取一个缓冲区
ByteBuffer byteBuffer= ByteBuffer.wrap(msg.getBytes());
socketChannel.write(byteBuffer);
}
}
}
public static void main(String[] args) throws Exception {
Server chatServer = new Server();
//启动服务器,监听
chatServer.listen();
}
}
客户端
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 Client {
private Selector selector;
private SocketChannel socketChannel;
private String userName;
public Client(){
try{
//打开选择器
this.selector=Selector.open();
//连接服务器
socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",Server.PORT));
//设置为非阻塞
socketChannel.configureBlocking(false);
//注册到选择器中
socketChannel.register(selector, SelectionKey.OP_READ);
//获取用户名
userName=socketChannel.getLocalAddress().toString().substring(1);
System.out.println(userName+" is ok~");
}catch (Exception e){
e.printStackTrace();
}
}
//发送消息到服务端
private void sendMsg(String msg) {
msg = userName + "说:" + msg;
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
}
//读取服务端发送过来的消息
private void readMsg() {
try {
int count = selector.select();
if (count > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//判断是读就绪事件
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
//创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//从服务器的通道中读取数据到缓冲区
channel.read(byteBuffer);
//缓冲区的数据,转成字符串,并打印
System.out.println(new String(byteBuffer.array()));
}
iterator.remove();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
Client chatClinet = new Client();
//启动线程,读取服务器转发过来的消息
new Thread(() -> {
while (true) {
chatClinet.readMsg();
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
//主线程发送消息到服务器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
chatClinet.sendMsg(msg);
}
}
}
转载于:NIO从入门到踹门