Buffer
一个容器,用来存储需要传递的数据。 常见分类如下:
Buffer创建
Buffer分为两种,直接缓冲区与非直接缓冲区:
非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
Buffer的使用
三个常用的属性:
属性 | 说明 |
---|---|
容量(Capacity) | 缓冲区能够容纳的数据元素的最大数量,缓冲区创建时被设定,永远不能被改变 |
上界(Limit) | 缓冲区第一个不能被读或写的元素,或者说缓冲区中现存元素的计数 |
位置(Position) | 下一个要被读或写的元素的索引,位置会自动由相应的get()和put()方法更新 |
用法实验
put方法:将数据存储到容器里。
public static void main(String[] args) {
// 创建一个ByteBuffer,容量为10
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
// 看一下初始时4个核心变量的值
System.out.println("初始时-->limit--->" + byteBuffer.limit());
System.out.println("初始时-->position--->" + byteBuffer.position());
System.out.println("初始时-->capacity--->" + byteBuffer.capacity());
System.out.println("--------------------------------------");
// 添加一些数据到缓冲区中
String s = "bobo";
byteBuffer.put(s.getBytes());
// 看一下初始时4个核心变量的值
System.out.println("put完之后-->limit--->" + byteBuffer.limit());
System.out.println("put完之后-->position--->" + byteBuffer.position());
System.out.println("put完之后-->capacity--->" + byteBuffer.capacity());
}
初始时-->limit--->10
初始时-->position--->0
初始时-->capacity--->10
--------------------------------------
put完之后-->limit--->10
put完之后-->position--->4
put完之后-->capacity--->10
flip方法:可以将Buffer切换为只读模式,那么我们可以读position 0 到limit长度的数据。如下:
byteBuffer.flip();
System.out.println("flip完之后-->limit--->" + byteBuffer.limit());
System.out.println("flip完之后-->position--->" + byteBuffer.position());
System.out.println("flip完之后-->capacity--->" + byteBuffer.capacity());
flip完之后-->limit--->4
flip完之后-->position--->0
flip完之后-->capacity--->10
get方法:借助position读取数据。
// 一个字节一个字节的读取
System.out.println((char)byteBuffer.get());
System.out.println((char)byteBuffer.get());
System.out.println((char)byteBuffer.get());
System.out.println((char)byteBuffer.get());
System.out.println("get完之后-->mark--->" + byteBuffer.mark());
// get方法 读取了多少个字节,position就会移动多少位,相应再次重新读取需要flip
byteBuffer.flip();
byte[] b = new byte[byteBuffer.limit()];
// 批量读取数据
byteBuffer.get(b);
System.out.println(new String(b,0,b.length));
b
o
b
o
bobo
rewind方法:当调用get()读完一遍数据后,我们还想再读取一遍,此时可以考虑rewind方法恢复到上一次状态。
clear方法:数据操作完成后我们还想要继续写入数据,这时我们可以使用clear方法来’清空’缓冲区。数据没有真正被清空,只是被遗忘掉了
mark方法:标记position的位置,在往后读取的过程中,如果想从该标记的位置读取,则需要再调用reset()即可。
hasRemaining方法:检查position和limit之间是否还有元素。判断是否还有剩余元素,与remaining读取剩余个数相互配合。
总结:
Channel
一个通道,用于将容器中的数据从一个地方放到另一地方。
Channel的发展史
关于DMA的介绍:https://baike.baidu.com/item/DMA/2385376?fr=aladdin
可以单纯的理解,channel的出现是为了减轻CPU的负载。可以把channel理解为处理器,与GPU的出现意图差不多。
Channel的分类
java.nio.channels.Channel 接口
|-- FileChanel
|-- SelectableChannel
|-- SocketChanel
|-- ServerSockerChanel
|-- DatagramChanel
Channel的用法
channel如何获取
1:Java 针对支持通道的类提供了 getChannel() 方法
本地IO:
FileInputStream / FileOutputStream
RandomAccessFile
网络IO:
Socket
ServerSocket
DatagramSocket
2:在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
3:在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
channel如何操作buffer
可以想象buffer是车,channel是铁路,那么将数据装满buffer后,就可以放到channel上。
// 不等于-1就说明读取到数据,读取到数据后buff要切换到读取模式才能获取数据
sChannel.read(buff)
buff.flip();
buff.get();
// 写数据可以通过下面这种
sChannel.write(buff);
方法2:使用直接缓冲区完成文件的复制,直接缓冲区
@Test
public void testUseChannelToCopyFile2() throws Exception {
FileChannel inChannel = FileChannel.open(
Paths.get("/Users/gxc/tmp/sl.mp4"),
StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(
Paths.get("/Users/gxc/tmp/sl3.mp4"),
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE_NEW);
// 内存映射文件
MappedByteBuffer inMappedBuf = inChannel.map(
FileChannel.MapMode.READ_ONLY, 0,
inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(
FileChannel.MapMode.READ_WRITE, 0,
inChannel.size());
// 直接对缓冲区 进行数据的读写操作
byte[] dst = new byte[inMappedBuf.limit()];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
inChannel.close();
outChannel.close();
}
Selector
Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。
传统的Socket编程需要服务端不断的阻塞等待,极其浪费资源,NIO是对通讯服务区的一种改善,示意图如下:
传统的阻塞代码:
@Test
public void client() throws Exception{
//创建通道
FileChannel open = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.READ);
//分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//创建socket通道
SocketChannel open2 = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9988));
//读取本地文件,并发送到服务端
while(open.read(buffer)!=-1){
buffer.flip();
open2.write(buffer);
buffer.clear();
}
//强制告诉服务端,已发送完数据,否则无法读取服务端的返回数据
open2.shutdownOutput();
//获取服务端反馈
int len=0;
while((len=open2.read(buffer))!=-1){
buffer.flip();
System.out.println(new String (buffer.array(),0,len));
buffer.clear();
}
open.close();
open2.close();
};
@Test
public void server() throws IOException{
// 获取通道
FileChannel open = FileChannel.open(Paths.get("11.jpg"), StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
ServerSocketChannel open2 = ServerSocketChannel.open();
//分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//绑定连接
open2.bind(new InetSocketAddress(9988));
//获取客户端连接的通道
SocketChannel accept = open2.accept();
//接收客户端的数据,并保存到本地
while(accept.read(byteBuffer)!=-1){
byteBuffer.flip();
open.write(byteBuffer);
byteBuffer.clear();
}
//发送反馈给客户端
byteBuffer.put("图片收到了".getBytes());
byteBuffer.flip();
accept.write(byteBuffer);
accept.close();
open.close();
open2.close();
}
非阻塞式 socket 通信
@Test
public void client() throws Exception{
//1. 获取通道
SocketChannel clientSoc=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9988));
//2. 切换非阻塞模式
clientSoc.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer allocate = ByteBuffer.allocate(1024);
//4. 获取用户输入数据,发送数据给服务端
Scanner sc=new Scanner(System.in);
while(sc.hasNext()){
String next = sc.next();
allocate.put((LocalDate.now()+"--客户端说"+"\n"+next).getBytes());
allocate.flip();
clientSoc.write(allocate);
allocate.clear();
}
sc.close();
//5. 关闭通道
clientSoc.close();
}
@Test
public void Server() throws Exception{
//1. 获取通道,切换非阻塞模式
ServerSocketChannel serSoc=ServerSocketChannel.open();
serSoc.configureBlocking(false);
serSoc.bind(new InetSocketAddress(9988));
//2. 获取选择器
Selector selector = Selector.open();
//3. 将通道注册到选择器上, 并且指定“监听接收事件”
serSoc.register(selector, SelectionKey.OP_ACCEPT);
//4. 轮询式的获取选择器上已经“准备就绪”的事件
while(selector.select()>0){
//5. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey sk = iterator.next();
//6. 判断具体是什么事件准备就绪
if(sk.isAcceptable()){
//7. 若“接收就绪”,获取客户端连接,切换非阻塞模式,并将该通道注册到选择器上
SocketChannel sChannel = serSoc.accept();
sChannel.configureBlocking(false);
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable()){
//8. 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//9. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = sChannel.read(buf)) > 0 ){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
}
//10. 每次处理完一个选择键事件,都要取消选择键 SelectionKey
iterator.remove();
}
}
非阻塞式的操作思路:
- 通过调用 sChannel.configureBlocking(false); 改阻塞为非阻塞,后面每个channel 都需要。
- 所要使用cannel注册到selector,由selector来调度:sChannel.register(selector, int ops);
- 对于ops,可以理解为一个个的事件,常见如下,如果要注册的不止一个事件,可以使用 | 操作符连接:
读 : SelectionKey.OP_READ (1) 写 : SelectionKey.OP_WRITE (4) 连接 : SelectionKey.OP_CONNECT (8) 接收 : SelectionKey.OP_ACCEPT (16)