一,五种IO模型:
一个IO操作可以分为两个步骤:发起IO请求(判断此时IO是否允许将网卡中的数据往内核缓冲区中读或将内核中的往网卡中写)(可写)
实际的IO操作(从内核往应用程序读或从应用程序往网卡中写)(真正的写 )
例如:
1、操作系统的一次写操作分为两步:第一步,将数据从用户空间拷贝到系统空间;第二步,从系统空间往网卡写。
2、一次读操作也分为两步:第一步,将数据从网卡拷贝到系统空间;第二步,将数据从系统空间拷贝到用户空间。
对于读操作来说,阻塞IO和非阻塞IO的区别在于第一步:发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
而同步IO和异步IO的区别就在于第二步:如果实际的IO读写阻塞请求进程,因为读写过程是阻塞的,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果实际的IO读写过程不阻塞,也就是操作系统做完IO两个阶段的操作再将结果返回,那么就是异步IO。为了方便理解可结合下图来看。
1.1 同步阻塞
模型特点 :在Linux中,对于一次读取IO的操作,数据并不会直接拷贝到程序的程序缓冲区。通常包括两个不同阶段:
- 等待数据准备好,到达内核缓冲区;
- 从内核向进程复制数据。
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区。
阻塞I/O(blocking I/O)模型,进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回。进程从调用recvfrom开始到它返回的整段时间内是被阻塞的。
故事描述:
小明从家里面先到演唱会现场问售票业务员买票,但是票还没出来,三天以后才出来,小明直接打了个地铺睡在举办商售票大厅,一直等票出来,然后买票。
1.2 同步非阻塞
模型特点 :
与阻塞式I/O不同的是,非阻塞的recvform系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error(EAGAIN 或 EWOULDBLOCK)。进程在返回error之后,可以处理其他的业务逻辑,过会儿再发起recvform系统调用。采用轮询的方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。 在linux下,可以通过设置socket套接字选项使其变为非阻塞。
当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。
故事描述:
小明从家里面先到演唱会现场问售票业务员买票,但是票还没出来,然后小明走了,办理其他事情去了,然后过了2个小时,又去举办商售票大厅买票来了,如果票还没有出来,小明又先去办其他事情了,重复上面的操作,直到有票可以买。
1.3 I/O复用(事件驱动)
模型特点 :
IO 多路复用的好处就在于单个进程就可以同时处理多个网络连接的IO。它的基本原理就是不再由应用程序自己监视连接,取而代之由内核替应用程序监视文件描述符。以select为例,当用户进程调用了select,那么整个进程会被阻塞,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
IO多路复用方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。
由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。
故事描述:
小明想买票看演唱会,都直接给黄牛(selector/epoll)打电话了,说帮我留意买个票,票买了通知我,我自己去取(当我接到黄牛的电话时,我需要花费整个路成的时间去读这个数据,买拿这个票),那么票没出来之前,小明完全可以做自己的事情。
1.4 信号I/O
模型特点 :
允许Socket进行信号驱动IO,并注册一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
故事描述:
小明想买票看演唱会,给举办商售票业务员说,给给你留个电话,有票了请你给我打个电话通知一下(是看人家操作系统提不提供这种功能,Linux提供,windows没有这种机制),我自己再来买票(小明完全可以做自己的事情,但是票还是需要小明自己去拿的)。
1.5 异步非阻塞:
模型特点:
上述四种IO模型都是同步的。相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,就可以去处理其他的逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。等到数据准备好了,内核直接复制数据到进程空间,然后从内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理了。
在 Linux 中,通知的方式是 “信号”,分为三种情况:
- 如果这个进程正在用户态处理其他逻辑,那就强行打断,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
- 如果这个进程正在内核态处理,例如以同步阻塞方式读写磁盘,那就把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
- 如果这个进程现在被挂起了,例如陷入睡眠,那就把这个进程唤醒,等待CPU调度,触发信号通知。
故事描述:
小明想买票看演唱会,给举办商售票业务员说(异步非阻塞i/o)打电话了,给你留个地址,有票了请通知快递员,把这张票送到这个地址来,当小明听到敲门声,看见快递员,就知道票好了,而且指导票好了的时候,票已经到他手上了,票不用小明自己去取(应用不用自己再去read数据了)。
2、select(Java)、poll(c++)、epoll(c++)
Linux支持IO多路复用的系统调用有select、poll、epoll,这些调用都是内核级别的。但select、poll、epoll本质上都是同步I/O,先是block住等待就绪的socket,再是block住将数据从内核拷贝到用户内存。
3、两种I/O多路复用模式:Reactor和Proactor
在这两种模式下的事件多路分离器反馈给程序的信息是不一样的:
1.Reactor模式下说明你可以进行读写(收发)操作了。
2.Proactor模式下说明已经完成读写(收发)操作了,具体内容在给定缓冲区中,可以对这些内容进行其他操作了。
Reactor关注的是I/O操作的就绪事件,而Proactor关注的是I/O操作的完成事件
一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。
Reactor模式采用同步IO,而Proactor采用异步IO。
在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。
而在Proactor模式中,处理器或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。
Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备。
二, 常用网络模型
2.1 BIO模型
BIO 全称Block-IO 是一种同步阻塞的通信模式。我们常说的Stock IO 一般指的是BIO。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。
网络编程的基本模型是C/S模型,即两个进程间的通信。服务端提供IP和监听端口,客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。而传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
简单的描述一下BIO的服务端通信模型,即BIO 设计原理:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通信模型。
主线程负责监听当有新的连接的时候创建一个新的子线程处理任务。如下图所示:
缺点:
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,由于服务端的线程个数和客户端并发访问数呈1:1的正比关系,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死掉了。
BIO的编程流程
服务端:
1、创建ServerSocket实例
2、绑定端口
3、通过accept来监听客户端的连接,有客户端连接会返回socket实例
4、进行读写操作
5、关闭资源
客户端:
1、创建socket实例
2、通过connect并指定服务端的IP+端口连接服务端
3、进行读写操作
4、关闭资源
代码展示:
【BIO单线程代码】
public class BIOClent { public static void main(String[] args) { //创建Socket实例 Socket socket=new Socket(); OutputStream outputStream=null; BufferedReader bufferedReader=null; Scanner in=new Scanner(System.in); try { //连接服务器 socket.connect(new InetSocketAddress(9999)); System.out.println("客户端连接服务器成功!"); while (true){ //发送数据给服务端,先得到socket的流,然后将数据写到流中 outputStream = socket.getOutputStream(); System.out.println("请写入数据:"); String str=in.nextLine(); outputStream.write((str+" ").getBytes()); //接收服务端的消息 bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream())); String msg=bufferedReader.readLine(); System.out.println("服务端发来消息:"+msg); } } catch (IOException e) { e.printStackTrace(); }finally { try { socket.close(); outputStream.close(); bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
public class BIOServer { public static void main(String[] args) { ServerSocket serverSocket=null; Socket socket=null; BufferedReader reader=null; try{ //创建serversocket实例 serverSocket=new ServerSocket(); //绑定端口 serverSocket.bind(new InetSocketAddress(9999)); System.out.println("服务端启动了..."); //进行监听,等待客户端连接,返回的是客户端的socket实例 socket=serverSocket.accept(); System.out.println("客户端:"+socket.getRemoteSocketAddress()+"连接上了");//获取客户端的ip和port //服务端来读取消息,从socket的流中读取数据 reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));//获取客户端的读取流 String msg=null; System.out.println("正在等待客户端发送消息...."); while ((msg=reader.readLine())!=null){ System.out.println("客户端发来消息:"+msg); //回复消息 OutputStream write=socket.getOutputStream();//获取服务端的输出流 write.write(("echo:"+msg+" ").getBytes()); } }catch (IOException e){ e.printStackTrace(); }finally { try{ if(reader!=null)reader.close(); if(socket!=null)socket.close(); if(serverSocket!=null)serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
运行结果:
【BIO多线程代码】
public class BIOClient_2 { public static void main(String[] args) { //创建Socket实例 Socket socket=new Socket(); OutputStream outputStream=null; BufferedReader bufferedReader=null; Scanner in=new Scanner(System.in); try { //连接服务器 socket.connect(new InetSocketAddress("127.0.0.1",8888)); System.out.println("客户端连接服务器成功!"); while (true){ //发送数据给服务端,先得到socket的流,然后将数据写到流中 outputStream = socket.getOutputStream(); System.out.println("请写入数据:"); String str=in.nextLine(); outputStream.write((str+" ").getBytes()); outputStream.flush(); //接收服务端的消息 bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream())); String msg=bufferedReader.readLine(); System.out.println("服务端发来消息:"+msg); } } catch (IOException e) { e.printStackTrace(); }finally { try { socket.close(); outputStream.close(); bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
class BIOthread extends Thread{ private Socket socket=null;//客户端的Socket public BIOthread(Socket socket){//构造函数 this.socket=socket; } @Override public void run(){ BufferedReader reader=null; try { System.out.println(Thread.currentThread().getName()+"已启动..."); //服务端来读取消息,从socket的流中读取数据 reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));//获取客户端的读取流 String msg=null; System.out.println("正在等待客户端发送消息...."); while ((msg=reader.readLine())!=null){ System.out.println("客户端发来消息:"+msg); //回复消息 OutputStream outputStream=socket.getOutputStream();//获取服务端的输出流 outputStream.write(("echo:"+msg+" ").getBytes()); } }catch (Exception e){ e.printStackTrace(); }finally { if(socket!=null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } } public class BIOServer_2 { public static void main(String[] args) { ServerSocket serverSocket=null; //服务端的ServerSocket Socket socket=null;//客户端的Socket try{ //创建serversocket实例 serverSocket=new ServerSocket(); //绑定端口 serverSocket.bind(new InetSocketAddress(8888)); System.out.println("服务端启动了..."); while(true){ //进行监听,等待客户端连接,返回的是客户端的socket实例 socket=serverSocket.accept(); System.out.println("客户端:"+socket.getRemoteSocketAddress()+"连接上了");//获取客户端的ip和port //当有客户端连接,就为其创建一个子线程 new BIOthread(socket).start(); } }catch (IOException e){ e.printStackTrace(); }finally { try{ if(serverSocket!=null)serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
运行结果:
2.2 NIO模型
NIO 全称New IO,也叫Non-Block IO 是一种同步非阻塞的通信模式。是面向块编程的。NIO相关的类都被放在java.nio包及其子包下,
NIO 设计原理:
NIO 相对于BIO来说一大进步。客户端和服务器之间通过Channel通信。NIO可以在Channel中进行读写操作,但使用时要与Buffer结合。如果客户有请求时,服务器会为每个Client分配一个Channel,这些Channel都会被注册在Selector多路复用器上。如果某个Channel中没有事件,线程不会一直阻塞到Channel中,此时Selector通过一个线程不停的轮询这些Channel,找出已经准备就绪的Channel执行IO操作,如果注册到Selector上的每一个Channel都没有事件要读取,则线程还可以去做其他的业务。所以说NIO 是通过一个线程轮询,实现千万个客户端的请求,这也就是非阻塞NIO的特点。
1 .Selector(多路复用器)
Selector 称为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。Selector提供选择已经就绪任务的能力:就是Selector会不断地轮询注册在其上的通道(Channel),如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。服务器端提供一个线程负责Selector的轮询,就可以接入成千上万个客户端,这就是JDK NIO库的巨大进步。在NIO中一个Selector对应一个线程,但一个线程可以对应多个Selector。注意:当客户端过多时,为了提高效率,可以多分配给服务器几个线程,如有100000个客户端时,就可以分配给50个线程来执行,但在BIO模型中,如果有100000个客户端,就必须分配100000线程。
Selector类是一个抽象类,常用方法如下: public abstract class Selector implements Closeable{ public static Selector open();//得到一个选择器 public int select(Long timeout);//参数用来设置阻塞时间,即表示在一定时间内,监控所有注册到的通道,当其中有IO操作时,将对应的SelectionKey加入到内部集合中并返回发生事件的通道个数 public int select();//不带参数的select是阻塞型的监听,即当其中有IO操作时,才会返回 public int selectNow();//表示的是没有事件发生时,会立即返回 public Set<SelectionKey> selectedKeys();//从内部集合中得到所有发生事件的SelectionKey public Set<SelectionKey> keys();//返回被注册到selector上的所有SelectionKey }
SelectionKey表示Selector和网络通道的注册关系,NIO所有监听的事件都定义在SelectionKey下,而每一个被注册的SelectionKey都是存储到selector的Keys(hashSet<SelectionKey>)集合中的。
int OP_ACCEPT:有新的网络可以连接,值为16.可以调用accpet()来生成socketChannel int OP_CONNECT:代表连接已经建立,值为8 int OP_READ:代表读操作,值为1 int OP_write:代表写操作,值为4 SelectionKey常用方法: public abstract Selector selector();//得到与之关联的Selector对象 public abstract SelecttableChannel channel();//得到与之相关联的Channel通道 public final Object attachment();//得到与之关联的共享数据,如buffer public abstract SelectionKey interestOps(int ops);//设置或改变监听的事件,如将OP_READ改为OP_WRITE public final boolean isAcceptable();//是否需要连接 public final boolean isReadable();//是否需要读 public final boolean isWriteable();//是否可写
2. Buffer(缓冲区)
它是NIO与BIO的一个重要区别。BIO是将数据直接写入或读取到Stream对象中。而NIO的数据操作都是在缓冲区Buffer中进行的。缓冲区可以理解为是一个数组,但它又与简单数组不同,Buffer 类相比一个简单数组的而言,它是将关于数据的数据内容和信息包含在一个单一的对象中存储的,并且这个对象还提供了一组方法,用来操作缓冲区。
在NIO中,Buffer是一个顶级父类,它是一个抽象类,它的子类最常见的类型是ByteBuffer,另外还有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
常用属性:
position:下一个要读或写的元素的索引,每次读或写时都会改变这个值。在往Buffer中写数据时会从Buffer数组中的position位置开始写。从Buffer中读数据时会从Buffer的position开始读。(由于Buffer既可以写也可以读,为了区别,在写完进行读取时,必须要调用flip()方法反转,一般是调用flip方法使limit=position;poslition=0。在写的时候也要先调用clear()方法,使position=0,limit=capacity,否则在写的时候会一直返回0)
limit:Buffer最多可操作的数据的位置。在往Buffer中写数据时表示最多可写到数据量为limit。从Buffer中读数据时需要开启Buffer的读模式,读从position到limit位置的数据。
capcity:Buffer数组的容量。即可以容纳的最大的数据量,在缓冲区创建时被设定并且不能改变。
mark:备忘位置,调用mark()使得mark=position,调用reset(),恢复postion使position=mark。
补充:1.调用put()方法可以往buffer中添加数据,调用get()方法可以从buffer中获取数据(该方法内部会自动移动指针)。
2.channel.read(buffer)也是从channel往buffer中写数据,channel.write(buffer)是从buffer中读取往channel中写。
3.Channel.transferFrom(targetChannel,position,count)表示从目标通道中复制数据到当前通道,position表示起始位,count表示复制的长度。Channel.transferTo(position,count,targetChannel)表示将当前通道的数据复制到目标通道。
3.调用asReadOnlyBuffer()方法返回的buffer为只读buffer,只能从中读取数据,不能往里写入数据。
4.MappedBuffer可以让文件直接在内存中进行修改。
创建buffer的方法:
ByteBuffer allocate(int capacity):在堆上创建指定大小的缓冲
ByteBuffer allocate(int capacity):在堆外空间创建指定大小的缓冲
ByteBuffer wrap(byte[] array):通过byte数组实例创建一个实例
3. Channel(通道)
通道是双向的,和流不同,流是单向的。NIO可以通过Channel进行数据的读,写和同时读写操作,但是不能直接访问数据,需要和缓冲区Buffer进行交互。与Selector一起使用时,Channel必须处于非阻塞模式下。
通道分为两大类:一类是网络读写(SelectableChannel),一类是用于文件操作(FileChannel)。另外我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类,都是用于TCP的数据连接。在创建服务器的时候会先创建一个ServerSocketChannel(ServerSocketChannelimp),当有客户端来请求连接时,服务器端会给客户端生成一个SocketChannel,也就是我们所说的Channel通道。(DatagramChannel用于UDP的数据读写)
ServerSocketChannel的主要作用是在服务器端监听新的客户端的连接,然后调用accept()为客户端生成一个SocketChannel。
SocketChannel,网络IO通道,具体负责进行进行读写操作,负责把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
channel与buffer之间的关系:
举例说明: 山西有煤,我们想要,于是建立一条铁路到山西,这条铁路就是这里的"Channel",那么煤通过什么运过来呢?铁路建好了,就差火车了,因此这里的火车就像是缓冲区"Buffer",火车把山西的煤运到这边来,把我们这里的钱运过去。
【案例】:编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)。
//服务端
import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; public class NIOserver { public static void main(String[] args) throws Exception{ //通过open静态方法创建Selector多路复用器 Selector selector=Selector.open(); //通过open静态方法创建ServerSocketChannel ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); //绑定端口号6666,在服务端监听 serverSocketChannel.socket().bind(new InetSocketAddress(6666)); //设置为非阻塞,在NIO编程中都需要将serverSocketChannel设置为false,非阻塞的 serverSocketChannel.configureBlocking(false); //然后将serverSocketChannel注册到seletor,它关心事件为OP_ACCEPT serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); //循环等待客户端进行连接 while (true){ //如果没有事件发生,则一直会while循环。只有检测到有事件发生才会走到下一步 if(selector.select(500)==0){//轮询检测是否有事件(连接事件)发生 System.out.println("服务器等待中,暂无事件发生。。。"); continue; } //通过selector.selectedKeys()返回发生事件的集合,然后通过selectionKeys反向获取通道 Set<SelectionKey> selectionKeys=selector.selectedKeys(); //先遍历Set<SelectionKey> Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()){ //获取到SelectionKey SelectionKey key = keyIterator.next(); if(key.isAcceptable()){//如果是OP_ACCEPT,表示有新的客户端连接 //给该客户端生成一个SocketChannel SocketChannel socketChannel=serverSocketChannel.accept(); //将socketChannel注册到selector,关注的事件为OP_READ,同时给socketChannel关联一个buffer socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024)); } if(key.isReadable()){//OP_READ 读事件 //通过key反向获取到对应的channel SocketChannel channel = (SocketChannel) key.channel(); //获取到该channel关联的buffer ByteBuffer buffer=(ByteBuffer)key.attachment(); //将channel中的事件读取到buffer channel.read(buffer); System.out.println("from 客户端"+new String(buffer.array())); } //手动从集合中移动当前selectionKey,防止重复操作 keyIterator.remove(); } } } }
//客户端 public class NIOClient { public static void main(String[] args) throws Exception{ Scanner in=new Scanner(System.in); //得到一个网络通道 SocketChannel socketChannel=SocketChannel.open(); //得到一个多路复用器 Selector selector=Selector.open(); //设置非阻塞 socketChannel.configureBlocking(false); //提供服务器端的ip和端口 InetSocketAddress inetSocketAddress=new InetSocketAddress("127.0.0.1",6666); //尝试连接服务器 if(!socketChannel.connect(inetSocketAddress)){ //将socketChannel注册到多路复用器上 socketChannel.register(selector,SelectionKey.OP_CONNECT); } while(true){ //等待连接完成 selector.select();//阻塞的 //通过selector返回事件集 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //迭代遍历 Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { //得到一个事件集 SelectionKey selectionKey = keyIterator.next(); //对事件类型进行判断 if (selectionKey.isConnectable()) {//连接事件 //可连接事件 SocketChannel socketchannel_1 = (SocketChannel) selectionKey.channel(); //连接操作完成 socketchannel_1.finishConnect(); System.out.println("连接成功"); socketchannel_1.register(selector,SelectionKey.OP_READ); } if(selectionKey.isReadable()) { ByteBuffer buffer=ByteBuffer.allocate(100);//根据str的情况定义一个buffer //发送数据,将buffer数据写入channel socketChannel.read(buffer); String Smsg=new String(buffer.array()); System.out.println("服务端返回的消息:"+Smsg); if("exit".equals(Smsg)){ System.out.println("退出命令"); break;//退出条件 } } keyIterator.remove(); } //如果连接成功,就发送数据 String Cmsg=in.nextLine(); ByteBuffer buffer=ByteBuffer.allocate(100);//根据str的情况定义一个buffer buffer.put((Cmsg+" ").getBytes());//添加数据 //因为写是从头开始的,所以进行读写切换 buffer.flip(); //发送数据,将buffer数据写入channel socketChannel.write(buffer); } } }
上面是单线程方式的,下面就演示下使用多线程如何编码。(客户端代码不变)
public class NIOserver { public static void main(String[] args) { Selector selector; ServerSocketChannel serverSocketChannel; try{ //通过open静态方法创建Selector多路复用器 selector=Selector.open(); //通过open静态方法创建ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); //绑定端口号6666,在服务端监听 serverSocketChannel.socket().bind(new InetSocketAddress(6666)); //设置为非阻塞,在NIO编程中都需要将serverSocketChannel设置为false,非阻塞的 serverSocketChannel.configureBlocking(false); //然后将serverSocketChannel注册到seletor,它关心事件为OP_ACCEPT serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); // 创建线程池 ExecutorService newFixedThreadPool =Executors.newFixedThreadPool(4); //循环等待客户端进行连接 while (true){ //如果没有事件发生,则一直会while循环。只有检测到有事件发生才会走到下一步 if(selector.select(500)==0){//轮询检测是否有事件(连接事件,读事件)发生 // System.out.println("服务器等待中,暂无事件发生。。。"); continue; } //通过selector.selectedKeys()返回发生事件的集合,然后通过selectionKeys反向获取通道 Set<SelectionKey> selectionKeys=selector.selectedKeys(); //先遍历Set<SelectionKey> Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()){ //获取到SelectionKey SelectionKey key = keyIterator.next(); //手动从集合中移动当前selectionKey,防止重复操作 keyIterator.remove(); if(key.isAcceptable()){//如果是OP_ACCEPT,表示有新的客户端连接 //先获取到服务器的SeverSocketChannel ServerSocketChannel serverSocketChannel_1=(ServerSocketChannel)key.channel(); //服务端进行等待连接,并返回客户端的SocketChannel SocketChannel socketChannel=serverSocketChannel_1.accept(); System.out.println("客户端连接成功,并生成了一个Channel"); //开启一个子线程 newFixedThreadPool.execute(new ServersubThread(socketChannel)); } } } }catch (Exception e){ }finally { //if(selector!=null)selector.close(); //if(serverSocketChannel!=null)serverSocketChannel.close(); } } } class ServersubThread implements Runnable{ SocketChannel socketchannel; public ServersubThread(SocketChannel socketchannel){ this.socketchannel=socketchannel; } @Override public void run() { try{ //获取多路复用器 Selector subSelector = Selector.open(); //将其设置为非阻塞 socketchannel.configureBlocking(false); //将socketChannel注册到selector,关注的事件为OP_READ socketchannel.register(subSelector,SelectionKey.OP_READ); while (true){ if(subSelector.select()==0) continue;//没有事件就一直监听 //返回事件集 Set<SelectionKey> selectionsKeys = subSelector.selectedKeys(); //对事件集遍历 Iterator<SelectionKey> keyIterator = selectionsKeys.iterator(); while (keyIterator.hasNext()){ SelectionKey key = keyIterator.next(); if(key.isReadable()){//OP_READ 读事件 //通过key反向获取到对应的channel //为什么上面通过key.channel得到的是ServerSocketChannel,而这块得到的确实 SocketChannel //因为连接事件是通过服务端的severSocket注册的,而读事件是通过客户端的socket注册的 SocketChannel socketchannel = (SocketChannel) key.channel(); //定义buffer ByteBuffer buffer=ByteBuffer.allocate(1024); //将channel中的事件读取到buffer socketchannel.read(buffer); String msg=new String(buffer.array()); if("exit".equals(msg)){ System.out.println("退出命令"); break; } System.out.println(Thread.currentThread().getName()+"from 客户端"+msg); buffer.flip(); //将内容返回给客户端 socketchannel.write(buffer); } } } }catch (Exception e){ e.getMessage(); }finally { try { socketchannel.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用多线程相当于在服务端的主线程中只进行监听客户端的连接事件,如果连接成功就为每一个客户端创建一个子线程用来监听IO读事件,这样主线程与子线程配合使用,减少了服务端的业务,可更好的适应访问高并发问题,大大提高服务端的执行效率。
【案例】:群聊系统
//客户端
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Scanner; import java.util.Set; public class GroupChatClient { //定义相关属性 private Selector selector; private SocketChannel socketChannel; private String username; //构造器 public GroupChatClient(){ try { //获取到多路选择器 selector=Selector.open(); //获取到Channel,并连接服务器 socketChannel=socketChannel.open(new InetSocketAddress("127.0.0.1",6667)); //设置为非阻塞的 socketChannel.configureBlocking(false); //将其注册到selector socketChannel.register(selector,SelectionKey.OP_READ); username=socketChannel.getLocalAddress().toString().substring(1); System.out.println(username+"is ok!"); } catch (IOException e) { e.printStackTrace(); } } public void sendInfo(String info){ String msg=username+"说:"+info; try { //将消息写入通道发送给服务端 socketChannel.write(ByteBuffer.wrap(msg.getBytes())); } catch (IOException e) { e.printStackTrace(); } } public void readInfo(){ try { int readChannels=selector.select(500); //有事件发生 if(readChannels>0){ //获取有事件发生的selectionKey Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); //判断事件的类型 if(key.isReadable()){ //得到相关的通道 SocketChannel channel = (SocketChannel) key.channel(); //创建一个buffer用来存储从通道中读取到达数据 ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer);//从通道读取数据 System.out.println(new String(buffer.array())); } } iterator.remove(); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { //启动客户端 GroupChatClient groupChatClient=new GroupChatClient(); //启动一个线程,来读取服务器端发来的数据 new Thread(){ @Override public void run() { while (true){//每隔一秒就去读取一下 groupChatClient.readInfo(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); //发送数据给服务器端 Scanner scanner=new Scanner(System.in); while (scanner.hasNext()){ groupChatClient.sendInfo(new String(scanner.nextLine())); } } }
//服务端
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; public class GroupChatServer { //定义属性 private Selector selector;//多路复用选择器 private ServerSocketChannel listenChannel;//监听器 private static final int PORT=6667;//端口号 //构造器 public GroupChatServer(){ try { //获得选择器 selector=Selector.open(); //获取监听器 listenChannel=ServerSocketChannel.open(); //绑定端口 listenChannel.socket().bind(new InetSocketAddress(PORT)); //将Channel设置为非阻塞的 listenChannel.configureBlocking(false); //将ServerSocketChannel注册到selector上 listenChannel.register(selector,SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } public void listen(){ try { //轮询监听是否有事件发生 while (true){ if(selector.select(500)==0){//若没有检测到,则继续检测 //System.out.println("等待。。。。"); continue; } System.out.println("监测到事件"); //检测到有事件发生,就获取发生事件的selectionKey Set<SelectionKey> selectionKeys=selector.selectedKeys(); //进行遍历 Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()){ SelectionKey key = keyIterator.next(); //判断是否有客户端需要连接 if(key.isAcceptable()){ //给客户端分配一个soketChannel SocketChannel socketChannel = listenChannel.accept(); //然后将其设置成非阻塞的 socketChannel.configureBlocking(false); //接着注册给客户端 socketChannel.register(selector,SelectionKey.OP_READ); System.out.println(socketChannel.getRemoteAddress()+"上线了"); } if(key.isReadable()){ readData(key); } keyIterator.remove();//删除当前SelectionKey,防止重复操作 } } } catch (IOException e) { e.printStackTrace(); } } public void readData(SelectionKey key){ //先获取Channel SocketChannel channel = (SocketChannel) key.channel(); //获取buffer ByteBuffer buffer = ByteBuffer.allocate(1024); //将channel中的数据读到buffer中 try { channel.read(buffer); } catch (IOException e) { try { System.out.println(channel.getRemoteAddress()+"下线了"); //取消注册 key.cancel(); // 关闭通道 channel.close(); } catch (IOException e1) { e1.printStackTrace(); } } String msg=new String(buffer.array()); System.out.println("from"+msg); //从客户端接收到消息后,接着转发给其他客户 send(msg,channel); } //转发消息给其他客户 public void send(String msg,SocketChannel channel){ System.out.println("消息转发中。。。。"); //遍历所有注册到selector上的channel for (SelectionKey key:selector.keys()) { //通过key获取channel Channel targetchannel=key.channel(); if(targetchannel instanceof SocketChannel&&channel!=targetchannel){//排除自己 ByteBuffer buffer=ByteBuffer.wrap(msg.getBytes()); try { ((SocketChannel) targetchannel).write(buffer); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) { //创建服务器端对象 GroupChatServer groupChatServer=new GroupChatServer(); //开始一直监听 groupChatServer.listen(); } }
2.3 AIO模型
AIO 也叫NIO2.0 是一种异步非阻塞的通信模式。在NIO的基础上引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。
AIO 并没有采用NIO的多路复用器,而是使用异步通道的概念。其read,write方法的返回类型都是Future对象。而Future模型是异步的,其核心思想是:去主函数等待时间。内核先把数据准备好,然后再把数据主动拷贝到程序空间,准备数据和拷贝数据程序都不参与,拷贝好了,系统通知程序可以执行程序了,但AIO现在没有真正的实例,所以一般谈得比较少。
小结:AIO模型中通过AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道的实现。非阻塞,异步。
常见面试题:
1BIO,NIO,AIO区别
BIO 阻塞同步通信模式,客户端和服务器连接需要三次握手,使用简单,但吞吐量小
NIO 非阻塞同步通信模式,客户端与服务器通过Channel连接,采用多路复用器轮询注册的Channel。提高吞吐量和可靠性。
AIO 非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,其read和write方法均是异步方法。
BIO,NIO的区别
1.BIO以流的方式处理数据的。而NIO以块的方式处理数据,块I/O的效率比流I/O的效率高很多。
2.BIO是阻塞的,NIO是非阻塞的。
3.BIO是基于字节流和字符流进行操作的,要么输入流,要么输出流,不能双向。而NIO是基于Channel(通道)和Buffer(缓冲区)进行操作,Buffer既可以读也可以写,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中,Channel也是双向的。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单线程就可以监听多个客户端通道。
2 Stock通信的伪代码实现流程
服务器绑定端口:server = new ServerSocket(PORT)
服务器阻塞监听:socket = server.accept()
服务器开启线程:new Thread(Handle handle)
服务器读写数据:BufferedReader PrintWriter
客户端绑定IP和PORT:new Socket(IP_ADDRESS, PORT)
客户端传输接收数据:BufferedReader PrintWriter
3 TCP协议与UDP协议有什么区别
TCP : 传输控制协议是基于连接的协议,在正式收发数据前,必须和对方建立可靠的连接。速度慢,合适传输大量数据。
UDP : 用户数据报协议是与TCP相对应的协议。面向非连接的协议,不与对方建立连接,而是直接就把数据包发送过去,速度快,适合传输少量数据。
4 什么是同步阻塞BIO,同步非阻塞NIO,异步非阻塞AIO
同步阻塞IO : 用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。
同步非阻塞IO: 用户进程发起一个IO操作以后,可做其它事情,但用户进程需要经常询问IO操作是否完成,这样造成不必要的CPU资源浪费。
异步非阻塞IO: 用户进程发起一个IO操作然后,立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。类比Future模式。
总结:
BIO | NIO | AIO 以Java的角度,理解如下:
- BIO,同步阻塞式IO,简单理解:一个线程处理一个连接,发起和处理IO请求都是同步的。通过Socket和ServerSocket完成套接字通道实现。阻塞,同步,连接耗时。
- NIO,同步非阻塞IO,简单理解:一个线程处理多个连接,发起IO请求是非阻塞的但处理IO请求是同步的。通过SocketChannel和ServerSocketChannel完成套接字通道实现。非阻塞/阻塞,同步,避免TCP建立连接使用三次握手带来的开销。
- AIO,异步非阻塞IO,简单理解:一个有效请求一个线程,发起和处理IO请求都是异步的。通过AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道实现。非阻塞,异步。
借鉴:https://www.cnblogs.com/diegodu/p/6823855.html
https://www.cnblogs.com/itdragon/p/8337234.html
当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪