转自b站《黑马程序员netty》
阻塞式
- 阻塞模式下,这些方法会导致线程阻塞
- ServerSocketChannel.accept 会在没有连接建立时让线程暂停 (等待客户端连接)
- SocketChannel.read 会在没有数据可读时让线程暂停 (等待客户端发数据)
- 在单线程的情况下,阻塞方法之间会互相影响,基本上不能正常工作。 举个例子: A客户端建立连接,B客户端也建立连接,当遍历每个连接时,B客户端发送了数据给服务端, 但是如果先遍历到A,而A没有发送数据,则一直卡在这里,B的数据也无法处理到。
- 而如果在多线程的情况下,则每个连接都需要一个线程来支持,当有大量连接时,严重可能造成OOM
- 如果使用线程池来限定线程数量,很多连接建立但是迟迟没有释放,则会阻塞进程,因此不适合长连接,只适合短连接。
public static void main(String[] args) throws Exception {
// 创建ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(30);
// 创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9999));
// 使用一个List保存连接
ArrayList<SocketChannel> socketChannels = new ArrayList<>();
while (true) {
// accept 建立与客户端连接, SocketChannel 用来与客户端之间通信。
log.debug("连接中");
SocketChannel sc = ssc.accept();
log.debug("连接完成");
socketChannels.add(sc);
// 遍历每个连接,接收客户端发出的数据
for (SocketChannel socketChannel : socketChannels) {
socketChannel.read(buffer); // 阻塞方法,如果没有数据,则阻塞在此。
buffer.flip();
System.out.println(Charset.defaultCharset().decode(buffer));
log.debug("after reading... {}", sc);
}
}
}
非阻塞式
- 非阻塞式需要设置 ServerSocketChannel 和 SocketChannel 的 configureBlocking为false.
- 非阻塞模式下,相关方法都会不会让线程暂停
- 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
- SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept
- 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去
- 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu
- 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方
// 0. ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
// 1. 创建了服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 非阻塞模式
// 2. 绑定监听端口
ssc.bind(new InetSocketAddress(8080));
// 3. 连接集合
List<SocketChannel> channels = new ArrayList<>();
while (true) {
// 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
SocketChannel sc = ssc.accept(); // 非阻塞,线程还会继续运行,如果没有连接建立,但sc是null
if (sc != null) {
log.debug("connected... {}", sc);
sc.configureBlocking(false); // 非阻塞模式
channels.add(sc);
}
for (SocketChannel channel : channels) {
// 5. 接收客户端发送的数据
int read = channel.read(buffer);// 非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0
if (read > 0) {
buffer.flip();
debugRead(buffer);
buffer.clear();
log.debug("after read...{}", channel);
}
}
}
多路复用
单线程可以配合Selector来完成对多个Channel可读写事件的监控,这称为多路复用。
- 只有网络IO可以进行多路复用,普通IO不能
- 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
- 有可连接事件时才去连接
- 有可读事件才去读取
- 有可写事件才去写入
- 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
好处:
- 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功
- 让这个线程能够被充分利用
- 节约了线程的数量
- 减少了线程上下文切换
@Slf4j
public class ChannelDemo6 {
public static void main(String[] args) {
try (ServerSocketChannel channel = ServerSocketChannel.open()) {
channel.bind(new InetSocketAddress(8080));
System.out.println(channel);
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int count = selector.select();
// int count = selector.selectNow();
log.debug("select count: {}", count);
// if(count <= 0) {
// continue;
// }
// 获取所有事件
Set<SelectionKey> keys = selector.selectedKeys();
// 遍历所有事件,逐一处理
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 判断事件类型
if (key.isAcceptable()) {
ServerSocketChannel c = (ServerSocketChannel) key.channel();
// 必须处理
SocketChannel sc = c.accept();
log.debug("{}", sc);
}
// 处理完毕,必须将事件移除
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}