个人随笔
目录
netty(一):一文搞懂BIO、NIO、AIO
2021-01-19 21:31:07

IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO.

一、BIO(Blocking IO)

同步阻塞模型,一个客户端连接对应一个处理线程

1、缺点

  • 1、IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源
  • 2、如果线程很多,会导致服务器线程太多,压力太大。

2、应用场景

BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。

3、BIO代码示例

服务端

  1. public class BIOServer {
  2. public static void main(String[] args) {
  3. try {
  4. ServerSocket serverSocket = new ServerSocket(9090);
  5. while(true) {
  6. //阻塞调用
  7. System.out.println("阻塞调用,监听客户端的请求");
  8. Socket socket = serverSocket.accept();
  9. System.out.println("有客户端进来...");
  10. //handler(socket);
  11. new Thread(()->{
  12. try {
  13. handler(socket);
  14. } catch (IOException e) {
  15. // TODO Auto-generated catch block
  16. e.printStackTrace();
  17. }
  18. }).start();
  19. /**
  20. new Thread(new Runnable() {
  21. @Override
  22. public void run() {
  23. handler(socket);
  24. }
  25. }).start();
  26. **/
  27. }
  28. } catch (IOException e) {
  29. // TODO Auto-generated catch block
  30. e.printStackTrace();
  31. }
  32. }
  33. private static void handler(Socket socket) throws IOException {
  34. System.out.println("thread id = "+Thread.currentThread().getId());
  35. byte[] bytes = new byte[1024];
  36. System.out.println("准备read。。。");
  37. //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
  38. System.out.println("阻塞调用,监听客户端发送消息事件");
  39. int read =socket.getInputStream().read(bytes);
  40. System.out.println("read 完毕。。。");
  41. if(read!=-1) {
  42. System.out.println("接受服务器的数据:"+new String(bytes,0,read));
  43. System.out.println("thread id = "+Thread.currentThread().getId());
  44. }
  45. socket.getOutputStream().write("客户端您好,我是大佬".getBytes());
  46. socket.getOutputStream().flush();
  47. }
  48. }

客户端

  1. public class BIOClient {
  2. public static void main(String[] args) throws UnknownHostException, IOException {
  3. Socket socket = new Socket("localhost",9090);
  4. //向服务端发数据
  5. socket.getOutputStream().write("您好服务器,我是客户端".getBytes());
  6. socket.getOutputStream().flush();
  7. System.out.println("向服务器发送数据结束");
  8. byte[] bytes = new byte[1024];
  9. //阻塞调用,等待服务端发数据
  10. int read =socket.getInputStream().read(bytes);
  11. System.out.println("read 完毕。。。");
  12. if(read!=-1) {
  13. System.out.println("接受客户端的数据:"+new String(bytes,0,read));
  14. System.out.println("thread id = "+Thread.currentThread().getId());
  15. }
  16. }
  17. }

BIO是阻塞调用的,服务端阻塞体现在下面两段代码逻辑:

  1. //这里会阻塞监听客户端的请求
  2. Socket socket = serverSocket.accept();
  1. //阻塞调用,监听客户端发送消息事件
  2. int read =socket.getInputStream().read(bytes);

客户端的阻塞体现在如下代码逻辑:

  1. //阻塞调用,等待服务端发数据
  2. int read =socket.getInputStream().read(bytes);

二、NIO(Non Blocking IO)

同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理。
I/O多路复用底层一般用的Linux API(select,poll,epoll)来实现,他们的区别如下表:

1、应用场景

NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂, JDK1.4 开始支持。

NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(选择器)

下面是我之前写的三篇博文,可以参考下。

一、实战-缓冲区Buffer(NIO)
二、实战-通道(Channel)(NIO)
实战:Selector选择器(NIO)

1、channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组
2、channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
3、selector 可以对应一个或多个线程
4、NIO 的 Buffer 和 channel 都是既可以读也可以写

2、NIO代码示例

服务端

  1. public class NIOServer {
  2. public static void main(String[] args) throws IOException {
  3. //创建一个在本地端口进行监听的服务Socket通道,并设置为非阻塞方式
  4. ServerSocketChannel ssc = ServerSocketChannel.open();
  5. //必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
  6. ssc.configureBlocking(false);
  7. ssc.socket().bind(new InetSocketAddress(9090));
  8. //创建一个选择器selector
  9. Selector selector = Selector.open();
  10. //把ServerSocketChannel注册到selector上,并且slector对客户端accept连接操作感兴趣
  11. ssc.register(selector, SelectionKey.OP_ACCEPT);
  12. //此时selector里面有一个channel了
  13. while(true) {
  14. System.out.println("等待事件发生。。");
  15. //轮询监听channel里的key,select是阻塞的,accept()也是阻塞的
  16. int select = selector.select();
  17. System.out.println("有事件发生了");
  18. //有客户端请求,轮询监听到
  19. Iterator<SelectionKey> it = selector.selectedKeys().iterator();
  20. while (it.hasNext()) {
  21. SelectionKey key = it.next();
  22. //删除本次已处理的key,防止下次select重复处理
  23. it.remove();
  24. handle(key);
  25. }
  26. }
  27. }
  28. private static void handle(SelectionKey key) throws IOException {
  29. if(key.isAcceptable()) {
  30. System.out.println("有客户端连接事件发生了");
  31. ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
  32. //NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为发生了连接事件,所以这个方法会
  33. //马上执行,不会阻塞
  34. //处理完连接请不会继续等待客户端的数据发送
  35. SocketChannel sc = ssc.accept();
  36. //也要设置成非阻塞模式,然后跟ServerSocketChannel一样,放进Selector中去
  37. sc.configureBlocking(false);
  38. //通过Selector监听Channel时对读事件感兴趣
  39. //因为对于服务端,channel是读数据进来:channel既可以写,也可以读,跟流不一样
  40. sc.register(key.selector(), SelectionKey.OP_READ);
  41. //此时selector里面有两个channel了,一个ServerSocketChannel,一个SocketChannel
  42. //后面每多一个连接就多一个channel
  43. }else if(key.isReadable()) {
  44. //有客户端数据可读事件发生了。。
  45. SocketChannel sc = (SocketChannel)key.channel();
  46. ByteBuffer buffer = ByteBuffer.allocate(1024);
  47. //NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定时发生了客户端发送数据的事件
  48. int len =sc.read(buffer);
  49. if(len!=-1) {
  50. System.out.println("读取客户端发送的数据:"+new String(buffer.array(),0,len));
  51. }
  52. ByteBuffer bufferToWrite = ByteBuffer.wrap("您好,客户端,我是大佬".getBytes());
  53. sc.write(bufferToWrite);
  54. //对该channel设置对读和写事件的兴趣
  55. key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);
  56. }else if(key.isWritable()) {
  57. System.out.println("write事件");
  58. //NIO事件触发是水平触发的
  59. //使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件
  60. //在有数据往外写的时候再注册写事件
  61. key.interestOps(SelectionKey.OP_READ);
  62. }
  63. }
  64. }

客户端

  1. public class NIOClient {
  2. public static void main(String[] args) throws IOException {
  3. //获得一个Socket通道
  4. SocketChannel channel = SocketChannel.open();
  5. //设置通道为非阻塞
  6. channel.configureBlocking(false);
  7. //客户端连接服务器,其实方法执行并没有实现连接,需要listen()方法中
  8. //调用channel.finishConnect();才能完成连接
  9. channel.connect(new InetSocketAddress("127.0.0.1",9090));
  10. //获得一个通道选择器
  11. Selector selector = Selector.open();
  12. channel.register(selector, SelectionKey.OP_CONNECT);
  13. //采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
  14. while(true) {
  15. selector.select();
  16. System.out.println("有事件发生了");
  17. //有客户端请求,轮询监听到
  18. Iterator<SelectionKey> it = selector.selectedKeys().iterator();
  19. while (it.hasNext()) {
  20. SelectionKey key = it.next();
  21. //删除本次已处理的key,防止下次select重复处理
  22. it.remove();
  23. handle(key);
  24. }
  25. }
  26. }
  27. private static void handle(SelectionKey key) throws IOException {
  28. if(key.isConnectable()) {
  29. System.out.println("有连接事件发生了");
  30. SocketChannel sc = (SocketChannel)key.channel();
  31. //如果正在连接,则完成连接
  32. if(sc.isConnectionPending()) {
  33. sc.finishConnect();
  34. }
  35. //设置成非阻塞
  36. sc.configureBlocking(false);
  37. //再这里可以给服务端发送信息
  38. ByteBuffer bufferToWrite = ByteBuffer.wrap("您好,服务端,我是客户端".getBytes());
  39. sc.write(bufferToWrite);
  40. //再和服务端连接成功之后,为了可以接收到服务端的信息,
  41. //需要给通道设置读的权限
  42. key.interestOps(SelectionKey.OP_READ);
  43. }else if(key.isReadable()) {
  44. //有客户端数据可读事件发生了。。
  45. SocketChannel sc = (SocketChannel)key.channel();
  46. ByteBuffer buffer = ByteBuffer.allocate(1024);
  47. //NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定时发生了客户端发送数据的事件
  48. int len =sc.read(buffer);
  49. if(len!=-1) {
  50. System.out.println("客户端收到信息:"+new String(buffer.array(),0,len));
  51. }
  52. }
  53. }
  54. }

NIO服务端程序详细分析:

  • 1、创建一个 ServerSocketChannel 和 Selector ,并将 ServerSocketChannel 注册到 Selector 上
  • 2、 selector 通过 select() 方法监听 channel 事件,当客户端连接时,selector 监听到连接事件, 获取到 ServerSocketChannel 注册时
  • 绑定的 selectionKey
  • 3、selectionKey 通过 channel() 方法可以获取绑定的 ServerSocketChannel
  • 4、ServerSocketChannel 通过 accept() 方法得到 SocketChannel
  • 5、将 SocketChannel 注册到 Selector 上,关心 read 事件
  • 6、注册后返回一个 SelectionKey, 会和该 SocketChannel 关联
  • 7、selector 继续通过 select() 方法监听事件,当客户端发送数据给服务端,selector 监听到read事件,获取到 SocketChannel 注册时
  • 绑定的 selectionKey
  • 8、selectionKey 通过 channel() 方法可以获取绑定的 socketChannel
  • 9、将 socketChannel 里的数据读取出来
  • 10、用 socketChannel 将服务端数据写回客户端

总结:NIO模型的selector 就像一个大总管,负责监听各种IO事件,然后转交给后端线程去处理

NIO相对于BIO非阻塞的体现:

BIO的后端线程需要阻塞等待客户端写数据(比如read方法),如果客户端不写数据,线程就要阻塞,NIO把等待客户端操作的事情交给了大总管 selector,selector 负责轮询所有已注册的客户端,发现有事件发生了才转交给后端线程处理,后端线程不需要做任何阻塞等待,直接处理客户端事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。还有就是 channel 的读写是非阻塞的。

Redis就是典型的NIO线程模型,selector收集所有连接的事件并且转交给后端线程,线程连续执行所有事件命令并将结果写回客户端.

不管是ServerSocketChannel还是SocketChannel都是扔进selector管理,其实一个channel可以看作是直连客户端和服务端并且两边都可以读写数据。

三、AIO(NIO 2.0)

异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用。

1、应用场景

AIO方式适用于连接数目多且连接比较长(重操作) 的架构,JDK7 开始支持

2、代码示例

服务端

  1. public class AIOServer {
  2. public static void main(String[] args) throws IOException, InterruptedException {
  3. AsynchronousServerSocketChannel serverChannel =
  4. AsynchronousServerSocketChannel.open().
  5. bind(new InetSocketAddress(9090));
  6. serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
  7. //异步回调
  8. @Override
  9. public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
  10. //再此接收客户端连接,如果不写这行代码后面的客户端连接不上服务器端
  11. try {
  12. serverChannel.accept(attachment, this);
  13. System.out.println(socketChannel.getRemoteAddress());
  14. ByteBuffer buffer = ByteBuffer.allocate(024);
  15. socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
  16. //异步回调
  17. @Override
  18. public void completed(Integer result, ByteBuffer attachment) {
  19. buffer.flip();
  20. System.out.println(new String(buffer.array(),0,result));
  21. socketChannel.write(ByteBuffer.wrap("您好,客户端,我是大佬".getBytes()));
  22. }
  23. @Override
  24. public void failed(Throwable exc, ByteBuffer attachment) {
  25. exc.printStackTrace();
  26. }
  27. });
  28. } catch (IOException e) {
  29. // TODO Auto-generated catch block
  30. e.printStackTrace();
  31. }
  32. }
  33. @Override
  34. public void failed(Throwable exc, Object attachment) {
  35. exc.printStackTrace();
  36. }
  37. });
  38. System.out.println("arrive down。。。");
  39. //因为AIO是异步非阻塞的,所以这里必须设置等待
  40. Thread.sleep(Integer.MAX_VALUE);
  41. }
  42. }

客户端

  1. public class AIOClient {
  2. public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
  3. AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
  4. socketChannel.connect(new InetSocketAddress("127.0.0.1",9090)).get();
  5. //发送数据到服务端
  6. socketChannel.write(ByteBuffer.wrap("服务端您好,我是客户端".getBytes()));
  7. ByteBuffer buffer = ByteBuffer.allocate(1024);
  8. Integer len = socketChannel.read(buffer).get();
  9. if(len!=-1) {
  10. System.out.println("客户端收到信息:"+new String(buffer.array(),0,len));
  11. }
  12. }
  13. }

AIO的accept和read方法都通过回调来实现异步,其实AIO是对NIO进行了封装,当然后续要研究的netty也是对NIO的封装,毕竟直接NIO代码太容易出错了。

四、BIO、 NIO、 AIO 对比

 216

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号-2