文章目录
- 1. 简介
- 2. 一个示例服务器
- 3. 缓冲区
- 4. 创建缓冲区
- 5. 填充与排空
- 6. 批量方法
- 7. 数据转换
- 8. 视图缓冲区
- 9. 压缩缓冲区
- 10. 复制缓冲区
- 11. 分片缓冲区
1. 简介
与CPU和内存相比,甚至和磁盘相比,网络都很慢,但要允许CPU速度高于网络,传统的Java解决方法是缓冲和多线程。多线程可以同时为几个不同的连接生成数据,并将数据存储在缓冲区内,直到网络可以确实准备好发送这些数据。这种方法的效果虽然好,但是频繁的线程切换以会造成很大的开销,而且想要正常工作,这种方法需要得到底层操作系统的支持。幸运的是,可能作为大吞吐量服务器的所有现代操作系统几乎都支持这种非阻塞I/O(它不会像阻塞I/O一样,当资源不可用时就一直阻塞等待资源可用,如果资源不可用非阻塞I/O会立即离开),Java的NIO(New Input/Output)是Java提供的一种基于通道(Channel)和缓冲区(Buffer)的I/O操作方式,相对于传统的基于流(Stream)的I/O操作,它具有更高的效率和灵活性。
在介绍基础知识之前,将为RFC 864中定义的字符生成器协议实现一个客户端,这个协议是为测试客户端设置,服务器在端口19监听连接,当客户端连接时,服务器将发送连续的字符序列,知道客户端端口连接为止。客户端的所有输入都会被忽略。
在实现利用这个新I/O API客户端时,首先要调用静态工厂方法
SocketChannel.open()
来创建一个新的java.nio.channels.SokcetChannel
对象。这个方法的参数是一个jave.net.SocketAddress
对象,指示要连接的主机和端口。
SocketAddress rama=new InetSocketAddress("rama.poly.edu",19);
SocketChannel client=SocketChannel.open(rama);
通道以阻塞模式打开,所以下一行代码在真正建立连接之前不会执行。如果连接无法建立就会发抛出一个IOException异常。如果这是传统的客户端,你可能会获取Socket的输入和(或)输出流。但这不是传统的客户端。利用通道 ,你可以直接写入通道本身。不是写入字节数组,而时要写入ByteBuffer对象。所以使用静态方法
allocate()
创建一个容量为74字节的ByteBuffer(因为输入的字符数为72,还有一个回车和换行):
ByteBuffer buffer=ByteBuffer.allocate(74);
将这个ByteBffer对象传递给通道的read()方法,通道就会从Socket读取的数据填充这个缓存区,它返回成功读取并存储在缓冲区的字节数:
int bytesRead=client.read(buffer);
默认情况下,会至少读取一个字节,或者返回-1表示数据结束,这与InputStream完全一样。如果没有字节可用时会立即返回0。假定缓冲区中有一些数据,这些数据就被复制到System.out。有几个方法可以从ByteBuffer中提取一个字节数组,然后再写入传统的OutputStream(如System.out)。不过坚持采用一种完全基于通道的解决方案会更有好处,这样的解决方案需要利用Channels工具类(确切地将是该工具类的newChannel()方法),将OutputStream System.out封装在一个通道中:
WritableByteChannel output=Channels.newChannel(System.out);
然后可以将读取的数据写入System.out连接的这个输出通道中。不过,在这样做之前,必须回绕(flip)缓冲区,使得输出通道会从所读取的数据的开头而不是末尾开始写入:
buffer.flip();
output.write(buffer);
你不必告诉输出通道要写入多少字节,缓冲区会记住其中包含多少字节。不过,一般情况下,输出通道不能保证写入缓冲区中的所有字节。不过,在这个特定的例子中,它是阻塞通道,要么写入全部字节,要么抛出一个IOException。不要每次读/写都创建一个缓冲区,那么会降低性能。相反,要重用现在的缓冲区,在再次读取之前要清空缓中区:
//这和回绕不同,回绕保持缓冲区的数据不变,只是准备写入而不是读取,清空则把缓冲区重置为初始状态
buffer.clear()
下面是完整的代码:
public static void main(String[] args) {
try{
SocketAddress address=new InetSocketAddress("rama.poly.edu",19);
SocketChannel client=SocketChannel.open(address);
ByteBuffer buffer=ByteBuffer.allocate(74);
WritableByteChannel out= Channels.newChannel(System.out);
while(client.read(buffer)!=-1){
buffer.flip();
out.write(buffer);
buffer.clear();
}
}catch (IOException ex){
ex.printStackTrace();
}
}
到目前为止,这个程序还很简单,完全可以很容易地用流来编写。 只有当你希望客户端有更多的功能时,即除了将所有的输入复制到输出之外还要作一些其他工作,才会真正的出现新的特性。你可以在阻塞模式下运行这个连接,在非阻塞模式下,即使没有任何可用的数据,read()也会立即返回。这就允许程序在试图读取前做其它操作。它不必等待慢速的网络连接。要改变阻塞模式,可以向configureBlocking()方法传入truie(阻塞)或false(不阻塞)。
client.configureBlocking(false);
2. 一个示例服务器
客户端使用通道和缓冲区是可以的,不过实际上通道和缓冲区主要用于需要高效处理很多并发连接的服务器系统。要处理服务器,除了用于客户端的缓冲区和通道外,还需要新的第3个部分,具体来讲,需要有一些选择器,运行服务器查找所有准备好接收输出或发送输入的连接。这里将实现前面的字符生成服务器。在实现利用新I/O API的服务器是,首先要调用静态工厂方法ServerSocketChannel.open()创建一个新的ServerSocketChannel对象
ServerSocketChannel serverChannel=ServerSocketChannel.open()
开始,这个通道并没有具体监听任何端口,要把它绑定到一个端口了,可以用Socket()方法获取其ServerSocket对等端对象,然后使用bind()方法绑定到对等端。下面代码将通道绑定到端口19的服务器Socket
ServerSocket ss=serverChannel.socket();
ss.bind(new InetSocketAddress(19);
在java7级以后的绑定中 ,可以直接绑定而不用获得底层java.net.ServerSocket
serverChannel.bind(new InetSocketAddress(19));
服务器Socket通道现在在端口19监听入站连接。要接受连接,可以调用accept()方法,它会返回一个SocketChannel对:
SocketChannel clientChannel= serverChannel.accept();
在服务器端,你肯定希望客户端出于非阻塞模式,以允许服务器处理多个并发连接:
clientChannel.configureBlocking(false);
你可能还希望ServerSocketChannel也处于非阻塞模式,默认情况下,这个accept方法阻塞,直到有一个入站连接为止,这与ServerSocket的accept()方法类似。为了改变这一点,只需要在调用accept()之前调用configureBlocking(false)
serverChannel.configureBloking(false);
如果没有入站连接,非阻塞的accept()几乎会立即返回null,要确保对此进行检查,否则当试图使用这个socket(而它实际是null)时,会得到一个讨厌的NullPointerException。现在有两个打开的通道:服务器通道和客户端通道,两个通道都需要处理,它们都无限运行下去。此外,处理服务器通道会创建更多打开的客户端通道。在传统的方法中,要为每个连接分配一个线程,线程数目会随着客户端连接迅速攀升。相反在非阻塞I/O中,可以创建一个Selector,允许程序迭代处理所有准备好的线程,要构造一个新的Selector,只需要调用Selector.open()
Selector selector=Selector.open()
接下来,需要使用每个通道register ()方法向监视这个通道的选择器进行组成。在注册时,要使用SelectionKey类提供的命名常量指定所关注的操作。对于服务器Socket,唯一关心的操作就是OP_ACCEPT,也就是服务器Socket通道是否准备好接受一个新连接?
serverChannel.register(selector,SelectionKey.OP_ACCEPT);
对于客户端通道,你要知道的稍微又些不同,确切的讲,你可能希望知道是否已经准备好数据可以写入通道。为此,要使用OP_WRITE键:
SelectionKey key=client.register(selector,SelectionKey.OP_WRITE);
连个register方法都返回一个SelectionKey对象,不过只需要使用对应客户端通道的键意味可能会有多个这样的键。每个SelectionKey都有一个任意的Object类型的“附件”。它通常用于保存一个指示当前连接状态的对象。在这里,可以讲通道要写入网络的缓存区存储在这个对象中。一旦缓冲区完全排空,将重新填满,要用将复制到各缓冲区的数据来填充数组。并不是写到缓冲区的末尾,而是要回转到缓冲区开始位置重新写入。 为了检查是否有可操作的数据,可以调用Select()方法,对于长时间运行的服务器,这一般要放在一个无限循环里:
键(Key)通常表示一个通道(Channel)与选择器(Selector)之间的注册关系
whilie(true){
selector.select()
}
假定选择器确实找到一个就绪的通道吗,其selectedKeys()方法回返回一个java.util.Set,其中对应各个就绪通道分别保护一个SelecitonKey对象。否则它会返回一个空集。在这两种情况下,都可以通过一个Java.util.Iterator循环处理
Set<SelectionKey> readyKeys=selector.selectedKeys();
Iterator iterator=readyKeys.iterator()
while(iterator.hasNext()){
SelectionKey key=iterator.next();
//从集合删除这个键,从而不会处理两次
iterator.remove();
//处理通道
通过从集合中删除键,这就告诉选择器这个键已经处理过了,这样Selector就不需要在每次调用Select()时将这个键返回给我们。再次调用select()时,如果这个通道再次就绪。Selector就会把该通道再增加到集合中。不过,在这里删除就绪集合中的键确实很重要。如果就绪通道时服务器通道,程序就会接受一个新的Socket通道,将其添加到选择器。如果就绪通道是Socket通道,程序就会向通道写入缓存区中尽可能多的数据。如果没有通道就绪,选择器就会等待,一个线程可以同时处理多个连接。在这里很容易判断所选择的通道是客户端通道还是服务器通道,因为服务器通道只接受准备,而客户端通道只准备写入。二者都是I/O操作,由于多种原因,它们都可能抛出IOException异常,所以需要将他们都包围在try块中。
try{
if(key.isAcceptable){
ServerSocketChannel server=(ServerSocketChannel)key.channel();
SocketChannel connection=server.accpet();
connection.configureBlocking(false);
connection.register(selector,SelectionKey.OP_WRITE);
//为客户端建立缓存区
}else if(key.isWritable()){
SocketChannel client=(SocketChannel)key.channel();
//向客户端写入数据
}
}
向通道吸入数据很简单,首先获取键的附件,将它转换为ByteBuffer,调用hasRemaining()检查缓冲区中是否还有剩余没写的数据。如果有,就写入通道。否则用rotation数组(我们要发送的数据的字节数组)中的下一行数据重新填充缓存区,并写入通道。
//从键获取一个附件并将其转换为 ByteBuffer。在 Java NIO 中,键(Key)通常表示一个通道(Channel)与选择器(Selector)之间的注册关系。附件是一种可选的、与键相关联的对象,用于保存某种状态或其他有用的信息
ByteBuffer buffer=(ByteBuffer)key.attachment();
if(!buffer.hasRemaining()){
//用下一行数据重新填充缓存区
//确定下一行从哪里开始
//buffer.rewind(); 是一个在 Java NIO(New Input/Output)库中使用的方法,具体是用于重设缓冲区(buffer)的位置(position)到缓冲区的开始。在处理缓冲区时,你可以使用rewind()方法来将缓冲区的position设为0,这样你就可以重新读取或写入缓冲区的数据了。它不会改变缓冲区的数据,仅仅只是将读/写的位置设置回了开始。
buffer.rewind();
//从缓冲区读取一个数据字节
int first=buffer.get()
//递增到下一个字节
buffer.rewind()
int position=first- '' +1;
buffer.put(rotation,postion,72);
buffer.put((byte)'\r');
buffer.put((byte) '\n');
buffer.flip();
)
client.write(buffer);
要确定从哪里获取下一行数据,这个算法依赖于ASCII字符顺序存储在rotation数组中的字符,buffer.get()从缓冲区中读取第一个数据字节,这个数字要减去空格字符(32),因为空格是rotatino数组中的第一个字符,由此可以知道缓冲区当前从数组的那个所索引开始,要加1来得到下一行索引开始索引,并重新填充缓冲区。
下面是字符服务器的完整代码
public static void main(String[] args) {
//定义服务器的监听端口
int port=8080;
System.out.println("Listening for connections on port "+port);
//定义rotation数组,也就是我们要发送的数据
byte[] rotation=new byte[95*2];
//每一行由一个空格开始,`~`结束,要理解这个for循环就需要理解ASCII码的特点
for(byte i= ' ';i <= '~' ;i++){
rotation[i-' ']=i; //第一个字符是空格
//这里加上 95 是因为从空格到波浪线一共有 95 个字符,所以这行代码实际上是将相同的字符集再次存储到 rotation 数组的后半部分,使得数组中包含了两个完整的字符集
rotation[i+95-' ']=i;
}
//定义服务器管道
ServerSocketChannel serverSocketChannel;
//定义选择器
Selector selector = null;
try {
serverSocketChannel=ServerSocketChannel.open();
//获取Socket对象
ServerSocket ss=serverSocketChannel.socket();
InetSocketAddress address=new InetSocketAddress(port);
ss.bind(address);
//开启非阻塞模式
serverSocketChannel.configureBlocking(false);
selector=Selector.open();
//将管道绑定到选择器
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
} catch (IOException e) {
logger.severe("错误:"+e);
}
//循环等待判断是否有管道准备就绪
while (true){
try {
//这个方法让选择器去检查已经注册的通道是否有任何通道已经准备好进行读或写操作。这个方法会阻塞,直到至少有一个通道准备好,或者其他线程调用了此选择器的
// wakeup() 方法
selector.select();
} catch (IOException e) {
logger.severe("错误:"+e);
break;
}
}
Set<SelectionKey> readKeys=selector.selectedKeys();
Iterator<SelectionKey> iterator=readKeys.iterator();
while(iterator.hasNext()){
//获取选择键
SelectionKey key=iterator.next();
logger.log(Level.INFO,"key:"+key.toString());
iterator.remove();
try {
if(key.isAcceptable()){
ServerSocketChannel serverSocketChannel1=(ServerSocketChannel) key.channel();
SocketChannel client=serverSocketChannel1.accept();
logger.log(Level.INFO,"Accepted connection from "+client);
client.configureBlocking(false);
SelectionKey key2=client.register(selector,SelectionKey.OP_WRITE);
ByteBuffer buffer= ByteBuffer.allocate(74);
buffer.put(rotation,0,72);
buffer.put((byte) '\r');
buffer.put((byte) '\n');
buffer.flip();
key2.attach(buffer);
}else if(key.isWritable()){
SocketChannel client=(SocketChannel) key.channel();
ByteBuffer buffer=(ByteBuffer) key.attachment();
if(!buffer.hasRemaining()){
buffer.rewind();
//从缓冲区读取一个数据字节
int first=buffer.get();
buffer.rewind();
int position=first-' ' +1;
buffer.put(rotation,position,72);
buffer.put((byte)'\r');
buffer.put((byte) '\n');
buffer.flip();
}
.write(buffer);
}
} catch (IOException e) {
key.channel();
try {
key.channel().close();
}catch (IOException ex){
}
}
}
}
}
这个例子只使用了一个线程。还有一些情况下仍要使用多个线程,特别是当不同操作有不同优先级时。例如,可能要在一个高优先级线程中接受新连接,而在一个低优先级的线程中对现有的连接提供服务。不过,线程和连接之间不再需要1:1的比例,这样可以极大地提升用Java编写服务器的可扩展性。
3. 缓冲区
缓冲区时NIO的基础部分,在NIO中不再向输入流写入数据和从输出流读取数据,而是要从缓冲区中读写数据。像在缓冲流中一样,缓冲区可能是字节数组,不过,原始实现可以将缓冲区直接与硬件或内存连接,或者使用其他非常高效的实现。从编程角度来说,流和通道之间的关键区别在于流是基于字节的,而通道是基于块的。流设计为按顺序一个字节一个字节的发送,通道会传送缓冲区中的数据。可以读写通道的字节之前,这些字节必须已经存储在缓冲区中了,并且一次会读/写一个缓冲区的数据。除了数据以外,每个缓冲区都记录了信息的4个关键部分,无论缓冲区还是何种类型,都有相同的方法来获取和设置这些值:
- 位置
缓冲区中将读取或写入的下一个位置。这个位置值从0开始,最大值为缓冲区的大小,可以使用下面两个方法来获取和设置:
public final int position()
public final Buffer position(int newPosition)
- 容量
缓冲区可以保存的元素的最大数量。容量值通常是在创建缓冲区的时候设置,以后不能改变。可以用以下方法获取:
public final int capacity()
- 限度
缓冲区中可访问数据的末尾位置。只要不改变限度,就无法读/写超过这个位置的数据,即使缓冲区有更大的容量也没用。限度可以用下面两种方法获取和设置
public final int limit()
public final Buffer limit(int newLimit)
- 标记
缓冲区中客户端指定的索引。通过调用mark()可以将标记设置为当前位置,调用reset()方法可以将当前位置设置为标记的位置,如果将位置设为低于现有标记,则丢弃这个标记
public final Buffer mark()
public final Buffer reset()
与读取InputStream不同,读取缓冲区实际上不会以任何方式改变缓冲区中的数据,只可能向前或向后设置位置,从而可以从缓冲区中某个特定位置开始读取,类似的,程序可以调整限度,从而控制将要读取的数据的末尾,只有容量是固定的。公共的Buffer超类还提供了几个方法,可以通过这些公共属性的引用来进行操作
//clear()方法将位置设置为0,并将限度设置为容量,从而将缓冲区清空,这样就可以完全重新填充缓存区了
//注意:clear方法并没有删除缓冲区的老数据,这些数据仍然存在,还可以使用get方法或改变限度和位置来获取
public final Buffer clear()
//将位置设置为0,但不改变限度,这允许重新读取缓冲区
public final Buffer rewind()
//flip方法将限度设置为当前位置,位置设置为0,希望排空刚刚填充的缓冲区时可以调用这个方法
public final Buffer flip()
//返回缓冲区当前位置与限度之间的元素数
public final int remaining()
//返回当前位置和限度之间是否还有元素
public final boolean hasRemaining()
4. 创建缓冲区
缓冲区类的层次时基于继承的,而不基于多态(指在设计缓冲区类的层次结构时,更多的是通过继承来实现的。也就是说,每个子类都会继承父类的属性和方法,并可能在此基础上添加新的功能,但并不会对父类的方法进行大规模的重写,也就是说,多态在这个层次结构中并不常用)。一般需要知道你要处理的事IntBuffer、ByteBuffer、CharBuffer还是其他类型。要用其中一个子类编写代码,而不是一般的Buffer超类。每种类型的缓冲区都有几个工厂方法,以各种方式创建这个特定于实现的子类。空的缓冲区一般由分配(allocate)方法创建。预填充数据的缓冲区由包装方法创建。分配方法通常用于输入,而包装方法一般用于输出。
- 分配
基本的allocate()方法返回一个指定固定容量的缓冲区,这是一个空缓冲区。用该方法创建的缓冲区基于Java数组实现,可以通过array()和arrayOffset()方法来访问。
array(): 这个方法返回 ByteBuffer 对象的 “backing array”,即用于存储数据的 byte 数组。需要注意的是,不是所有的 ByteBuffer 对象都有一个可访问的 “backing array”,只有那些用数组作为存储基础的 ByteBuffer 对象才有。对于那些没有 “backing array” 的 ByteBuffer 对象,如果你调用 array() 方法,会抛出 UnsupportedOperationException。
arrayOffset(): 这个方法返回你的 ByteBuffer 对象的第一个元素在 “backing array” 中的索引。这对于处理子缓冲区(即从其他 ByteBuffer 对象派生出来的 ByteBuffer 对象)特别有用,因为这些子缓冲区的第一个元素可能并不是 “backing array” 的第一个元素。和 array() 方法一样,如果你的 ByteBuffer 对象没有一个可访问的 “backing array”,调用 arrayOffset() 会抛出 UnsupportedOperationException。
- 直接分配
ByteBuffer还有一个allocateDirect方法,这个方法不为缓冲区创建后备数组"backing array"(所以不能调用array()和arrayOffset()),VM会对以太网卡、核心内存或其它位置的缓冲区直接使用直接内存访问(即存放在计算内存,而不是vm内存),一次实现直接分配ByteBuffer,它的使用方法和allocate一样。直接缓冲在一些虚拟机上会很快,尤其是缓冲区很大时。不过创建直接缓存的代价比缓存高,所以只能在缓冲区可能只持续较短时间时才分配这种直接缓冲区,大多数情况下不建议用。
- 包装
如果已经有要输出的数据数组,一般要用缓冲区进行包装,而不是分配一个新缓冲区,然后一次一部分地复制到这个缓冲区。
byte[] data ="Some data".getBytes("UTF-8");
ByteBuffer buffer1=ByteBuffer.wrap(data);
char[] text="Some. data".toCharArray();
CharBuffer buffer2=CharBuffer.wrap(data);
5. 填充与排空
缓冲区是为了顺序访问而设计的,put方法会向缓冲区中放置元素,缓冲区最多填充到其容量大小,且每次put,缓冲区的position游标就会向后移动一位,如果此时调用get方法获取会返回null。如果试图读取数据,需要调用flip方法回绕缓冲区。Buffer类还有一些绝对方法,可以在缓冲区指定的位置进行排空和填充,而无需更新position,例如ByteBuffer有以下两个方法:
public abstract byte get(int index);
public abstract ByteBuffer put (int index,byte b)
这样我们可以使用更新postion来对数据进行读或写了
6. 批量方法
即使是缓冲区,操作数据块通常比一次填充和排空一个元素要快,不同缓冲区类都有一些批量方法来填充和排空相应元素类型的数组。例如BufferByte
//get的数据放到dst中
public ByteBuffer get(byte[] dst,int offset,int length)
public ByteBuffer get(byte[] dst)
//将array的数据put到缓冲区中
public ByteBuffer put(byte[] array,int offset, int length)
public ByteBuffer put(byte[] array)
7. 数据转换
Java中的所有数据最终都解析为字节,所有的基本数据类型-int、double、float等都可以写为字节。ByteBuffer类(只有ByteBuffer类)提供了相对和绝对的put方法,可以用简单类型参数的相应字节填充缓存区,ByteBuffer类还提供了相对和绝对的get方法,可以读取适当数量的字节来形成一个新的基本类型数据:
public abstract char getChar()
public abstract ByteBuffer putChar(char value)
public abstract char getChar(int index)
public abstract ByteBuffer putChar(int index,char value)
public abstract short getShort()
public abstract ByteBuffer putShort(short value)
public abstract short getShort(int index)
public abstract ByteBuffer putShort(int index,short value)
public abstract int getIntt()
public abstract ByteBuffer putInt(int value)
public abstract int getInt(int index)
public abstract ByteBuffer putInt(int index,int value)
public abstract long getLong()
public abstract ByteBuffer putLong(long value)
public abstract long getLong(int index)
public abstract ByteBuffer putLong(int index,long value)
public abstract float getFloat()
public abstract ByteBuffer putFloat(long value)
public abstract float getFloat(int index)
public abstract ByteBuffer putFloat(int index,float value)
public abstract double getDouble()
public abstract ByteBuffer putDouble(double value)
public abstract double getDouble(int index)
public abstract ByteBuffer putDouble(int index,double value)
我们可以选择将字节序列解释为big-endain(大端方式)和little-endain(小端方式)。默认情况下都是大打端方式读写的,即最高字节在前面。ByteBuffer提供了两个order方法,可以使用ByteOrder类命名常量来检查和设置缓冲区的字节顺序。例如可以将缓冲区改为小端模式:
if(buffer.order().equals(ByteOrder.BIG_ENDIAN)){
buffer.order(ByteOrder.LITTLT_ENDIAN)
}
前面写的字符协议有一些问题,如有些老的网关配置会去除每个字节的高位。我们可以通过发送每一个可能的int来测试网络是否存在这些问题。在大约43亿次迭代后,就能测试完所有可能的4字节序列,在接收端,可以通过简单的数值比较,很容易地测试出接受的是否为期望的数据,如果找到任何问题,就很容易指出问题究竟出现在哪里,换句话说,这个协议的行为如下:
- 客户端连接服务器
- 服务器立即开始发送4字节的大端整数,从0开始每次增加1,服务器最后会回绕到负数
- 服务器无限运行,客户端在得到足够多的信息后关闭连接
下面代码实现上面功能:
public class QuizCardBuilder {
public static void main(String[] args) throws InterruptedException {
server server1=new server();
Thread mythread1=new Thread(server1);
//启动服务器
mythread1.start();
Thread.sleep(3000);
client client1=new client();
Thread mythread2=new Thread(client1);
mythread2.start();
}
}
class server implements Runnable {
@Override
public void run() {
System.out.println("服务器开始运行:");
//定义服务器的监听端口
int port = 8080;
//定义通道
ServerSocketChannel serverSocketChannel;
Selector selector;
try {
serverSocketChannel = ServerSocketChannel.open();
ServerSocket ss = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address);
serverSocketChannel.configureBlocking(false);
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException ex) {
ex.printStackTrace();
return;
}
while (true) {
try {
selector.select();
} catch (IOException e) {
throw new RuntimeException(e);
}
Set<SelectionKey> readKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = readKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();
SocketChannel client = serverSocketChannel1.accept();
System.out.println("Accepted connection from " + client);
client.configureBlocking(false);
SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);
ByteBuffer outpur = ByteBuffer.allocate(4);
outpur.putInt(0);
outpur.flip();
key2.attach(outpur);
} else if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
if (!output.hasRemaining()) {
output.rewind();
int value = output.getInt();
output.clear();
output.putInt(value + 1);
output.flip();
}
client.write(output);
}
} catch (IOException ex) {
key.channel();
try {
key.channel().close();
} catch (IOException e) {
}
}
}
}
}
}
class client implements Runnable{
@Override
public void run() {
System.out.println("客户端开始运行:");
try{
InetSocketAddress address=new InetSocketAddress("localhost",8080);
SocketChannel client=SocketChannel.open(address);
//非阻塞模式
client.configureBlocking(false);
ByteBuffer buffer=ByteBuffer.allocate(4);
//client.read(buffer) 从通道中读取数据,并将读取到的数据存储到 buffer 缓冲区中。在这种情况下,read() 方法将会将所有字节的数据读取到缓冲区
while(client.read(buffer)!=-1){
buffer.flip();
while (buffer.hasRemaining()) {
int valueFromServer = buffer.getInt();
System.out.println("Received integer from server: " + valueFromServer);
}
buffer.clear();
}
client.close();
}catch (IOException ex){
ex.printStackTrace();
}
}
}
8. 视图缓冲区
SocketChannel读取的ByteBuffer只包含一种特定基本数据类型的元素,所以有必要创建一个视图缓冲区。这是一个适当类型(如DoubleBuffer、IntBuffer等)的新的Buffer对象,它从当前位置开始由底层ByteBuffer提取数据。修改视图缓冲区会反映底层的缓冲区,反之亦然。不过每个缓冲区都有自己独立的限度、容量、标记和位置。视图缓冲区可以用ByteBuffer的以下6种方法创建:
public abstract ShortBuffer asShortBuffer()
public abstract CharBuffer asCharBuffer()
public abstract IntBuffer asIntBuffer()
public abstract LongBuffer asLongBuffer()
public abstract FloatBuffer asFloatBuffer()
public abstract DoubleBuffer asDoubleBuffer()
下面使用IntBuffer对前面的客户端进行改写
public void run() {
System.out.println("客户端开始运行:");
try{
InetSocketAddress address=new InetSocketAddress("localhost",8080);
SocketChannel client=SocketChannel.open(address);
//非阻塞模式
client.configureBlocking(false);
ByteBuffer buffer=ByteBuffer.allocate(4);
IntBuffer view=buffer.asIntBuffer();
//client.read(buffer) 从通道中读取数据,并将读取到的数据存储到 buffer 缓冲区中。在这种情况下,read() 方法将会将所有字节的数据读取到缓冲区
while(client.read(buffer)!=-1){
buffer.flip();
while (buffer.hasRemaining()) {
int valueFromServer = view.get();
System.out.println("Received integer from server: " + valueFromServer);
}
buffer.clear();
}
client.close();
}catch (IOException ex){
ex.printStackTrace();
}
}
这里需要注意:虽然可以完全使用IntBuffer类的方法来填充和排空缓冲区,但数据必须使用原生的ByteBuffer(IntBuffer就是这个ByteBuffer的视图)对通道进行读写/SocketChannel类只有读写ByteBuffer的方法,它无法读写其它缓冲区。
9. 压缩缓冲区
大多数缓冲区都支持compact方法
public abstract ByteBuffer compact()
public abstract IntBuffer compact()
public abstract ShortBuffer compact()
public abstract FloatBuffer compact()
public abstract CharBuffer compact()
public abstract DoubleBuffer compact()
压缩时将缓冲区中所有剩余数据移到缓冲区的开头,为元素释放更多空间。这些位置上的任何数据都将被覆盖。缓冲区的位置设置为数据末尾,从而可以写入更多数据。使用非阻塞I/O进行复制时(读取一个通道,再把数据写入另一个通道),压缩是一个特别有用的操作,可以将一些数据读入缓冲区,再写出缓冲区,然后压缩数据,这样所有没有写出的数据就在缓冲区开头,位置则在缓冲区中剩余数据的末尾,准备接受更多数据。这样只使用一个缓冲区就能完成比较随机的交替读写,可以连续几次读取,或连续几次写入。下面使用这种技术实现echo服务器,echo协议只是用客户端发送的数据向客户端进行响应。
public class QuizCardBuilder {
public static void main(String[] args) throws InterruptedException {
server server1=new server();
Thread mythread1=new Thread(server1);
mythread1.start();
client client1 =new client();
Thread mythread2=new Thread(client1);
mythread2.start();
}
}
class server implements Runnable {
@Override
public void run() {
System.out.println("服务器开始运行:");
//定义服务器的监听端口
int port = 8080;
//定义通道
ServerSocketChannel serverSocketChannel;
Selector selector;
try {
serverSocketChannel = ServerSocketChannel.open();
ServerSocket ss = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address);
serverSocketChannel.configureBlocking(false);
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException ex) {
ex.printStackTrace();
return;
}
while (true) {
try {
selector.select();
} catch (IOException e) {
throw new RuntimeException(e);
}
Set<SelectionKey> readKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = readKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();
SocketChannel client = serverSocketChannel1.accept();
System.out.println("Accepted connection from " + client);
client.configureBlocking(false);
SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);
ByteBuffer outpur = ByteBuffer.allocate(4);
outpur.putInt(0);
outpur.flip();
key2.attach(outpur);
}
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
output.flip();
client.write(output);
output.compact();
}
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
client.read(output);
}
} catch (IOException ex) {
key.channel();
try {
key.channel().close();
} catch (IOException e) {
}
}
}
}
}
}
class client implements Runnable{
@Override
public void run() {
System.out.println("客户端开始运行:");
try{
InetSocketAddress address=new InetSocketAddress("localhost",8080);
SocketChannel client=SocketChannel.open(address);
//非阻塞模式
client.configureBlocking(false);
Selector selector = Selector.open();
client.register(selector, SelectionKey.OP_WRITE);
while(true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if(key.isWritable()){
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(10); // 将一个整数写入buffer
buffer.flip();
SocketChannel sc = (SocketChannel) key.channel();
sc.write(buffer); // 将buffer的内容写入通道
key.interestOps(SelectionKey.OP_READ); // 切换关注点到读
} else if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(4);
SocketChannel sc = (SocketChannel) key.channel();
sc.read(buffer); // 从通道中读取数据
buffer.flip();
int valueFromServer = buffer.getInt(); // 将buffer的内容解析为整数
System.out.println("Received integer from server: " + valueFromServer); // 打印接收到的整数
key.interestOps(SelectionKey.OP_WRITE); // 切换关注点到写
}
}
}
}catch (IOException ex){
ex.printStackTrace();
}
}
}
10. 复制缓冲区
经常需要建立缓冲区的副本,从而将相同的信息发到两个或多个通道。6种特定类型的缓冲区类都提供了duplicate方法来完成这个工作。
public abstract ByteBuffer duplicate()
public abstract IntBuffer duplicate()
public abstract ShortBuffer duplicate()
public abstract FloatBuffer duplicate()
public abstract CharBuffer duplicate()
public abstract DoubleBuffer duplicate()
在 Java NIO 中,如果我们调用 ByteBuffer 的 duplicate() 方法,就可以创建一个新的 ByteBuffer,它与原始的 ByteBuffer 共享相同的底层数据,但有着自己独立的(position,limit,mark)索引。这意味着对原始 ByteBuffer 的修改会反映在新的 ByteBuffer 上,反之亦然。但是通过改变每个 ByteBuffer 的索引值,可以独立地、相互不影响地读写两个 ByteBuffer。
复制缓冲区的主要作用是:当需要创建一个与原始缓冲区共享数据,但又需要单独控制索引的缓冲区时,可以使用这种方式。这在一些需要同时进行读和写操作,或者需要同时从不同的地方读取或写入缓冲区数据的场景下非常有用。
例如,你可以在一个缓冲区上执行写操作,同时在另一个复制的缓冲区上执行读操作,读写操作互不干扰,可以同时进行
返回值并不是克隆。复制的缓冲区共享相同的数据,如果原来是一个简介缓冲区,那么复制缓冲区会包含相同的后备数组。修改一个缓冲区中的数据会反映到另一个缓冲区中。所有这个方法主要用在只准备读取缓冲区时。尽管共享相同的数据,但初始和复制缓冲区有独立的标记、限度和位置。一个缓冲区可以超前或者落后另一个缓冲区。希望通过多个通道大致并行地传输相同的数据时,复制非常有用,可以为每个通道建立主缓冲区副本,让每个通道以其自己的速度运行。这里用多通道实现单文件HTTP服务器。代码如下:
public class QuizCardBuilder {
public static void main(String[] args) throws InterruptedException {
try{
String contentType=URLConnection.getFileNameMap().getContentTypeFor("/Users/jackchai/Desktop/自学笔记/java项目/leetcode/leetcodetest/myimg.jpg");
Path file= FileSystems.getDefault().getPath("/Users/jackchai/Desktop/自学笔记/java项目/leetcode/leetcodetest/myimg.jpg");
byte[] data= Files.readAllBytes(file);
ByteBuffer input=ByteBuffer.wrap(data);
int port =8080;
String encoding="UTF-8";
server serers=new server(input,encoding,contentType,port);
serers.run();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
class server {
private ByteBuffer contentBuffer;
private int port;
public server(ByteBuffer data, String encoding, String MIMEType,int port) {
this.port = port;
//响应头
String header = "HTTP/1.0 200 OK\r\n" +
"Server: NonblockingSingleFileHTTPServer\r\n" +
"Content-length: " + data.limit() + "\r\n" +
"Content-type: " + MIMEType + "\r\n\r\n";
byte[] headerData = header.getBytes(Charset.forName("ISO-8859-1"));
ByteBuffer buffer = ByteBuffer.allocate(data.limit() + headerData.length);
buffer.put(headerData);
buffer.put(data);
buffer.flip();
this.contentBuffer = buffer;
}
public void run() {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(this.port);
serverSocket.bind(address);
Selector selector=Selector.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
while (true){
selector.select();
Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key=iterator.next();
iterator.remove();
if(key.isAcceptable()){
ServerSocketChannel serverSocketChannel1=(ServerSocketChannel) key.channel();
SocketChannel channel=serverSocketChannel1.accept();
channel.configureBlocking(false);
channel.register(selector,SelectionKey.OP_READ);
} else if (key.isReadable()) {
//不用费力的解析HTTP首部
//只需读取
SocketChannel socketChannel=(SocketChannel) key.channel();
ByteBuffer buffer=ByteBuffer.allocate(4096);
socketChannel.read(buffer);
//将通道切换为只写模式
key.interestOps(SelectionKey.OP_WRITE);
key.attach(contentBuffer.duplicate());
} else if (key.isWritable()) {
SocketChannel channel= (SocketChannel) key.channel();
ByteBuffer buffer=(ByteBuffer) key.attachment();
if(buffer.hasRemaining()){
channel.write(buffer);
}else {
channel.close();
}
}
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}
11. 分片缓冲区
分片缓冲区市复制的一个变形及,分片也会创建一个新的缓冲区,与原缓冲区共享数据。不过,分片的起始位置(位置0)是原缓冲区的当前位置,而且其容量最大不超过原缓冲区的限度。也就是,分片是原缓冲区的一个子序列,只包含从当前位置到限度的所有元素。当创建分片时,倒回分片只移动到原缓冲区的位置,分片无法看到原缓冲区中该点之前的内容。6中特定的类型的缓冲区都有单独的slice()方法
public abstract ByteBuffer slice()
public abstract InteBuffer slice()
public abstract LongBuffer slice()
public abstract DoubleBuffer slice()
public abstract FloatBuffer slice()
public abstract CharBuffer slice()
如果你有一个很长的缓冲区,很容易地分成多个部分(如协议首部以及数据),此时分片就很有用。可以读出首部然后分片,将只包含数据的新缓冲区传递给一个单独的方法或类