1 非阻塞 vs 阻塞
1.1 阻塞
- 阻塞模式下,相关方法都会导致线程暂停
- ServerSocketChannel.accept 会在没有连接建立时让线程暂停
- SocketChannel.read 会在没有数据可读时让线程暂停
- 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
- 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
- 但多线程下,有新的问题,体现在以下方面
- 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
- 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接
服务器端
package com.itcxc.nio.c4;
import com.itcxc.nio.c2.ByteBufferUtil;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
/**
* @author chenxc
* @date 2021/8/7 21:07
*/
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
//使用nio来理解阻塞模式 单线程 阻塞
//0.创建一个ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
//1.创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.绑定监听端口
ssc.bind(new InetSocketAddress(8080));
//3.创建连接集合
List<SocketChannel> channels = new ArrayList<>();
while (true){
log.debug("connecting...");
//4.accept 建立与客户端的连接
SocketChannel sc = ssc.accept(); //是阻塞方法,线程停止运行
log.debug("conneted... {}",sc);
channels.add(sc);
for (SocketChannel channel : channels) {
log.debug("before read... {}",channel);
channel.read(buffer); //是阻塞方法,线程停止运行
buffer.flip();
ByteBufferUtil.debugAll(buffer);
buffer.clear();
log.debug("after read... {}",channel);
}
}
}
}
客户端
package com.itcxc.nio.c4;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
/**
* @author chenxc
* @date 2021/8/7 21:52
*/
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("ss");
}
}
1.2 非阻塞
- 非阻塞模式下,相关方法都会不会让线程暂停
- 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
- SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept
- 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去
- 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu
- 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)
服务器端代码,客户端代码不变
package com.itcxc.nio.c4;
import com.itcxc.nio.c2.ByteBufferUtil;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
/**
* @author chenxc
* @date 2021/8/7 21:17
*/
@Slf4j
public class Server1 {
public static void main(String[] args) throws IOException {
//使用nio来理解阻塞模式 单线程 非阻塞
//0.创建一个ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
//1.创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
// 设置为非阻塞 ssc.accept()方法不再阻塞
ssc.configureBlocking(false);
//2.绑定监听端口
ssc.bind(new InetSocketAddress(8080));
//3.创建连接集合
List<SocketChannel> channels = new ArrayList<>();
while (true){
//4.accept 建立与客户端的连接
SocketChannel sc = ssc.accept(); //非阻塞,线程还会继续运行,如果没有建立连接,sc返回null
if (sc != null){
log.debug("conneted... {}",sc);
// 设置为非阻塞 channel.read(buffer)方法不再阻塞
sc.configureBlocking(false);
channels.add(sc);
}
for (SocketChannel channel : channels) {
final int read = channel.read(buffer);//非阻塞,线程还好继续运行,如果没有读到数据,read返回0
if (read > 0){
buffer.flip();
ByteBufferUtil.debugAll(buffer);
buffer.clear();
log.debug("after read... {}",channel);
}
}
}
}
}
1.3 多路复用
单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用
- 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
- 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
- 有可连接事件时才去连接
- 有可读事件才去读取
- 有可写事件才去写入
- 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
服务器端代码,客户端代码不变
package com.itcxc.nio.c4;
import com.itcxc.nio.c2.ByteBufferUtil;
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;
/**
* @author chenxc
* @date 2021/8/7 21:37
*/
@Slf4j
public class Server2 {
public static void main(String[] args) throws IOException {
//1、创建selector,用于管理多个channel
final Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
//2、建立selector和channel的联系(注册)
// selectorKey就是将来发生事件后,通过他可以知道事件,和那个channel的事件 0代表不绑定任何事件
final SelectionKey sscKey = ssc.register(selector, 0, null);
//绑定key所关心的事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("sscKey:{}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while (true){
//3、select()方法,没有事件发生,线程阻塞,有事件发生线程恢复运行,
// selrct 当事件未处理,线程就不会阻塞,所以要么把事件出来,要么把事件取消,不能置之不理
selector.select(); //阻塞事件
//4、处理事件,selectedKey内部包含所以发生的事件
final Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
final SelectionKey key = iterator.next();
//处理完的事件,要自己移除
iterator.remove();
log.debug("key:{}",key);
//区分事件类型
if (key.isAcceptable()){ //如果是accept
final ServerSocketChannel channel = (ServerSocketChannel) key.channel();
final SocketChannel sc = channel.accept();
sc.configureBlocking(false);
final SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ);
log.debug("scKey,{}",scKey);
}else if (key.isReadable()){ //如果是read
try {
ByteBuffer buffer = ByteBuffer.allocate(16);
final SocketChannel sc = (SocketChannel) key.channel();
final int i = sc.read(buffer); //如果是正常断开,read方法的返回值是-1
if (i == -1){
key.cancel();
break;
}
buffer.flip();
ByteBufferUtil.debugAll(buffer);
}catch (IOException e){
e.printStackTrace();
key.cancel(); //因为客户端异常断开了,因此需要将key取消(从selector的kyes集合中将key真正的删除)
}
}
}
}
}
}