文章目录
- 写在前面
- 1、Buffers
- 1.1、Buffer的方法
- 1.2、Scatter/Gather
- 2、Channels
- 2.1、FileChannel
- 2.1.1、从文件读取数据
- 2.1.2、将数据写入文件
- 2.1.3、FileChannel方法
- 2.2、SocketChannel
- 2.3、ServerSocketChannel
- 2.4、DatagramChannel
- 3、Selector
- 3.1、将channel注册到selector
- 3.2、interest集合
- 3.3、SelectionKey
写在前面
Java NIO 由三个核心组成:Channels、Buffers、Selectors
Buffer是数据的载体,
Channel里面如果有数据,通过 Channels.receive(Buffer) 接收,
Channel里面需要数据,通过 Channels.send(Buffer,接收地址) 发送。
Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。
1、Buffers
Java NIO 有以下Buffer类型:
ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
1.1、Buffer的方法
ByteBuffer buf = ByteBuffer.allocate(48); // 设置缓存区大小为48
buf.flip(); // 将Buffer从写模式切换到读模式
int bytesRead = channel.read(buf); // 从channel中读取数据放入buf
int bytesWritten = channel.write(buf); // 将数据写入到channel
buf.put(127); // 将数据写入buf
byte aByte = buf.get(); // 从buf读取数据
buf.rewind(); // 重读
buf.clear(); // 清空
buf.compact(); // 清空已读数据
buf.mark(); // 标记当前读取位置
buf.reset(); // 跳到标记位置
buf.equals(buf1); // 判断两个Buffer内容是否相等
buf.compareTo(buf1); // 比较两个Buffer的剩余元素大小
1.2、Scatter/Gather
scatter:分散,从Channel中读取是指在读操作时将读取的数据写入多个buffer中。
gather:聚集,写入Channel是指在写操作时将多个buffer的数据写入同一个Channel。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufs = { header, body };
// 分散读取
channel.read(buffs);
// 聚集写入
Channel.write(buffs);
2、Channels
Channels有下面几个实现类:
实现类 | 用途 |
FileChannel | 从文件中读写数据 |
DatagramChannel | 通过UDP读写网络中的数据 |
SocketChannel | 通过TCP读写网络中的数据 |
ServerSocketChannel | 监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel |
2.1、FileChannel
FileChannel读取文件
2.1.1、从文件读取数据
RandomAccessFile aFile = new RandomAccessFile("1.txt", "rw");
FileChannel channel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = channel.read(buf);
while (bytesRead != -1) {
// 从写模式切换到读模式
buf.flip();
while (buf.hasRemaining()) {
// 一次从buf中读取一个字节,并且转化为char输出
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = channel.read(buf);
}
aFile.close();
}
2.1.2、将数据写入文件
transferFrom()方法可以将数据从源通道传输到FileChannel中
RandomAccessFile fromFile = new RandomAccessFile("1.txt", "rw");
FileChannel channel1 = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
// 从文件1写入到文件2
channel1.transferFrom(position, count, channel2);
position=0表示从0处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于计数 个字节,则所传输的字节数要小于请求的字节数。
要注意,在SoketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足计数字节)。因此,SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中。
transferTo()方法将数据从FileChannel传输到其他的channel中。
RandomAccessFile fromFile = new RandomAccessFile("1.txt", "rw");
FileChannel channel1 = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
// 从文件1写入到文件2
channel1.transferTo(position, count, channel2);
2.1.3、FileChannel方法
long pos = channel.position(); // 获取FileChannel的当前位置
channel.position(pos +123);
long fileSize = channel.size(); // 返回该实例所关联文件的大小
channel.truncate(1024); // 将文件中指定长度后面的部分将被删除。
channel.force(true); // 将文件数据和元数据强制写到磁盘上
2.2、SocketChannel
SocketChannelTCP网络套接字的通道
// 打开一个tcp通道
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://**.com", 8080));
// 读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
// 关闭SocketChannel
socketChannel.close();
写入数据
// 打开一个tcp通道
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://**.com", 8080));
ByteBuffer buf = ByteBuffer.allocate(48);
String data = "这是一串需要传递的文字";
buf.clear();
buf.put(data.getBytes());
// buffer从写模式转为读模式
buf.flip();
// 循环写入
while(buf.hasRemaining()) {
channel.write(buf);
}
非阻塞模式
// 打开一个tcp通道
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://**.com", 8080));
while(! socketChannel.finishConnect() ){
// 写入数据
}
非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。
非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。
2.3、ServerSocketChannel
ServerSocketChannel 是一个可以监听新进来的TCP连接的通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
// ...
}
serverSocketChannel.accept();监听新进来的连接
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
// ...
}
2.4、DatagramChannel
DatagramChannel是一个能收发UDP包的通道。
接收数据
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
发送数据
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
String data = "数据...";
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(data.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("127.0.0.1", 80));
连接到特定的地址
channel.connect(new InetSocketAddress("127.0.0.1", 80));
3、Selector
Selector可以用一个线程去管理多个channel
3.1、将channel注册到selector
// 创建一个Selector
Selector selector = Selector.open();
// 将channel注册到selector上
channel.configureBlocking(false); // 切换为非阻塞状态,与Selector一起使用时,Channel必须处于非阻塞模式
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
3.2、interest集合
register()方法的第二个参数,表示监听类型,是一个“interest集合”,有下列4种类型:
类型 | 说明 |
SelectionKey.OP_CONNECT | 连接就绪 |
SelectionKey.OP_ACCEPT | 接收就绪 |
SelectionKey.OP_READ | 读就绪 |
SelectionKey.OP_WRITE | 写就绪 |
对多个就绪状态监听:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey读写interest集合
// 获取事件集合
int interestSet = selectionKey.interestOps();
// 判断某个事件是否在集合中,返回true表示在
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
3.3、SelectionKey
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。
selectionKey对象可调用的方法:
Channel channel = selectionKey.channel(); // 返回通道
Selector selector = selectionKey.selector(); // 返回selector
int interestSet = selectionKey.interestOps(); // 返回监听类型集合
Object attachedObj = selectionKey.attachment(); // 返回附加对象
int readySet = selectionKey.readyOps(); // 返回通道已经准备就绪的操作的集合
boolean b = selectionKey.isAcceptable(); // 判断是否“接收就绪”
boolean b = selectionKey.isConnectable(); // 判断是否“连接就绪”
boolean b = selectionKey.isReadable(); // 判断是否“读就绪”
boolean b = selectionKey.isWritable(); // 判断是否“写就绪”
下面是写入附加对象
// 添加附加对象
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
//或者在注册时就加入附加对象:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
sselector对象可调用的方法
selector.select(); // 阻塞,直到监听类型已就绪
selector.selectNow(); // 不阻塞,不管什么通道就绪都立刻返回,没有通道变成可选择的,则此方法直接返回零。
Set selectedKeys = selector.selectedKeys(); // 一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,访问“已选择键集”中的就绪通道。
selector.wakeup(); // 调用后,阻塞在select()方法上的线程会立马返回
selector.close();
遍历Set selectedKeys = selector.selectedKeys();
这个已选择的键集合来访问就绪的通道
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// 接收已就绪
} else if (key.isConnectable()) {
// 连接已就绪
} else if (key.isReadable()) {
// 读就绪
} else if (key.isWritable()) {
// 写就绪
}
keyIterator.remove();
}
示例
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
} else if (key.isConnectable()) {
} else if (key.isReadable()) {
} else if (key.isWritable()) {}
keyIterator.remove();
}
}