系列文章目录
第一篇 Java NIO(一)I/O简介
第一篇 Java NIO(二)BIO
第一篇 Java NIO(三)NIO之Buffer
第一篇 Java NIO(四)NIO之Channel
第一篇 Java NIO(五)NIO之Selector
第一篇 Java NIO(六)Netty
Java NIO(三)NIO之Buffer
前言
上一篇文章中我们简单的聊了下Java的IO体系中的BIO,接下来我们来聊一下Java中的NIO,NIO是对BIO的一次大的革新,在有了底层IO模型——IO多路复用模型的支持,NIO在网络IO方面的表现异常卓越。NIO中核心部分有以下三个,分别是Buffer、Channel、Selector。本文将简单介绍下NIO中的Buffer部分。
注:每篇文章中都会有NIO一个Server端和客户端的简单例子,帮助大家从整体上对NIO有个把握
一、示例代码
客户端代码:
public class TestClient {
public void start(String host, int port) {
try {
for (int i = 0; i < 10; i++) {
// 连接阶段
Socket client = new Socket(host, port);
// 写数据阶段 (对服务器而言是读)
OutputStream os = client.getOutputStream();
String name = UUID.randomUUID().toString();
String msg = "hello server";
System.out.println("client " + name + " say: " + msg);
os.write(msg.getBytes());
// 读服务器传过来的数据 (对服务器而言是写)
byte[] byteArray = new byte[1024];
InputStream is = client.getInputStream();
int k = is.read(byteArray);
while (k <= 0) {
k = is.read(byteArray);
}
String content = new String(byteArray);
System.out.println("server response to client" + name + ": " + content);
is.close();
os.close();
client.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TestClient().start("localhost", 8801);
}
}
服务端代码:
public class NIOServer {
private int port = 8002;
// 处理器,负责管理所有的I/O事件
private Selector selector;
// 缓冲区 Buffer等待区
private final ByteBuffer buffer = ByteBuffer.allocate(1024);
public NIOServer(int port) {
this.port = port;
try {
// 服务器管道
ServerSocketChannel server = ServerSocketChannel.open();
// 处理器启动
selector = Selector.open();
// 服务器管道的处理模式设置为非阻塞
server.configureBlocking(false);
// 服务器管道注册到处理器上,并告知说:"我关心服务器管道的'接入'活动"
server.register(selector, SelectionKey.OP_ACCEPT);
// 管道告知接入地址
server.bind(new InetSocketAddress(this.port));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 开始监听
*/
public void listen() {
// 处理器上班,喇叭喊一声:"营业啦!!!"
System.out.println("Server Listening on " + port);
try {
while (true) {
// 四处观察,开始接待
selector.select();
// 拿到接待的所有客户(即所有的I/O事件)
Set<SelectionKey> keySet = selector.selectedKeys();
Iterator<SelectionKey> iter = keySet.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
// 开始处理客户(I/O事件)
process(key);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void process(SelectionKey key) throws Exception {
if (key.isAcceptable()) { // 这个客户需要接入
// 拿到这个客户要接入的服务器管道
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 服务器管道接入,底层调用native方法,获取到客人的客户端管道
SocketChannel socketChannel = server.accept();
// 客户端管道也设置为非阻塞
socketChannel.configureBlocking(false);
// (程序)将客户端管道也注册到处理器上,并说:"我关心该管道的'读'活动",相当于服务器准备好接受该客户端发送的数据
//(这里完全是程序自我定制,非必须步骤,可以该客户端只接受数据不发送数据,模拟客户端发送数据过程)
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { // 客人向服务器发送"需求",即发送数据
// 拿到客户端管道
SocketChannel socketChannel = (SocketChannel) key.channel();
// 将管道里面的数据写到缓存
int len = socketChannel.read(buffer);
if (len > 0) {
// 操作缓存
buffer.flip();
String content = new String(buffer.array(), 0, len);
// (程序)将客户端管道再次注册到处理器上,并说:"我关心这个通道的'写'活动"
//(这里完全是程序自我定制,非必须步骤,可以该客户端只发送数据不接受数据,模拟客户端写数据过程)
socketChannel.register(selector, SelectionKey.OP_WRITE);
System.out.println("Read Content: " + content);
}
} else if (key.isWritable()) {
// 拿到客户端管道
SocketChannel channel = (SocketChannel) key.channel();
// 服务器要发送的内容即response
String content = "Hello,client";
// 把数据写到管道里
channel.write(ByteBuffer.wrap((":::Response::: " + content).getBytes()));
// 整个场景模拟结束,关闭
channel.close();
}
}
public static void main(String[] args) {
new NIOServer(8801).listen();
}
}
二、Buffer
1. 介绍
Buffer,顾名思义就是缓冲区的意思,它是NIO中数据交换的载体,实质上是一种承载数据的容器。在上一篇BIO文章中我们提到BIO的工作模式是使用流来进行数据交换,并且根据操作的不同,分为输入流和输出流两种,而在NIO中则是以Buffer来进行数据交换的,例如后面将要提到的Channel中,都是通过Buffer来进行读写操作的,Buffer可以同时进行输入和输出操作。
2. Buffer分类
关于Buffer的类文件都封装在jdk的java.nio包中,Buffer类在jdk中是一个抽象类,它实现了Buffer的一些共性的设计,譬如属性部分的position、limit、capacity等,再譬如方法部分的clear()、flip()等,这些在后面讲解Buffer相关操作的时候还会详细说明。这里先做下简单的了解。特别说明,Buffer在多线程的场景下是不安全的,它的底层并未做同步方面的控制,因此,在有多线程并发的情景下,我们需要对其做好同步控制。
除此之外,jdk底层还为我们设计好了一些常用的buffer,这些实现类开箱即用,大大简化了我们的开发工作。主要有以下这些
- ByteBuffer(字节类型的缓冲器)
- IntBuffer(整型类型的缓冲器)
- CharBuffer(字符类型的缓冲器)
- FloatBuffer(浮点数型的缓冲器)
- LongBuffer(长整型的缓冲器)
- DirectByteBuffer(直接缓冲器)(后面讲解)
除了这些以外,jdk中还有许多其他的Buffer实现类,感兴趣的同学可以自己去jdk中跟一下源码,这里便不再赘述。
3. Buffer的基本原理
Buffer的基本原理在与Buffer类中定义的几个共性变量position、limit、capacity
private int position = 0;
private int limit;
private int capacity;
那么这几个属性代表什么含义呢?我们以ByteBuffer为例一起来学习下
final byte[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly;
ByteBuffer中定义了一个字节数组 hb,这个就是用于真正存储数据的容器。刚刚提到的三个属性都是基本该容器而存在的,通过和它们的协作,保证了数据的正确读写。
- position:代表下一个要操作的数据的索引(读或者写),它的值由get/put操作自动做更新。在新创建一个buffer的时候,position的值自动初始化为0
- limit:代表数据的终点的索引,它和position之间的数据代表了剩余在缓冲器中还需要被操作的数据
- capacity:代表缓冲区承载数据的能力
通过上图我们可以更直观的了解到这三个属性的含义。
4. Buffer的操作
在了解了Buffer的一些特性之后,我们来了解下Buffer的一些操作
- flip() 该操作会将limit设置到position的位置,然后将position设置为0,即读取数据前的准备操作
- get() 该操作从缓冲器中获取数据,position往后移动,limit保持不变
- clear() 该操作将状态变化的值全部设置为初始值
下面结合代码看一下
public class BufferUse {
public static void main(String[] args) throws Exception {
File file = new File("test1.txt");
FileInputStream fin = new FileInputStream(file);
FileChannel fc = fin.getChannel();
ByteBuffer bb = ByteBuffer.allocate(20);
output("初始化", bb);
fc.read(bb);
output("调用read()", bb);
bb.flip();
output("调用flip()", bb);
while (bb.remaining() > 0) {
byte b = bb.get();
System.out.println((char) b);
output("调用get()", bb);
}
bb.clear();
output("调用clear()", bb);
fin.close();
}
private static void output(String step, Buffer buffer) {
System.out.println(step + " ");
System.out.println("capacity: " + buffer.capacity());
System.out.println("position: " + buffer.position());
System.out.println("limit: " + buffer.limit());
System.out.println("-------------------------------------------");
}
}
文件中的内容为hello,输出结果如下:
初始化
capacity: 20
position: 0
limit: 20
-------------------------------------------
调用read()
capacity: 20
position: 6
limit: 20
-------------------------------------------
调用flip()
capacity: 20
position: 0
limit: 6
-------------------------------------------
h
调用get()
capacity: 20
position: 1
limit: 6
-------------------------------------------
e
调用get()
capacity: 20
position: 2
limit: 6
-------------------------------------------
l
调用get()
capacity: 20
position: 3
limit: 6
-------------------------------------------
l
调用get()
capacity: 20
position: 4
limit: 6
-------------------------------------------
o
调用get()
capacity: 20
position: 5
limit: 6
-------------------------------------------
调用get()
capacity: 20
position: 6
limit: 6
-------------------------------------------
调用clear()
capacity: 20
position: 0
limit: 20
-------------------------------------------
总结
本文主要对Buffer做了一个简单的介绍,至于一些细节性的实现,大家如果感兴趣的话可以自己去jdk的源码中去研究。甚至还有直接内存缓冲器(如DirectByteBuffer)、Buffer的切片等特性,感兴趣的话大家也可以去深入的了解一下。