BIO NIO 网络编程笔记

BIO

阻塞IO,是指线程在访问IO资源的时候,如果资源不存在也会一直等待。使用线程池的BIO的并发能力基本就是跟定义的线程池的大小一致,甚至更糟。

BIO编程主要用的就是java net api和io api,这2种api都是阻塞的

服务端:

public class BIOSocketServer {
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(8081);
        System.out.println("服务器启动成功,端口8081。");
        while (!serverSocket.isClosed()) {
            Socket request = serverSocket.accept(); //阻塞线程的api
            System.out.println("收到新连接:" + request.toString());
            try{
                System.out.println("开始打印接受信息:");
                // 开始io操作
                InputStream inputStream = request.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
                String msg;
                while ((msg = bufferedReader.readLine()) != null) { // 阻塞线程的api
                    if (msg.length() == 0) {
                        break;
                    }
                    System.out.println(msg);
                }
                System.out.println("数据来自:" + request.toString());
            }catch (IOException e) {
                e.printStackTrace();
            }finally {
                request.close(); // 关闭此次连接
            }
        }
    }
}

客户端:

public class BIOSocketClient {
    private static Charset charset = Charset.forName("UTF-8");

    public static void main(String[] args) throws IOException {
        String url = "localhost";
        int port = 8081;
        System.out.println("请求连接 "+ url + ":" + port);
        Socket socket = new Socket(url, port);
        OutputStream out = socket.getOutputStream();

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入:");
        String msg = scanner.nextLine(); // 阻塞线程io

        System.out.println("开始发送数据...");
        out.write(msg.getBytes(charset)); // 阻塞线程io

        scanner.close();
        socket.close();
        System.out.println("数据发送完毕。");
    }
}

BIO的代码是直接面向底层网络接口的,要想实现应用级的功能是非常麻烦的,相当于自己要设计访问协议(比如请求、断开、状态管理怎样控制),多线程管理等。

NIO

非阻塞IO,是指线程在访问IO资源的时候,如果资源不存在就会返回一个标识,线程就可以去做别事情而不是傻傻的等。从而可以实现单线程处理多网络连接。

NIO的出现就是为了替代原始的BIO这种直接操作Java Networking和Java IO的方式。

NIO有三个组件:缓冲区(Buffer),通道(Channel),选择器(Selector)。主要思路就是通过缓存来解决线程阻塞的问题,从而解放线程提高效率。

缓冲区 Buffer

Buffer本质就是一个内存块,我们可以通过相关api方便的存取数据。

Buffer的3个主要属性:
1)容量 capacity:代表这个内存块的固定大小。
2)位置 position:写入模式代表写入的位置;读取模式代表读取的位置。
3)限制 limit:写入模式等价于容量大小;读取模式等价于之前写入的数据量。

buffer的2种内存:

1)Direct内存,也叫堆外内存:
特点是直接跟操作系统交互,IO操作的时候相比堆内存少了一次copy,速度更快(不经过jvm堆,也不受GC整理影响)。
虽然GC不能直接回收对外内存,但是jvm帮我们实现好了方法,DirectByteBuffer中有个cleaner对象,通过虚引用关联着堆外内存,GC的时候是可以回收DirectByteBuffer对象的,cleaner对象被回收之前会执行clean方法,调用DirectByteBuffer内定义好的回收堆外内存的方法。
堆外内存适合性能要求较高,频繁使用并且占用空间较大的情况。

2)非直接内存,也叫堆内存:特点是直接由JVM管理,适用于绝大多数场景。


image.png

image.png

通道 Channel

channel是nio包封装的对象,用于简化网络开发。相比bio需要socket和io2套api去操作,使用nio网络读写只需要使用channel一套api。

nio最大的特点是支持非阻塞,也因为这个特点,nio的io操作需要在循环中判断执行(因为调用的时候不见得就立即会执行,所以需要不停的判断)。


image.png

选择器 Selector:

可以看到,以上nio程序里有各种网络状态相关的循环和判断。先不讨论写法,这种单线程不停循环的方式在处理高并发场景下效率也是低下的,因为会有很多无效的执行。

selector使用了事件驱动机制,底层原理是操作系统的多路复用机制。他可以监听多个channel的网络状态,状态改变就会触发相关事件回调,实现了单线程管理多channel。

selector监听的的事件:

1)SelectionKey.OP_CONNECT: connect连接
2)SelectionKey.OP_ACCEPT: accept准备就绪
3)SelectionKey.OP_READ: read读
4)SelectionKey.OP_WRITE: write写

public void testChannelWithSelector () throws IOException {
        // 1.创建服务端channel
        int port = 8081;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 设置非阻塞

        // 2.使用selector选择器注册channel
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
        // 表示对sserverSocketChannel上的accept事件关注,serverSocketChannel只能注册accept事件。
        selectionKey.interestOps(selectionKey.OP_ACCEPT);

        // 3。绑定端口,开启通道
        serverSocketChannel.socket().bind(new InetSocketAddress(port));

        // 循环检查selector
        while(true) {
            selector.select(); // 这是一个阻塞方法,有事件通知才会执行,因为只注册了accept事件,所以有新连接才会往下执行

            // 获取事件
            Set selectionKeys = selector.selectedKeys();

            // 使用迭代器遍历
            Iterator iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                // 处理accept事件,受益于事件机制,这里服务端accept一定会拿到socketChannel
                // 同时注册read事件,方便后续读数据
                if (key.isAcceptable()) {
                    // 先拿到当前ServerSocketChannel
                    ServerSocketChannel server = (ServerSocketChannel) key.attachment();

                    // 获取socketChannel
                    SocketChannel clientChannel = server.accept();
                    clientChannel.configureBlocking(false);

                    // 使用selector注册read事件
                    clientChannel.register(selector, SelectionKey.OP_READ, clientChannel);

                    System.out.println("收到新连接:"+clientChannel.getRemoteAddress());
                }

                // 处理read事件
                if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.attachment();

                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while (socketChannel.isOpen() && socketChannel.read(byteBuffer) != -1) {
                        // TODO 长连接情况下需要手动判断是否读结束,此处暂时简单处理
                        if (byteBuffer.position() > 0) {
                            break;
                        }
                    }

                    // 如果读不到数据则跳出此次事件处理
                    if (byteBuffer.position() == 0) continue;

                    // 缓存读模式
                    byteBuffer.flip();
                    byte[] content = new byte[byteBuffer.limit()];
                    byteBuffer.get(content);
                    System.out.println("收到"+socketChannel.getRemoteAddress()+"数据:" + new String(content));

                    // 模拟响应
                    String response = "HTTP/1.1 200 OK\r\n"+
                            "Content-Length: 1\r\n\r\n"+
                            "Hello World";

                    ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
                    while (responseBuffer.hasRemaining()) {
                        socketChannel.write(responseBuffer); // 非阻塞写入通道
                    }
                }
            }

        }
    }

上述代码最大特点就是使用循环检查selector事件监听代替了循环accept,利用事件驱动机制提高了执行效率;同时读取请求数据的逻辑也使用了selector进行了调整,优化了实现。

NIO 进一步改进

以上的NIO的写法是基于单线程的,能尽量提高单线程的利用率。但是俗话说”双拳难敌四手“,面对现实生产场景中的海量并发,一个线程再怎么优化,执行效率也是有瓶颈的,而且目前的cpu大多都是多核,只是单线程也不能充分利用硬件资源。解决这个问题的方案就是大名鼎鼎的React设计模式。

Dog Lea 的React设计思想:React的设计思路主要是在NIO(非阻塞)的基础上使用发布订阅模式进一步优化了网络连接(accept)、网络IO(read,send),并且把业务线程和网络线程隔离开,各自分工,尽可能放大各环节的效率,尽量保证系统的网络并发能力。

打个比方,一个顾客要去逛商城,开门,导购,买卖交易如果始终是一个人在服务的话,这个商城也接纳不了几个顾客。各自分工专业,各个环节都变的效率了,整体效率才会提高。

下图的重要的几个环节是:
1)mainReactor:接收线程组,专注连接接收,并且通过注册分发给网络io线程组处理。
2)subReactor:网络io线程组,专注处理网络io,同时把网络数据注册给工作线程组处理
3)workerThreads:工作线程组,专注处理业务。

image.png

参考文章:
ByteBuffer常用操作
NIO技术1
NIO技术2

你可能感兴趣的:(BIO NIO 网络编程笔记)