上一篇已经说到了缓冲区,NIO编程需要用到的。但是说到NIO编程的基础和重点,还是非Selector莫属,就是多路复用器。
下面先简单介绍一下Selector,然后再放个NIO编程的例子。
多路复用器(Selector),他是NIO编程的基础,非常的重要,它提供选择已经就绪的任务的能力。简单点说,就是Selector会不断地轮询注册在其上的通道(Channel),当然包括服务端和客户端的。如果某个通道发生了读写操作操作,这个通道就处于就绪状态,然后会被Selector轮询出来,然后通过SelectionKey可以取得就绪状态的Channel集合,从而进行后续的IO操作。
一个多路复用器(Selector)可以负责成千上万的Channel通道,没有上限的那种,这也是JDK使用了epoll代替了传统的select实现,获得连接句柄没有限制。这也意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这也是JDK库的巨大进步。
Selector线程就类型一个管理者(Master),为什么说是线程,因为上面说道只要一个线程负责Selector的轮询。它可以管理成千上万的管道,只要轮询获得Channel集合,就可以获得就绪好的管道,然后获取管道准备好的数据,然后通知CPU执行IO的读取或者写入操作。
下面简单说一下这种模式。当IO事件(管道)注册到选择器以后,selector会分配给每个管道一个key值,相当于这个管道的标签。selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当我们的IO事件(管道)准备就绪后,selector就会识别,通过key值来找到对应的管道,进行相关的数据处理操作(从管道中读或写数据,写到我们的数据缓冲区中)。
下面说一下管道注册到多路复用器的不用的事件状态有哪些。这些状态是为了方便选择器查找。
SelectionKey.OP_CONNECT 连接状态
SelectionKey.OP_ACCEPT 接收状态
SelectionKey.OP_READ 读取状态
SelectionKey.OP_WRITE 写入状态
下面的例子只是简单的一个客户端连接服务端,然后往服务端写数据,服务端读取后打印出来。
Server代码:
public class Server implements Runnable{
private Selector selector; //多路复用器
private ByteBuffer readBuf = ByteBuffer.allocate(1024); //缓冲区
public Server(int port){ //构造函数
try {
//1、打开多路复用器
this.selector = Selector.open();
//2、打开服务器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3、服务器通道记得设置为feizuse
ssc.configureBlocking(false);
//4、服务器通道绑定地址
SocketAddress local = new InetSocketAddress(port);
ssc.bind(local);
//5、将服务器通道注册到多路复用器那,并且监听阻塞事件
ssc.register(this.selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器已经启动,端口号为:"+port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){ //一直循环,让多路复用器一直监听
try {
//1、让多路复用器开始监听(必须的)
this.selector.select();
//2、返回多路复用器已经选择的结果集
Set<SelectionKey> keys= this.selector.selectedKeys();
//3、遍历结果集,进行处理
Iterator<SelectionKey> ite = keys.iterator();
while(ite.hasNext()){
SelectionKey key = ite.next();
//获取后直接从容器中移出就可以了
ite.remove();
if(key.isValid()){//如果key是有效的
if(key.isAcceptable()){ //如果key为阻塞状态(这个和注册的时候绑定的状态对应)
this.accept(key);
}
if(key.isReadable()){ //如果是可读状态
this.read(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void accept(SelectionKey key) {
try {
// 先获取服务器通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//调用服务器端的accpet方法获取客户端通道
SocketChannel sc = ssc.accept();
//设置为非阻塞
sc.configureBlocking(false);
//将客户端通道注册到多路复用器中
sc.register(this.selector,SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 这个方法是读取客户端发送给服务端的数据的。因为客户端通道注册时的注册状态为read
* @param key
*/
private void read(SelectionKey key) {
try {
//1、先清空缓冲区,防止有上一次的读数据
this.readBuf.clear();
//2、获取客户端通道
SocketChannel sc = (SocketChannel) key.channel();
//3、看客户端是否有输入
int count = sc.read(this.readBuf);
if(count == -1){//如果没有数据
sc.close();
key.cancel();
}
//4、如果有数据则进行读取,读取之前记得要进行缓冲区复位。
this.readBuf.flip();
//5、根据缓冲区的数据长度创建对应大小的byte数组,接受缓冲区的数据
byte[] data = new byte[this.readBuf.remaining()];
//6、将缓冲区数据弄到byte数组里面
this.readBuf.get(data);
//7、将byte数组转为字符串打印出来
String result = new String(data);
System.out.println("Server接受到client的数据:"+result);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//开启一个线程,保证多路复用器一直在轮询
new Thread(new Server(8765)).start();
}
}
Client代码:
public class Client {
public static void main(String[] args){
try {
//1、创建以一个客户端通道
SocketChannel sc = SocketChannel.open();
//这里是服务器端的IP地址和端口号
InetSocketAddress add = new InetSocketAddress("127.0.0.1", 8765);
//客户端通道连接服务器端通道
sc.connect(add);
//定义缓冲区,拿来接收用户的输入
ByteBuffer buf = ByteBuffer.allocate(1204);
while(true){ //死循环,用户可以无限输入
//定义一个字节数组,然后使用系统的输入功能
byte[] bytes = new byte[1024];
System.in.read(bytes);
//将数据放入缓冲区
buf.put(bytes);
//记得进行复位操作
buf.flip();
//写出数据
sc.write(buf);
//清空缓冲区
buf.clear();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}