JavaIO网络应用

62 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第24天,点击查看活动详情

2.2.JavaIO网络应用

2.2.1.IO网络技术

  • 直接 IO:磁盘--->内核空间的内核缓冲区--->应用程序内存--->内核空间的Socket缓冲区--->网络。
  • 内存文件映射:内核空间的内核缓冲区与应用程序内存直接映射,磁盘--->内核空间的内核缓冲区--->内核空间的Socket缓冲区--->网络。
  • 0拷贝:内核空间的内核缓冲区与Socket缓冲区直接映射,磁盘读入,直接输出给网络。

2.2.2.BIO

  • 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

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

    问题:当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大;连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费

  • 下面是一个使用BIO设计方式的服务器案例,也是最为简单的网络通信案例。(在基础的时候已经介绍过)

    public class BIOServer{
        public static void main(String[] args) throws IOException {
            ServerSocket server = new ServerSocket(8888);
            System.out.println("BIO server is listening to " + server.getLocalSocketAddress());
            while(true){
                Socket client = server.accept();
                System.out.println("connect from " + client.getRemoteSocketAddress());
                Scanner input = new Scanner(client.getInputStream());
    
                while(true){
                    String reqStr = input.nextLine();
                    if("quit".equals(reqStr)) break;
                    System.out.println(String.format("from %s, %s",client.getRemoteSocketAddress(), reqStr));
                    String res = "from server hello " + reqStr + "/n";
                    client.getOutputStream().write(res.getBytes());
                }
            }
        }
    }
    

    这个非常简陋的服务器,便可以支持客户端的连接。当然有着非常大的优化空间。

2.2.3.NIO

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

    场景:适用于连接数目多且连接比较短 (轻操作) 的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK 1.4 开始支持

  • 下面是一个使用NIO设计方式的服务器案例,该服务器最大的改进是支持多个连接,无需创建多个线程。

    public class NIOServer{
        public static void main(String[] args){
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);
            serverChannel.bind(new InetSocketAddress(8888));
            System.out.println("NIO server is listening to " + serverChannel.getLocalAddress());
    
            while(true){
                SocketChannel clientChannel = serverChannel.accept();
                if(clientChannel != null)
                    System.out.println("connect from " clientChannel.getRemoteAddress());
            }
        }
    }
    

    虽然该方式允许多个连接,但是,速度依然很慢,因为每次进行读写数据的时候都要进行内核调用,而内核调用是无法避免的阻塞状态的。

  • 下面的例子使用一个选择器,选择一批符合要求的通道,统一调用内核,即将多次调用内核的方式合并为一次调用,类似于批处理,程序设计中很多运用了这种思想 (如数据库的批处理) 。

    public class NIOServerNew{
        public static void main(String[] args) throws IOException {
            //获取选择器
            Selector selector = Selector.open();
            //获取通道
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            //通道设为非阻塞
            serverChannel.configureBlocking(false);
            //绑定链接端口
            serverChannel.bind(new InetSocketAddress(8888));
            System.out.println("NIO server is listening to " + serverChannel.getLocalAddress());
    
            //将通道注册到选择器上,并注册IO事件为等待连接
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            //轮询IO事件
            while(selector.select() > 0){
                //获取选择键集合
                Iterator<SelectionKey> selectKeys = selector.selectedKeys().iterator();
    
                while(selectKeys.hasNext()){
                    //获取单个选择键,后续根据不同的选择键处理
                    SelectionKey selectionKey = selectKeys.next();
                    //判断事件类型
                    if(selectionKey.isAcceptable()){
    
                        SocketChannel clientChannel = serverChannel.accept();
                        clientChannel.configureBlocking(false);
                        clientChannel.register(selector, SelectionKey.OP_READ);
    
                    }else if(selectionKey.isReadable()){
                        SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
    
                        int length = 0 ;
    
                        while((length = clientChannel.read(buffer)) > 0) {
                            buffer.filp();
                            System.out.println(new String(buffer.array(),0,length));
                            buffer.clear();
                        }
                        clientChannel.close();
                    }
                    selectKeys.remove();
                }
            }
            serverChannel.close();
        }
    }
    

    使用 NIO 的设计思想编写客户端测试,当然可以编写多个客户端,只要将代码复制,启动多个即可:

    public class NIOClient{
        public static void main(String[] args){
            InetSocketAddress address = new InetSocketAddress(127.0.0.1, 8888);
    
            //获取通道(channel)
            SocketChannel clientChannel = SocketChannel.open(address);
            //切换成非阻塞模式
            clientChannel.configureBlocking(false);
            //不断的自旋、等待连接完成,或者做一些其他的事情
            while (!clientChannel.finishConnect()) {
                //. . .
            }
            System.out.println("客户端连接成功");
            
           
            //分配指定大小的缓冲区
            ByteBuffer buffer = buffer.allocate(1024);
            buffer.put("Msg to the world.".getBytes());
            buffer.flip();
            clientChannel.write(byteBuffer);
    
            while(true){
                try {
                    Thread.sleep(1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            //socketChannel.shutdownOutput();
            //socketChannel.close();
        }
    }
    

2.2.4.AIO

  • 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成再通知服务器应用去启动线程进行处理。

    场景:用于连接数目多且连接比较长 (重操作) 的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK 7 开始支持。目前并未得到广泛应用,Linux 对 AIO 的支持也并不完善。