一、最简单的nio程序
public static void main(String[] args) throws Exception{
List<SocketChannel> list = new ArrayList<>();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定9000端口
serverSocketChannel.bind(new InetSocketAddress(9000));
//配置非阻塞(nio程序也可以设置成阻塞io,效果和bio一样)
serverSocketChannel.configureBlocking(false);
System.out.println("服务启动完成");
while (true){
//非阻塞模式,accept方法不会阻塞 accept函数
//Nio的非阻塞是由操作系统内部实现的,底层调用操作系统的
SocketChannel channel = serverSocketChannel.accept(); // sign:1
if(channel != null){
System.out.println("链接成功");
//SocketChannel设置为非阻塞
channel.configureBlocking(false);
list.add(channel);
}
Iterator<SocketChannel> iterator = list.iterator();
//遍历所有的链接,读取数据
while (iterator.hasNext()){ // sign:3
SocketChannel sc = iterator.next();
ByteBuffer allocate = ByteBuffer.allocate(128);
int read = sc.read(allocate); //sign:2
if(read > 0){
//读取到消息
System.out.println("收到消息:"+new String(allocate.array()));
}else if(read == -1){
//链接断开
System.out.println("有链接断开");
list.remove(sc);
}
}
}
}
在nio的程序中,默认是阻塞模式的,当然也可以通过配置,配置成非阻塞模式。当配置了非阻塞模式,上面程序的sign:1位置,和sign:2位置都不会阻塞。那么外层的while循环就会一直循环(空转)。
上述程序只是一个简单的写法,还是存在很多问题,比如sign:3位置,每次都需要遍历所有的客户端链接,读取数据。那么如果客户端链接很多但是真正有数据发送的客户端很少的情况下,那么代码执行的效率是很低的。
因为依然需要处理没有发送数据的客户端。
二、以上程序该怎么优化?
上述的程序的问题已经很明确了,就是当客户端链接数很大,而且真正发送数据的客户端很少的情况下,依然要遍历所有的客户端去读取数据。造成效率低下。
那么有没有办法使得程序在读取客户端信息的时候只读取有消息发送的客户端呢?也就是说比如有一万个链接,但是只有三个给服务器发送了数据,那么我仅仅只读取这三个客户端的数据就可以了,而不是所有的客户端都要遍历一遍。
三、Selector多路复用器
什么是多路复用器?
为了快速理解多路复用技术,我们以生活中的小案例进行说明。老张开大排档,刚刚起步的时候,客人比较少。
接待,炒菜,上菜都是老张一个人负责。老张的手艺不错,炒出来的菜味道可以。客人越来越多,每来个客人,老张都得花时间去接待,忙不过来。
于是老张就招了服务员,服务员收集每桌需要点的菜,然后把菜单交给老张,老张只负责做菜即可。在这里,服务员就充当了选择器,客户把自己的要求告诉服务员,服务员告诉老张。

那么对应我们的java程序也是一样,需要定义一个Selector,然后将ServerSocketChannel和SocketChannel注册到Selector上,当有客户端链接或者有客户端发送消息给服务端的时候,Selector就会通知程序来处理。

程序案例:
public static void main(String[] args) throws Exception{
List<SocketChannel> list = new ArrayList<>();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定9000端口
serverSocketChannel.bind(new InetSocketAddress(9000));
//配置非阻塞(nio程序也可以设置成阻塞io,效果和bio一样)
serverSocketChannel.configureBlocking(false);
//打开selector处理channel,即创建epoll
Selector selector = Selector.open();
//把ServerSocketChannel注册到Selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //sign:1
System.out.println("服务启动完成");
while (true){
//阻塞等待需要处理的事件发生
selector.select(); //sign:3
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//遍历所有的链接,读取数据
while (iterator.hasNext()){
SelectionKey sk = iterator.next();
if(sk.isAcceptable()){
System.out.println("有客户端链接");
//建立客户端链接
ServerSocketChannel sc = (ServerSocketChannel)sk.channel();
SocketChannel accept = sc.accept();
//设置为非阻塞
accept.configureBlocking(false);
//将链接注册到selector上,监听消息发送事件
accept.register(selector,SelectionKey.OP_READ); //sign:2
}else if(sk.isReadable()){
//客户端发送消息
SocketChannel socketChannel = (SocketChannel)sk.channel();
ByteBuffer allocate = ByteBuffer.allocate(128);
int read = socketChannel.read(allocate);
if(read > 0){
//读取到消息
System.out.println("收到消息:"+new String(allocate.array()));
}else if(read == -1){
//链接断开
System.out.println("有链接断开");
socketChannel.close();
}
}
iterator.remove();
}
}
}
可以重程序中看到,sign:1是将监听客户端链接的事件注册到Selector,sign:2是将读取客户端发送消息的事件注册到Selector。
那么这样无论是有客户端链接,还是有客户端发送消息,都会在sign:3位置监听到。从而处理对应事件。
至于为什么效率要比bio或者没有Selector的时候效率高,接下来的文章会介绍。
至此我们通过上述代码我们得到了一个很重要的结论,那就是nio不再需要为每个客户端链接创建一个线程去处理读取数据的问题。因为nio是非阻塞的,可以在一个线程内完成创建客户端链接,和读取客户端发送的消息的任务。