版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qinshi965273101/article/details/81676164
1、概念介绍
- 选择器(Selector)中的三种键集
- 键集中每一个键(SelectionKey)的构造
- 四种操作详情
操作 | 定义 | 二进制 | 就绪条件 |
read | int OP_READ = 1 << 0; | 0000 0001 | OS的读缓冲区中有数据可读 |
write | int OP_WRITE = 1 << 2; | 0000 0100 | 底层缓冲区有空闲空间 |
connect | int OP_CONNECT = 1 << 3; | 0000 1000 | 客户端调用connect(),客户端才用 |
accept | int OP_ACCEPT = 1 << 4; | 0001 0000 | 收到一个客户端的连接请求 |
- 操作配合逻辑运算符使用
| | (或) |
& | (与) |
^ | (异或)相同为真,不同为假 |
~ | (非)取反 |
操作是用一个int类型的变量表示,例如 0000 0101(OP_READ | OP_WRITE) 则表示 读和写两种操作。
2、代码实现
看代码时请结合后面的‘代码重点解释’,更利于理解。
package day03.nio.selector;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
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 NioSelectorServer {
public static void main(String[] args) throws Exception {
//1、创建一个选择器
Selector selc = Selector.open();
//2、创建一个服务端
ServerSocketChannel ssc = ServerSocketChannel.open();
//3、绑定端口
ssc.bind(new InetSocketAddress(12345));
//4、服务端设置为非阻塞模式
ssc.configureBlocking(false);
//5、把 ServerSocketChannel 注册到选择器中,并绑定对应的操作
ssc.register(selc, SelectionKey.OP_ACCEPT);
//6、一直选择,一直处理
while(true) {
//7、唯一一个会阻塞的方法,直到选择器中有准备好的通道
selc.select();
//8、获取准备好的键集
Set<SelectionKey> selectedKeys = selc.selectedKeys();
if(selectedKeys.size()>0) {
Iterator<SelectionKey> iterator = selectedKeys.iterator();
//9、根据键集的操作类型,做出相应的处理
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isAcceptable()) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel sc = null;
while(sc == null) {
sc = ssChannel.accept();
}
sc.configureBlocking(false);
sc.register(selc, SelectionKey.OP_READ);
}else if(key.isConnectable()) {
}else if(key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer lenBuf = ByteBuffer.allocate(4);
//hasRemaining 就是position-limit
while(lenBuf.hasRemaining()) {
sc.read(lenBuf);
}
lenBuf.flip();
int len = lenBuf.getInt();
ByteBuffer buf = ByteBuffer.allocate(len);
while(buf.hasRemaining()) {
sc.read(buf);
}
System.out.println(new String(buf.array()));
sc.register(selc, key.interestOps() | SelectionKey.OP_WRITE);
}else if(key.isWritable()) {
SocketChannel sc = (SocketChannel) key.channel();
String str = "我是服务端,已经收到客户端请求";
ByteBuffer lenBuf = ByteBuffer.allocate(4);
lenBuf.putInt(str.getBytes().length);
lenBuf.flip();
while(lenBuf.hasRemaining()) {
sc.write(lenBuf);
}
ByteBuffer dataBuf = ByteBuffer.wrap(str.getBytes());
while(dataBuf.hasRemaining()) {
sc.write(dataBuf);
}
//删除写的操作,并重新注册读操作
sc.register(selc, key.interestOps()& ~SelectionKey.OP_WRITE);
}else {
throw new Exception("对不起,这是一个未知的键!");
}
}
iterator.remove();
}
}
}
}
package day03.nio.selector;
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.Set;
import sun.security.krb5.SCDynamicStoreConfig;
public class NioSelectorClient {
public static void main(String[] args) throws Exception {
//1、创建一个选择器
Selector selc = Selector.open();
//2、创建一个客户端
SocketChannel sc = SocketChannel.open();
//3、设置为非阻塞模式
sc.configureBlocking(false);
//4、连接服务端
sc.connect(new InetSocketAddress(12345));
//5、把 SocketChannel 注册到选择器中,并绑定对应的操作
sc.register(selc, SelectionKey.OP_CONNECT);
//6、一直选择,一直处理
while(true) {
//7、唯一一个会阻塞的方法,直到选择器中有准备好的通道
selc.select();
//8、获取准备好的键集
Set<SelectionKey> selectedKeys = selc.selectedKeys();
if(selectedKeys.size() > 0) {
Iterator<SelectionKey> iterator = selectedKeys.iterator();
//9、根据键集的操作类型,做出相应的处理
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isAcceptable()) {
}else if(key.isConnectable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
while(!sChannel.isConnected()) {
sChannel.finishConnect();
}
sChannel.register(selc, SelectionKey.OP_WRITE);
}else if(key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
ByteBuffer lenBuf = ByteBuffer.allocate(4);
while(lenBuf.hasRemaining()) {
sChannel.read(lenBuf);
}
lenBuf.flip();
int len = lenBuf.getInt();
ByteBuffer dataBuf = ByteBuffer.allocate(len);
while(dataBuf.hasRemaining()) {
sChannel.read(dataBuf);
}
System.out.println(new String(dataBuf.array()));
}else if(key.isWritable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
String str = "我是客户端,正在发送信息给服务端!";
ByteBuffer lenBuf = ByteBuffer.allocate(4);
lenBuf.putInt(str.getBytes().length);
lenBuf.flip();
while(lenBuf.hasRemaining()) {
sChannel.write(lenBuf);
}
ByteBuffer dataBuf = ByteBuffer.wrap(str.getBytes());
while(dataBuf.hasRemaining()) {
sChannel.write(dataBuf);
}
//删除写的操作,并重新注册读操作
//sChannel.register(selc, key.interestOps()& ~SelectionKey.OP_WRITE | SelectionKey.OP_READ);
sChannel.register(selc, key.interestOps() & ~SelectionKey.OP_WRITE);
sChannel.register(selc, SelectionKey.OP_READ);
}else {
throw new Exception("对不起,这是一个未知的键!");
}
}
iterator.remove();
}
}
}
}
3、代码重点解释
3.1、调用select()方法的执行逻辑:
- 把‘已取消键集合’中的键,从‘已选择键集合’中移除
- 清空‘已取消键集合’
- 判断‘注册键集合’中的各个键,是否有键准备就绪,即通道对应的 interestOps(兴趣操作) 是否就绪。若没有,则会阻塞。
- 当‘注册键集合’中有键就绪,则判断‘已选择键的集合’中是否存在这个键。若不存在,则清空该键的 readOps,并写上对应的就绪操作。若存在,则在原有的 readOps 的基础上,加上就绪操作。
3.2、register方法的执行逻辑
- 如果这个通道在这个选择器上已经注册过,则直接覆盖该键的 interestOps (兴趣操作),若不存在,则重新添加一个键。
- 源码如下:
3.3、注册write操作须知:
- 写操作的就绪条件为底层缓冲区有空闲空间,所以注册写操作后,一旦写完就立马注销掉,否则写操作一直就绪。
- 注销写操作,只需要用 register方法覆盖 interestOps(兴趣操作)。
3.4、数据传输的协议
- ByteBuffer中首部用一个整型来存放字节个数,表示内容的到校
- 紧跟着整型的是真正的传输数据内容。
3.5、socket变成实现UDP协议通信,与这个类似。