学习完nio的一个小笔记吧
- 这是一个nio网络通信服务端的demo,主要就学习了selector的一些用法,以及它里面的事件类型
- selector是对nio的一个优化,它能保证既能高效处理线程中的事件,又能保证线程不会一直占用cpu
其中我认为最重要的是selector的执行流程,机制
-
将 channel和 selector建立联系。(联系就是指客户端向服务端发送的某个事件会被selector给监听到)。
-
当客户端给服务器发送了相应事件后,selector就会将这个事件的 selectionKey加入到集合 selectionKeys中,并且处理该事件。
-
在处理事件的时候,可以根据事件类型来进行处理。比如说使用 channel.accept()、channel.read(xx)来处理。
-
对一些客户端异常关闭,或者说正常关闭要特殊处理。客户端关闭,可以直接调 key.cancel()将该key从 selector中删除。
服务端
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import static com.pzistart.utils.ByteBufferUtil.debugRead;
/**
* @author Pzi
* @create 2022-09-12 17:47
*/
@Slf4j
public class ServerUp {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel
.open()
.bind(new InetSocketAddress(8080));
// 1.创建 selector,管理多个 channel
Selector selector = Selector.open();
// 2.开启非阻塞模式,并且将ssc注册到selector中
ssc.configureBlocking(false);
// 3.建立 ssc(channel) 和 selector 的联系
SelectionKey sscKey = ssc.register(selector, SelectionKey.OP_ACCEPT);
log.debug("regist key:{}", sscKey);
while (true) {
// 如果selector监听到有事件发生,那么就向 selectionKeys 这个集合中加入key。
// 并且执行下面的操作,否则就阻塞线程
int count = selector.select();
// selectionKeys中包含所有的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 4.处理事件
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 在selectionKeys中即使是处理完毕事件,也不会自动移除key。
// 导致下一次处理事件的时候,还会找到事件类型为 accept 的那个 SelectionKey
// 但是由于accept事件已经被消耗,所以获取不到对应的 ServerSocketChannel。所以在用完该key,就要立马移除集合中该key
iterator.remove();
// 如果客户端发过来的是 accept相关的事件,那么就会被 selector 识别并且将该时间加入到 selectionKeys集合中。从而在下面的事件处理分支进行处理。
// 如果客户端发过来的是 read相关的之间,同样的事件处理方式
if (key.isAcceptable()) {
log.debug("key{}", key);
ServerSocketChannel c = (ServerSocketChannel) key.channel();
// 调用accept()方法处理事件
SocketChannel sc = c.accept();
sc.configureBlocking(false);
// 将sc注册到selector中,建立sc(channel)和selector的联系
SelectionKey scKey = sc.register(selector, SelectionKey.OP_READ);
log.debug("connected... {}", sc);
// 也可以调用cancel()方法处理事件 key.cancel();
} else if (key.isReadable()) {
try {
SocketChannel channel = (SocketChannel) key.channel();
// 将sc注册到selector中
ByteBuffer buffer = ByteBuffer.allocate(16);
int read = channel.read(buffer);
if (read == -1) {
key.cancel();
} else {
buffer.flip();
debugRead(buffer);
buffer.clear();
}
} catch (Exception e) {
e.printStackTrace();
// 在客户端异常断开连接时,客户端会向服务器发送一个 read()事件
// read()事件没有得到处理,那么就会多次被 selector监听到,从而循环抛异常。
// 解决方法就是将该 key删除,就是直接从 selector中反注册,那么 selector自然不会监听到该 channel的相应事件
// key.cancel();
}
}
}
}
}
}
客户端
package com.pzistart.netcoding.bio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
/**
* @author Pzi
* @create 2022-09-12 15:04
*/
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
System.out.println("ccc");
}
}