一.示例代码
package nio.base.socket.server;
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 WebServer {
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("192.168.31.198", 8000));
ssc.configureBlocking(false);
Selector selector = Selector.open();
// 注册 channel,作为服务端唯一感兴趣的事件是 Accept
ssc.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuff = ByteBuffer.allocate(1024);
while (true) {
int nReady = selector.select();
if (nReady == 0)
continue;
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
// 处理OP_ACCEPT事件
if (key.isAcceptable()) {
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
System.out.println("来自" + socketChannel.getRemoteAddress() + "的新连接接入");
// 把连接注册到selector上,指定这个channel只对读操作感兴趣
socketChannel.register(selector, SelectionKey.OP_READ);
continue;
}
// 处理OP_READ事件
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuff.clear();
if (socketChannel.read(readBuff) == -1) {
key.cancel();
continue;
}
readBuff.flip();
System.out.println("received : " + new String(readBuff.array()));
key.interestOps(SelectionKey.OP_WRITE);
continue;
}
// 处理OP_WRITE事件
if (key.isWritable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(ByteBuffer.wrap("received".getBytes()));
key.interestOps(SelectionKey.OP_READ);
continue;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
二.详细说明
1.创建ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("192.168.31.198", 8000));
ssc.configureBlocking(false);
首先新建ServerSocketChannel ssc,配置非阻塞,基操
2.创建selector注册事件
Selector selector = Selector.open();
// 注册 channel,作为服务端唯一感兴趣的事件是 Accept
ssc.register(selector, SelectionKey.OP_ACCEPT);
然后是nio的重点,selector
selector其实就是一个线程,帮助你轮询你注册的通道有没有产生相应事件,产生了就会给你自动插入一条对应的key
这里把我们的服务端通道ssc和OP_ACCEPT注册到selector上,这样selector会在ssc可以accept时会插入一条key
那么这里的重点是关于如何注册事件
selector如何知道指定的通道对什么事件感兴趣,这里我们看到是通过注册方法来指定的
具体实现是改变了通道对应的key的interest set的位值
这个位值不只是在注册时可以指定,也可以通过key.interestOps(SelectionKey.OP_XX)来改变,但这个改变需要再下一次调用selector.select()后才会起作用
3.接收Selector通知的key
while (true) {
int nReady = selector.select();
if (nReady == 0)
continue;
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
...
}
}
然后我们开始写while,在循环里调用selector.select(),如果返回值不为0就表示有一个或多个key需要处理了,然后去调用
selector.selectedKeys();获得需要处理的key的集合
获得key之后,就在集合里把这个remove掉,表示已经处理过了,不然会一直需要你处理
4.处理SelectionKey事件
下面分别处理各种key
1.OP_ACCEPT
if (key.isAcceptable()) {
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
System.out.println("来自" + socketChannel.getRemoteAddress() + "的新连接接入");
// 把连接注册到selector上,指定这个channel只对读操作感兴趣
socketChannel.register(selector, SelectionKey.OP_READ);
continue;
}
ssc通道关于accept的事件我们注册到selector里了,所以这里是selector通知我们有新连接接入了,那么就accept这个新连接socketChannel ,并且把socketChannel和OP_READ注册到selector上,就是说selector在这个socketChannel有新消息可读时会插入一条对应的key通知我们
2.OP_READ
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuff.clear();
if (socketChannel.read(readBuff) == -1) {
key.cancel();
continue;
}
readBuff.flip();
System.out.println("received : " + new String(readBuff.array()));
key.interestOps(SelectionKey.OP_WRITE);
continue;
}
socketChannel通道可读时,我们需要判断一下读出来是不是-1
-1是约定的中止值,如果通道在该selector的位值包含有read事件,并且被客户端中断了,那么selector会无限通知我们有可读事件,其实读出来的是中止值-1,那这里需要处理一下,是-1的时候就把key对应的通道直接cancel掉了
在读完数据之后,我想对他发送一条回包,那么此时调用key.interestOps(SelectionKey.OP_WRITE); 通知selector对于这个通道帮我们只监听可写事件,一般来说通道99%的时间都是可写的,不出意外selector在下次调用selector.select();后会马上插入关于此通道可写的key通知我们
2.OP_WRITE
if (key.isWritable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(ByteBuffer.wrap("received".getBytes()));
key.interestOps(SelectionKey.OP_READ);
continue;
}
此时我们发送对应的回包然后马上调用key.interestOps(SelectionKey.OP_READ);通知selector对于这个通道帮我们只监听可读事件,因为通道99%的时间都是可写的,如果不置回去的话selector会不停地通知我们该通道可写,死循环调用这一方法
三.总结
以上就是调用nio基本的api编写服务端的基本流程,重点在于要理解selector到底是怎么工作的,才会减少困惑;实际上nio基本的api还是太简略了,一般还是用框架,下面是我关于用springboot+netty实现服务端的另一个demo
https://blog.csdn.net/qq_24516549/article/details/88985217
有疑惑或者我哪里写错了的欢迎评论交流