IO线程模型
文章目录
- IO线程模型
-
- 一、BIO
-
- 1、概念
- 2、Demo
-
- 2.1、Demo1.0
- 2.2、Demo2.0
- 2.3、小结
- 二、NIO
-
- 1、概念
- 2、Demo
-
- 2.1、Demo1.0
- 2.2、Demo2.0
IO线程模型
一、BIO
1、概念
BIO 全称 Block-IO
是一种**同步且阻塞
**的通信模式。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。
同步: 可以理解为干这件事中间,不能干其他事
阻塞: 可以理解为有事把游戏暂停了,干完事了再来继续游戏
概念不好理解,直接上Demo
2、Demo
2.1、Demo1.0
public class SocketServer {private static void handler(Socket clientSocket) throws Exception {byte[] bytes = new byte[1024];System.out.println("准备read。。。");int read = clientSocket.getInputStream().read(bytes);System.out.println("read完毕!");if(read != -1){System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));}}public static void main(String[] args) throws Exception {ServerSocket serverSocket = new ServerSocket(8001);while(true){System.out.println("等待连接。。。");// 阻塞住了Socket clientSocket = serverSocket.accept();System.out.println("有客户端连接了。。。");handler(clientSocket);}}
}
先理解一下这段代码里面的 Socket clientSocket = serverSocket.accept();
和 int read = clientSocket.getInputStream().read(bytes);
,这两端代码都是阻塞
的,也就是当执行到这里的时候,就会卡住了,暂时不会执行下面的东西了
启动的时候,这里控制台输出完等待连接以后,就会卡住了
然后这里使用一个Telnet的东西,百度一下即可
使用Telnet搭建一个客户端连接到上面的服务端中
我们这个时候再开一个Telnet客户端连接到上面的服务端
这时候再第一个Telnet中随便摁下键盘,你会发现控制台输出
我觉的这里有两点:
- 一是当你两个Telnet连接的时候,只有第一个Telnet先显示连接,另一个Telnet没有显示,同一时间只能处理一件事(
这感觉是同步
) - 二是第一个Telnet接收完之后又显示出来了第二个Telnet的连接信息,说明第二个Telnet没有被抛弃,等到第一个搞完了再轮到它(
这应该就是阻塞
)
现在应该能体验到这种方式比较局限,它适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
2.2、Demo2.0
稍微改进一些
public class SocketServer {private static void handler(Socket clientSocket) throws Exception {byte[] bytes = new byte[1024];System.out.println("准备read。。。");int read = clientSocket.getInputStream().read(bytes);System.out.println("read完毕!");if(read != -1){System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));}}public static void main(String[] args) throws Exception {ServerSocket serverSocket = new ServerSocket(8001);while(true){System.out.println("等待连接。。。");// 阻塞住了Socket clientSocket = serverSocket.accept();System.out.println("有客户端连接了。。。");new Thread(() -> {try {handler(clientSocket);}catch (Exception e){e.printStackTrace();}}).start();}}
}
修改的地方也就是多开了线程去连接下一个客户端,也就是每连接一个客户端就会新开一个线程(也就是打破了阻塞
),使用两个Telnet试一试
并且当你在两个Telnet嗯东西的时候,控制台也有反应
2.3、小结
两种方式都是BIO:
- 第一种就是在服务端处理完第一个客户端的所有事件之前,无法为其他服务端提供服务
- 第二种弥补了第一种的缺点,但是会产生大量空闲线程,徒增压力,浪费资源
这样通过通过多线程的方式,确实可以解决一些问题,但是还是会带来一些新的问题,所以要寻求更好的解决方法
二、NIO
1、概念
Java NIO,全程 Non-Block IO ,是 Java SE 1.4 版以后,针对网络传输效能优化的新功能。是一种非阻塞同步的通信模式。
2、Demo
2.1、Demo1.0
public class NioServer {// 保存客户端连接static List<SocketChannel> channelList = new ArrayList<>();public static void main(String[] args) throws IOException {// 创建NIO ServerSocketChannel,与BIO的serverSocket类似ServerSocketChannel serverSocket = ServerSocketChannel.open();serverSocket.socket().bind(new InetSocketAddress(8001));// 设置ServerSocketChannel为非阻塞serverSocket.configureBlocking(false);System.out.println("服务启动成功");while (true) {// 非阻塞模式accept方法不会阻塞,否则会阻塞// NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数SocketChannel socketChannel = serverSocket.accept();if (socketChannel != null) { // 如果有客户端进行连接System.out.println("连接成功");// 设置SocketChannel为非阻塞socketChannel.configureBlocking(false);// 保存客户端连接在List中channelList.add(socketChannel);}// 遍历连接进行数据读取 10w - 1000 读写事件Iterator<SocketChannel> iterator = channelList.iterator();while (iterator.hasNext()) {SocketChannel sc = iterator.next();ByteBuffer byteBuffer = ByteBuffer.allocate(128);// 非阻塞模式read方法不会阻塞,否则会阻塞int len = sc.read(byteBuffer);// 如果有数据,把数据打印出来if (len > 0) {System.out.println(Thread.currentThread().getName() + " 接收到消息:" + new String(byteBuffer.array()));} else if (len == -1) { // 如果客户端断开,把socket从集合中去掉iterator.remove();System.out.println("客户端断开连接");}}}}
}
自己看懂就可以,看不懂,看我下面
怎么去理解这个非阻塞是什么意思呢?你Debug启动一下项目然后,在SocketChannel socketChannel = serverSocket.accept();
打一个断点,然后点下面那个按钮,你就会发现它会一直循环,这就是非阻塞了
然后当我们连接一个客户端的时候
这里就可以看出来,accept后,通过不断的轮询channelist中的连接,有则打印出来,没有就继续accept,没有中间阻塞的情况,这里也没有使用多线程,也就是说用一个线程,完成了BIO那里开多个线程完成的事情
这里可以同时开几个Telnet,然后Debug启动服务端,进行一下联调,差不多就能理解点了
这里会发现还有优化的空间,如果我们这里连接了10w个客户端,但是只有1w个客户端有真正的事件发生,我们的关注点应该在那1w个上面,如果我们每次都要去遍历这10w个客户端的话,很头疼的
2.2、Demo2.0
这个就是解决了上面的那个问题,使用了多路复用器
public class NioSelectorServer {public static void main(String[] args) throws IOException {int OP_ACCEPT = 1 << 4;System.out.println(OP_ACCEPT);// 创建NIO ServerSocketChannelServerSocketChannel serverSocket = ServerSocketChannel.open();serverSocket.socket().bind(new InetSocketAddress(8001));// 设置ServerSocketChannel为非阻塞serverSocket.configureBlocking(false);// 打开Selector处理Channel,即创建epollSelector selector = Selector.open();// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服务启动成功");while (true) {// 阻塞等待需要处理的事件发生 已注册事件发生后,会执行后面逻辑selector.select();// 获取selector中注册的全部事件的 SelectionKey 实例Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();// 遍历SelectionKey对事件进行处理while (iterator.hasNext()) {SelectionKey key = iterator.next();// 如果是OP_ACCEPT事件,则进行连接获取和事件注册if (key.isAcceptable()) {ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel socketChannel = server.accept();socketChannel.configureBlocking(false);// 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);System.out.println("客户端连接成功");} else if (key.isReadable()) { // 如果是OP_READ事件,则进行读取和打印SocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(128);int len = socketChannel.read(byteBuffer);// 如果有数据,把数据打印出来if (len > 0) {System.out.println(Thread.currentThread().getName() + "接收到消息:" + new String(byteBuffer.array()));} else if (len == -1) { // 如果客户端断开连接,关闭SocketSystem.out.println("客户端断开连接");socketChannel.close();}}//从事件集合里删除本次处理的key,防止下次select重复处理iterator.remove();}}}
}
这个为了解决上面那个问题,加入了多路复用,为不让他做一些无用的循环遍历,抛弃了channellist集合,把连接都注册到多路复用器里面
我大致理解的就是这样子的,可能不太周到
这样子处理的话,如果连接了10w个连接,当有事件的连接过来的时候,就会去处理该连接,而不会全局的循环,也就避免了时间上的消耗
还有一个AIO,之后遇到了再总结吧!