概述
Selector <---> Channel <---> Buffer,三者都支持双向的数据传递
在NIO网络编程中,Selector&Channel&Buffer三者的关系是十分紧密的,Buffer从Channel中读写,Channel注册在Selector中。在以往的网络编程中,通常都是通过创建一个线程来维护一个socket通讯,在业务量较小时,是可以很好的完成工作的,但是一旦客户端增多,创建的线程也随之增多,对硬件的开销是非常大的。这时候NIO的Selector就体现出了价值:
Selector在Java NIO中可以检测到一个或者多个Channel,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个Channel,从而管理多个网络连接。这样的单个线程管理管理多个Channel可以极大的减少线程间切换的开销。
示例
package com.leolee.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.channels.spi.SelectorProvider;
import java.util.Iterator;
import java.util.Set;
/**
* @ClassName SelectorTest
* @Description: NIO socket编程demo,用于理解Selector
* @Author LeoLee
* @Date 2020/9/22
* @Version V1.0
**/
public class SelectorTest {
//端口数组,用于和多个客户端建立连接后分配端口
int[] ports = null;
//起始端口
int tempPort = 5000;
//构造器初始化 端口数组ports,并从起始端口tempPort开始分配[size]个端口号
public SelectorTest (int size) {
this.ports = new int[size];
for (int i = 0; i < size; i++) {
this.ports[i] = tempPort + i;
}
}
public void selectorTest () throws IOException {
Selector selector = Selector.open();
//windows系统下是sun.nio.ch.WindowsSelectorProvider,如果是linux系统,则是KQueueSelectorProvider
//由于Selector.open()的源码涉及 sun 包下的代码,是非开源代码,具体实现不得而知
// System.out.println(SelectorProvider.provider().getClass());//sun.nio.ch.WindowsSelectorProvider
// System.out.println(sun.nio.ch.DefaultSelectorProvider.create().getClass());//sun.nio.ch.WindowsSelectorProvider
for (int i = 0; i < ports.length; i++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);//非阻塞模式
ServerSocket serverSocket = serverSocketChannel.socket();
//绑定端口
InetSocketAddress address = new InetSocketAddress("127.0.0.1", ports[i]);
serverSocket.bind(address);
//注册selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("[step1]监听端口:" + ports[i]);
}
//阻塞代码,始终监听来自客户端的连接请求
while (true) {
//获取我们“感兴趣的时间”已经准备好的通道,上面代码感兴趣的是SelectionKey.OP_ACCEPT,这里获取的就是SelectionKey.OP_ACCEPT事情类型准备好的通道
//number为该“感兴趣的事件“的通道数量
int number = selector.select();
System.out.println("number:" + number);
if (number > 0) {
//由于selector中会有多个通道同时准备好,所以这里selector.selectedKeys()返回的是一个set集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("[step2]selectionKeys:" + selectionKeys);
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//由于我们”感兴趣“的是SelectionKey.OP_ACCEPT,所以如下判断
if (selectionKey.isAcceptable()) {
//selectionKey.channel()返回是ServerSocketChannel的爷爷类SelectableChannel,所以做强制类型转换
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);//非阻塞模式
//重点重点重点重点重点重点重点重点重点重点
//将接收到的channel同样也注册到Selector上,Selector<--->channel<--->buffer,三者是双向的
socketChannel.register(selector, SelectionKey.OP_READ);//这时候”感兴趣的事件“是读操作,因为要接收客户端的数据了
//重点重点重点重点重点重点重点重点重点重点
//当以上代码执行完毕后,已经建立了服务端与客户端的socket连接,这时候就要移除Set集合中的selectionKey,以免之后重复创建该selectionKey对应的通道
iterator.remove();
System.out.println("[step3]成功获取客户端的连接:" + socketChannel);
} else if (selectionKey.isReadable()) {//判断selectionKey可读状态
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int byteRead = 0;
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
byteBuffer.clear();
int read = socketChannel.read(byteBuffer);
//判断数据是否读完
if (read <= 0) {
socketChannel.register(selector, SelectionKey.OP_READ);
break;
}
//写回数据,这里为了简单:读取什么数据,就写回什么数据
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteRead += read;
}
System.out.println("[step4]读取:" + byteRead + ",来自与:" + socketChannel);
//重点重点重点重点重点重点重点重点重点重点
//当以上代码执行完毕后,已经完成了对某一个已经“读准备好”通道的读写操作,这时候就要移除Set集合中的selectionKey,以免之后重复读写该selectionKey对应的通道
iterator.remove();
}
}
}
}
}
/*
* 功能描述: <br> 使用nc命令连接服务端:nc 127.0.0.1 5000
* 〈〉
* @Param: [args]
* @Return: void
* @Author: LeoLee
* @Date: 2020/9/23 12:59
*/
public static void main(String[] args) throws IOException {
SelectorTest selectorTest = new SelectorTest(5);
selectorTest.selectorTest();
}
}
基本思路:
- 通过构造方法定义5个监听端口
- 创建Selector,并将已经初始化完成的ServerSocketChannel注册在Selector上,Selector开始监听Channel
- 构造while死循环(阻塞代码),始终监听来自客户端的请求,通过判断Selector注册通道之后返回的SelectionKey集合中每一个SelectionKey状态,来处理不同的操作(建立连接、读、写)
”感兴趣的事件“是一个需要特别注意的概念:
主要分为四种,在SelectionKey类中定义为了四个常量
- Connect
- Accept
- Read
- Write
Channel向Selector注册的时候都要给定一个 int 类型的 参数 [ops],代表了监听“感兴趣”的通道类型,说人话就是之后返回的SelectionKey的状态。可以是复合状态:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
这是通过一种事件的形式,当某些事件完成后,标识了Channel的状态随之改变,所以SelectionKey所代表的Channel的状态也发生了改变,通过区分判断不同的状态,我们知道应该对这些Channel做对应的操作(建立连接、读、写)。
运行
运行服务端demo,服务端监听了5个端口
[step1]监听端口:5000
[step1]监听端口:5001
[step1]监听端口:5002
[step1]监听端口:5003
[step1]监听端口:5004
使用nc命令来连接服务端
服务端5000端口监听到客户端建立连接的请求并建立连接:
[step2]selectionKeys:[sun.nio.ch.SelectionKeyImpl@179d3b25]
[step3]成功获取客户端的连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]
客户端发送消息到服务端,当服务端收到消息后,将消息内容原封不动的返回给了客户端
[step2]selectionKeys:[sun.nio.ch.SelectionKeyImpl@20ad9418]
[step4]读取:13,来自与:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]
PS.
可以尝试多建立几个客户端,连接不同的端口来感受一下代码思路
需要代码的来这里拿嗷:demo项目地址