netty(1)–nio
原生JDK 网络编程NIO
NIO主要特点
面向流与面向缓冲
Java NIO 和 IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。 Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地 方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它 缓存到一个缓冲区。 Java NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓 冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查 是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要 覆盖缓冲区里尚未处理的数据。
阻塞与非阻塞 IO
Java IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目 前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以 直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线 程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在 可以管理多个输入和输出通道(channel)。
NIO三大核心组件
Selector 选择器、Channel 管道、Buffer 缓冲区
Selector
应用程序将向 Selector 对象注册需要它关注的 Channel,以及具体的某一个 Channel 会 对哪些 IO 事件感兴趣。Selector 中也会维护一个“已经注册的 Channel”的容器。
Channel
通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操 作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数 据,也可以通过通道向操作系统写数据,而且可以同时进行读写。
Buffer
缓冲区
核心概念 SelectionKey
SelectionKey 是一个抽象类,表示 selectableChannel 在 Selector 中注册的标识.每个 Channel 向 Selector 注册时,都将会创建一个 SelectionKey。SelectionKey 将 Channel 与 Selector 建立了 关系,并维护了 channel 事件。
可以通过 cancel 方法取消键,取消的键不会立即从 selector 中移除,而是添加到 cancelledKeys 中,在下一次 select 操作时移除它.所以在调用某个 key 时,需要使用 isValid 进行 校验.
SelectionKey 类型和就绪条件
在向 Selector 对象注册感兴趣的事件时,JAVA NIO 共定义了四种:OP_READ、OP_WRITE、 OP_CONNECT、OP_ACCEPT(定义在 SelectionKey 中),分别对应读、写、请求连接、接受 连接等网络 Socket 操作。
操作类型 | 就绪条件及说明 |
---|---|
OP_READ | 当操作系统读缓冲区有数据可读时就绪。并非时刻都有数据可读,所 以一般需要注册该操作,仅当有就绪时才发起读操作,有的放矢,避免浪 费 CPU。 |
OP_WRITE | 当操作系统写缓冲区有空闲空间时就绪。一般情况下写缓冲区都有空 闲空间,小块数据直接写入即可,没必要注册该操作类型,否则该条件不 断就绪浪费 CPU;但如果是写密集型的任务,比如文件下载等,缓冲区很 可能满,注册该操作类型就很有必要,同时注意写完后取消注册 |
OP_CONNECT | 当 SocketChannel.connect()请求连接成功后就绪。该操作只给客户端 使用。 |
OP_ACCEPT | 当接收到一个客户端连接请求时就绪。该操作只给服务器使用。 |
服务端和客户端分别感兴趣的类型
ServerSocketChannel 和 SocketChannel 可以注册自己感兴趣的操作类型,当对应操作类 型的就绪条件满足时 OS 会通知 channel,下表描述各种 Channel 允许注册的操作类型,Y 表 示允许注册,其中服务器 SocketChannel 指由服务器 ServerSocketChannel.accept()返回的对象。
OP_READ | OP_WRITE | OP_CONNECT | OP_ACCEPT | |
---|---|---|---|---|
服务器 ServerSocketChannel | Y | |||
服务器SocketChannel | Y | Y | ||
客户端SocketChannel | Y | Y | Y |
服务器启动 ServerSocketChannel,关注 OP_ACCEPT 事件, 客户端启动 SocketChannel,连接服务器,关注 OP_CONNECT 事件
服务器接受连接,启动一个服务器的 SocketChannel,这个 SocketChannel 可以关注 OP_READ、OP_WRITE 事件,一般连接建立后会直接关注 OP_READ 事件
客户端这边的客户端 SocketChannel 发现连接建立后,可以关注 OP_READ、OP_WRITE 事件,一般是需要客户端需要发送数据了才关注 OP_READ 事件
连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注 OP_READ、 OP_WRITE 事件。
Buffer缓冲区
Buffer 用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。 以写为例,应用程序都是将数据写入缓冲,再通过通道把缓冲的数据发送出去,读也是一样, 数据总是先从通道读到缓冲,应用程序再读缓冲的数据。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存( 其实就是数组)。 这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。
重要属性
capacity
作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里写 capacity 个 byte、long,char 等类型。一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据) 才能继续写数据往里写数据。
position
当你写数据到 Buffer 中时,position 表示当前能写的位置。初始的 position 值为 0.当一 个 byte、long 等数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单 元。position 最大可为 capacity – 1.
当读取数据时,也是从某个特定位置读。当将 Buffer 从写模式切换到读模式,position 会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer 的 limit 表示你最多能往 Buffer 里写多少数据。 写模式下,limit 等 于 Buffer 的 capacity。
当切换 Buffer 到读模式时, limit 表示你最多能读到多少数据。因此,当切换 Buffer 到 读模式时,limit 会被设置成写模式下的 position 值。换句话说,你能读到之前写入的所有数 据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)
Buffer的分配
要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有 allocate 方法(可以在堆上分配,也可以在直接内存上分配)。
分配 48 字节 capacity 的 ByteBuffer 的例子:ByteBuffer buf = ByteBuffer.allocate(48);
分配一个可存储 1024 个字符的 CharBuffer:CharBuffer buf = CharBuffer.allocate(1024);
wrap 方法:把一个 byte 数组或 byte 数组的一部分包装成 ByteBuffer:
ByteBuffer wrap(byte [] array)
ByteBuffer wrap(byte [] array, int offset, int length)
Buffer 的读写
向 Buffer 中写数据
写数据到 Buffer 有两种方式:
-
读取 Channel 写到 Buffer。
int bytesRead = inChannel.read(buf); //read into buffer.
-
通过 Buffer 的 **put()**方法写到 Buffer 里。
buf.put(127);
从 Buffer 中读取数据
从 Buffer 中读取数据有两种方式:
-
从 Buffer 读取数据写入到 Channel。
int bytesWritten = inChannel.write(buf);
-
使用 **get()**方法从 Buffer 中读取数据。
byte aByte = buf.get();
使用 Buffer 读写数据常见步骤
- 写入数据到 Buffer
- 调用 flip()方法
- 从 Buffer 中读取数据
- 调用 clear()方法或者 compact()方法,准备下一次的写入
当向 buffer 写入数据时,buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 buffer 的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空 缓冲区:调用 clear()或 compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清 除已经读过的数据。
NIO实战
1、
Selector 对象是通过调用静态工厂方法 open()来实例化的
Selector Selector=Selector.open();
2、
要实现 Selector 管理 Channel,需要将 channel 注册到相应的 Selector 上
channel.configureBlocking(false);
SelectionKey key= channel.register(selector,SelectionKey,OP_READ);
通过调用通道的 register()方法会将它注册到一个选择器上。与 Selector 一起使用时, Channel 必须处于非阻塞模式下,否则将抛出 IllegalBlockingModeException 异常,这意 味着不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式, 而套接字通道都可以。另外通道一旦被注册,将不能再回到阻塞状态,此时若调用通道的 configureBlocking(true)将抛出 BlockingModeException 异常。
register()方法的第二个参数是“interest 集合”,表示选择器所关心的通道操作, 它实际上是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。比如一个选 择器对通道的 read 和 write 操作感兴趣,那么选择器在检查该通道时,只会检查通道的 read 和 write 操作是否已经处在就绪状态。
如果 Selector 对通道的多操作类型感兴趣,可以用“位或”操作符来实现:
int interestSet=SelectionKey.OP_READ|SelectionKey.OP_WRITE;
同时 一个 Channel 仅仅可以被注册到一个 Selector 一次, 如果将 Channel 注册 到 Selector 多次, 那么其实就是相当于更新 SelectionKey 的 interest set。
通过 SelectionKey 可以判断 Selector 是否对 Channel 的某种事件感兴趣
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
通过 SelctionKey 对象的 readyOps()来获取相关通道已经就绪的操作。它是 interest 集合的子集,并且表示了 interest 集合中从上次调用 select()以后已经就绪的那些操作。 JAVA 中定义几个方法用来检查这些操作是否就绪selectionKey.isAcceptable();
同时,通过SelectionKey可以取出这个SelectionKey所关联的Selector和Channel。
如果我们要取消关联关系,怎么办?SelectionKey 对象的 cancel()方法来取消特定的 注册关系。
在实际的应用中,我们还可以为 SelectionKey 绑定附加对象,在需要的时候取出。 SelectionKey key=channel.register(selector,SelectionKey.OP_READ,theObject);
或 selectionKey.attach(theObject);
取出这个附加对象,通过:Object attachedObj = key.attachment();
3、
在实际运行中,我们通过 Selector 的 select()方法可以选择已经准备就绪的通道(这 些通道包含你感兴趣的的事件)。
下面是 Selector 几个重载的 select()方法:
select():阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout):和 select()一样,但最长阻塞事件为 timeout 毫秒。
selectNow():非阻塞,立刻返回。
select()方法返回的 int 值表示有多少通道已经就绪,是自上次调用 select()方法后有 多少通道变成就绪状态。
一旦调用 select()方法,并且返回值不为 0 时,则可以通过调用 Selector 的 selectedKeys()方法来访问已选择键集合。
Set selectedKeys=selector.selectedKeys();
这个时候,循环遍历 selectedKeys 集中的每个键,并检测各个键所对应的通道的就绪 事件,再通过 SelectionKey 关联的 Selector 和 Channel 进行实际的业务处理。
注意每次迭代末尾的 keyIterator.remove()调用。Selector 不会自己从已选择键集中 移除 SelectionKey 实例。必须在处理完通道时自己移除,否则的话,下次该通道变成就绪 时,Selector 会再次将其放入已选择键集中。
实战代码
Server端
public class NioServer {
private static NioServerHandle nioServerHandle;
public static void main(String[] args){
nioServerHandle = new NioServerHandle(DEFAULT_PORT);
new Thread(nioServerHandle,"Server").start();
}
}
public class NioServerHandle implements Runnable{
private volatile boolean started;
private ServerSocketChannel serverSocketChannel;
private Selector selector;
/**
* 构造方法
* @param port 指定要监听的端口号
*/
public NioServerHandle(int port) {
try {
/*创建选择器的实例*/
selector = Selector.open();
/*创建ServerSocketChannel的实例*/
serverSocketChannel = ServerSocketChannel.open();
/*设置通道为非阻塞模式*/
serverSocketChannel.configureBlocking(false);
/*绑定端口*/
serverSocketChannel.socket().bind(new InetSocketAddress(port));
/*注册事件,表示关心客户端连接*/
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
started = true;
System.out.println("服务器已启动,端口号:"+port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(started){
try {
/*获取当前有哪些事件*/
selector.select(1000);
/*获取事件的集合*/
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
/*我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活
的键出现,这会导致我们尝试再次处理它。*/
iterator.remove();
handleInput(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*处理事件的发生*/
private void handleInput(SelectionKey key) throws IOException {
if(key.isValid()){
/*处理新接入的客户端的请求*/
if(key.isAcceptable()){
/*获取关心当前事件的Channel*/
ServerSocketChannel ssc
= (ServerSocketChannel) key.channel();
/*接受连接*/
SocketChannel sc = ssc.accept();
System.out.println("==========建立连接=========");
sc.configureBlocking(false);
/*关注读事件*/
sc.register(selector,SelectionKey.OP_READ);
}
/*处理对端的发送的数据*/
if(key.isReadable()){
SocketChannel sc = (SocketChannel) key.channel();
/*创建ByteBuffer,开辟一个缓冲区*/
ByteBuffer buffer = ByteBuffer.allocate(1024);
/*从通道里读取数据,然后写入buffer*/
int readBytes = sc.read(buffer);
if(readBytes>0){
/*将缓冲区当前的limit设置为position,position=0,
用于后续对缓冲区的读取操作*/
buffer.flip();
/*根据缓冲区可读字节数创建字节数组*/
byte[] bytes = new byte[buffer.remaining()];
/*将缓冲区可读字节数组复制到新建的数组中*/
buffer.get(bytes);
String message = new String(bytes,"UTF-8");
System.out.println("服务器收到消息:"+message);
/*处理数据*/
String result = Const.response(message);
/*发送应答消息*/
doWrite(sc,result);
}else if(readBytes<0){
/*取消特定的注册关系*/
key.cancel();
/*关闭通道*/
sc.close();
}
}
}
}
/*发送应答消息*/
private void doWrite(SocketChannel sc,String response) throws IOException {
byte[] bytes = response.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
buffer.flip();
sc.write(buffer);
}
public void stop(){
started = false;
}
}
public class NioServerHandleWriteable implements Runnable{
private Selector selector;
private ServerSocketChannel serverChannel;
private volatile boolean started;
/**
* 构造方法
* @param port 指定要监听的端口号
*/
public NioServerHandleWriteable(int port) {
try{
//创建选择器
selector = Selector.open();
//打开监听通道
serverChannel = ServerSocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;
// 如果为 false,则此通道将被置于非阻塞模式
serverChannel.configureBlocking(false);//开启非阻塞模式
//绑定端口 backlog设为1024
serverChannel.socket()
.bind(new InetSocketAddress(port),1024);
//监听客户端连接请求
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
//标记服务器已开启
started = true;
System.out.println("服务器已启动,端口号:" + port);
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run() {
//循环遍历selector
while(started){
try{
//阻塞,只有当至少一个注册的事件发生的时候才会继续.
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Throwable t){
t.printStackTrace();
}
}
//selector关闭后会自动释放里面管理的资源
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
//处理新接入的请求消息
if(key.isAcceptable()){
//获得关心当前事件的channel
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//通过ServerSocketChannel的accept创建SocketChannel实例
//完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
SocketChannel sc = ssc.accept();
System.out.println("======socket channel 建立连接=======");
//设置为非阻塞的
sc.configureBlocking(false);
//连接已经完成了,可以开始关心读事件了
sc.register(selector, SelectionKey.OP_READ);
}
//读消息
if(key.isReadable()){
System.out.println("======socket channel 数据准备完成," +
"可以去读==读取=======");
SocketChannel sc = (SocketChannel) key.channel();
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position,position=0,
// 用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String message = new String(bytes,"UTF-8");
System.out.println("服务器收到消息:" + message);
//处理数据
String result = response(message) ;
//发送应答消息
doWrite(sc,result);
}
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
//TODO
}
}
//发送应答消息
private void doWrite(SocketChannel channel,String response)
throws IOException {
//将消息编码为字节数组
byte[] bytes = response.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//TODO
}
public void stop(){
started = false;
}
}
Client端
public class NioClient {
private static NioClientHandle nioClientHandle;
public static void start(){
nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP,DEFAULT_PORT);
new Thread(nioClientHandle,"Server").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
nioClientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) throws Exception {
start();
Scanner scanner = new Scanner(System.in);
while(NioClient.sendMsg(scanner.next()));
}
}
public class NioClientHandle implements Runnable{
private String host;
private int port;
private volatile boolean started;
private Selector selector;
private SocketChannel socketChannel;
public NioClientHandle(String ip, int port) {
this.host = ip;
this.port = port;
try {
/*创建选择器的实例*/
selector = Selector.open();
/*创建ServerSocketChannel的实例*/
socketChannel = SocketChannel.open();
/*设置通道为非阻塞模式*/
socketChannel.configureBlocking(false);
started = true;
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop(){
started = false;
}
@Override
public void run() {
try{
doConnect();
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
//循环遍历selector
while(started){
try{
//无论是否有读写事件发生,selector每隔1s被唤醒一次
selector.select(1000);
//获取当前有哪些事件可以使用
Set<SelectionKey> keys = selector.selectedKeys();
//转换为迭代器
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
/*我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活
的键出现,这会导致我们尝试再次处理它。*/
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Exception e){
e.printStackTrace();
System.exit(1);
}
}
//selector关闭后会自动释放里面管理的资源
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
//具体的事件处理方法
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
//获得关心当前事件的channel
SocketChannel sc = (SocketChannel) key.channel();
//连接事件
if(key.isConnectable()){
if(sc.finishConnect()){
socketChannel.register(selector,
SelectionKey.OP_READ);}
else System.exit(1);
}
//有数据可读事件
if(key.isReadable()){
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position,position=0,
// 用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("客户端收到消息:" + result);
}
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel channel,String request)
throws IOException {
//将消息编码为字节数组
byte[] bytes = request.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
/*关心事件和读写网络并不冲突*/
channel.write(writeBuffer);
}
private void doConnect() throws IOException{
/*非阻塞的连接*/
if(socketChannel.connect(new InetSocketAddress(host,port))){
socketChannel.register(selector,SelectionKey.OP_READ);
}else{
socketChannel.register(selector,SelectionKey.OP_CONNECT);
}
}
//写数据对外暴露的API
public void sendMsg(String msg) throws Exception{
doWrite(socketChannel, msg);
}
}