以上两篇内容主要讲了NIO的三大组件、ByteBuffer、文件编程等,需要了解像详情的请移步查看。
步入正题: 说到网络编程不得不提网络编程中的阻塞概念、以及非阻塞概念,相信大家都有所耳闻,接下来逐步为大家解析其中的主要概念:
阻塞:
public static void main(String[] args) throws IOException {
// 使用NIO来理解阻塞模式
ByteBuffer buffer = ByteBuffer.allocate(16);
// 创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定端口
ssc.bind(new InetSocketAddress(8080));
// 连接集合
List<SocketChannel> channels = new ArrayList<>();
while (true){
// accept 建立与客户端的连接 SocketChannel 用来与客户端进行通信
log.debug("connecting......");
SocketChannel sc = ssc.accept(); // 阻塞方法 线程停止运行
log.debug("connected......{}",sc);
channels.add(sc);
for (SocketChannel channel : channels) {
// 接收客户端发送的数据
log.debug("before read...{}",channel);
channel.read(buffer); // 阻塞方法 线程停止运行
buffer.flip();
debugRead(buffer);
buffer.clear();
log.debug("after read...{}",channel);
}
}
}
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("waiting.......");
}
阻塞的表现其实就是线程暂停了,暂停期间不会占用CPU,但相当于线程处于闲置状态。
非阻塞
但非阻塞模式下,即使没有建立连接和可读取数据时,线程仍然在不断运行,白白浪费CPU public static void main(String[] args) throws IOException {
// 使用NIO来理解阻塞模式
ByteBuffer buffer = ByteBuffer.allocate(16);
// 创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 非阻塞模式
// 绑定端口
ssc.bind(new InetSocketAddress(8080));
// 连接集合
List<SocketChannel> channels = new ArrayList<>();
while (true){
// accept 建立与客户端的连接 SocketChannel 用来与客户端进行通信
SocketChannel sc = ssc.accept(); // 非阻塞,线程会继续运行,如果没有连接建立返回的是null
if (sc != null){
log.debug("connected......{}",sc);
sc.configureBlocking(false);
channels.add(sc);
}
for (SocketChannel channel : channels) {
// 接收客户端发送的数据
int read = channel.read(buffer);// 非阻塞方法 线程会继续运行 如果没有读到数据会返回0
if (read > 0){
buffer.flip();
debugRead(buffer);
buffer.clear();
log.debug("after read...{}",channel);
}
}
}
}
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("waiting.......");
}
}
以上就是单线程情况下,阻塞模式和非阻塞模式的两种案例,相信仔细的你已经发现:
阻塞某个方法的执行都会影响其他方法的执行,你accept了就不能read 反之亦是如此。非阻塞模式即使在你没有连接请求和数据发送的时候它也在那里进行空转,大大浪费了CPU资源,
为此我们使用之前讲到的selector来进行优化,也引出了 多路复用
单线程可以配合Selector完成对多个Channel可读事件的监控,这称之为多路复用
网络IO。普通文件IO没法利用多路复用限于网络的传输能力,Channel未必时时可写,一旦Channel可写,会触发Selector的可写事件。上述文字里有一只提到事件,细心的小伙伴已经发现了。
主要的事件类型有:
accept - 会在连接请求时触发 (客户端连接成功时触发) connect - 是客户端,连接建立后触发 (服务端成功接收连接时触发) read - 可读事件 (数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况) write - 可写事件 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
accept事件interestOps(SelectionKey.OP_ACCEPT)
read,write事件key,所以要有 iterator.remove(); // 切记要删除 否则空指针demo
public class Server {
public static void main(String[] args) throws IOException {
// 创建selector 管理多个channel
Selector selector = Selector.open();
// ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 非阻塞模式
// 建立selector和channel之间的联系
// SelectionKey 就是事件发生后 通过它可以知道哪个channel发生的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while (true){
// select 方法 没有事件就阻塞 有事件恢复 在事件未处理时 它不会阻塞 事件发生后 要么处理 要么取消 不能置之不理
selector.select();
// 4. 处理事件 selectedKeys内部包含了所有发生的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove(); // 切记要删除 否则空指针
log.debug("key:{}",key);
// 5. 区分事件类型
if (key.isAcceptable()) { // accept事件
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ); // 关注读事件
log.debug("{}",sc);
log.debug("scKey:{}",scKey);
// key.cancel(); // 事件发生后 要么处理 要么取消 不能置之不理
}
else if (key.isReadable()){ // read事件
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
int read = channel.read(buffer); // 正常断开返回值是-1
if (read == -1){
key.cancel();
}else {
buffer.flip();
debugRead(buffer);
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();// 客户端断开(无论是正常断开还是异常 都会产生一个read事件) 从selector keys的集合中真正删除
}
}
}
}
}
}
需要注意代码中注释的地方 以上的demo案例已经完全展示了selector的主要用法以及注意事项,简单总结一下 如图:

使用Selector的好处:
Selector的主要方法:
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectorKey key = channel.register(selector,0,null) //具体后面解释
int count = selector.select();
int count = selector.select(long timeout);
int count = selector.selectNow();
💡 select 何时不阻塞
💡 为何要 iter.remove()
因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如
💡cancel 的作用
cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件
写到这里不知道小伙伴是否有消化,本章节的网络编程阻塞和非阻塞模式、以及selector、channel的简单了解就到这里了。后续后继续更新。