前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >传统IO与NIO比较 顶

传统IO与NIO比较 顶

作者头像
算法之名
发布2019-09-09 17:10:07
3840
发布2019-09-09 17:10:07
举报
文章被收录于专栏:算法之名算法之名

我们先来看一段传统IO的代码

代码语言:javascript
复制
public class OioServer {
    public static void main(String[] args) throws IOException {
        //这里可以直接写成ServerSocket server = new ServerSocket(10101);
        ServerSocket server = new ServerSocket();
        server.bind(new InetSocketAddress(10101));
        System.out.println("服务器启动");
        while (true) {
            //此处会阻塞
            Socket socket = server.accept();
            System.out.println("来了一个新客户端");
            handler(socket);
        }
    }
    public static void handler(Socket socket) {
        try {
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();
            while (true) {
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes,0,read));
                }else {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                System.out.println("socket关闭");
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

使用telnet连接

admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

我们会看到OioServer的运行情况

服务器启动

来了一个新客户端

但是当我们又使用一个telnet连接进来的时候,OioServer的运行情况没变,说明一个服务端只能接收一个客户端点连接,原因在于Socket socket = server.accept();发生了堵塞,现在我们将其改写成多线程

代码语言:javascript
复制
public class OioServerThread {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(10101);
        ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
        System.out.println("服务器启动");
        while (true) {
            Socket socket = server.accept();
            System.out.println("来了一个新客户端");
            service.execute(() -> handler(socket));
        }
    }
    public static void handler(Socket socket) {
        try {
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();
            while (true) {
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes,0,read));
                }else {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                System.out.println("socket关闭");
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

运行可知,当我们启动了多个telnet进行连接的时候,它是可以一起连接进来的

服务器启动

来了一个新客户端

来了一个新客户端

但是这里有一个问题,我们线程池的可用线程是有限的,不可能无限提供线程来接收大量客户端的连接,迟早它会无响应被堵塞的。

我们现在来看一下NIO,NIO其实是使用传统IO的特性创建一个channel(通道),通过该通道来注册事件SelectionKey

SelectionKey有四种事件

  • SelectionKey.OP_ACCEPT —— 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
  • SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功
  • SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
  • SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)

这里 注意,下面两种,SelectionKey.OP_READ ,SelectionKey.OP_WRITE ,

1.当向通道中注册SelectionKey.OP_READ事件后,如果客户端有向缓存中write数据,下次轮询时,则会 isReadable()=true;

2.当向通道中注册SelectionKey.OP_WRITE事件后,这时你会发现当前轮询线程中isWritable()一直为ture,如果不设置为其他事件

代码语言:javascript
复制
public class NIOServer {
   // 通道管理器
   private Selector selector;

   /**
    * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
    * 
    * @param port
    *            绑定的端口号
    * @throws IOException
    */
   public void initServer(int port) throws IOException {
      // 获得一个ServerSocket通道
      ServerSocketChannel serverChannel = ServerSocketChannel.open();
      // 设置通道为非阻塞
      serverChannel.configureBlocking(false);
      // 将该通道对应的ServerSocket绑定到port端口
      serverChannel.socket().bind(new InetSocketAddress(port));
      // 获得一个通道管理器
      this.selector = Selector.open();
      // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
      // 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
      serverChannel.register(selector, SelectionKey.OP_ACCEPT);
   }

   /**
    * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
    * 
    * @throws IOException
    */
   public void listen() throws IOException {
      System.out.println("服务端启动成功!");
      // 轮询访问selector
      while (true) {
         // 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
         selector.select();
         // 获得selector中选中的项的迭代器,选中的项为注册的事件
         Iterator<?> ite = this.selector.selectedKeys().iterator();
         while (ite.hasNext()) {
            SelectionKey key = (SelectionKey) ite.next();
            // 删除已选的key,以防重复处理
            ite.remove();
            handler(key);
         }
      }
   }

   /**
    * 处理请求
    * 
    * @param key
    * @throws IOException
    */
   public void handler(SelectionKey key) throws IOException {
      
      // 客户端请求连接事件
      if (key.isAcceptable()) {
         handlerAccept(key);
         // 获得了可读的事件
      } else if (key.isReadable()) {
         handelerRead(key);
      }
   }

   /**
    * 处理连接请求
    * 
    * @param key
    * @throws IOException
    */
   public void handlerAccept(SelectionKey key) throws IOException {
      ServerSocketChannel server = (ServerSocketChannel) key.channel();
      // 获得和客户端连接的通道
      SocketChannel channel = server.accept();
      // 设置成非阻塞
      channel.configureBlocking(false);

      // 在这里可以给客户端发送信息哦
      System.out.println("新的客户端连接");
      // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
      channel.register(this.selector, SelectionKey.OP_READ);
   }

   /**
    * 处理读的事件
    * 
    * @param key
    * @throws IOException
    */
   public void handelerRead(SelectionKey key) throws IOException {
      // 服务器可读取消息:得到事件发生的Socket通道
      SocketChannel channel = (SocketChannel) key.channel();
      // 创建读取的缓冲区
      ByteBuffer buffer = ByteBuffer.allocate(1024);
      int read = channel.read(buffer);
      if(read > 0){
         byte[] data = buffer.array();
         String msg = new String(data).trim();
         System.out.println("服务端收到信息:" + msg);
         
         //回写数据
         ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
         channel.write(outBuffer);// 将消息回送给客户端
      }else{
         System.out.println("客户端关闭");
         key.cancel();
      }
   }

   /**
    * 启动服务端测试
    * 
    * @throws IOException
    */
   public static void main(String[] args) throws IOException {
      NIOServer server = new NIOServer();
      server.initServer(10101);
      server.listen();
   }

}

NIO与传统IO最大的不同

  1. NIO有通道的概念,传统IO没有这个概念,但通道的概念是基于传统IO的
  2. 传统IO的字符接受处理是也是实用的Java原生的序列化流的方式,而NIO是使用ByteBuffer的缓冲区机制。

使用telnet测试,NIO是肯定支持多个客户端同时操作的,但很重要的一点是NIO是单线程的,传统IO和NIO的逻辑如下

传统IO

NIO

至于NIO如何多线程,可以参考NIO如何多线程操作 ,这其实也是Netty的原理。

分别用两个telnet连接

admindeMacBook-Pro:IOServer admin$ telnet 127.0.0.1 10101

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

dsfds

好的

admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

22222

好的

服务端显示如下

服务端启动成功!

新的客户端连接

服务端收到信息:dsfds

新的客户端连接

服务端收到信息:22222

当我们退出其中一个的时候

admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

22222

好的^]

telnet> quit

Connection closed.

服务端显示如下

服务端启动成功!

新的客户端连接

服务端收到信息:dsfds

新的客户端连接

服务端收到信息:22222

客户端关闭

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档