Java NIO编程实例
前言
基于NIO的网络编程实例
提示:以下是本篇文章正文内容,下面案例可供参考
一、NIO与BIO的比较
NIO与BIO的比较 :
- BIO是以流的方式处理的 , NIO是以块的方式处理的(因为有Buffer) , 块I/O效率更高
- BIO是基于字节流字符流处理的,NIO是基于Channel和Buffer处理的,单个线程可监听多个客户端的通道
NIO三个核心组件之间的关系
- 每个Channel都对应一个Buffer , Channel是双向的,可读可写;
- Selector对应一个线程 ;
- 一个线程对应多个Channel ;
- 程序切换到哪个Channel是由时间Event决定的 ;
- Selector会根据不同的事件在各个通道上切换 ;
- Buffer 就是一个内存块 , 底层是一个数组 ;
- 数据的读取/写入是通过Buffer进行的 ,是可以读也可以写的 ,但需要flip方法做读写切换 ,与BIO有本质区别
二、Buffer的机制及其子类
1.Buffer的使用
Buffer的子类型 : 除了Boolean类型,其余7个Java子类型Buffer都有
public static void main(String[] args) {
// buffer的使用
// 1. 创建一个Buffer
IntBuffer intBuffer = IntBuffer.allocate(5); // 创建一个容量为5的Buffer
// 2. 向Buffer中存放数据
for (int i = 0; i < 5; i++) {
intBuffer.put(i*2);
}
// 3. 从Buffer中读取数据
// 将Buffer做读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get()); // get方法里维护了一个索引
}
}
2.Buffer的四个基本类型
以InteBuffer为例 , 真正的数据是存放在 final int[] hb;
数组里的
Buffer中定义了所有缓冲区都具有的4个属性
private int mark = -1; //标记,一般不被修改
private int position = 0; //下一个要被读写的元素的索引
private int limit; //缓冲区的当前终点 (数组索引的最大值)
private int capacity; // 最大容量
其中 , position不能超过limit , position可以被理解为一个游标 , 读写的时候是根据position的位置进行的
当调用了 flip()
函数反转过后 , position会被置为0
同时 , 上述的4个参数都有其对应的函数来修改他们的值
public final Buffer clear()
方法能将这4个参数恢复到初始状态 , 但是数据不会真正的被擦除
三、Channel的使用
1. Channel的特征
- 通道是可以同时读写的
- 可以实现异步读写数据
- 可以从Buffer中读取数据 , 也可以向Buffer写入数据
2. Channel的子类
Channel的子类 : FileChannel文件数据的读写 , DatagramChannel UDP数据的读写 , ServerScoketChannel和ScoketChannel用于TCP数据的读写
我们用于网络编程最常用的当然就是ServerScoketChannel和ScoketChannel了
(1) FileChannel实例:
public static void main(String[] args) throws IOException {
String str = "hello world";
// 创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("d://file01.txt");
// 通过输出流获取对应的FileChannel , 其真实类型为FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个ButeBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将str放到byte Buffer
byteBuffer.put(str.getBytes());
// filp反转
byteBuffer.flip();
//将byteBuffer数据写入fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close(); //关闭最底层的流
}
这里需要注意 : 当需要buffer从读转为写时,需要调用flip
函数做读写切换
(2) 拷贝文件
/**
* 拷贝文件
* */
public static void copy() throws IOException {
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel channel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel channel1 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true){
// 这里必须要清空一次数据,将关键属性重置
/**
* 如果这里不做复位,read的值会一直是0,程序会一直读取数据,进入死循环
* */
byteBuffer.clear();
int read = channel.read(byteBuffer);
if (read == -1){
break;
}
// 将buffer中的数据写入到channel1
byteBuffer.flip();
channel1.write(byteBuffer);
}
fileInputStream.close();
fileOutputStream.close();
}
四、Buffer类型化和只读
1. 类型化
所谓的类型化就是指 , 存进去的时什么数据类型的 . 读取的就要是什么数据类型 , 否则会报错
public static void main(String[] args) {
// 创建一个buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
for (int i = 0; i < 64; i++) {
byteBuffer.put((byte)i);
}
//反转并读取
byteBuffer.flip();
//获取一个只读的buffer
ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();
while (readOnlyBuffer.hasRemaining()){
System.out.println(readOnlyBuffer.get());
}
}
2. Buffer的分散和聚合
Scattering: 将数据写入到buffer中,可以采用buffer数组,依次写入
Gathering : 从buffer读取数据时 , 采用buffer数组以此读
分散和聚合涉及到同时操作多个Buffer
五、MappedByteBuffer
操作系统级别 , 性能比较高
MappedByteBuffer可以直接在内存(堆外内存)中修改文件 , 操作系统不需要再拷贝一次;
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
FileChannel channel = randomAccessFile.getChannel();
/**
* p1 FileChannel.MapMode.READ_WRITE 使用读写模式
*
* p2 直接修改的起始位置
*
* p3 目标文件将多少个字节映射到内存中
* p2 p3 表示程序可以直接修改的范围
* */
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
// 修改对应内容
mappedByteBuffer.put(0,(byte)'A');
mappedByteBuffer.put(3,(byte)9);
randomAccessFile.close();
}
六、Selector
能够检测多个注册上来的通道中是否有时间发生
只有连接/通道上真正有读写事件发生时,才会进行读写
避免了多线程上下文切换导致的开销
1. SelectionKey在NIO体系中的作用
- 当客户端连接时, 会通过ServerSocketChannel得到对应的SocketChannel;
- 将SocketChannel注册到Selector上, 使用的是register(Selector sel, int ops), 一个selector上可以注册多个SocketChannel;
- 注册后会返回一个SelectionKey , 会和该Selector关联起来
- Selector进行监听selector方法, 返回有事件发生的channel;
- 进一步得到各个有事件发生的SelectionKey , 并通过SelectionKey反向获取SocketChannel的channel
- 根据得到的channel完成业务处理
七、NIO非阻塞网络编程的快速入门
服务器端
public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//创建一个Selector对象,
Selector selector = Selector.open();
// 绑定端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 把serverSocketChannel注册到selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环等待用户连接
while (true){
if (selector.select(1000) == 0){ //等待(阻塞)一秒, 没有事件发生
// if (selector.selectNow() == 0){ // 也可以设置成非阻塞的
System.out.println("服务器等待了一秒,无连接");
continue;
}
// 如果返回的>0 , 就获取相关的selectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 返回关注事件的集合
// 遍历selectionKeys
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
// 获取到selectionKey
SelectionKey key = keyIterator.next();
//根据key对应的通道获取事件并做相应处理
if (key.isAcceptable()){
//如果是OP_ACCEPT, 表示有新的客户端产生
//给该客户端生成SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//将socketChannnel设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel注册到selector上, 设置事件为OP_READ,同时给socketChannel关联一个buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()){
// 发生了OP_READ
SocketChannel channel=(SocketChannel)key.channel();
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("from 客户端"+new String(buffer.array()));
}
// 手动从集合中移除当前的selectionKey, 防止多线程情况下的重复操作
keyIterator.remove();
}
}
}
}
客户端
public class NIOClient {
public static void main(String[] args) throws IOException {
// 得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞
socketChannel.configureBlocking(false);
//设置服务器端ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
if (!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
//如果没有连接成功,客户端是非阻塞的,可以做其它工作
System.out.println("等待连接...");
}
}
// 如果连接成功,就发送数据
String str = "hello world";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
// 发送数据 , 将buffer中的数据写入到channel中
socketChannel.write(buffer);
System.in.read();
}
}