当前位置: 首页 > 科技观察

原来你是这样的IO模型

时间:2023-03-16 17:56:18 科技观察

在学习Netty框架之前,有一个话题是绕不开的,那就是:网络编程IO模型,有的同学听到IO模型就开始背八股文章了,Java中常见的IO模型有:同步BlockingBIO同步非阻塞NIO异步非阻塞AIO今天就和大家一起回顾一下这些知识点。Socket网络编程网络编程中一个重要的概念是:Socket,我们简单了解一下。在网络通信中,客户端和服务器通过双向通信连接交换数据,连接的任何一端都可以称为Socket。话不多说,看图,Socket网络通信的基本流程如下图所示:总结一下流程,可以简单描述为这四个步骤:(1)服务器启动,监听指定的端口,并等待客户端连接;(2)客户端尝试与服务器建立连接,建立可信的数据传输通道;(3)客户端与服务器端交换数据;(4)客户端或服务端断开并终止通信;了解了基本流程后,有些朋友可能会对Socket很感兴趣。什么是套接字?Socket中文翻译为套接字,是网络通信对象的抽象表达。听起来还是很模糊。从coder的角度来看,本质上就是一套编程接口,就是把复杂的TCP/IP协议封装起来,供上层应用使用,所以应该理解。Socket对象一般包括什么?一般包括五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。从这里我们可以看出,Socket包含了很多信息,也就是说,得到一个Socket对象就相当于知己知彼。上面传统BIO方式的一段,从理论的角度解释了什么是Socket。现在让我们回到开发语言实现层面。以Java为例,Java语言从1.0版本开始就封装了Socket相关的接口供开发者使用。这部分对代码感兴趣的朋友可以出去左转,查看java.net包下的源码。下面我们尝试用一个demo来演示传统的网络编程:servercode:publicstaticvoidmain(String[]args)throwsIOException{//创建一个ServerSocket,监听端口8888ServerSocketss=newServerSocket(8888);//循环监听客户端的请求while(true){//这将一直阻塞直到客户端连接到Socketsocket=ss.accept();//输入流用于接收消息InputStreaminputStream=socket.getInputStream();BufferedInputStreambufferedInputStream=newBufferedInputStream(inputStream);//输出流用于回复消息OutputStreamoutputStream=socket.getOutputStream();最终PrintStreamprintStream=newPrintStream(outputStream);//循环接收并回复客户端发送的消息byte[]bytes=newbyte[1024];国际长度;while((len=bufferedInputStream.read(bytes))!=-1){printStream.print("服务器收到:"+newString(bytes,0,len));}}}效果演示:服务端运行后,使用telnet命令模拟客户端发送信息:telnet127.0.0.18888客户端每次发送消息,服务器都会回复,演示效果如下:仔细想想,上面的代码可能有问题,如果之前的客户端一直断开连接,服务器将无法处理来自的消息其他客户端,也就是说程序不具备并发能力。我们稍微修改一下,将之前的处理逻辑代码全部抽取出来,放到一个新的handle()方法中,每当客户端连接时,就开启一个新的线程进行处理:publicstaticvoidmain(String[]args)throwsIOException{//创建一个ServerSocket并监听8888端口ServerSocketss=newServerSocket(8888);//循环监听客户端的请求while(true){//这会一直阻塞直到客户端连接到Socketsocket=ss.accept();//启动一个新线程来处理newThread(()->handle(socket)).start();}}这里为了演示方便,直接新建了一个线程,当然更好的办法是使用线程池,但是解决不了根本问题。看完两段代码,简单总结一下BIO模式的缺点:如果BIO使用单线程接收连接,会阻塞其他连接,效率较低。如果使用多线程,虽然削弱了单线程的影响,但是当有大并发进来的时候,会造成服务器线程过多,压力过大而崩溃。即使使用线程池,也只允许有限数量的线程同时连接。如果并发量远大于线程池设置的数量,还是和单线程一样。IO代码中的读操作是阻塞操作。如果连接不进行数据读写操作,线程就会被阻塞,也就是说只有连接被占用,没有数据发送,会造成资源浪费。比如线程池的500个连接中,只有100个是频繁读写的连接,其他都是占坑浪费资源!另外,多线程也会消耗线程切换。综上所述,BIO模式无法满足大并发业务场景,只适用于连接数比较少且固定的架构。同步阻塞BIO模式根据上面的例子,我们画一张图来抽象一下BIO网络编程场景:传统BIO的特点是只要有新的客户端连接过来,服务端就会开启一个线程来处理客户端请求,但是客户端连接上之后,不会一直对服务器进行IO操作,这样会导致服务器阻塞,一直占用线程资源,造成很多不必要的开销。为了解决这个问题,Java引入了NIO,我们往下看。NIOBIO是Java1.4之前开发者的唯一选择,1.4版本引入了NIO框架。NIO的N有两种含义,一种是:NewIO,一种是NonBlockingIO。“新”是相对于传统的BIO,在当时确实很新;NonBlockingIO又叫:同步非阻塞IO,同步非阻塞体现在:synchronous:本次调用后会返回调用的结果,不存在异步线程回调。非阻塞:表示线程不会一直等待。连接加入集合后,线程会一直轮询集合中的连接,有则处理,无则继续接受请求。NIO的三个基本组成部分学习NIO必须知道以下三个基本组成部分:(1)Buffer(缓冲区)IO是面向流的(字节流或字符流),而NIO是面向块的,block是指Buffer缓冲区。面向块的方法可以一次获取或写入一整块数据,而不需要逐字节从流中读取,这样处理数据的速度会比流方法更快。Buffer缓冲区的底层实现是一个数组,根据数组类型可以细分为:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer等。(2)Channel(通道)Channel翻译成中文为channel的意思,作用类似于IO中的Stream流。但是Channel和Stream的区别在于Channel是双向的,Stream只朝一个方向移动,而Channel可以用于读,写,或者两者兼而有之。CommonChannel类型:FileChannel用于文件操作场景;ServerSocketChannel和SocketChannel主要用于TCP网络通信IO,是本文的重点;DatagramChannel:从UDP网络读取或写入数据。Channel和Buffer的关系:每个Channel对应一个Buffer缓冲区,数据永远不能直接从Channel写入或读取。需要通过Buffer与Channel进行交互。(3)Selector(多路复用器)NIO服务器的实现方式是将多个连接(请求)放到一个集合中,多个请求(连接)只能由一个线程处理,即多路复用,Linux中多路复用的底层环境主要由内核函数(select、poll)实现。为了提高效率,Java1.5版本开始使用epoll。关于select、poll、epoll的比较,有兴趣的朋友可以自行上网搜索。在NIO中,我们将多路复用器称为:Selector,Channel会被注册到Selector中,Selector会根据Channel读写事件的发生,交给空闲线程处理。Buffer、Channel、Selector这三个组件之间的关系可以用下图来描述:基本工作流程如下:(1)首先将Channel注册到Selector中;(2)初始化Selector并调用select()方法,select方法会阻塞直到感兴趣的事件到来;(3)当一个Channel有连接或者读写事件发生时,Channel会处于就绪状态;(4)Selector开始轮询所有处于就绪状态的SelectionKey,通过SelectionKey可以获取对应的Channel集合;NIO比BIO好在哪里?NIO相对于BIO最大的改进是使用了多路复用技术,用少量的线程处理大量的客户端IO请求,提高了并发度,降低了资源消耗;此外,NIO的操作是非阻塞的。比如在单线程中,从通道中读取数据到缓冲区,同时继续做其他事情。当数据读入缓冲区后,线程继续处理数据。写数据也是一样。NIO的问题NIO这么好,是终极解决方案吗?其实不是,蔚来也有很多问题。我们来看看NIO存在的问题?(1)NIO的API使用起来很麻烦,门槛比较高。开发者需要精通:Selector、ServerSocketChannel、SocketChannel、ByteBuffer等类。(2)NIO编程涉及Reactor模式。开发者需要非常熟悉多线程和网络编程才能编写出高质量的NIO程序;(3)处理异常场景比较麻烦,比如:客户端断线重连、网络闪断、拆包粘包、网络拥塞等;(4)NIO有bug,不稳定,如:臭名昭著的Epollbug,会导致Selector轮询为空,最终导致CPU占用100%。NIO出现了这么多问题,让一些开发者终于忍无可忍,终于诞生了Netty框架。Netty框架解决了哪些问题,有哪些优秀的特性,我们下期继续聊。