NIO、BIO编程模型与零拷贝

爷,独闯天下 提交于 2020-02-21 12:42:28

Java IO模型

  • Java共支持3种网络编程模型/IO模式:BIO、NIO、AI

BIO

  • 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销

  • 适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。

  • 存在问题:

    • 每个请求都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据Write
    • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
    • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在Read操作上,造成线程资源浪费
      在这里插入图片描述
  • 客户端

    public class BIOClient {
    	public static void main(String[] args) {
    		// 通过构造函数创建Socket,并且连接指定地址和端口的服务端
    		try {
    			Socket socket = new Socket(localhost, 6666);
    			//开启一个线程接收消息
    			new ReadMsg(socket).start();
    			
    			System.out.println("请输入信息");
    			PrintWriter pw = null;
    			// 写数据到服务端
    			while (true) {
    				pw = new PrintWriter(socket.getOutputStream());
    				pw.println(new Scanner(System.in).next());
    				pw.flush();
    			}
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    
    	}
    
    	public static class ReadMsg extends Thread {
    		Socket socket;
    
    		public ReadMsg(Socket socket) {
    			this.socket = socket;
    		}
    
    		@Override
    		public void run() {
    			try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
    				String line = null;
    				// 通过输入流读取服务端传输的数据
    				while ((line = br.readLine()) != null) {
    					System.out.printf("%s\n", line);
    				}
    
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    
  • 服务端

    public class BIOServer {
    
        public static void main(String[] args) throws Exception {
    
            // 创建一个线程池
            // 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
            ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
    
            //创建ServerSocket
            ServerSocket serverSocket = new ServerSocket(6666);
    
    
            System.out.println("服务器启动了");
    
            while (true) {
    
                System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
                //监听,等待客户端连接
                System.out.println("等待连接....");
                final Socket socket = serverSocket.accept();
                System.out.println("连接到一个客户端");
    
                //就创建一个线程,与之通讯(单独写一个方法)
                newCachedThreadPool.execute(new Runnable() {
                    public void run() { //我们重写
                        //可以和客户端通讯
                        handler(socket);
                    }
                });
    
            }
        }
    
        //编写一个handler方法,和客户端通讯
        public static void handler(Socket socket) {
    
            try {
                System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
                byte[] bytes = new byte[1024];
                //通过socket 获取输入流
                InputStream inputStream = socket.getInputStream();
    
                //循环的读取客户端发送的数据
                while (true) {
    
                    System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
    
                    System.out.println("read....");
                   int read =  inputStream.read(bytes);
                   if(read != -1) {
                       System.out.println(new String(bytes, 0, read
                       )); //输出客户端发送的数据
                   } else {
                       break;
                   }
                }
    
    
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                System.out.println("关闭和client的连接");
                try {
                    socket.close();
                }catch (Exception e) {
                    e.printStackTrace();
                }
    
            }
        }
    }
    

NIO

  • 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理

    • NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
    • HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级
  • 适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。

    在这里插入图片描述

  • NIOClient

    public class NIOClient {
        public static void main(String[] args) throws Exception{
    
            //得到一个网络通道
            SocketChannel socketChannel = SocketChannel.open();
            //设置非阻塞
            socketChannel.configureBlocking(false);
            //提供服务器端的ip 和 端口
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
            //连接服务器
            if (!socketChannel.connect(inetSocketAddress)) {
    
                while (!socketChannel.finishConnect()) {
                    System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
                }
            }
    
            //...如果连接成功,就发送数据
            String str = "hello, 尚硅谷~";
            //Wraps a byte array into a buffer
            ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
            //发送数据,将 buffer 数据写入 channel
            socketChannel.write(buffer);
            System.in.read();
    
        }
    }
    
  • NIOServer

    public class NIOServer {
        public static void main(String[] args) throws Exception{
    
            //创建ServerSocketChannel -> ServerSocket
    
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
            //得到一个Selecor对象
            Selector selector = Selector.open();
    
            //绑定一个端口6666, 在服务器端监听
            serverSocketChannel.socket().bind(new InetSocketAddress(6666));
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
    
            //把 serverSocketChannel 注册到  selector 关心 事件为 OP_ACCEPT
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
            System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
    
    
    
            //循环等待客户端连接
            while (true) {
    
                //这里我们等待1秒,如果没有事件发生, 返回
                if(selector.select(1000) == 0) { //没有事件发生
                    System.out.println("服务器等待了1秒,无连接");
                    continue;
                }
    
                //如果返回的>0, 就获取到相关的 selectionKey集合
                //1.如果返回的>0, 表示已经获取到关注的事件
                //2. selector.selectedKeys() 返回关注事件的集合
                //   通过 selectionKeys 反向获取通道
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                System.out.println("selectionKeys 数量 = " + selectionKeys.size());
    
                //遍历 Set<SelectionKey>, 使用迭代器遍历
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
    
                while (keyIterator.hasNext()) {
                    //获取到SelectionKey
                    SelectionKey key = keyIterator.next();
                    //根据key 对应的通道发生的事件做相应处理
                    if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
                        //该该客户端生成一个 SocketChannel
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
                        //将  SocketChannel 设置为非阻塞
                        socketChannel.configureBlocking(false);
                        //将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
                        //关联一个Buffer
                        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
    
                        System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
    
    
                    }
                    if(key.isReadable()) {  //发生 OP_READ
    
                        //通过key 反向获取到对应channel
                        SocketChannel channel = (SocketChannel)key.channel();
    
                        //获取到该channel关联的buffer
                        ByteBuffer buffer = (ByteBuffer)key.attachment();
                        channel.read(buffer);
                        System.out.println("form 客户端 " + new String(buffer.array()));
    
                    }
    
                    //手动从集合中移动当前的selectionKey, 防止重复操作
                    keyIterator.remove();
    	
                }
    
            }
    
        }
    }
    

基于NIO的零拷贝

  • 说明
    • 传统IO有四次拷贝
      在这里插入图片描述
    • DMP优化(通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数)
      在这里插入图片描述
    • sendfile函数优化(数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换)
      在这里插入图片描述
    • NIO的Channel的TranseferTo方法,底层为sendFile方式的零拷贝,比基于DMP的MappedByteBuffer性能好。
  • NewIOClient
    public class ZeroIOClient {
        public static void main(String[] args) throws Exception {
    
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("localhost", 7001));
            String filename = "protoc-3.6.1-win32.zip";
    
            //得到一个文件channel
            FileChannel fileChannel = new FileInputStream(filename).getChannel();
    
            //准备发送
            long startTime = System.currentTimeMillis();
    
            //在linux下一个transferTo 方法就可以完成传输
            //在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件
            //transferTo 底层使用到零拷贝
            long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
    
            System.out.println("发送的总的字节数 =" + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));
    
            //关闭
            fileChannel.close();
    
        }
    }
    
  • NewIOServer
    public class ZeroIOServer {
        public static void main(String[] args) throws Exception {
    
            InetSocketAddress address = new InetSocketAddress(7001);
    
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
            ServerSocket serverSocket = serverSocketChannel.socket();
    
            serverSocket.bind(address);
    
            //创建buffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
    
            while (true) {
                SocketChannel socketChannel = serverSocketChannel.accept();
    
                int readcount = 0;
                while (-1 != readcount) {
                    try {	
                        readcount = socketChannel.read(byteBuffer);	
                    }catch (Exception ex) {
                       // ex.printStackTrace();
                        break;
                    }
                    byteBuffer.rewind(); //倒带 position = 0 mark 作废
                }
            }
        }
    }
    
  • 注意
    • 在linux下一个transferTo方法就可以完成传输
    • 在windows下一次调用transferTo只能发送8m,就需要分段传输文件
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!