BIO:80 年代屌丝追妹
80 年代屌丝男买了一个 BP 机用来追妹,男士使用传呼台给女生留言:
男:下午一起看个电影?[早晨 10 点]
这是男生唯一心动的女生,所以一直守着自己的 BP 机,等待女生回复,就这样一天过去了,直到:
男:BP 没电,自动关机。
名词解释
- BP 机和传呼台指的是 BIO 中的的流单向传输的特性,屌丝男士通过传呼台给 BP 机发送消息是单向的,如果是女生通过传呼台回复屌丝男士,也是单向的。
- 这个女生是男士唯一心动的女生,所以他傻等着 BP 机回复,即便可能一直不会有消息,这就是同步阻塞 IO,中 B 的概念。
BIO 的缺点
- 同步阻塞 IO,如果存在多个请求的时候,服务端必须通过多线程处理,增加了服务端的压力和创建销毁线程的开销。
- 如果连接一直没有响应,服务端也需要一直监听端口等待,浪费了服务端资源。
NIO:80 年代公子哥把妹
80 年代的公子哥买了一个大哥大,关键是这个公子哥太花心,同时中意了两个妹子,于是他就开始了把妹过程。
公子哥用大哥大给女 A 打电话:
公子哥 -> 女 A:下午一起看个电影?[早晨 10 点]
女 A -> 公子哥:我正在吃饭,你晚点再打过来?
公子哥用大哥大给女 B 打电话:
公子哥 -> 女 B:下午一起看个电影?[早晨 10 点 1 分]
女 B -> 公子哥:我正在吃饭,你晚点再打过来?
过了 10 分钟公子哥再次打电话询问
公子哥 -> 女 A:怎么样,有空吗?[早晨 10 点 10 分]
女 A -> 公子哥:我下午陪爸爸打马球,不去了。
公子哥 -> 女 B:怎么样,有空吗?[早晨 10 点 11 分]
女 B -> 公子哥:好呀,下午 3 点来我家接我吧。
公子哥 -> 女 B:好嘞,我开车去接你。
最终公子哥成功了追求到了女 B,这个故事告诉我们,成功的前提是有钱。(你怎么看?)
名词解释
- 公子哥用上了大哥大,可以实现双向的通话,这就是 NIO 中的 Channel,可以实现双向的数据流传输。
- 公子哥不用想 BP 机小哥一样死等着回复,每次打电话都能得到回复,挂断电话一会儿再来询问即可,这就是 Channel 的非阻塞特性,也就是 “N” 的体现。
- 公子哥可以同时撩两个妹子,这就是 NIO 的 IO 多路复用,也就是 Selector。
- 公子哥只能同时和一个人通话,这就是同步,所以 NIO 的全称叫做同步非阻塞 IO。
优缺点
- 非阻塞 IO 模型,不需要阻塞在特定的请求。
- 一个线程可以处理多个请求,不需要客户端和服务器端一比一的对应,没有多线程创建和销毁带来的系统开销。
- 服务端不需要死等请求,减少了服务端压力。
关键名词
- Channel,双向传输,非阻塞的通道,有FileChannel,DatagramChannel,ServerSocketChannel/SocketChannel 等。
- Buffer,数据块的读写,可以理解字节数组,效率高。四个重要属性:capacity 容量,position 位置, limit 上限,用户切换读写时候的游标,mark 标记,标记 position 的位置,分为堆内内存和堆外内存,也是 NIO 性能的关键内容。
- Selector,IO 多路复用的关键,实现了循环查看可以使用的 Channel,解决死等问题。
- Selector 实现有多种方式,自己写一个数组循环也是方式,也可以实现 IO 多路复用,只是性能好坏而已,所以基于底层 poll、select和epoll 也是实现“遍历”可用通道的方式不同而已。select 使用轮询的方式,有 1024 个连接的限制,poll 去掉了这个限制依然是轮询的方式,epoll 是基于系统的注册回调的方式,监听系统的事件实现。
- NIO 引入了 Buffer 的概念,每次使用 Buffer 拷贝数据其实是一次从用户空间(JVM) 向系统空间(系统内存) 的一次拷贝, Java 里面提供了 DirectByteBuffer 堆外内存,如果使用使用堆外内存,可以减少一次系统空间和用户空间的拷贝,这种现象叫做零拷贝。强调一下,并不是操作系统不能直接操作 HeapByteBuffer(对内内存),而且在 GC 的作用下,内存地址可能随时变化,操作的内存数据不一定准备。
- IO 多路复用性能更好,针对的 I/O 密集型应用程序,如果是 CPU 密集型应用程序,还是通过多线程的方案。所以很多写 IO 多路复用的文章都会说“多线程的创建,必然存在创建销毁和切换的开销,在高并发系统中,会拖慢整个系统”,其实并不是非常的准确,虽然是想说明 I/O 多路复用的利好,但是确实有点以偏概全。
AIO:21 世纪非智能时代大学生把妹
21 世纪初期,还没有智能机,不过诺基亚 1110 砸核桃神机已经普及了,下面就是新时代大学生小王用自己的诺基亚 1110 的把妹过程。
小王给中意的两个女生直接发短信留言(群发):
小王 -> 女 A:下午一起看个电影?[早晨 10 点]
小王 -> 女 B:下午一起看个电影?[早晨 10 点]
发完短信小王去看《西游记》去了。10分钟以后电话响起,收到了妹子的短信,小王拿起了手机阅读了消息并进行回复。
女 B -> 小王:好呀,下午 3 点来我家接我吧 [早晨 10 点 10 分]
小王 -> 女 B:好的,我去接你不见不散。
名词解释
- 小王发完短信不需要盯着手机看,也不需要时不时看一下手机,有短信回复会有通知,再来阅读就好了。这就是 AIO 中的 AsynchronousServerSocketChannel,可以注册一个回调 CompletionHandler,等待有消息的时候直接通知回调处理即可。
优缺点
AIO 包括了 NIO 的所有优缺点的同时,增加了异步回调的能力,由此解决的问题是不需要同步的等待非阻塞 IO 的反馈,所以 NIO 叫做异步非阻塞 IO 模型。
是时候展示真正的技术了
说了这么多,用 NIO 实现一个把妹聊天程序呗?
服务器端
public class NioServer {
public void start() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(6789));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功");
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
iterator.remove();
handleAccept(serverSocketChannel, selector);
} else if (selectionKey.isReadable()) {
handleRead(selectionKey);
} else {
System.out.println("其他请求");
}
}
}
}
private void handleRead(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
StringBuilder request = new StringBuilder();
while (socketChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
request.append(StandardCharsets.UTF_8.decode(byteBuffer));
}
if (request.length() > 0) {
System.out.println("服务端收到消息:" + request.toString());
}
}
private void handleAccept(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("有新人进入聊天室");
socketChannel.write(StandardCharsets.UTF_8.encode("进入聊天室,现在可以聊天了"));
}
public static void main(String[] args) throws IOException {
new NioServer().start();
}
}
复制代码
客户端
public class NioClient {
public void start() throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(6789));
Selector selector = Selector.open();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
new Thread(new NioClientHandler(selector)).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String request = scanner.nextLine();
if (request != null && request.length() > 0) {
socketChannel.write(StandardCharsets.UTF_8.encode(request));
}
}
}
public static void main(String[] args) throws IOException {
new NioClient().start();
}
private class NioClientHandler implements Runnable {
private Selector selector;
public NioClientHandler(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
try {
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = (SelectionKey) iterator.next();
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
StringBuilder response = new StringBuilder();
while (socketChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
response.append(StandardCharsets.UTF_8.decode(byteBuffer));
}
if (response.length() > 0) {
System.out.println("接收服务端消息:" + response);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
复制代码
原文链接:
https://juejin.cn/post/6943384824283398151
如果觉得本文对你有帮助,可以转发关注支持一下