1、Selector(选择器)
1.1、基本介绍
- Java的NIO使用了非阻塞的io方式->一个线程处理若干个客户端连接,使用到了selector(选择器)。
- Selector能够检测到多个注册通道上是否有事件发生(多个Channel以事件的形式注册到同一个selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。
- 只有在连接/通道真正有读写事件发生时,才会进行读写,减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
- 避免了多线程之间上下文切换导致的开销。
Netty的IO线程NioEventLoop聚合了Selector(选择器 / 多路复用器),可以并发处理成百上千个客户端连接。
当线程从某客户端Socket通道进行读写时,若没有数据可用,该线程可以进行其他任务。
线程通常将非阻塞IO的空闲时间用于其他通道上执行IO操作,所以单独的线程可以管理多个输入输出通道。
由于读写操作都是非阻塞的,就可以充分提高IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起。
一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO一连接一线程模型,架构性能、弹性伸缩能力和可靠性都得到极大地提升。
1.2、Selector类相关方法
1.2.1、Selector类是一个抽象类,常用方法:
Public abstract class Selector implement Closeable{
Public static Selector open(); //得到一个选择器对象
Public int select(long timeout); //监控所有注册的通道,当其中的IO操作可以进行时,将对应的selectionkey加入内部集合并返回,参数设置超时时间
Public Set<SelectionKey> selectionKeys(); //从内部集合中得到所有的SelectionKey
}
1.2.2、Selector相关方法说明:
Selector.select(); //若未监听到注册管道中有事件,则持续-阻塞
Selector.select(1000); //阻塞1000毫秒,1000毫秒后返回
Selector.wakeup(); //唤醒selector
Selector.selectNow(); //不阻塞,立即返回
1.3、NIO非阻塞网络编程原理分析
1.3.1、NIO非阻塞网络编程相关(Selector,SelectionKey,ServerSocketChannel,SocketChannel)
说明
- 当客户端连接时,会通过SeverSocketChannel得到对应的SocketChannel
- Selector进行监听,调用select()方法,返回注册该Selector的所有通道中有事件发生的通道个数
- 将socketChannel注册到Selector上,public final SelectionKey register(Selector sel, int ops),一个selector上可以注册多个SocketChannel
- 注册后返回一个SelectionKey,会和该Selector关联(以集合的形式)
- 进一步得到各个SelectionKey(有事件发生
- 再通过SelectionKey反向获取SocketChannel,使用channnel()方法
- 可以通过得到的channel,完成业务
【注】SelectionKey中定义了四个操作标志位:OP_READ—表示通道中发生读事件;OP_WRITE—表示通道中发生写事件;OP_CONNECT—表示建立连接;OP_ACCEPT—请求新连接
1.3.2、NIO非阻塞网络编程案例
【1】要求:编写一个NIO入门案例,实现服务器和客户端之间的数据简单通讯(非阻塞)
【2】设计思路
服务器端:
1)创建ServerSocketChannel,将其设置为非阻塞,并绑定一个端口进行监听。
2)创建一个Selector对象,将ServerSocketChannel注册到Selector上,并设置该Selector关注的事件(OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT)
3)调用select()方法,循环等待客户端channel的连接
4)一旦有客户端连接,则返回一个SelectionKey的集合
5)使用迭代器将集合中的Key取出,获取其对应通道的事件{
isAcceptable(): | 有新的客户端请求连接 |
isConnectable(): | 数据传输完成,连接结束 |
isReadable(): | 测试该通道是否有数据可以读取 |
isWritable(): | 测试该通道是否有数据写入 |
}
6)每处理完一个通道的请求,需要将其对应的Key从SelectionKey的集合中移除,防止操作被重复执行。
import java.io.IOException;
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.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel,类似ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//创建一个Selector对象
Selector selector = Selector.open();
//绑定一个端口,在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把ServerSocketChannel注册到selector,关心事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey的数量 = "+selector.keys().size());
//循环等待客户连接
while(true){
//这里设置了等待1秒,如果1秒内没有事件发生,返回
if (selector.select(1000)==0){ //无事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的大于0,就获取到相关的selectionKey集合
//1、如果返回的>0,表示已经获取到关注的事件
//2、selector.selectedKeys()方法返回关注事件的集合
// 通过selectedKeys反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionkeys的数量 = "+ selectionKeys.size());
//遍历集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//获取到selectionKey
SelectionKey key = iterator.next();
//根据Key所对应的通道发生的事件做相应处理
if (key.isAcceptable()){
//如果是OP_ACCEPT,表示有新的客户端连接
//给该客户端生成一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
System.out.println("客户端连接成功,生成一个socketChannel "+socketChannel.hashCode());
//将当前的socketChannel注册到selector,关注事件为读事件,同时给socket Channel关联一个`buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后,注册的selectionkey的数量 = "+selector.keys().size());
}
if (key.isReadable()){ //发生读事件
//通过key 反向获取到对应的channel
SocketChannel channel = (SocketChannel) key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("from 客户端 "+ new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey,防止重复操作
iterator.remove();
}
}
}
}
客户端:
1)首先同样要创建一个通道SocketChannel,类似与Socket通信编程,将其设置为非阻塞形式,并提供服务器IP和端口号进行连接。
2)客户端请求连接,服务端进行处理需要一定的时间,不妨设置一个等待时间,如果在规定的时间内没能连接到服务端,返回连接失败信息。
3)如果连接成功,则发送数据,发送方式参照之前的,通过SocketChannel的write方法写入到byteBuffer中。
import org.w3c.dom.ls.LSInput;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞模式
socketChannel.configureBlocking(false);
//提供服务器端的IP和端口
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(socketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("连接需要时间,客户端不会阻塞,可以做其他工作");
}
}
//如果连接成功,就发送数据
String str = "hello,NIO";
//Wraps a byte array into a buffer
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
//发送数据,即将buffer中的数据写入到channel中
socketChannel.write(byteBuffer);
System.in.read();
}
}
1.4、SelectionKey的相关方法
public abstract class SelectionKey { public abstract Selector selector(); public abstract SelectableChannel channel(); public final Object attachment() public abstract SelectionKey interestOps(int ops); public final boolean isReadable(); public final boolean isWritable() public final boolean isAcceptable() } |
//得到与之关联的Selector对象 //得到与之关联的通道 //得到与之关联的共享数据 //设置或改变监听的事件类型 //测试通道是否可以读 //测试是否可写 //测试是否可以建立连接ACCEPT |
1.5、SeverSocKetChannel API介绍
1、SeverSocKetChannel在服务器端监听新的客户端Socket连接
2、相关方法如下:
public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{ | |
public static ServerSocketChannel open(); | //得到一个ServerSocketChannel通道 |
public void bind(SocketAddress endpoint); | //设置服务器端口号 |
public final SelectableChannel configureBlocking(boolean block); | //设置阻塞或非阻塞模式,取值false表示采用非阻塞模式 |
public abstract SocketChannel accept(); | //接受一个连接,返回代表这个连接的通道对象 |
public final SelectionKey register(Selector sel, int ops); | //注册一个选择器并设置监听事件 |
} |
1.6、SocketChannel API介绍
- SocketChannel,网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。相关方法如下:
public abstract class SocketChannel
extends AbstractSelectableChannel Implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{
public static SocketChannel open();
//得到一个SocketChannel通道
public final SelectableChannel configureBlocking(boolean block);
//设置阻塞或非阻塞模式,取值false为非阻塞模式
public abstract boolean connect(SocketAddress remote);
//连接服务器
public abstract boolean finishConnect();
//如果上述方法连接失败,接下来则通过该方法完成链接
public abstract int write(ByteBuffer src);
//向通道中写入数据
public abstract int read(ByteBuffer dst);
//从通道中读取数据
public final SelectionKey register(Selector sel, int ops,Object att);
//注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
public final void close();
//关闭通道 }
2、NIO网络编程应用实例-群聊系统
2.1、要求:
- 1、编写一个NIO群聊系统,实现服务器端与客户端的数据简单通讯(非阻塞)实现多人群聊
- 2、服务器端:可以检测用户上线,离线,并实现消息转发功能
- 3、客户端:通过channel可以无阻塞发送数据给其他所有用户,同时可以接收其他用户发送的消息(由服务器转发得到)
-
2.2、目的:进一步理解NIO非阻塞网络编程机制
2.3、设计思路:
2.3.1、先编写服务器端
- 服务器端自动并监听端口6667
- 服务器提示客户端上线和离线
- 服务器接收客户端信息,并实现转发
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; public class GroupChatServer { //定义相关的属性 private Selector selector; private ServerSocketChannel listenChannel; private static final int PORT = 6667; //构造器 初始化操作 public GroupChatServer(){ try{ //得到选择器 selector = Selector.open(); //初始化ServerSocketChannel listenChannel = ServerSocketChannel.open(); //绑定端口 listenChannel.socket().bind(new InetSocketAddress(PORT)); //设置非阻塞 listenChannel.configureBlocking(false); //将该listenChannel注册到selector listenChannel.register(selector, SelectionKey.OP_ACCEPT); }catch (IOException e){ e.printStackTrace(); } } //监听 public void listen(){ try{ //循环处理 while(true){ int count = selector.select(); if(count > 0){ //如果有事件则处理 //遍历得到的selectorKey集合 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ //取出SelectionKey SelectionKey key = iterator.next(); //监听到ACCEPT事件 if (key.isAcceptable()){ SocketChannel sc = listenChannel.accept(); sc.configureBlocking(false); //将该sc注册到selector上 sc.register(selector,SelectionKey.OP_READ); //提示上线了 System.out.println(sc.getRemoteAddress()+"上线了。。。"); } if (key.isReadable()){ //通道发生read事件 //专门处理读数据的方法 readData(key); } //将当前的key删除,防止重复处理 iterator.remove(); } }else{ System.out.println("等待...."); } } }catch(Exception e){ e.printStackTrace(); }finally { //发生的异常处理 } } //读取客户端消息 private void readData(SelectionKey key){ //定义一个SocketChannel SocketChannel channel = null; try{ //取到关联的channel channel = (SocketChannel) key.channel(); //创建缓冲buffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int count = channel.read(byteBuffer); //根据count的值做处理 if (count>0){ //把缓冲区的数据转换成字符串 String msg = new String(byteBuffer.array()); //输出该消息 System.out.println("from 客户端:"+ msg); //向其他客户端转发消息,专门写一个方法处理 sendInfoToOthers(msg,channel); } }catch (IOException e){ //e.printStackTrace(); try { System.out.println(channel.getRemoteAddress() + "已下线"); //取消注册 key.cancel(); //关闭通道 channel.close(); }catch (Exception r){ r.printStackTrace(); } } } //转发消息给其他的客户,实际是转发给其他通道************需要排除自身 private void sendInfoToOthers(String msg, SocketChannel self) throws IOException { //服务器转发消息 System.out.println("服务器转发消息中。。。"); //遍历所有注册到selector的socketchannel并排除自身 for (SelectionKey key: selector.keys()){ //反向获取通道 Channel targetchannel = key.channel(); //排除自身 if (targetchannel instanceof SocketChannel && targetchannel !=self){ //转型 SocketChannel dest = (SocketChannel) targetchannel; //将msg存储到buffer中 ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); //将buffer中的数据写入通道 dest.write(buffer); } } } public static void main(String[] args) { //创建一个服务器对象 GroupChatServer groupChatServer = new GroupChatServer(); //监听 groupChatServer.listen(); } }
2.3.2、编写客户端
- 连接服务器
- 向服务器发送消息
- 接收服务器消息
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Scanner; import java.util.Set; public class GroupChatClient { //定义相关的属性 private final String HOST = "127.0.0.1"; private final int PORT = 6667; private Selector selector; private SocketChannel socketChannel; private String username; //构造器,完成初始化工作 public GroupChatClient() throws IOException { selector = Selector.open(); socketChannel = socketChannel.open(new InetSocketAddress(HOST,PORT)); socketChannel.configureBlocking(false); //将channel注册到selector、 socketChannel.register(selector, SelectionKey.OP_READ); //得到用户名 username = socketChannel.getLocalAddress().toString().substring(1); System.out.println(username + " is ready..."); } //向服务器发送消息 public void sendInfo(String info){ info = username + "说" + info; try { socketChannel.write(ByteBuffer.wrap(info.getBytes())); }catch (IOException e){ e.printStackTrace(); } } //从服务器端读取消息 public void readInfo(){ try { int readChannels = selector.select(); if (readChannels > 0){ //有可用的通道 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); if (key.isReadable()){ //得到相关的通道 SocketChannel sc = (SocketChannel) key.channel(); //得到一个buffer ByteBuffer buffer = ByteBuffer.allocate(1024); //读取 sc.read(buffer); //把读取到的缓冲区数据转成字符串 String msg = new String(buffer.array()); System.out.println(msg.trim()); } } iterator.remove(); //删除当前的selectionKey,防止重复操作 }else { //System.out.println("没有可用的通道"); } }catch (Exception e){ e.printStackTrace(); } } public static void main(String[] args) throws IOException { //启动客户端 GroupChatClient chatClient = new GroupChatClient(); //启动一个线程,每隔3秒,读取从服务器端发送的数据 new Thread(){ public void run(){ while (true){ chatClient.readInfo(); try{ Thread.currentThread().sleep(3000); }catch (InterruptedException e){ e.printStackTrace(); } } } }.start(); //发送数据给服务器 Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()){ String s = scanner.nextLine(); chatClient.sendInfo(s); } } }