BIO 编程
BIO 有的称之为 basic(基本) IO,有的称之为 block(阻塞) IO, 主要应用于文件 IO 和网络 IO,这里不再说文件 IO, 大家对此都非常熟悉, 本次课程主要讲解网络 IO。
在 JDK1.4 之前, 我们建立网络连接的时候只能采用 BIO, 需要先在服务端启动一个ServerSocket, 然后在客户端启动 Socket 来对服务端进行通信, 默认情况下服务端需要对每个请求建立一个线程等待请求, 而客户端发送请求后, 先咨询服务端是否有线程响应, 如果没有则会一直等待或者遭到拒绝, 如果有的话, 客户端线程会等待请求结束后才继续执行,这就是阻塞式 IO。
接下来通过一个例子复习回顾一下 BIO 的基本用法(基于 TCP) 。
//BIO 服务器端程序
public class TCPServer {
public static void main(String[] args) throws Exception {
//1.创建 ServerSocket 对象
ServerSocket ss=new ServerSocket(9999);
while (true) {
//2.监听客户端
Socket s = ss.accept(); //阻塞
//3.从连接中取出输入流来接收消息
InputStream is = s.getInputStream(); //阻塞
byte[] b = new byte[10];
is.read(b);
String clientIP = s.getInetAddress().getHostAddress();
System.out.println(clientIP + "说:" + new String(b).trim());
//4.从连接中取出输出流并回话
OutputStream os = s.getOutputStream();
os.write("没钱".getBytes());
//5.关闭
s.close();
}
}
}
上述代码编写了一个服务器端程序, 绑定端口号 9999, accept 方法用来监听客户端连接,如果没有客户端连接, 就一直等待, 程序会阻塞到这里。
//BIO 客户端程序
public class TCPClient {
public static void main(String[] args) throws Exception {
while (true) {
//1.创建 Socket 对象
Socket s = new Socket("127.0.0.1", 9999);
//2.从连接中取出输出流并发消息
OutputStream os = s.getOutputStream();
System.out.println("请输入:");
Scanner sc = new Scanner(System.in);
String msg = sc.nextLine();
os.write(msg.getBytes());
//3.从连接中取出输入流并接收回话
InputStream is = s.getInputStream(); //阻塞
byte[] b = new byte[20];
is.read(b);
System.out.println("老板说:" + new String(b).trim());
//4.关闭
s.close();
}
}
}
上
述代码编写了一个客户端程序, 通过 9999 端口连接服务器端, getInputStream 方法用来等待服务器端返回数据, 如果没有返回, 就一直等待, 程序会阻塞到这里。 运行效果如下图所示:
NIO 编程
概述
java.nio 全称 java non-blocking IO, 是指 JDK 提供的新 API。 从 JDK1.4 开始, Java 提供了一系列改进的输入/输出的新特性, 被统称为 NIO(即 New IO)。 新增了许多用于处理输入输出的类, 这些类都被放在 java.nio 包及子包下, 并且对原 java.io 包中的很多类进行改写, 新增了满足 NIO 的功能。
NIO 和 BIO 有着相同的目的和作用, 但是它们的实现方式完全不同, BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。 另外, NIO 是非阻塞式的,这一点跟 BIO 也很不相同, 使用它可以提供非阻塞式的高伸缩性网络。
NIO 主要有三大核心部分: Channel(通道), Buffer(缓冲区), Selector(选择器)。 传统的 BIO基于字节流和字符流进行操作, 而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作, 数据总是从通道读取到缓冲区中, 或者从缓冲区写入到通道中。 Selector(选择区)用于监听多个通道的事件(比如: 连接请求, 数据到达等) , 因此使用单个线程就可以监听多个客户端通道。
文件 IO
概述和核心 API
缓冲区(Buffer) : 实际上是一个容器, 是一个特殊的数组, 缓冲区对象内置了一些机制, 能够跟踪和记录缓冲区的状态变化情况。 Channel 提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer, 如下图所示:
在 NIO 中, Buffer 是一个顶层父类, 它是一个抽象类, 常用的 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 static ByteBuffer wrap(byte[] array); 把一个现成的数组放到缓冲区中使用
- public final Buffer flip(); 翻转缓冲区, 重置位置到初始位置
通道( Channel) : 类似于 BIO 中的 stream, 例如 FileInputStream 对象, 用来建立到目标( 文件, 网络套接字, 硬件设备等) 的一个连接, 但是需要注意: BIO 中的 stream 是单向的, 例如 FileInputStream 对象只能进行读取数据的操作, 而 NIO 中的通道(Channel)是双向的,既可以用来进行读操作, 也可以用来进行写操作。 常用的 Channel 类有: FileChannel、DatagramChannel、 ServerSocketChannel 和 SocketChannel。 FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写, ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
这里我们先讲解 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, WritableByteChannel target), 把数据从当前通道复制给目标通道
案例
接下来我们通过 NIO 实现几个案例, 分别演示一下本地文件的读、 写和复制操作, 并和 BIO 做个对比。
1. 往本地文件中写数据
@Test
public void test1() throws Exception{
String str="hello,nio,";
FileOutputStream fos=new FileOutputStream("basic.txt");
FileChannel fc=fos.getChannel();
ByteBuffer buffer=ByteBuffer.allocate(1024);
buffer.put(str.getBytes());
buffer.flip();
fc.write(buffer);
fos.close();
}
NIO 中的通道是从输出流对象里通过 getChannel 方法获取到的, 该通道是双向的, 既可以读, 又可以写。 在往通道里写数据之前, 必须通过 put 方法把数据存到 ByteBuffer 中, 然后通过通道的 write 方法写数据。 在 write 之前, 需要调用 flip 方法翻转缓冲区, 把内部重置到初始位置, 这样在接下来写数据时才能把所有数据写到通道里。 运行效果如下图所示:
2. 从本地文件中读数据
@Test
public void test2() throws Exception{
File file=new File("basic.txt");
FileInputStream fis=new FileInputStream(file);
FileChannel fc=fis.getChannel();
ByteBuffer buffer=ByteBuffer.allocate((int)file.length());
fc.read(buffer);
System.out.print(new String(buffer.array()));
fis.close();
}
上述代码从输入流中获得一个通道, 然后提供 ByteBuffer 缓冲区, 该缓冲区的初始容量和文件的大小一样, 最后通过通道的 read 方法把数据读取出来并存储到了 ByteBuffer 中。
运行效果如下图所示:
3. 复制文件
通过 BIO 复制一个视频文件, 代码如下所示:
@Test
public void test3() throws Exception{
FileInputStream fis=new FileInputStream("C:\\Users\\zdx\\Desktop\\oracle.mov");
FileOutputStream fos=new FileOutputStream("d:\\oracle.mov");
byte[] b=new byte[1024];
while (true) {
int res=fis.read(b);
if(res==-1){
break;
} f
os.write(b,0,res);
} f
is.close();
fos.close();
}
上述代码分别通过输入流和输出流实现了文件的复制, 这是通过传统的 BIO 实现的, 大家都比较熟悉, 不再多说。
通过 NIO 复制相同的视频文件, 代码如下所示:
@Test
public void test4() throws Exception{
FileInputStream fis=new FileInputStream("C:\\Users\\zdx\\Desktop\\oracle.mov");
FileOutputStream fos=new FileOutputStream("d:\\oracle.mov");
FileChannel sourceCh = fis.getChannel();
FileChannel destCh = fos.getChannel();
destCh.transferFrom(sourceCh, 0, sourceCh.size());
sourceCh.close();
destCh.close();
}
上述代码分别从两个流中得到两个通道, sourceCh 负责读数据, destCh 负责写数据, 然后直接调用 transferFrom 方法一步到位实现了文件复制。
网络 IO
概述和核心 API
前面在进行文件 IO 时用到的 FileChannel 并不支持非阻塞操作, 学习 NIO 主要就是进行网络 IO, Java NIO 中的网络通道是非阻塞 IO 的实现, 基于事件驱动, 非常适用于服务器需要维持大量连接, 但是数据交换量不大的情况, 例如一些即时通信的服务等等....
在 Java 中编写 Socket 服务器, 通常有以下几种模式:
- 一个客户端连接用一个线程, 优点: 程序编写简单; 缺点: 如果连接非常多, 分配的线程也会非常多, 服务器可能会因为资源耗尽而崩溃。
- 把每一个客户端连接交给一个拥有固定数量线程的连接池, 优点: 程序编写相对简单,可以处理大量的连接。 确定: 线程的开销非常大, 连接如果非常多, 排队现象会比较严重。
- 使用 Java 的 NIO, 用非阻塞的 IO 方式处理。 这种模式可以用一个线程, 处理大量的客户端连接。
1. Selector(选择器), 能够检测多个注册的通道上是否有事件发生, 如果有事件发生, 便获取事件然后针对每个事件进行相应的处理。 这样就可以只用一个单线程去管理多个通道, 也就是管理多个连接。 这样使得只有在连接真正有读写事件发生时, 才会调用函数来进行读写,就大大地减少了系统开销, 并且不必为每个连接都创建一个线程, 不用去维护多个线程, 并且避免了多线程之间的上下文切换导致的开销。
该类的常用方法如下所示:
- public static Selector open(), 得到一个选择器对象
- public int select(long timeout), 监控所有注册的通道, 当其中有 IO 操作可以进行时, 将对应的 SelectionKey 加入到内部集合中并返回, 参数用来设置超时时间
- public Set<SelectionKey> selectedKeys(), 从内部集合中得到所有的 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 int write(ByteBuffer src), 往通道里写数据
- public int read(ByteBuffer dst), 从通道里读数据
- public final SelectionKey register(Selector sel, int ops, Object att), 注册一个选择器并设置监听事件, 最后一个参数可以设置共享数据
- public final void close(), 关闭通道
入门案例
API 学习完毕后, 接下来我们使用 NIO 开发一个入门案例, 实现服务器端和客户端之间的数据通信( 非阻塞) 。
//网络服务器端程序
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){ //nio 非阻塞式的优势
System.out.println("Server:没有客户端搭理我, 我就干点别的事");
continue;
} /
/6.2 得到 SelectionKey,判断通道里的事件
Iterator<SelectionKey> keyIterator=selector.selectedKeys().iterator();
while(keyIterator.hasNext()){
SelectionKey key=keyIterator.next();
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.3 手动从集合中移除当前 key,防止重复处理
keyIterator.remove();
}
}
}
}
上面代码用 NIO 实现了一个服务器端程序, 能不断接受客户端连接并读取客户端发过来的数据。
//网络客户端程序
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();
}
}
上面代码通过 NIO 实现了一个客户端程序, 连接上服务器端后发送了一条数据, 运行效果如下图所示: