本次咱们主要来讲一下咱们的IO阻塞模型,只是很少,可是必定要理解,对于后面理解netty很重要的java
IO模型精讲 编程
IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO。数组
BIO服务器
BIO(Blocking IO) 同步阻塞模型,一个客户端链接对应一个处理线程。也是咱们熟悉的同步阻塞模型,先别管那个同步的概念,咱们先来看一下什么是阻塞,简单来一段代码。网络
服务端:多线程
package com.xiaocai.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000); while (true) { System.out.println("等待链接。。"); //阻塞方法 Socket socket = serverSocket.accept(); System.out.println("有客户端链接了。。"); handler(socket); } } private static void handler(Socket socket) throws IOException { System.out.println("thread id = " + Thread.currentThread().getId()); byte[] bytes = new byte[1024]; System.out.println("准备read。。"); //接收客户端的数据,阻塞方法,没有数据可读时就阻塞 int read = socket.getInputStream().read(bytes); System.out.println("read完毕。。"); if (read != -1) { System.out.println("接收到客户端的数据:" + new String(bytes, 0, read)); System.out.println("thread id = " + Thread.currentThread().getId()); } socket.getOutputStream().write("HelloClient".getBytes()); socket.getOutputStream().flush(); } }
客户端异步
package com.xiaocai.bio; import java.io.IOException; import java.net.Socket; public class SocketClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 9000); //向服务端发送数据 socket.getOutputStream().write("HelloServer".getBytes()); socket.getOutputStream().flush(); System.out.println("向服务端发送数据结束"); byte[] bytes = new byte[1024]; //接收服务端回传的数据 socket.getInputStream().read(bytes); System.out.println("接收到服务端的数据:" + new String(bytes)); socket.close(); } }
这个就是一个简单的BIO服务端代码,就是要准备接受线程访问的代码段。这一个单线程版本什么意思呢?socket
咱们先开启一个端口为9000的socket服务,而后运行Socket socket = serverSocket.accept();意思就是等待线程的出现,咱们来接收客户端的请求,这个方法时阻塞的,也是只有在阻塞状态才能够接收到咱们的请求。当有请求进来时,运行handler(socket);方法,中间是打印线程ID的方法不解释,int read = socket.getInputStream().read(bytes);准备读取咱们的客户端发送数据。read和write可能会混淆,我画个图来讲一下。ide
咱们也能够看到咱们的客户端也是先拿到socket链接(Socket socket = new Socket("127.0.0.1", 9000)),而后要往服务端写入数据(socket.getOutputStream().write("HelloServer".getBytes());)以byte字节形式写入。这时咱们的服务端等待read咱们的客户端weite的数据,会进入阻塞状态,若是咱们的客户端迟迟不写数据,咱们的客户端一直是阻塞状态,也就没法接收到新的请求,由于阻塞了,无法回到咱们的Socket socket = serverSocket.accept();去等待客户端请求,只要在serverSocket.accept阻塞时才能够接收新的请求。因而咱们采起了多线程的方式来解决这个问题,咱们来看一下代码。性能
package com.xiaocai.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000); while (true) { System.out.println("等待链接。。"); //阻塞方法 Socket socket = serverSocket.accept(); System.out.println("有客户端链接了。。"); new Thread(new Runnable() { @Override public void run() { try { handler(socket); } catch (IOException e) { e.printStackTrace(); } } }).start(); } } private static void handler(Socket socket) throws IOException { System.out.println("thread id = " + Thread.currentThread().getId()); byte[] bytes = new byte[1024]; System.out.println("准备read。。"); //接收客户端的数据,阻塞方法,没有数据可读时就阻塞 int read = socket.getInputStream().read(bytes); System.out.println("read完毕。。"); if (read != -1) { System.out.println("接收到客户端的数据:" + new String(bytes, 0, read)); System.out.println("thread id = " + Thread.currentThread().getId()); } socket.getOutputStream().write("HelloClient".getBytes()); socket.getOutputStream().flush(); } }
咱们这时每次有客户端来新的请求时,咱们就会开启一个线程来处理这个请求,及时你的客户端没有及时的write数据,虽然咱们的服务端read进行了阻塞,也只是阻塞了你本身的线程,不会形成其它请求没法接收到。
这样的处理方式貌似好了不少不少,其实否则,想一个实例,咱们的看小妹直播时,一句欢迎榜一大哥,弹幕不少,加入一次性来了100弹幕还好,咱们开启100个线程来处理,若是一块儿来了十万弹幕呢?难道你要开启十万个线程来处理这些弹幕嘛?很显然BIO仍是有弊端的,BIO仍是有优势的(代码少,不容易出错)。
NIO
NIO(Non Blocking IO) 同步非阻塞,服务器实现模式为一个线程能够处理多个请求(链接),客户端发送的链接请求都会注册到多路复用器selector上,多路复用器轮询到链接有IO请求就进行处理。 可能概念太抽象了,我来举个例子吧,如今有两个小区都有不少的房子出租,BIO小区和NIO小区,都有一个门卫,BIO小区,来了一个租客,门卫大爷就拿着钥匙,带这个租客去看房子了,后面来的租客都暂时没法看房子了,尴尬...想同时多人看房子,必须增长门卫大爷的数量,而咱们的NIO小区就很聪明,仍是一个门卫大妈,来了一个租客要看房子,门卫大妈,给了那个租客一把钥匙,而且告诉他哪房间是空的,你本身进去看吧,及时这个租客看房子慢,耽误了不少时间也不怕了,由于门卫大妈一直在门卫室,即便又来了新的租客,门卫大妈也是如此,只给钥匙和空房间地址就能够了。这个例子反正我记得很清楚,也以为很贴切,这里提到了一个钥匙的概念,一会告诉大家是作什么的,咱们先看一下代码。
服务端
package com.xiaocai.nio; 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.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class NIOServer { //public static ExecutorService pool = Executors.newFixedThreadPool(10); public static void main(String[] args) throws IOException { // 建立一个在本地端口进行监听的服务Socket通道.并设置为非阻塞方式 ServerSocketChannel ssc = ServerSocketChannel.open(); //必须配置为非阻塞才能往selector上注册,不然会报错,selector模式自己就是非阻塞模式 ssc.configureBlocking(false); ssc.socket().bind(new InetSocketAddress(9000)); // 建立一个选择器selector Selector selector = Selector.open(); // 把ServerSocketChannel注册到selector上,而且selector对客户端accept链接操做感兴趣 ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { System.out.println("等待事件发生。。"); // 轮询监听channel里的key,select是阻塞的,accept()也是阻塞的 int select = selector.select(); System.out.println("有事件发生了。。"); // 有客户端请求,被轮询监听到 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); //删除本次已处理的key,防止下次select重复处理 it.remove(); handle(key); } } } private static void handle(SelectionKey key) throws IOException { if (key.isAcceptable()) { System.out.println("有客户端链接事件发生了。。"); ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //NIO非阻塞体现:此处accept方法是阻塞的,可是这里由于是发生了链接事件,因此这个方法会立刻执行完,不会阻塞 //处理完链接请求不会继续等待客户端的数据发送 SocketChannel sc = ssc.accept(); sc.configureBlocking(false); //经过Selector监听Channel时对读事件感兴趣 sc.register(key.selector(), SelectionKey.OP_READ); } else if (key.isReadable()) { System.out.println("有客户端数据可读事件发生了。。"); SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); //NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时确定是发生了客户端发送数据的事件 int len = sc.read(buffer); if (len != -1) { System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len)); } ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes()); sc.write(bufferToWrite); key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } else if (key.isWritable()) { SocketChannel sc = (SocketChannel) key.channel(); System.out.println("write事件"); // NIO事件触发是水平触发 // 使用Java的NIO编程的时候,在没有数据能够往外写的时候要取消写事件, // 在有数据往外写的时候再注册写事件 key.interestOps(SelectionKey.OP_READ); //sc.close(); } } }
客户端
package com.xiaocai.nio; 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; public class NioClient { //通道管理器 private Selector selector; /** * 启动客户端测试 * * @throws IOException */ public static void main(String[] args) throws IOException { NioClient client = new NioClient(); client.initClient("127.0.0.1", 9000); client.connect(); } /** * 得到一个Socket通道,并对该通道作一些初始化的工做 * * @param ip 链接的服务器的ip * @param port 链接的服务器的端口号 * @throws IOException */ public void initClient(String ip, int port) throws IOException { // 得到一个Socket通道 SocketChannel channel = SocketChannel.open(); // 设置通道为非阻塞 channel.configureBlocking(false); // 得到一个通道管理器 this.selector = Selector.open(); // 客户端链接服务器,其实方法执行并无实现链接,须要在listen()方法中调 //用channel.finishConnect() 才能完成链接 channel.connect(new InetSocketAddress(ip, port)); //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。 channel.register(selector, SelectionKey.OP_CONNECT); } /** * 采用轮询的方式监听selector上是否有须要处理的事件,若是有,则进行处理 * * @throws IOException */ public void connect() throws IOException { // 轮询访问selector while (true) { selector.select(); // 得到selector中选中的项的迭代器 Iterator<SelectionKey> it = this.selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); // 删除已选的key,以防重复处理 it.remove(); // 链接事件发生 if (key.isConnectable()) { SocketChannel channel = (SocketChannel) key.channel(); // 若是正在链接,则完成链接 if (channel.isConnectionPending()) { channel.finishConnect(); } // 设置成非阻塞 channel.configureBlocking(false); //在这里能够给服务端发送信息哦 ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes()); channel.write(buffer); //在和服务端链接成功以后,为了能够接收到服务端的信息,须要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); // 得到了可读的事件 } else if (key.isReadable()) { read(key); } } } } /** * 处理读取服务端发来的信息 的事件 * * @param key * @throws IOException */ public void read(SelectionKey key) throws IOException { //和服务端的read方法同样 // 服务器可读取消息:获得事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 建立读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); int len = channel.read(buffer); if (len != -1) { System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len)); } } }
代码看到了不少不少,我来解释一下大概什么意思吧,这个NIO超级重要后面的netty就是基于这个写的,必定要搞懂,首先咱们建立了一个ServerSocketChannel和一个选择器selector,设置为非阻塞的(固定写法,没有为何),将咱们的 selector绑定到咱们的ServerSocketChannel上,而后运行selector.select();进入阻塞状态,别担忧,这个阻塞没影响,为咱们提供了接收客户端的请求,你没有请求,我阻塞着,不会耽误大家什么的。
回到咱们的客户端,仍是差很少的样子,拿到咱们的NioClient开始链接咱们的服务端,这个时候,咱们的服务端接收到了咱们的客户端请求,阻塞状态的selector.select()继续运行,而且给予了一个SelectionKey(Iterator<SelectionKey> it = selector.selectedKeys().iterator())也就是咱们刚才的小例子中提到的钥匙,key=钥匙,还算是靠谱吧~!开始运行咱们的handle方法,有个if else,这个是说,你是第一次请求要创建通道,仍是要写数据,仍是要读取数据,记住啊,读写都是相对的,本身多琢磨几回就能够转过圈来了,就是我上面画图说的read和write。拿咱们的创建通道来讲,经过咱们的钥匙key你就能够获得ServerSocketChannel,而后进行设置下次可能会发生的读写事件,而后看咱们的读事件,咱们看到了int len = sc.read(buffer)这个读在咱们的BIO中是阻塞的,而咱们的NIO这个方法不是阻塞的,这也就体现出来了咱们的BIO同步阻塞和NIO同步非阻塞,阻塞和非阻塞的区别也就说完了。画个图,咱们来看一下咱们的NIO模型。
NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(选择器)
这里咱们的Buffer没有去说,到netty会说的, Channel(通道), Buffer(缓冲区)都是双向的,如今回过头来想一想我举的小例子,selector门卫大妈,SelectionKey钥匙。对于NIO有了一些理解了吧,NIO看着很棒的,可是你有想过写上述代码的痛苦吗?
AIO
AIO(NIO 2.0) 异步非阻塞, 由操做系统完成后回调通知服务端程序启动线程去处理, 通常适用于链接数较多且链接时间较长的应用。其实AIO就是对于NIO的二次封装,要不怎么叫作NIO2.0呢,咱们来简单看一下代码。
服务端:
package com.xiaocai.aio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; public class AIOServer { public static void main(String[] args) throws Exception { final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000)); serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { @Override public void completed(AsynchronousSocketChannel socketChannel, Object attachment) { try { // 再此接收客户端链接,若是不写这行代码后面的客户端链接连不上服务端 serverChannel.accept(attachment, this); System.out.println(socketChannel.getRemoteAddress()); ByteBuffer buffer = ByteBuffer.allocate(1024); socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer buffer) { buffer.flip(); System.out.println(new String(buffer.array(), 0, result)); socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes())); } @Override public void failed(Throwable exc, ByteBuffer buffer) { exc.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); Thread.sleep(Integer.MAX_VALUE); } }
客户端:
package com.xiaocai.aio; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; public class AIOClient { public static void main(String... args) throws Exception { AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get(); socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes())); ByteBuffer buffer = ByteBuffer.allocate(512); Integer len = socketChannel.read(buffer).get(); if (len != -1) { System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len)); } } }
阻塞非阻塞都明白了,这里来解释一下同步,咱们看到咱们的AIO在serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {}直接开启了线程,也就是说accept直接之后,我再也不须要考虑阻塞状况,能够继续运行下面的代码了,也就是咱们说到的异步执行,内部仍是咱们的NIO,不要以为AIO多么的6B,内部就是封装了咱们的NIO,性能和NIO其实差很少的,可能有些时候还不如NIO(未实测)。
遗漏一个知识点,NIO的多路复用器是如何工做的,在咱们的JDK1.5之前的,多路复用器是数组和链表的方式来遍历的,到了咱们的JDK1.5采用hash来回调的。
总结:
咱们此次主要说了BIO、NIO、AIO三个网络编程IO模式,最重要的就是咱们的NIO,一张图来总结一下三个IO的差异吧。
最进弄了一个公众号,小菜技术,欢迎你们的加入