今天继续来复习NIO三剑客的最后1个:selector

selector的工作原理,简单来看,就是上面这张图,Channel必须先向Selector注册(注:register的时候,可以选择关注哪些事件,比如:有新连接 或 有数据可读 等),注册成功后,Selector通过select方法来检查这些Channel上是否有事件发生,比如:有数据发过来,channel就可以把数据读到Buffer中。

这三者在类设计的层面是怎么串起来的?下面是Selector类的示意图:

当Channel注册上来时,会被包装成一个SelectionKey放到Set中,通过keys()方法可以得到所有注册的SelectionKey。当Channel上有事件发生时,通过selectedKeys()方法,可以得到所有当前有事件发生的Set集合。很显然,selectedKeys是keys的子集。

SelectionKey类中,又通过channel()方法,持有Channel的引用,这样就能通过该引用来向Buffer读/写数据(注:记住NIO中,向Buffer中写入数据,在网络编程中其实就是向对方发数据)。

看起来并不复杂,但真正用NIO写一个基本的Server端Demo,还是要很多步骤的,正所谓知易行难,梳理了一张图,大家可以感受一下:

值得一提的是,这里用到二类Channel,它们都继承自SelectableChannel

下面来热热身,写一个最基本的ServerDemo:

package test.nio.study;

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.Arrays;
import java.util.Iterator; /**
* @author 菩提树下的杨过(http://yjmyzz.cnblogs.com)
*/
public class ServerDemo { public static void main(String[] args) throws IOException { InetSocketAddress addr = new InetSocketAddress(8086); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定ip:port
serverSocketChannel.bind(addr);
//配置为非阻塞
serverSocketChannel.configureBlocking(false); //获取Selector
Selector selector = Selector.open(); //将channel注册到Selector(仅关注:新连接事件)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //轮询事件
while (true) {
//每100ms轮一次
if (selector.select(100) == 0) {
continue;
} //如果有事件发生,则拿到一个SelectionKey集合的迭代器
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) {
SelectionKey key = iterator.next(); try {
//判断事件类型(当有新连接请求时)
if (key.isAcceptable()) {
//(接受)创建新连接,同时返回新连接的Channel(注:accept方法是阻塞的)
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
System.out.println(socketChannel.getRemoteAddress() + " is connected");
//向client回显一句话
socketChannel.write(ByteBuffer.wrap(("hello:" + socketChannel.getRemoteAddress() + "\n").getBytes()));
//新连接的Channel,也要注册到Selector,并关注读取事件(以响应客户端发过来的消息)
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} //有数据可读取时
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
try {
int count = channel.read(buffer);
if (count != -1) {
System.out.println(channel.getRemoteAddress() + " say:" + new String(Arrays.copyOf(buffer.array(), count)));
}
} catch (IOException e) {
System.err.println(channel.getRemoteAddress() + " disconnected");
continue;
}
buffer.clear();
}
} finally {
//处理完后,一定要将自身移除,否则下一次select有事件触发时,无法正常处理
iterator.remove();
}
}
}
}
}

关键地方都加了详细注释,应该不难理解,把这个程序跑起来。

然后开2个终端(我是mac环境),都输入 telnet localhost 8160,相当于2个client端连接上来,然后每个终端里打几个字,向Server端发点数据。

接下来改造一下,写一个多人聊天室的原型,要实现的基本功能如下:
server端:

1、client连接成功时,server自动发问候语

2、新client上线时,通知其它client

3、有client说话时,转发给其它client(即:所有人,都能看到其它人发的最新消息)

4、有client下线(或断网时),通知其它人

client端:

1、连接到server端

2、能正常收发消息

先写client端代码,这个相对容易点:

package test.nio.study;

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.Arrays;
import java.util.Iterator;
import java.util.Set; /**
* 菩提树下的杨过
*/
public class ChatRoomClient { public static void main(String[] args) {
new ChatRoomClient("localhost", 8086).start();
} SocketChannel socketChannel;
Selector selector;
InetSocketAddress serverAddress;
boolean isConnected = false; public ChatRoomClient(String host, int port) {
serverAddress = new InetSocketAddress(host, port);
} /**
* 连接到server
*
* @param address
*/
private void connect(InetSocketAddress address) {
try {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
boolean connect = socketChannel.connect(address);
if (!connect) {
//注:建立连接是需要时间的,调用完connect方法后,这里返回的connect大概率是false
int timeout = 5000;
long start = System.currentTimeMillis();
//所以要通过finishConnect来轮询,才能知道最终是否连接成功,
while (!socketChannel.finishConnect()) {
Thread.sleep(50);
if (System.currentTimeMillis() - start > timeout) {
System.err.println("connect to " + address + " timeout");
return;
}
}
}
selector = Selector.open();
//注册,只关注读事件,同时关联一个buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} catch (Exception e) {
System.err.println(e);
}
isConnected = true;
System.err.println(address + " connect successfully"); } /**
* 接收消息
*
* @param selector
*/
private void readMessage(Selector selector) {
if (!isConnected) {
return;
}
while (true) {
try {
//注:这里使用了阻塞版本的select方法(不想阻塞的话,可以用selectNow)
if (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int count = channel.read(buffer);
if (count != -1) {
System.out.println(new String(Arrays.copyOf(buffer.array(), count)));
}
buffer.clear();
//千万要记得这个,不然下次事件触发,无法正常处理
iterator.remove();
}
}
} catch (IOException e) {
System.err.println("read message error:" + e);
}
}
} /**
* 发送消息
*
* @param channel
*/
private void sendMessage(SocketChannel channel) {
if (!isConnected) {
return;
}
byte[] buffer = new byte[1024];
while (true) {
try {
/**
* 等待键盘输入内容
*/
int count = System.in.read(buffer);
if (count > 0) {
channel.write(ByteBuffer.wrap(Arrays.copyOf(buffer, count - 1)));
}
} catch (IOException e) {
System.err.println("send message error:" + e);
}
}
} /**
* 启动
*/
private void start() {
connect(serverAddress);
new Thread(() -> {
sendMessage(socketChannel);
}, "send-thread").start(); new Thread(() -> {
readMessage(selector);
}, "read-thread").start();
}
}

  

服务端:

package test.nio.study;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set; /**
* @author 菩提树下的杨过
*/
public class ChatRoomServer { ServerSocketChannel serverSocketChannel;
Selector selector; public ChatRoomServer(int port) {
InetSocketAddress addr = new InetSocketAddress(port);
try {
serverSocketChannel = ServerSocketChannel.open();
//绑定ip:port
serverSocketChannel.bind(addr);
//配置为非阻塞
serverSocketChannel.configureBlocking(false);
//获取Selector
selector = Selector.open();
//将channel注册到Selector(仅关注:新连接事件)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
System.err.println("server init fail:" + e);
}
System.out.println("server started");
} void connHandle(SelectionKey key) throws IOException {
//(接受)创建新连接,同时返回新连接的Channel(注:accept方法是阻塞的)
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
System.err.println(socketChannel.getRemoteAddress() + " connected");
//向client回显一句话
String message = "hello," + socketChannel.getRemoteAddress() + "\n";
socketChannel.write(ByteBuffer.wrap(message.getBytes()));
sendMessageToOther(key, socketChannel, "hi, all");
//新连接的Channel,也要注册到Selector,并关注读取事件(以响应客户端发过来的消息)
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } void sendMessageToOther(SelectionKey key, SocketChannel channel, String message) throws IOException {
Set<SelectionKey> keys = selector.keys();
//转发到其它client
for (SelectionKey otherKey : keys) {
if (!otherKey.equals(key)) {
SelectableChannel otherChannel = otherKey.channel();
if (otherChannel instanceof SocketChannel) {
((SocketChannel) otherChannel).write(ByteBuffer.wrap((channel.getRemoteAddress() + " say:" + message).getBytes()));
}
}
}
} boolean readHandle(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
try {
int count = channel.read(buffer);
if (count != -1) {
String message = new String(Arrays.copyOf(buffer.array(), count));
System.out.println(channel.getRemoteAddress() + " say:" + message);
//转发到其它人
sendMessageToOther(key, channel, message);
}
} catch (IOException e) {
String message = channel.getRemoteAddress() + " disconnected";
System.err.println(message);
sendMessageToOther(key, channel, "disconnected");
return false;
}
buffer.clear();
return true;
} void start() {
//轮询事件
while (true) {
try {
//每100ms轮一次
if (selector.select(100) == 0) {
continue;
} //如果有事件发生,则拿到一个SelectionKey集合的迭代器
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) {
SelectionKey key = iterator.next();
try {
//判断事件类型(当有新连接请求时)
if (key.isAcceptable()) {
connHandle(key);
} //有数据可读取时
if (key.isReadable()) {
if (!readHandle(key)) {
continue;
}
} } finally {
//处理完后,一定要将自身移除,否则下一次select有事件触发时,无法正常处理
iterator.remove();
}
}
} catch (IOException e) {
System.err.println(e);
}
}
} public static void main(String[] args) {
new ChatRoomServer(8086).start();
}
}

跑起来看看,不过这里有1个小技巧,使用idea的话,默认情况下,每个程序都是单实例运行,如果要同时启用多个client是不行,可参考下图设置:

server端运行截图:

client-1运行截图:

client-2截图:

client-3运行截图:

参考文章:

https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/nio/channels/Selector.html

NIO复习(3):selector的更多相关文章

  1. epoll浅析以及nio中的Selector

    出处: https://my.oschina.net/hosee/blog/730598 首先介绍下epoll的基本原理,网上有很多版本,这里选择一个个人觉得相对清晰的讲解(详情见reference) ...

  2. epoll 浅析以及 nio 中的 Selector

    首先介绍下epoll的基本原理,网上有很多版本,这里选择一个个人觉得相对清晰的讲解(详情见reference): 首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作 ...

  3. 两种 NIO 实现:Selector 与 Epoll

    [总结]两种 NIO 实现:Selector 与 Epoll 时间2012-11-17 08:38:42 开源中国新闻原文  http://my.oschina.net/ielts0909/blog/ ...

  4. Java I/O(4):AIO和NIO中的Selector

    您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来- 在Java NIO的三大核心中,除了Channel和Buffer,剩下的就是Selector了.有的地方叫它选择器,也有叫多路复用器的(比如Ne ...

  5. 7. 彤哥说netty系列之Java NIO核心组件之Selector

    --日拱一卒,不期而至! 你好,我是彤哥,本篇是netty系列的第七篇. 简介 上一章我们一起学习了Java NIO的核心组件Buffer,它通常跟Channel一起使用,但是它们在网络IO中又该如何 ...

  6. JAVA NIO复习笔记

    1. JAVA NIO是什么? 从JDK1.4开始,java提供了一系列改进的输入/输出处理的新功能,这些功能被统称为新IO(New IO,简称NIO),新增了许多用于处理输入/输出的类,这些类都被放 ...

  7. NIO相关概念之Selector

    选择器(selector): 选择器管理者一个被注册的通道的集合信息和它们的就绪状态.通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态,当这么做的时候,可以选择被激发的线程挂起,直到有就 ...

  8. 【总结】两种 NIO 实现:Selector 与 Epoll

    时间2012-11-17 08:38:42 开源中国新闻原文  http://my.oschina.net/ielts0909/blog/89849 我想用这个话题小结下最近这一阶段的各种测试和开发. ...

  9. NIO复习03

    SocketChannel: 1. Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道.可以通过以下2种方式创建SocketChannel: 打开一个SocketChan ...

  10. NIO复习02

    Selector 1. Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件.这样,一个单独的线程可以管理多个channel,从而管 ...

随机推荐

  1. 【记录】环境|Ubuntu18.04 中搭建 Python 开发和调试环境的完整记录

    文章目录 安装Python并切换 1 安装某个版本 方式一:pyenv安装(强烈推荐) 方式二:apt安装(不推荐) Python3 Python2 查看所有apt装上的版本 2 切换python版本 ...

  2. 【FAQ】HarmonyOS SDK 闭源开放能力 — PDF Kit

    1.问题描述: 预览PDF文件,文档上所描述的loadDocument接口,可以返回文件的状态,并无法实现PDF的预览,是否有能预览PDF相关接口? 解决方案: 1.执行loadDocument进行加 ...

  3. 代理模式-Proxy(动态代理)

    代理模式(Proxy) 一.作用 又叫"动态代理" 为其他对象提供一种代理以控制对这个对象的访问 二.结构图 三.场景1 远程代理: 也就是为一个对象在不同的地址空间提供局部代表. ...

  4. kubernetes部署1.15.0版本

    部署环境 centos7.4 master01: 192.168.85.110 node01: 192.168.85.120 node02: 192.168.85.130 所有节点都要写入hosts ...

  5. ArkUI-X跨平台框架接入指南

    ArkUI跨平台框架(ArkUI-X)进一步将ArkUI开发框架扩展到了多个OS平台:目前支持OpenHarmony.Android. iOS,后续会逐步增加更多平台支持.开发者基于一套主代码,就可以 ...

  6. 赣CTF-Misc方向wp

    checkin 下载附件,一张图片,拖进010,在文件尾看到隐藏文本,提取并用社会主义价值解密 ez_forensics 提示为结合题目进行想象,我们会想到取证第一步vc挂载,但是需要密码,研究图片, ...

  7. 玩客云 OEC/OECT 笔记

    外观 内部 PCB正面 PCB背面 PCB背面 RK3566 1Gbps PHY 配置 OEC 和 OECT(OEC-turbo) 都是基于瑞芯微 RK3566/RK3568 的网络盒子, 没有HDM ...

  8. The length of parametric curve (x + sin x, cos x)

    问题引入 一个硬币(圆)的周长上有一个点,将硬币在一条线上无滑动地滚动.假设那个点开始时在最上面,滚了半圈到了最下面,求这个点相对于地面的运动轨迹的长度. 或者说,再简单点,自行车总骑过吧.假如你在骑 ...

  9. 网络 | Linux ping任何ip均出现 Destination Host Unreachable 排查思路与方法

    Linux ping任何地址出现 Destination Host Unreachable 基本的排错步骤如下: 1.ping 127.0.0.1ping的通说明tcp协议栈没有问题 2.ping 主 ...

  10. stm32cubemx+freertos+中断实现IIC从机

    最近做一个项目需要将stm32配置为iic的从机模式来响应总线的读写需求,看了网上的大部分资料讲解的都不是很全面,因此这里做一个小分享. iic通信流程 要编写iic从机模式的代码,就得对iic得整个 ...