看了看Java的nio类库,整理一下思路。
1,Buffer
jdk官方文档上对Buffer的描述为:
Sun 写道
缓冲区是特定基本类型元素的线性有限序列。除内容外,缓冲区的基本属性还包括容量、限制和位置:
缓冲区的容量 是它所包含的元素的数量。缓冲区的容量不能为负并且不能更改。
缓冲区的限制 是第一个不应该读取或写入的元素的索引。缓冲区的限制不能为负,并且不能大于其容量。
缓冲区的位置 是下一个要读取或写入的元素的索引。缓冲区的位置不能为负,并且不能大于其限制。
对于每个非 boolean 基本类型,此类都有一个子类与之对应。
Buffer里面的这个几个变量,控制了buffer的put和get行为,之间的相关关系为:
0 <= 标记 <= 位置 <= 限制 <= 容量
新创建的缓冲区总有一个 0 位置和一个未定义的标记。初始限制可以为 0,也可以为其他值,这取决于缓冲区类型及其构建方式。一般情况下,缓冲区的初始内容是未定义的。
几个常见的操作说明:
clear()
使缓冲区为一系列新的通道读取或相对放置 操作做好准备:它将限制设置为容量大小,将位置设置为 0。flip()
使缓冲区为一系列新的通道写入或相对获取 操作做好准备:它将限制设置为当前位置,然后将位置设置为 0。 这个操作尤其在read完buffer以后,使用之前调用一次rewind()
使缓冲区为重新读取已包含的数据做好准备:它使限制保持不变,将位置设置为 0。
我们常用的也就是ByteBuffer、CharBuffer了。
2,Channel
写道
用于 I/O 操作的连接。
通道表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接。
通道可处于打开或关闭状态。创建通道时它处于打开状态,一旦将其关闭,则保持关闭状态。一旦关闭了某个通道,试图对其调用 I/O 操作就会导致 ClosedChannelException 被抛出。通过调用通道的 isOpen 方法可测试通道是否处于打开状态。
Channel有很多实现,AbstractInterruptibleChannel, AbstractSelectableChannel, DatagramChannel, FileChannel, Pipe.SinkChannel, Pipe.SourceChannel, SelectableChannel, ServerSocketChannel, SocketChannel ,下面演示一下用FileChannel进行文件复制:
/**
* Copy file using nio.
* @param inFile
* @param outFile
*/
public static void copyFile(String inFile, String outFile) {
try {
FileInputStream in = new FileInputStream(inFile);
FileOutputStream out = new FileOutputStream(outFile);
FileChannel cIn = in.getChannel();
FileChannel cOut = out.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = 0;
while(true){
buffer.clear();
length = cIn.read(buffer);
if(length == -1){
break;
}
buffer.flip();
cOut.write(buffer);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
3,Selector
selector是nio实现非阻塞式通信的核心,它是SelectableChannel
对象的多路复用器。
关于selector的介绍,SUN的jdk 文档里面已经有很多了,这里不赘述了,下面演示一下如何使用selector和channel实现非阻塞的网络通信。
ServerDemo.java
/**
* @author
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
boolean readAllready = true;
Charset charset = Charset.forName("utf-8");
ByteBuffer buffer = ByteBuffer.allocate(1024);
ServerSocketChannel ssocketChannel = ServerSocketChannel.open();
ssocketChannel.socket().bind(new InetSocketAddress(6018));
LogUtil.info("启动了一个ServerSocketChannel");
ssocketChannel.configureBlocking(false);
Selector selector = Selector.open();
ssocketChannel.register(selector, SelectionKey.OP_ACCEPT);
LogUtil.info("等待客户端连接......");
String content = "";
while (selector.select() > 0) {
Set<SelectionKey> set = selector.selectedKeys();
for (SelectionKey key : set) {
SocketChannel channel;
if(key.isAcceptable()){
channel = ssocketChannel.accept();
LogUtil.error("有新的客户端连接:" + channel);
LogUtil.error("地址是: " + channel.socket());
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
if(key.isReadable()){
readAllready = true;
LogUtil.error("有新的读取");
channel = (SocketChannel) key.channel();
channel.read(buffer);
buffer.flip();
content = charset.decode(buffer).toString() + "\r\n";
LogUtil.info("Read from clent <<<<<<<<<<< " + content);
buffer.clear();
}
if(key.isWritable()){
if(readAllready){
channel = (SocketChannel) key.channel();
buffer.put(("Write into client >>>>>>>>> " + content).getBytes());
buffer.flip();
channel.write(buffer);
buffer.clear();
Thread.sleep(1000);
readAllready = false;
}
}
}
set.clear();
}
LogUtil.info("服务器推出");
}
}
其中LogUtil是一个打印日志的泪类,可以用System.out代替。
测试这个服务器,可以自己启动一个连接到6018端口的客户端,比如telnet 本地IP 6018,就可以看到效果了