首先我们先讨论一下nio和我们大家众所周知的bio的区别
java.io的核心是流(Stream)
,是面向流的编程,一个流只能进或者出****,没有有两种功能都有的流,既不存在可以进而且可以出的流。而且java.io是阻塞的,打一个比方把,我们现在的服务器要接收一张图片,而客户端发送的时候因为网络问题,传到一半就传不动了,而这时我们的服务器的read方法是一直阻塞等待客户端继续发送数据的,这种io模型下我们需要一个请求一个线程,当链接多的时候就需要经常的上下文切换,而且链接太多可能还会出现内存占用过多的现象。
java.nio 是面向块(block) 或者说是缓冲区(buffer)的编程,NIO有三个基本概念Selector ,Channel ,Buffer。
Buffer,底层其实就是数组,读和取都是buffer实现的。
数据读写永远不会发现直接访问Channel,必须通过buffer。
Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写,等状态。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。
首先我们讨论一下Buffer
直接上代码
public static void test1(){
//分配长度为128的intbuffer
IntBuffer intBuffer = IntBuffer.allocate(3);
//随机的向IntBuffer添加数据
for (int i = 0; i < intBuffer.capacity(); i++) {
int i1 = new SecureRandom().nextInt(20);
intBuffer.put(i1);
}
//翻转
intBuffer.flip();
//读
while(intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
我们先定义了一个IntBuffer 然后随机的向IntBuffer 添加int类型数据,然后翻转,然后就开始读。
很多小伙伴会问,为什么还要翻转,才开始读,我不翻转可以吗,会发生什么事情。
让我们进入intBuffer.flip()这个方法一探究竟把
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
A buffer’s capacity is the number of elements it contains. The
capacity of a buffer is never negative and never changes.
A buffer’s limit is the index of the first element that should
not be read or written. A buffer’s limit is never negative and is never
greater than its capacity.
A buffer’s position is the index of the next element to be
read or written. A buffer’s position is never negative and is never
greater than its limit.
这是java.nio.Buffer类上面的说明
大概意思就是
-
capacity 就是缓冲区的大小。初始化的时候也就是我们之前设定的IntBuffer.allocate(3),也就是3。
-
limit
指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。初始化完它等于capacity -
position
下一个要被读或写的元素的索引,永远不会大于limit。
他们的大小关系是
0 <= position <= limit <= capacity
了解了这三个属性的意义之后我们来分析flip()这个方法
- 首先我们向buffer写数据,每写一个数据position 会+XX,capacity保持不变,写完了之后我们要开始读,而position指向的是下一个要读的数据
- 如果这时候我们直接开始读,下一个数据根本就不存在。所以我们需要将position归零
- 在flip()里面把limit = position是为了标记,读写数据的时候的最后一位在哪里,这样才好判断我们再buffer里面写了多少数据读到哪里的时候停止
结论:读完数据要写数据,或者写完要读,中间必须调用flip()
Channel
上代码
//写
public static void test3() {
try {
FileOutputStream fileOutputStream = new FileOutputStream("noiotest3.txt");
FileChannel channel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
byte[] msg = "hello nio !!!!".getBytes();
for (int i = 0; i < msg.length; i++) {
byteBuffer.put(msg[i]);
}
// 将缓存字节数组的指针设置为数组的开始序列即数组下标0
byteBuffer.flip();
channel.write(byteBuffer);
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//读
public static void test2() {
try {
FileInputStream fileInputStream = new FileInputStream("niotest1.txt");
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
channel.read(byteBuffer);
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
byte b = byteBuffer.get();
System.out.println("byte : "+(char)b);
}
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
直接上读文件和写文件的代码,开头还是我们熟悉的 FileOutputStream ,只不过我们从fileInputStream(fileOutputStream )里面获取了一个通道Channel,然后定义了一个buffer ,然后还是 flip(),然后开始读或者写,最后关闭。很简单的操作。
我们再上一个例子
public static void test4() throws Exception{
FileInputStream inputStream = new FileInputStream("input.txt");
FileOutputStream outputStream = new FileOutputStream("output.txt");
FileChannel inputChannel = inputStream.getChannel();
FileChannel outputChannel = outputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(2);
while (true){
//重置position =0 limit=capacity
//主要是为了下一次循环
buffer.clear();
//这里里面会调用buffer.put()
int len = inputChannel.read(buffer);
System.out.println(len);
if (-1 == len){
break;
}
//让position等于0然后好开始写
buffer.flip();
//这里会让limit和position一起移动
outputChannel.write(buffer);
}
inputChannel.close();
outputChannel.close();
}
这个例子里面是上面的结合版,但是多了一个buffer.clear();
首先还是获取文件的channel,然后定义buffer,然后一个循环,开始读,如果读到数据的长度等于-1,就退出循环。最后还是要关闭。为什么要clear()呢?clear()究竟是起了什么作用,其实我注释上面已经写的很明白了,它是为了下一次循环。我们进入这个方法
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
它也是操作了position,limit,capacity这三个属性。它把position,和limit都初始化到我们的buffer刚刚创建的时候。
如果我们不buffer.clear();会发生什么事情呢?
1、第一次循环:
- 调用inputChannel.read(buffer);把buffer写满
- buffer.flip();开始outputChannel.write(buffer);正常的写数据。
2、第二次循环:
- 调用inputChannel.read(buffer);因为position==limit 所有不可能再往buffer写了,因为position不能大于limit,所以数据进不了buffer,
- 然后buffer.flip() 把之前的数据写到文件里面。
这样就造成了一个问题,数据永远读不完,就成了死循环,所以我们循环读数据的时候一定要记住buffer.clear()
现在我们来讲一个netty一直吹的概念:零拷贝
public static void test1() throws Exception{
FileInputStream inputStream = new FileInputStream("input2.txt");
FileOutputStream outputStream = new FileOutputStream("output2.txt");
FileChannel inputChannel = inputStream.getChannel();
FileChannel outputChannel = outputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocateDirect(2);
while (true){
//重置position =0 limit=capacity
//主要是为了下一次循环起作用
buffer.clear();
//这里里面会调用buffer.put()
int len = inputChannel.read(buffer);
System.out.println(len);
if (-1 == len){
break;
}
//让position等于0然后好开始写
buffer.flip();
//这里会让limit和position一起移动
outputChannel.write(buffer);
}
inputChannel.close();
outputChannel.close();
}
细心的同学可能看到了创建buffer的方式改变了,从 allocate(2) 变成了allocateDirect(2),这有啥区别呢Direct字面意思是直接的,大概就是分配直接内存的buffer。
其实是这两个创建buffer位置有不,allocate是在java堆上面创建,可以理解为在jvm里面创建的,而直接在堆外内存上面创建buffer。
- allocate是在JVM里面创建的对象
- allocateDirect是在操作系统的内存上面创建的对象
为啥要这样呢,因为jvm不能直接从操作系统的io直接获得数据传输通道,需要先把数据从网络读到操作系统的内存再读到JVM里面,这样就造成了内存拷贝。少量的数据可能没啥感觉,但是数据多了肯定是有很大影响的。
而
allocateDirect是在JVM外面的内存创建了一个buffer,在
++java.nio.Buffer++里面有一个++long address++属性我们可以把这个address属性理解为它直接指向了堆外内存的buffer,直接和堆外内存交互。
还有一个问题: 为什么操作系统不直接访问java内存,这样多简单啊,还装这么多的X
我们都知道JVM会自动的帮我们管理内存,就比如说分代回收吧,它分年轻代,存活代,老年代,gc的时候会移动对象,内存地址会变,如果这时候native正在操作数据的时候gc会乱套
再来几个例子
创建分片buffer
/** slice buffer 与 原来的buffer共享底层数组
* @author guoyitao
* @date 2019/3/18 21:07
* @params
* @return
*/
public static void test1SliceBuffer(){
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i <buffer.capacity() ; i++) {
buffer.put((byte) i);
}
buffer.position(2);
buffer.limit(6);
//分片,并不是复制出来的
ByteBuffer slice = buffer.slice();
for (int i = 0; i < slice.capacity(); i++) {
byte b = slice.get(i);
b *=2;
slice.put(i, b);
}
buffer.clear();
while (buffer.hasRemaining()){
System.out.println(buffer.get());
}
}
创建只读buffer
/** 创建只读buffer
* @author guoyitao
* @date 2019/3/18 21:22
* @params
* @return
*/
public static void testOnlyBuffer(){
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i <buffer.capacity() ; i++) {
buffer.put((byte) i);
}
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(buffer.getClass());
System.out.println(readOnlyBuffer.getClass());
readOnlyBuffer.put("as".getBytes());
}
关于NIO网络编程
buffer的Scattering和Gathering
public static void main(String[] args)throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress(8899);
serverSocketChannel.socket().bind(address);
int messagelen = 2 + 3 + 4;
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = ByteBuffer.allocate(2);
buffers[1] = ByteBuffer.allocate(3);
buffers[2] = ByteBuffer.allocate(4);
SocketChannel socketChannel = serverSocketChannel.accept();
while (true){
int bytesRead = 0;
while (bytesRead < messagelen){
//Scattering
long read = socketChannel.read(buffers);
bytesRead += read;
System.out.println("bytesread: " + bytesRead);
Arrays.asList(buffers).stream().map(
buffer -> "position:" +buffer.position() + ",limit: " + buffer.limit()).
forEach(System.out::println);
}
Arrays.asList(buffers).forEach(byteBuffer -> {
byteBuffer.flip();
});
long bytesWritten = 0;
while (bytesWritten < messagelen){
//Gathering
long l = socketChannel.write(buffers);
bytesWritten += l;
}
Arrays.asList(buffers).forEach(byteBuffer -> {
byteBuffer.clear();
});
System.out.println("bytesRead: "+ bytesRead + ",bytesWritten " + bytesWritten + ",messageLength" + messagelen);
}
}
++关于niosocket的我们先不看++,先只看我//Gathering//Scattering的;两个地方
- Scattering 把来自一个channel的数据读到多个buffer,按照顺序,读满第一个再第二个。。。。。。。
- Gathering 把多个buffer写到channel,按照顺序,写完第一个再第二个
再上代码
public static void test1() throws IOException, InterruptedException {
int[] prots = new int[5];
prots[0] = 5000;
prots[1] = 5001;
prots[2] = 5002;
prots[3] = 5003;
prots[4] = 5004;
Selector selector = Selector.open();
// System.out.println(SelectorProvider.provider().getClass());
// System.out.println(sun.nio.ch.DefaultSelectorProvider.create().getClass());
for (int i = 0; i < prots.length; i++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket socket = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(prots[i]);
socket.bind(address);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("监听端口" + prots[i]);
}
while (true){
int select = selector.select();
System.out.println("number :" + select);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys: " + selectionKeys);
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//连接事件
if (selectionKey.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
iterator.remove();
System.out.println("获得客户端连接: " + socketChannel);
//读事件
} else if (selectionKey.isReadable()){
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int byteRead = 0;
while (true){
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
byteBuffer.clear();
int read = socketChannel.read(byteBuffer);
if (read <= 0){
break;
}
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteRead += read;
}
System.out.println("读取了:" + byteRead + ",来自" + socketChannel);
iterator.remove();
}
}
}
}
现在我们开始讲解nio网络编程的helloworld
selectedKey将Channel与Selector建立了关系,并维护了channel事件
可以通过cancel方法取消键,取消的键不会立即从selector中移除,而是添加到cancelledKeys中,在下一次select操作时移除它,所以在调用某个key时,最好使用isValid进行校验。
一些事件类型:
OP_ACCEPT:连接可接受操作,仅ServerSocketChannel支持
OP_CONNECT:连接操作,Client端支持的一种操作
OP_READ/OP_WRITE 读和写时间
一些方法介绍:
-
public abstract SelectableChannel channel():返回此选择键所关联的通道.即使此key已经被取消,仍然会返回.
-
public abstract Selector selector():返回此选择键所关联的选择器,即使此键已经被取消,仍然会返回.
-
public abstract boolean isValid():检测此key是否有效.当key被取消,或者通道被关闭,或者selector被关闭,都将导致此key无效.在AbstractSelector.removeKey(key)中,会导致selectionKey被置为无效.
-
public abstract void cancel():请求将此键取消注册.一旦返回成功,那么该键就是无效的,被添加到selector的cancelledKeys中.cancel操作将key的valid属性置为false,并执行selector.cancel(key)(即将key加入cancelledkey集合)
-
public abstract int interesOps():获得此键的interes集合.
-
public abstract SelectionKey interestOps(int ops):将此键的interst设置为指定值.此操作会对ops和channel.validOps进行校验.如果此ops不会当前channel支持,将抛出异常.
-
public abstract int readyOps():获取此键上ready操作集合.即在当前通道上已经就绪的事件.
-
public final boolean isReadable(): 检测此键是否为"read"事件.等效于:k.,readyOps() & OP_READ != 0;还有isWritable(),isConnectable(),isAcceptable()
-
public final Object attach(Object ob):将给定的对象作为附件添加到此key上.在key有效期间,附件可以在多个ops事件中传递.
-
public final Object attachment():获取附件.一个channel的附件,可以再当前Channel(或者说是SelectionKey)生命周期中共享,但是attachment数据不会作为socket数据在网络中传输.
-
首先定义了了一个prots里面存放了端口号
-
创建一个Selector
-
循环的创建对个ServerSocketChannel,并将configureBlocking(false)记住这里必须是false,设置为非阻塞。
-
然后将刚才定义的端口绑定到socket里面,在把serverSocketChannel,注册到Selector里面
-
开始一个死循环,
服务器一起的就会阻塞在selector.select()一直检查通道,直到通道的状态发送变化,如果发送变化会返回selectedKeys的个数 -
获得selectionKeys 遍历它,if判断selectionKey是什么事件,并作出相应的逻辑
-
这里必须重新注册到selector,然后把事件设置为OP_ACCEPT之后的下一个想处理的事件,然后iterator.remove()
要知道,一码事归一码事,channel是注册在selector中的,在后面的轮询中,是先将已准备好的channel挑选出来,即selector.select(),再通过selectedKeys()生成的一个SelectionKey迭代器进行轮询的,一次轮询会将这个迭代器中的每个SelectionKey都遍历一遍,每次访问后都remove()相应的SelectionKey,但是移除了selectedKeys中的SelectionKey不代表移除了selector中的channel信息(这点很重要),注册过的channel信息会以SelectionKey的形式存储在selector.keys()中,也就是说每次select()后的selectedKeys迭代器中是不能还有成员的,但keys()中的成员是不会被删除的(以此来记录channel信息)。
那么为什么要删除呢,要知道,迭代器如果只需要访问的话,直接访问就好了,完全没必要remove()其中的元素啊,查询了相关资料,一致的回答是为了防止重复处理,后来又有信息说明:每次循环调用remove()是因为selector不会自己从已选择集合中移除selectionKey实例,必须在处理完通道时自己移除,这样,在下次select时,会将这个就绪通道添加到已选择通道集合中,其实到这里就已经可以理解了,selector不会自己删除selectedKeys()集合中的selectionKey,那么如果不人工remove(),将导致下次select()的时候selectedKeys()中仍有上次轮询留下来的信息,这样必然会出现错误,假设这次轮询时该通道并没有准备好,却又由于上次轮询未被remove()的原因被认为已经准备好了,这样能不出错吗?
即selector.select()会将准备好的channel以SelectionKey的形式放置于selector的selectedKeys()中供使用者迭代,使用的过程中需将selectedKeys清空,这样下次selector.select()时就不会出现错误了。
下面再来最后一个demo
聊天客户端和服务端 群聊()
Server
/**
*
* remove SelectKey是为了防止下一次select的时候重复处理
* @Description: 群聊服务器
* @Author: guo
* @CreateDate: 2019/3/19
* @UpdateUser:
*/
public class NIOServer {
public static final int serverPort = 5001;
public static final Map<String, SocketChannel> clientMap = new ConcurrentHashMap<>();
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//非阻塞
serverSocketChannel.configureBlocking(false);
//服务器接套字
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(serverPort));
//注册准备接收事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器端口: " + serverPort);
while (true){
//等待事件
selector.select();
//获取事件token
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//读取事件
SelectionKey selectionKey = iterator.next();
//处理事件
if (selectionKey.isAcceptable() && selectionKey.isValid()){
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
//接受与此通道的套接字建立的连接, 这是用户连接!!!!!!!!!
SocketChannel client = server.accept();
//非阻塞
client.configureBlocking(false);
//连接成功准备接受读事件
client.register(selector,SelectionKey.OP_READ);
//保存连接用户
clientMap.put(UUID.randomUUID().toString(),client);
iterator.remove();
}else if (selectionKey.isReadable()&& selectionKey.isValid()){
SocketChannel client = (SocketChannel) selectionKey.channel();
//接受数据
try {
while (true){
ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
readBuffer.clear();
//来自哪个用户
String fromKey = null;
for (Map.Entry<String, SocketChannel> channelEntry : clientMap.entrySet()) {
if (channelEntry.getValue() == client){
fromKey = channelEntry.getKey();
break;
}
}
int read = 0;
try {
read = client.read(readBuffer);
} catch (IOException e) {
/*
* 客户端异常关闭处理 java.io.IOException: 远程主机强迫关闭了一个现有的连接。
* */
client.close();
selectionKey.cancel();
e.printStackTrace();
/*
* 必须把clientMap原来的SocketChannel剔除掉,因为原来的SocketChannel已经关闭
* 如果不剔除可能抛出java.nio.channels.ClosedChannelException
* */
clientMap.remove(fromKey);
break;
}
if (read == -1){
client.close();
selectionKey.cancel();
clientMap.remove(fromKey);
break;
}else if(read == 0){
break;
}
readBuffer.flip();
//转码取值
Charset charset = Charset.forName("utf-8");
String receiveData = String.valueOf(charset.decode(readBuffer).array());
//发送
System.out.println("服务器收到" + receiveData + ",来自:" + client);
for (Map.Entry<String, SocketChannel> channelEntry : clientMap.entrySet()) {
SocketChannel value = channelEntry.getValue();
ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
writeBuffer.put((fromKey + receiveData).getBytes());
writeBuffer.flip();
value.write(writeBuffer);
}
iterator.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
Client
/**
* @Description: 群聊客户端
* @Author: guo
* @CreateDate: 2019/3/19
* @UpdateUser:
*/
public class NIOClient {
public static void main(String[] args) throws IOException {
//创建一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//非阻塞
socketChannel.configureBlocking(false);
//创建选择器
Selector selector = Selector.open();
//注册 设置事件为 链接
socketChannel.register(selector, SelectionKey.OP_CONNECT);
//链接到服务器
socketChannel.connect(new InetSocketAddress("127.0.0.1",NIOServer.serverPort));
while (true){
//等待事件
selector.select();
//获取事件token
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//读取事件
SelectionKey selectionKey = iterator.next();
//处理事件 判断是不是OP_CONNECT事件
if (selectionKey.isConnectable()) {
//获得SocketChannel
SocketChannel client = (SocketChannel) selectionKey.channel();
// 建立连接
if (client.isConnectionPending()) {
//完成链接
client.finishConnect();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((LocalDateTime.now() + "连接成功").getBytes());
writeBuffer.flip();
//发送链接成功信息
client.write(writeBuffer);
//创建一个单个线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
executorService.submit(() -> {
while (true) {
try {
//这里之前讲过
writeBuffer.clear();
//标准输入,从键盘输入
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();
writeBuffer.put(line.getBytes());
writeBuffer.flip();
//从客户端向服务端发送数据
client.write(writeBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
//收数据
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = client.read(byteBuffer);
if (read > 0) {
String receiveData = new String(byteBuffer.array(),0,read);
System.out.println("收到消息: " + receiveData);
}
}
}
//之前讲过
selectionKeys.clear();
}
}
}