Netty


参考文章

  1. http://www.jasongj.com/java/nio_reactor/

I/O模型基本说明

同步 vs. 异步

同步I/O 每个请求必须逐个地被处理,一个请求的处理会导致整个流程的暂时等待,这些事件无法并发地执行。用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行。

异步I/O 多个请求可以并发地执行,一个请求或者任务的执行不会导致整个流程的暂时等待。用户线程发起I/O请求后仍然继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞 vs. 非阻塞

阻塞 某个请求发出后,由于该请求操作需要的条件不满足,请求操作一直阻塞,不会返回,直到条件满足。

非阻塞 请求发出后,若该请求需要的条件不满足,则立即返回一个标志信息告知条件不满足,而不会一直等待。一般需要通过循环判断请求条件是否满足来获取请求结果。

需要注意的是,阻塞并不等价于同步,而非阻塞并非等价于异步。事实上这两组概念描述的是I/O模型中的两个不同维度。

同步和异步着重点在于多个任务执行过程中,后发起的任务是否必须等先发起的任务完成之后再进行。而不管先发起的任务请求是阻塞等待完成,还是立即返回通过循环等待请求成功。

而阻塞和非阻塞重点在于请求的方法是否立即返回(或者说是否在条件不满足时被阻塞)。

  • I/O模型简单的理解,就是用什么样的通道进行数据的发送和接受,很大程度上决定了程序通信的性能
  • Java支持三种网络编程模型:BIO、NIO、AIO
  • BIO:同步并阻塞,服务器实现为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
  • NIO:同步非阻塞,即服务器实现模式为一个线程处理多个请求,将客户端发送请求注册到多路复用上,多路复用器轮询到连接有I/O请求就进行处理。
  • AIO:异步非阻塞

BIO

适用于连接数比较小且固定的架构

package io.netty.myexample.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {

    public static void main(String[] args) throws IOException {

        ExecutorService executorService = Executors.newCachedThreadPool();

        ServerSocket serverSocket = new ServerSocket(555);

        System.out.println("服务器启动");
        while (true) {
            Socket accept = serverSocket.accept();
            System.out.println("创建一个连接");

            executorService.execute(new Thread(() -> handler(accept)));
        }
    }

    public static void handler(Socket socket) {
        byte[] bytes = new byte[1024];

        try (InputStream inputStream = socket.getInputStream()) {

            while (true) {
            //可能会阻塞
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes, 0, read));
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

BIO问题分析

  • 每个请求都会创建独立的线程
  • 并发数比较大时,需要创建大量线程来处理连接,占用系统资源
  • 连接建立后,如果没有数据可read/write,线程会被阻塞

NIO

  • 全程java non-blocking IO ,同步非阻塞
  • 相关包在java.nio以及子包下
  • 三大核心:Channel、Buffer、Selector
  • NIO是面向缓冲区,数据在缓冲区可以移动,增加其灵活性,提供非阻塞式的高伸缩性网络

NIO和BIO的比较

  • BIO是以流的方式处理数据,NIO是以块的方式处理数据
  • BIO是阻塞的,NIO是非阻塞的
  • BIO基于字节流和字符流进行操作,而NIO基础channel、buffer进行操作,数据总是从通道进入缓冲区,或者从缓存区写入通道。selector用于监听多个通道的事件,因此单个线程就可以监听多个客户端通道。

三大核心组件关系

image

  • 每个buffer都会对应一个channel
  • selector对应一个线程,一个线程对应多个chananel
  • 程序切换到哪个channel是由事件决定的
  • Buffer就是一个内存块,底层是数组
  • 数据读取写入都是通过Buffer,NIO是可以双向的,不同于字节流和字符流。

Buffer

  • 本质上是一个可以读写数据的内存块,可以理解是一个容器对象,提供一组方法,可以更轻松使用内存块。
  • BUffer提供了Byte(常用)、short、char、int、long、doule、float
    public static void main(String[] args) {

        IntBuffer intBuffer = IntBuffer.allocate(5);

        intBuffer.put(10);
        intBuffer.put(11);
        intBuffer.put(12);
        intBuffer.put(13);
        //读写切换
        intBuffer.flip();
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }

    }

Buffer基本属性

Buffer 中有 4 个非常重要的属性:capacity、limit、position、mark 。代码如下:

读写复用这些标记

public abstract class Buffer {

    // Invariants: mark <= position <= limit <= capacity
    // Netty就处理的比较好 : 0 <= readerIndex <= writerIndex <= capacity

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
    
    // ... 省略具体方法的代码
}

image

MappedByteBuffer

public static void main(String[] args) throws IOException {

        //直接在内存种修改(堆外内存)
        RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\a.txt", "rw");

        FileChannel channel = randomAccessFile.getChannel();

        // 使用读写模式 可以直接修改的起始位置  映射内存大小
        // MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());

        mappedByteBuffer.put(0, (byte) 'H');
        mappedByteBuffer.put(3, (byte) '9');
        mappedByteBuffer.put(5, (byte) '9');

        randomAccessFile.close();
    }

Buffer数组


public static void main(String[] args) throws IOException {
        //scatter 将数据写入buffer 可以写入buffer数组 依次写入
        //gather  依次读取buffer数组
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
        serverSocketChannel.socket().bind(inetSocketAddress);

        //服务端 buffer
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(3);
        byteBuffers[1] = ByteBuffer.allocate(5);

        SocketChannel socketChannel = serverSocketChannel.accept();


        while (true) {
            int read = 0;
            if (read < 8) {
                //数据从客户端写入 服务端的buffer
                long read1 = socketChannel.read(byteBuffers);
                read += read1;
            }
            Stream.of(byteBuffers).forEach(Buffer::flip);

            long byteWrite = 0;
            while (byteWrite < 8) {
                // 服务端的buffer 写入channel
                long write = socketChannel.write(byteBuffers);
                byteWrite += write;
            }
            Stream.of(byteBuffers).forEach(Buffer::clear);
        }

    }

Channel

  • 通道可以同时进行读写
  • 可以异步读取写数据
  • 可以从缓存读数据,也可以写数据到缓存

子类

  • SocketChannel :一个客户端用来发起 TCP 的 Channel 。
  • ServerSocketChannel :一个服务端用来监听新进来的连接的 TCP 的 Channel 。对于每一个新进来的连接,都会创建一个对应的 SocketChannel 。
  • DatagramChannel :通过 UDP 读写数据。
  • FileChannel :从文件中,读写数据。

Selector

  • 能检测多个注册通道上是否有事件发送,如有获取事件进行相应处理,避免线程之间的开销

image

  • 相关方法说明
    • select() 阻塞
    • select(long) 阻塞多少毫秒
    • wakeup() 唤醒selector
    • selectNow() 不阻塞 立马返回
    • selector.keys 返回当前所有注册在selector中channel的selectionKey
    • selector.selectedKeys() 返回注册在selector中等待IO操作(及有事件发生)channel的selectionKey。

NIO非阻塞网络编程原理分析图

image

  • 客户端连接时,会通过ServerSocketChannel得到 SocketChannel
  • 将SocketChannel注册到 Selector 上
  • 注册后返回一个 SelectionKey
  • Selector进行监听 select()返回有事件发生的通道个数
  • 得到SelectionKey,获取SocketChannel 进行相应事件处理
//服务端
  public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        Selector selector = Selector.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置 非阻塞
        serverSocketChannel.configureBlocking(true);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            if (selector.select(1000) == 0) {
                System.out.println("no connect");
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            if (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    channel.read(buffer);
                }
                iterator.remove();
            }
        }
    }
    
    //客户端
     public static void main(String[] args) throws IOException {

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);

        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);

        if(!socketChannel.connect(inetSocketAddress)){
            while (!socketChannel.finishConnect()){
                System.out.println("connect loading------");
            }
        }

        String str = "hello1q111 ";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(buffer);
        System.in.read();
    }
    

NIO群聊

服务端

package com.example.springBoot.NIO;


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

/**
 * @author qiumeng
 * @version 1.0
 * @description
 * @date 2021/4/13 22:31
 */
public class GroupChatServer {


    private Selector selector;
    private ServerSocketChannel listenChannel;

    public GroupChatServer() {
        try {
            selector = Selector.open();
            listenChannel = ServerSocketChannel.open();
            listenChannel.socket().bind(new InetSocketAddress(8888));
            listenChannel.configureBlocking(false);
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void listen() {
        try {
            while (true) {
                int count = selector.select(2000);
                if (count > 0) {
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        readAccept(key);
                        readData(key);
                        //删除当前key 这里为什么要删除?大家可以仔细了解下
                        iterator.remove();
                    }
                }else {
                    System.out.println(" no change ");
                }
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    private void readData(SelectionKey key) throws IOException {
        if (key.isReadable()) {
            SocketChannel readChannel = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            if (readChannel.read(buffer) > 0) {
                String msg = new String(buffer.array());
                System.out.println("client:" + msg);
                sendToOtherClient(readChannel, msg);
            }
        }
    }

    private void sendToOtherClient(SocketChannel readChannel, String msg) throws IOException {
        System.out.println("server transfer msg");
        for (SelectionKey selectionKey : selector.keys()) {
            Channel channel = selectionKey.channel();
            if (channel instanceof  SocketChannel&&!readChannel.equals(channel)) {
                ByteBuffer targetBUffer = ByteBuffer.wrap(msg.getBytes());
                SocketChannel channel1 = (SocketChannel) channel;
                channel1.write(targetBUffer);
                System.out.println("server transfer success");
            }
        }
    }

    private void readAccept(SelectionKey key) throws IOException {
        if (key.isAcceptable()) {
            SocketChannel acceptChannel = listenChannel.accept();
            acceptChannel.configureBlocking(false);
            acceptChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
            System.out.println(acceptChannel.getRemoteAddress() + "on line ");
        }
    }

    public static void main(String[] args) {
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }

}

客户端

package com.example.springBoot.NIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

/**
 * @author qiumeng
 * @version 1.0
 * @description
 * @date 2021/4/14 9:21
 */
public class GroupChatClient {


    public static final String HOSTNAME = "127.0.0.1";
    public static final int PORT = 8888;

    private Selector selector;
    private SocketChannel socketChannel;
    private static String userName;


    public GroupChatClient() throws IOException {
        selector = Selector.open();
        socketChannel = SocketChannel.open(new InetSocketAddress(HOSTNAME, PORT));
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        userName = socketChannel.getLocalAddress().toString();

        System.out.println("init client");
    }


    private void sendMsg(String info) throws IOException {
        String sendMsg = userName + "说" + info;
         socketChannel.write(ByteBuffer.wrap(sendMsg.getBytes()));
    }


    private void readInfo() throws IOException {
        if (selector.select() > 0) {
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            if (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    if(null!=buffer){
                        socketChannel.read(buffer);
                        System.out.println("client receive server:" + new String(buffer.array()));
                        String msg = new String(buffer.array());
                        System.out.println(msg);
                    }

                }
                iterator.remove();
            }
        }
    }


    public static void main(String[] args) throws IOException {

        GroupChatClient groupChatClient = new GroupChatClient();


        new Thread(() -> {
            while (true) {
                try {
                    groupChatClient.readInfo();
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLine()) {
            groupChatClient.sendMsg(scanner.nextLine());
        }
    }
}

零拷贝(没有CPU拷贝)

  • 传统IO 四次拷贝 三次切换
  • mmap通过内存映射,将文件映射到内核缓冲区,用户空间可以共享内核空间的数据。少了一次拷贝
  • sendFile 三次拷贝 二次切换
  • 硬件DMA切换是没法避免的 ** 数据从硬盘进入内核,然后直接到协议栈**
  • NIO 零拷贝用了transferTo方法, win8m 多余8M需要分段传输

AIO

  • 异步不阻塞

Netty

  • 原生NIO的问题
    • NIO类库和API繁杂,需要熟练掌握Selector、Serversocketchannel、Socketchannel、buffer等
    • 需要具备java多线程技能
    • 开发工作和难度非常大:如 客户端重连、网络闪断、失败缓存等
    • Epoll bug会导致Selector空轮询,最终导致CPU100%
  • 是由JBOSS提供的一个Java开源框架
  • 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。(目前是同步非阻塞)
  • 主要针对在TCP协议下,面向Clients端的高并发应用,或者peer-to-peer场景下的大量数据持续传输的应用。
  • 本质是一个NIO框架,适用于服务器通讯相关的各种应用场景。
  • 优点
    • 高性能、吞吐量高、延迟低、减少资源消耗、最小化不必要内存复制。

线程模型概述

  • 传统I/O
  • Reactor模式
    • 单Reactor单线程
    • 单Reactor多线程
    • 主从Reactor单线程(Netty基于,做了一定改进)
    • 多个连接共用一个阻塞对象,线程分发。()

image

  • 通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动:每次有事件进行处理)

单Reactor单线程

image

单Reactor多线程

image

  • Reactor对象通过select监听客户端请求,收到事件后,dispatch进行分发
  • 建立连接请求,通过accept处理连接请求,然后创建一个handler处理完成连接后的各种事件
  • handler只负责响应事件,不做具体的业务处理,会分化后面的worker线程池的某个线程进行处理。
  • worker线程池分配独立线程进行处理,并将结果返回handler

优点: 可以利用cpu的能力

缺点: 多线程访问比较复杂,reactor运行在单线程中,处理所有事件和响应,高并发性能瓶颈

主从Reactor多线程

image

  • Reactor主线程负责通过accept处理连接事件。
  • accept处理连接事件后,将连接分给 SubReactor
  • SubReactor将连接加入到连接队列进行监听,并创建handler进行各种事件进行处理。
  • 当有新事件发生时,SubReactor会调用对应的handler进行处理
  • handler分发wroker线程池进行处理。
  • handler收到相应结果,返回client。

image

优点:父线程 子线程职责明确,父线程只需要把连接给子线程
缺点:编程复杂度较高

Netty线程模型

传送门:https://www.bilibili.com/video/BV1DJ411m7NR?p=43&spm_id_from=pageDriver

image

  • Netty抽象出2组线程池:BoosGroup 只负责客户端的连接;WorkerGroup 负责网络的读写

  • BoosGroup和WorkerGroup 都是 NioEventLoopGroup

  • NioEventLoopGroup:事件循环,每一个事件是 NioEventLoop

  • NioEventLoop:表示一个不断循环执行处理任务的线程,每一个NioEventLoop都有一个Selector,用于监听绑定在Selector上的网络通讯。一个taskQueue

  • NioEventLoopGroup:NioEventLoop=1:n (可以代码指定)

  • Boss下的NioEventLoop执行有三步

    • 轮询accept事件
    • 处理accept事件,与client建立连接,产生NiosocketChannel,并注册到某个Worker NioEventLoop上的Selector
    • 处理任务队列的任务 即runAllTasks 阻塞队列
  • Worker下的NioEventLoop执行有三步

    • 轮询 read/write事件
    • 处理IO事件
    • 处理任务队列的任务 即runAllTasks
  • Worker下的NioEventLoop会使用PipeLine,维护了很多处理器。

传送门:https://segmentfault.com/a/1190000017053730

  • 自定义TaskQueue,提交任务到ScheduleTaskQueue
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值