NIO网络编程原理与实战

一.NIO网络编程模型

1.NIO简介

  Non-blocking I/O或New I/O,JDK1.4版本开始有,高并发网络服务器支持。

2.编程模型

  模型:对事物共性的抽象

  编程模型:对编程共性的抽象

3.BIO网络模型

  BIO模型的缺点:在大量并发的情况下,接入客户端过多,出现问题。

4.BIO网络模型缺点

  阻塞式I/O模型、弹性伸缩能力差、多线程耗资源。

5.NIO网络模型猜想

6.NIO网络模型

7.NIO网络模型改进

  非阻塞式I/O模型、弹性伸缩能力强、单线程节省资源(因为服务端只有一个线程,线程的频繁创建与销毁、线程上下文之间的切换这两个问题都解决了)。

 

二.NIO网络编程详解

  Channel:通道;Buffer:缓冲区;Selector:选择器或多路复用器

1.NIO核心类之Channel

1).Channel简介

  a.双向性

  对信息传输的通道,JDK NIO中对输入、输出方式的另一种抽象,可以类比BIO中的流的概念,但与流不同的是,流是单向传输,有InputStream、OutStream之分,而通道支持双向传输,一个Channel既可读又可写。

  b.非阻塞性

  Channel可以工作在非阻塞模式下,也正是这个特点,构成了NIO网络编程的基础。

  c.操作唯一性

  NIO中操作Channel的唯一方式是使用Buffer,通过Buffer操作Channel实现数据块的读写。

2).Channel实现

  文件类:FileChannel,对文件处理读写的

  UDP类:DatagramChannel,UDP的数据读写

  TCP类:ServerSocketChannel/SocketChannel,基于TCP类的数据的读写

3).Socket回顾

  服务端:

  客户端:

4).Channel使用

2.NIO核心类之Buffer

1).Buffer简介

  Buffer是NIO API中新加入的类,提示唯一与Channel进行交互的方式,通过Buffer可以从Buffer中读取数据或者将数据写入到Buffer中。

  作用:读写Channel中数据

  本质:一块内存区域,是一块可以写入数据,也可以从中读取数据的内存,这块内存被NIO包装也了NIOBuffer对象,并提供了一组方法,用于方便操作这块内存。

2).Buffer属性

  四个核心属性。

  Capacity:容量。作用是标明数组最大可以容纳多少字节,一旦写入的字节数超过最大容量,需要将其清空后才能继续往里写入数据。

  Position:位置。当你写数据时,Position表示当前的位置,初始的Position为0,当一个byte写入到Buffer后,Position会向后移动到下一个可播入数据的Buffer单元,Position最大可为Capacity-1,相当于这个数组的下标最大值。当读取数据时,Buffer会从写模式切换到写模式,这个就是Buffer完成读写切换的模式,此时Position会被重置为0,当从Buffer的Position处读取数据时,Position会向后移动到下一个可读的位置。

  Limit:上限。在写模式下,Buffer的Limit表示最多能往Buffer中写多少数据,此模式下,Limit等于Position。当切换到读模式时,Limit表示最多能从Buffer中读取多少数据,此时,Limit会被设置成写模式下的Position值。

  Mark:标记。Mark存储一个特定的Position位置,之后可以通过调用Buffer的reset方法,可以恢复到这个Position位置,依然可以从这个位置处理数据。

3).Buffer使用

  另外,NIO为Buffer类提供了很多基础实现类,基本类型中除了Boolean类型外,其余所有类型都对应有Buffer类型的实现。但在网络编程中,使用最多的还是基于字节的ByteBuffer类。

3.NIO核心类之Selector

1).Selector简介

  Selector叫选择器,或者叫多路复用器,它是NIO网络编程的基础。

  作用:I/O就绪选择

  地位:NIO网络编程的基础

  在这里需重点强调一下:整个NIO网络编程,就是构建在非阻塞IO和Selector多路复用器之上的。Selector是Java NIO中能够检测1到多个NIO通道并能够知晓通道是否为注入读写事件做好准备的组件,通过它,一个单独的线程就可以管理多个Channel,从而管理多个网络连接。

2).Selector使用

  代码片段3的selector.select()方法,此方法内部根据不同操作系统底层针对IO的支持来实现的,目的就是检测注册到selector对象上的Channel,希望监听的事件是否已就绪,如果存在已就绪的,就会返回已就绪Channel个数,如果没有,就会一直阻塞。

3).SelectionKey简介

  四种就绪状态常量,分别是CONNECT连接就绪,ACCEPT接收就绪,READ读就绪,WRITE写就绪,再调用selector.selectedKeys方法时,会返回一个SelectionKey的集合,可以通过selectKeys集合获取到当前的Channel,获取Selector对象,获取该Channel已就绪事件集合和所关心事件集合,这就是SelectionKey对象一些有价值的属性。

4.NIO编程实现步骤

  第一步:创建Selector

  第二步:创建ServerSocketChannel,并绑定监听端口

  第三步:将Channel设置为非阻塞模式

  第四步:将Channel注册到Selector上,监听连接事件

  第五步:循环调用Selector的select方法,检测就绪情况

  第六步:调用selectedKeys方法获取就绪channel集合

  第七步:判断就绪事件种类,调用业务处理方法

  第八步:根据业务需要决定是否再次注册监听事件,重复执行第三步操作

 

三.NIO网络编程实战

  利用NIO编程知识,实现多人聊天室。

NioServer.java

package com.bijian;

import com.sun.security.ntlm.Server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * NIO服务器端
 */
public class NioServer {

    /**
     * 启动服务器端方法
     */
    public void start() throws IOException {

        /**
         * 1.创建Selector
         */
        Selector selector = Selector.open();

        /**
         * 2.通过ServerSocketChannel创建channel通道
         */
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        /**
         * 3.为channel通道绑定监听端口
         */
        serverSocketChannel.bind(new InetSocketAddress(8000));

        /**
         * 4.设置channel为非阻塞模式
         */
        serverSocketChannel.configureBlocking(false);

        /**
         * 5.将channel注册到selector上,监听连接事件
         */
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器启动成功!");

        /**
         * 6.循环等待新接入的连接
         */
        for(;;) {
            /**
             * 获取可用channel数量
             */
            int readyChannels = selector.select();

            /**
             * 为什么要这样?
             */
            if(readyChannels == 0) continue;

            /**
             * 获取可用channel的集合
             */
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            Iterator iterator = selectionKeys.iterator();
            while(iterator.hasNext()) {
                /**
                 * selectionKey实例
                 */
                SelectionKey selectionKey = (SelectionKey)iterator.next();

                /**
                 * 移除Set中的当前selectionKey,这是重点
                 */
                iterator.remove();

                /**
                 * 7.根据就绪状态,调用对应方法处理业务逻辑
                 */
                /**
                 * 如果是接入事件
                 */
                if(selectionKey.isAcceptable()) {
                    acceptHandler(serverSocketChannel, selector);
                }

                /**
                 * 如果是可读事件
                 */
                if(selectionKey.isReadable()) {
                    readHandler(selectionKey, selector);
                }
            }
        }
    }

    /**
     * 接入事件处理器
     */
    private void acceptHandler(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {

        /**
         * 如果要是接入事件,创建socketChannel
         */
        SocketChannel socketChannel = serverSocketChannel.accept();

        /**
         * 将socketChannel设置为非阻塞工作模式
         */
        socketChannel.configureBlocking(false);

        /**
         * 将channel注册到selector上,监听可读事件
         */
        socketChannel.register(selector, SelectionKey.OP_READ);

        /**
         * 回复客户端提示信息
         */
        socketChannel.write(Charset.forName("UTF-8").encode("你与聊天室里其他人都不是朋友关系,请注意隐私安全"));
    }

    /**
     * 可读事件处理器
     */
    private void readHandler(SelectionKey selectionKey, Selector selector) throws IOException {

        /**
         * 要从selectionKey中获取到已经就绪的channel
         */
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

        /**
         * 创建buffer
         */
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        /**
         * 循环读取客户端请求信息
         */
        String request = "";
        while(socketChannel.read(byteBuffer) > 0) {
            /**
             * 切换buffer为读模式
             */
            byteBuffer.flip();

            /**
             * 读取buffer中的内容
             */
            request += Charset.forName("UTF-8").decode(byteBuffer);
        }
        /**
         * 将channel再次注册到selector上,监听他的可读事件
         */
        socketChannel.register(selector, SelectionKey.OP_READ);

        /**
         * 将客户端发送的请求信息,广播给其他客户端
         */
        if(request.length() > 0) {
            //广播给其他客户端
            System.out.println("::" + request);
            broadCast(selector, socketChannel, request);
        }
    }

    /**
     * 广播给其他客户端
     */
    private void broadCast(Selector selector, final SocketChannel sourceChannel, final String request) {

        /**
         * 获取到所有已接入的客户端channel
         */
        Set<SelectionKey> selectionKeySet = selector.keys();

        /**
         * 循环向所有channel广播信息
         */
        selectionKeySet.forEach(selectionKey -> {
            Channel targetChannel = selectionKey.channel();
            //剔除发消息的客户端
            if(targetChannel instanceof SocketChannel && targetChannel != sourceChannel) {
                try {
                    //将信息发送到targetChannel客户端
                    ((SocketChannel) targetChannel).write(Charset.forName("UTF-8").encode(request));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

    }

    /**
     * 主方法
     * @param args
     */
    public static void main(String[] args) throws IOException {
        NioServer nioServer = new NioServer();
        nioServer.start();
    }
}

NioClient.java

package com.bijian;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * NIO客户端
 */
public class NioClient {

    /**
     * 启动
     */
    void start(String nickName) throws IOException {

        /**
         * 连接服务器端
         */
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8000));

        /**
         * 接收服务器端响应
         */
        //新开线程,专门负责来接收服务器端的响应数据
        Selector selector = Selector.open();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        new Thread(new NioClientHandler(selector)).start();

        /**
         * 向服务器端发送数据
         */
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextLine()) {
            String request = scanner.nextLine();
            if(request != null && request.length() > 0) {
                socketChannel.write(Charset.forName("UTF-8").encode(nickName + " : " + request));
            }
        }
    }
}

NioClientHandler.java

package com.bijian;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * 客户端线程类,专门接收服务器端响应信息
 */
public class NioClientHandler implements Runnable{

    private Selector selector;

    public NioClientHandler(Selector selector) {
        this.selector = selector;
    }

    @Override
    public void run() {

        try {
            for (; ; ) {
                int readyChannels = selector.select();

                if (readyChannels == 0) continue;

                /**
                 * 获取可用channel的集合
                 */
                Set<SelectionKey> selectionKeys = selector.selectedKeys();

                Iterator iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    /**
                     * selectionKey实例
                     */
                    SelectionKey selectionKey = (SelectionKey) iterator.next();

                    /**
                     * 移除Set中的当前selectionKey,这是重点
                     */
                    iterator.remove();

                    /**
                     * 根据就绪状态,调用对应方法处理业务逻辑
                     */

                    /**
                     * 如果是可读事件
                     */
                    if (selectionKey.isReadable()) {
                        readHandler(selectionKey, selector);
                    }
                }
            }
        }catch(IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 可读事件处理器
     */
    private void readHandler(SelectionKey selectionKey, Selector selector) throws IOException {

        /**
         * 要从selectionKey中获取到已经就绪的channel
         */
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

        /**
         * 创建buffer
         */
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        /**
         * 循环读取服务器端响应信息
         */
        String response = "";
        while(socketChannel.read(byteBuffer) > 0) {
            /**
             * 切换buffer为读模式
             */
            byteBuffer.flip();

            /**
             * 读取buffer中的内容
             */
            response += Charset.forName("UTF-8").decode(byteBuffer);
        }
        /**
         * 将channel再次注册到selector上,监听他的可读事件
         */
        socketChannel.register(selector, SelectionKey.OP_READ);

        /**
         * 将服务器端响应信息打印到本地
         */
        if(response.length() > 0) {
            System.out.println(response);
        }
    }
}

AClient.java

package com.bijian;

import java.io.IOException;

public class AClient {

    public static void main(String[] args) throws IOException {
        new NioClient().start("AClient");
    }
}

BClient.java

package com.bijian;

import java.io.IOException;

public class BClient {

    public static void main(String[] args) throws IOException {
        new NioClient().start("BClient");
    }
}

CClient.java

package com.bijian;

import java.io.IOException;

public class CClient {

    public static void main(String[] args) throws IOException {
        new NioClient().start("CClient");
    }
}

  先后启动NioServer.java、AClient.java、BClient.java、CClient.java,测试效果如下:

 

四.NIO网络编程缺陷

  麻烦:NIO类库和API繁杂

  心累:可靠性能力补齐,工作量和难度都非常大

  有坑:Selector空轮询,导致CPU100%。原生NIO有一个BUG,一直都没有改好,就是比较著名的epoll BUG,主要出现在类Linux系统上,问题是这样的:根据API规定,如果在调用selector的select方法时,如果没有准备就绪的channel,它应该阻塞在selector调用上,但Linux下的selector使用的是epoll IO事件通知工具,操作系统使用这一高性能的技术,与网络协议栈异步工作,从而导致就算没有准备就绪的channel,select方法也不会阻塞,最终造成CPU利用率100%现象。这个问题官方声称在JDK1.6版本的修复补丁里已经修复,但实际证明该问题在JDK1.8仍然存在,只不过发生的概率低了一些而已。如果程序需要兼容不同的操作系统、不同的环境,需要小心。

  如上代码就是为了防止它的空轮询【当然,如果仅仅是continue这样做的话,还是会导致CPU 100%】。

 

五.课程总结

 

学习地址:https://www.imooc.com/video/19321

posted on 2019-06-16 21:32  bijian1013  阅读(1119)  评论(0)    收藏  举报

导航