为什么需要使用Select ?
socket通讯总共有两种嵌套字(阻塞和非阻塞的),
阻塞式:socket.accpet();socket.read();耗时非常长,长达20多秒,不利于大数据的开发
非阻塞式:耗时短,程序比阻塞式的复杂.
Select (选择器)是Java Nio中能够检测一个到多个通道,并能够知道是否为诸如读写事件做好准备的组件。
(类似于道路管理员)
这样,一个单独的线程就可以管理多个channel,从而管理多个网络连接。
1、Selector 的创建
通过调用Selector.open()方法,创建Selector,代码如下:
Selector selector = Selector.open();
2、向Selector 注册通道
为了将Channel 和Selector 配合使用,必须将Channel 注册到Selector 上,主要通过SelectableChannel.register()方法实现,代码如下:
//启用通道非阻塞模式。
serverSocketChannel.configBlocking(false);
//Selector 监听Channel 事件类型以及SelectionKey 常量表示。
SelectionKey key = serverSocketChannel.register(selector,SelectionKey.OP_READ)
3、SelectionKey 对象
当向selector 注册channel时,register()方法会返回一个SelectionKey 对象。
4、检测channel中什么事件或操作已经就绪。同时,也可以使用以下四个方法,它们都会返回一个布尔类型:
1、selectionKey.isAcceptable();
2、selectionKey.isConnectable();
3、selectionKey.isReadable();
4、selectionKey.isWritable();
Channel +Selector
从SelectionKey访问Channel和Selector很简单。代码如下:
Channel channel = SelectionKey.channel();
Selector selector = SelectionKey.selector();
注意重点:
select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
SelectedKeys()
一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。如下所示: Set selectedKeys = selector. selectedKeys()
当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。
modbus RTU通讯实例
1、在电脑上管理modbus RTU协议的传感器,port端口是6031
2、服务器也就是电脑发送数据给客户机(传感器),然后客户机返回数据给服务器,是一问一答的模式
如果服务器没有发送数据给客户机,那么客户机也不会返回数据给服务器
3、我这里主要是实现服务器的功能!客户机我也不用管的!
4、我的代码每一句都有注解,不知道什么意思的可以看看
package cn.com.nio;
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 ServerSocketChannelDemo {
static ServerSocketChannel serverSocketChannel;
static Selector selector = null;
static SelectionKey key = null;
public static void main(String[] args) {
try {
//1.打开监听通道,这个通道类似于道路
serverSocketChannel = ServerSocketChannel.open();
//为这个通道设置阻塞模式-不阻塞
serverSocketChannel.configureBlocking(false);
//为这个通道绑定监听的端口号
serverSocketChannel.socket().bind(new InetSocketAddress(6031));
//开启选择器 类似于道路管理员
selector = Selector.open();
//把通道注册到选择器上面
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
while (true) {
try {
// select()阻塞到至少有一个通道在你注册的事件上就绪了
// 如果没有准备好的channel,就在这一直阻塞
// select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。
selector.select();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
// 返回已经就绪的SelectionKey,然后迭代执行
Set<SelectionKey> readKeys = selector.selectedKeys();
for (Iterator<SelectionKey> it = readKeys.iterator(); it.hasNext();) {
//第一个key返回的通道应该是OP_ACCEPT属性的;
key = it.next();
//把当前的移除,
it.remove();
SocketChannel client = null;
try {
//如果有新的连接
if (key.isAcceptable()) {
//获取新的连接
ServerSocketChannel server = (ServerSocketChannel) key
.channel();
//服务器和传感器之间建立连接
client = server.accept();
//设置不阻塞模式
client.configureBlocking(false);
//开始为这个通道绑定可写事件属性
client.register(selector, SelectionKey.OP_WRITE);
}
//当新的连接建立以后,如果这个通道有可写的属性,就这个里面执行,
else if (key.isWritable()) {
//获取通道,
client = (SocketChannel) key.channel();
//开始创建一个静态的缓存区,这个ByteBuffer就是相当于装载数据的大卡车
ByteBuffer buffer = ByteBuffer.allocate(8);
System.out.println("开始发送数据....");
//要发送的数据
byte[] by = new byte[] { 03, 03, 00, 02, 00, 01, 36, 40 };
//把数据放在buffer里面
buffer = ByteBuffer.wrap(by);
//写数据
client.write(buffer);
//这个是为读取数据做准备
////flip():Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
buffer.flip();
key.interestOps(SelectionKey.OP_READ);
//开启延迟,如果没有延迟就接受不到数据,程序运行的比接受数据的时间还要短
key.cancel();
// 延迟几秒以后在执行下面的任务,程序运行的比接受数据的时间还要短
try {
Thread.sleep(600);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//创建一个缓存区,接受、发送数据必备的工具
ByteBuffer readBuff = ByteBuffer.allocate(1024);
//清理缓存区,不然会接受到以前的信息,不准确
readBuff.clear();
//开始读取数据
int f = client.read(readBuff);
System.out.println("is kudcdhdjkk:" + f);
StringBuilder sn = new StringBuilder();
if (f > 0) {
//flip():Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
readBuff.flip();
//新建一个byte类型的数组
byte[] bytes = new byte[7];
//把数据放在byte类型的数组里面
readBuff.get(bytes, 0, f);
//对数据bytes里面的数据进行16进制的转化
for (int i = 0; i < bytes.length; i++) {
int a = bytes[i] & 0xff;
String w = Integer.toHexString(a);
if (w.length() == 1) {
w = "0" + w;
}
sn.append(w);
}
}
//打印数据
System.out.println("received : " + sn);
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
try {
key.channel().close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}
}
}
效果如果
以下是整理后的代码
package cn.com.nio;
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 ServerSocketChannelDemo {
static ServerSocketChannel serverSocketChannel;
static Selector selector = null;
static SelectionKey key = null;
public static void init() {
try {
// 1.打开监听通道,这个通道类似于道路
serverSocketChannel = ServerSocketChannel.open();
// 为这个通道设置阻塞模式-不阻塞
serverSocketChannel.configureBlocking(false);
// 为这个通道绑定监听的端口号
serverSocketChannel.socket().bind(new InetSocketAddress(6031));
// 开启选择器 类似于道路管理员
selector = Selector.open();
// 把通道注册到选择器上面
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
init();
while (true) {
try {
// select()阻塞到至少有一个通道在你注册的事件上就绪了
// 如果没有准备好的channel,就在这一直阻塞
// select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。
int i=selector.select();
System.out.println("连接的通道个数:"+i);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
// 返回已经就绪的SelectionKey,然后迭代执行
Set<SelectionKey> readKeys = selector.selectedKeys();
for (Iterator<SelectionKey> it = readKeys.iterator(); it.hasNext();) {
// 第一个key返回的通道应该是OP_ACCEPT属性的;
key = it.next();
// 把当前的移除,
it.remove();
SocketChannel client = null;
try {
// 如果有新的连接
if (key.isAcceptable()) {
// 获取新的连接
ServerSocketChannel server = (ServerSocketChannel) key
.channel();
// 服务器和传感器之间建立连接
client = server.accept();
// 设置不阻塞模式
client.configureBlocking(false);
// 开始为这个通道绑定可写事件属性
client.register(selector, SelectionKey.OP_WRITE);
}
// 当新的连接建立以后,如果这个通道有可写的属性,就这个里面执行,
else if (key.isWritable()) {
// 获取通道,
client = (SocketChannel) key.channel();
// 开始创建一个静态的缓存区,这个ByteBuffer就是相当于装载数据的大卡车
ByteBuffer buffer = ByteBuffer.allocate(8);
System.out.println("开始发送数据....");
byte[] by = new byte[] { 03, 03, 00, 02, 00, 01, 36, 40 };
SocketRead.writeData(buffer, client, by);
key.interestOps(SelectionKey.OP_READ);
// 开启延迟,如果没有延迟就接受不到数据,程序运行的比接受数据的时间还要短
key.cancel();
// 延迟几秒以后在执行下面的任务,程序运行的比接受数据的时间还要短
SocketRead.ready(client);
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
try {
key.channel().close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}
}
}
package cn.com.nio;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketRead {
/*author:命运的信徒
* date:2019/1/22
* arm:负责读取数据和写入数据
*/
public static String ready(SocketChannel client){
try {
Thread.sleep(600);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//创建一个缓存区,接受、发送数据必备的工具
ByteBuffer readBuff = ByteBuffer.allocate(1024);
//清理缓存区,不然会接受到以前的信息,不准确
readBuff.clear();
//开始读取数据
int f=0;
try {
f = client.read(readBuff);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("接受数据的长度是:" + f);
StringBuilder sn = new StringBuilder();
if (f > 0) {
//flip():Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
readBuff.flip();
//新建一个byte类型的数组
byte[] bytes = new byte[7];
//把数据放在byte类型的数组里面
readBuff.get(bytes, 0, f);
//对数据bytes里面的数据进行16进制的转化
for (int i = 0; i < bytes.length; i++) {
int a = bytes[i] & 0xff;
String w = Integer.toHexString(a);
if (w.length() == 1) {
w = "0" + w;
}
sn.append(w);
}
}
//打印数据
System.out.println("received : " + sn);
return sn.toString();
}
public static void writeData(ByteBuffer buffer,SocketChannel client,byte [] by){
//把数据放在buffer里面
buffer = ByteBuffer.wrap(by);
//写数据
try {
client.write(buffer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//这个是为读取数据做准备
////flip():Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
buffer.flip();
}
}