从业务开发的角度来看,应该很少有业务场景在使用NIO了。毕竟消息中间件用起来其实就应该满足诉求了。但如果是中间件开发同学应该比较了解这个。我其实在业务开发的过程中就从来没有用过,随手写写,不考虑业务实战情况。
QUICKSTART
其实重点主要是3个 Channel, Buffer和Selector(熟悉linux C语言的开发的,应该知道有个select函数,很久以前看过)。
-
Channel:渠道,其实理解就是和linux文件定义差不多,能产生和消费信息的都可以抽象成channel,比如文件,网络,键盘(能产生信息),显示器终端(能消费信息)。
-
Buffer:缓冲区。
-
Selector:如果知道linux select函数的就很好理解了,简单理解就是我都NIO了,我就不等你channel准备好,把消息发给我了。我先去忙其它的,等你准备好了,就通过selector来通知我,我再腾出手来处理你给我的消息。
比如说,当前我监听8080端口,基于TCP链接,接收别人给我发消息。然后处理这个消息。传统的做法,比如我接收到一个来自外面的连接请求,然后建立一个线程去管理这个链接,这个线程负责从连接上获取数据,并处理数据,主线程在这里只负责监听端口,等有请求过来就创立子线程去处理。换一种做法,我主线程既负责监听端口,也负责管理连接,监听连接上是否有数据过来。也就是我同时在处理多个事情,{BANNED}中国第一个,我监听端口是否有连接请求,第二个,有了多个连接请求后,我监听这些连接上是否有数据,我接收到数据并进行处理。下面上代码:
服务端代码:
-
package sty.zchi;
-
-
import java.net.InetSocketAddress;
-
import java.nio.ByteBuffer;
-
import java.nio.channels.SelectionKey;
-
import java.nio.channels.Selector;
-
import java.nio.channels.ServerSocketChannel;
-
import java.nio.channels.SocketChannel;
-
import java.nio.charset.StandardCharsets;
-
import java.util.Set;
-
-
/**
-
* Hello world!
-
*
-
*/
-
public class StyServerSocket {
-
public static void main( String[] args ) {
-
try {
-
//开始监听8080号端口,并且是非阻塞式
-
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
-
serverSocketChannel.configureBlocking(false);
-
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8080));
-
Selector selector = Selector.open();
-
//将端口的OP_ACCEPT事件注册给selector
-
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
-
while(true) {
-
/**
-
* 开始监控selector上所有的时间,注意这里是阻塞式的,主线程到这里如果没有事情发生,就会阻塞住。
-
* 另外如果是循环第一次进来,此时selector上只有一个8080的OP_ACCEPT在监控
-
*/
-
selector.select();
-
Set<SelectionKey> selectionKeys = selector.selectedKeys();
-
selectionKeys.forEach(key -> {
-
try {
-
final SocketChannel client;
-
if (key.isAcceptable()) {
-
//说明有人连进来
-
System.out.println("some one connect server");
-
ServerSocketChannel server = (ServerSocketChannel) key.channel();
-
//接受8080上的连接请求,并且也设置成非阻塞式,然后将连接的OP_READ事件注册到selector上
-
client = serverSocketChannel.accept();
-
client.configureBlocking(false);
-
client.register(selector, SelectionKey.OP_READ);
-
} else if(key.isReadable()) {
-
client = (SocketChannel) key.channel();
-
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
-
int count = client.read(readBuffer);
-
if(count > 0) {
-
readBuffer.flip();
-
String receiveMessage = String.valueOf(StandardCharsets.UTF_8.decode(readBuffer).array());
-
System.out.println("receive message = " + receiveMessage);
-
}
-
client.close();
-
}
-
} catch (Exception e) {
-
e.printStackTrace();
-
} finally {
-
selectionKeys.clear();
-
}
-
});
-
}
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
-
-
}
客户端代码:
-
package sty.zchi;
-
-
import java.net.InetSocketAddress;
-
import java.nio.ByteBuffer;
-
import java.nio.channels.SocketChannel;
-
import java.nio.charset.StandardCharsets;
-
import java.util.Scanner;
-
-
public class StyClient {
-
public static void main(String[] args) {
-
try {
-
SocketChannel socketChannel = SocketChannel.open();
-
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
-
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
-
-
while(true) {
-
Scanner sc = new Scanner(System.in);
-
String str = sc.nextLine();
-
if(str.equals("quit")) {
-
socketChannel.close();
-
return;
-
}
-
writeBuffer.put(str.getBytes(StandardCharsets.UTF_8));
-
writeBuffer.flip();
-
socketChannel.write(writeBuffer);
-
writeBuffer.clear();
-
}
-
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
-
}
基本上看代码注释就明白了,其实业务实际上也没这么简单。我这里只是个开门的样例,下面具体看看后面的内容。
CHANNEL
中文名叫通道,有人说类似于JAVA语言中的流,但我觉得其实更相似的类比应该是linux中关于文件的定义既:能产生或者消费信息的东西。所以文件,网络端口,甚至是键盘,显示器都可以这定。上面的例子是个TCP网络套接字(SOCKET的channel)。其实我们还有FileChannel, UDP协议的DatagramChannel等。
注意是的通道能产生和消费信息,这个过程是不能直接交互的,需要通过下面的BUFFER来中转。
BUFFER
中文名叫缓冲区,既然是用于交换信息的缓冲区,首先从类型上有(我理解是数据类型)
-
ByteBuffer
-
CharBuffer
-
ShortBuffer
-
IntBuffer
-
FloatBuffer
-
DoubleBuffer
-
LongBuffer
很容易理解不是么。说几个关键的方法操作
关键属性和数据结构理解
缓冲区是既能读也能写的,那么就需要一个结构来支持,我写数据,我也能读出数据出来,设计的思想就不赘述了,我直接写核心的属性
-
capacity:这个好理解,就是这个这个缓冲区最大容量,注意不是可用容量。他在allocate之后就分配了,当然可以改。除非你手动改,不然就是allocate分配后就是个固定值。
-
position:这个表示下一次操作的位置,这个操作可以是读,也可以是写。比如我现在分配了一个10char容量的缓冲区,那么position就是0,这标识我下次写操作就写到0号位。如果我没写之前就不讲武德的开始做get操作呢,我试了下,没有报错,就是个乱码。当然你实际业务不会出这种事情。
-
limit:标识实际可以操作数据的大小。是不是有点晕,实际操作大小不就是capacity么,怎么又冒出了个limit呢。下面讲读写切换filp时候就明白了。
开始讲翻转吧,我们设想下,我申请了一个10 char位置的charBuffer。刚申请完,就是往里面写啦。这个时候我们理解下:
-
capacity:这个就不解释了,肯定是10,怎么弄都是10.
-
position:这个好理解,肯定是0,我写的第一个字符肯定放在第0位
-
limit:容量是10么,我写肯定最多能写10个,所以就是10.
好了,我们向里面写了6个数据,这个时候容量肯定是10,position肯定是6,因为我写了6个数据占满了0,1,2,3,4,5 所以下一个能写操作就是6号位,limit还是10,毕竟一共能操作的是10个。
接下来,我就开始想读数据,此时想想,对于读操作来说,你的position应该是0,limit应该是6,capacity应该是10。那是不是需要我们去修改这些属性呢(有方法可以改的,但没必要)。我们只用调用flip来进行读写转换。这就是为什么每次在对缓冲区做读写操作之前,如果上一次操作和这次操作有变化,就需要调用flip的原因。那我们写个代码试试。
-
package sty.zchi;
-
-
import java.nio.CharBuffer;
-
-
public class BufferTest {
-
public static void main(String[] args) {
-
CharBuffer charBuffer = CharBuffer.allocate(10);
-
System.out.println("刚初始化的charBuffer关键属性 position = " + charBuffer.position()
-
+ " limit = " + charBuffer.limit() + " capacity = " + charBuffer.capacity());
-
-
System.out.println("不讲武德的操作,啥都没写,就开始读 结果是" + charBuffer.get());
-
-
System.out.println("在不讲武德的一次写操作后,charBuffer关键属性position = " + charBuffer.position()
-
+ " limit = " + charBuffer.limit() + " capacity = " + charBuffer.capacity());
-
-
charBuffer.clear();
-
System.out.println("好吧不讲武德的操作影响了我们属性,我们clear一把后,看看charBuffer关键属性position = " + charBuffer.position()
-
+ " limit = " + charBuffer.limit() + " capacity = " + charBuffer.capacity());
-
-
char[] charArray = {'0', '1', '2', '3', '4', '5'};
-
charBuffer.put(charArray);
-
System.out.println("在放入6个char数据后charBuffer关键属性 position = " + charBuffer.position()
-
+ " limit = " + charBuffer.limit() + " capacity = " + charBuffer.capacity());
-
-
charBuffer.flip();
-
System.out.println("在放入6个char数据后charBuffer经过翻转后关键属性 position = " + charBuffer.position()
-
+ " limit = " + charBuffer.limit() + " capacity = " + charBuffer.capacity());
-
}
-
}
好吧运行结果是
我理解BUFFER将的也挺明白的了。
阅读(172) | 评论(0) | 转发(0) |