ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

netty技术总结

2022-04-15 13:03:00  阅读:294  来源: 互联网

标签:总结 netty void 技术 System 内存 new public channel


netty技术总结

本篇博文涉及技术点:

  1. 网络:select、poll、epoll、多路复用器
  2. 序列化:java自带序列化、google protobuff(性能节省10倍)
  3. 零拷贝:直接内存(堆外内存)、mmap、sendfile等7种零拷贝策略
  4. 设计思想:
    • reactor编程(响应式编程,SpringMVC中的controller也是这种)
    • 无锁串行化设计(redis中的主线程也是这种)
  5. 功能:注册与发现中心的实现(dubbo、nacos底层)
  6. 问题:C10K、C10M问题

1,概述

Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。

主要运用于:

  1. 分布式进程通信框架
    • hadoop、dubbo、akka等具有分布式功能的框架,底层的RPC通信都是基于netty实现的
  2. 游戏服务器开发
    • 这个本质其实就是C10K,C10M的情景(万、千万级客户端连接)

2,io技术

2.1,bio(Blocking I/O)

2.1.1,初级bio

执行流程:
  1. 先阻塞监听新的连接建立
    • 如果没有新的连接建立,则一直阻塞
    • 如果有新的连接建立,则往下执行
  2. 执行数据流读取的阻塞操作
    • 如果该连接没有数据传输,则一直阻塞
    • 如果有数据传输,就执行读取,并跳转第一步

image-20220408154853006

阻塞点:

Socket accept = serverSocket.accept();// 建立新连接

int read = accept.getInputStream().read(bytes);// 读取连接的数据

缺点:
  1. 不支持多连接同时建立
  2. 读取数据阻塞时,不支持新建立连接
  3. 并且下面的初级代码还有一个很大的缺陷!新建立的连接,接收完数据流后,需要重新连接!!!
public class BioServiceTest {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8000);
            while (true) {
                System.out.println("等待socket连接");

                // 阻塞方法 serverSocket.accept()
                Socket accept = serverSocket.accept();
                System.out.println("监测到客户端连接");
                hander(accept);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void hander(Socket accept) {
        byte[] bytes = new byte[2048];
        System.out.println("①开始读取数据");

        try {
            int read = accept.getInputStream().read(bytes);
            System.out.println("②结束读取数据");
            if (read != -1) {
                System.out.println("接收到的数据为:" + new String(bytes, 0, read));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.1.2,中级bio

总结:

针对上述的阻塞问题,可以直接使用多线程来解决(这里执行使用了线程池);将新建立的连接直接放在线程池ThreadPoolExecutor中利用多线程进行处理,互不干扰。

以下面的类比来进行说明:

  • bio就好比一个餐厅,它的大门就是serverSocket,客人就是socket客户端,服务员就是线程池里面的线程
  • 这个餐厅比较奇葩,每个服务员只为一个客人服务(并且在旁边看着),如何客人的数量超过服务员的数量,那么它就会停止接待新顾客,只有等待已有顾客吃完饭离开后才会通知门卫让其他顾客进入

image-20220408171848828

image-20220319151548619

虽然之前的问题解决了,但是我们会发现新的问题:

  1. 线程池是需要额外耗费系统资源甚至导致OOM,如果来了1000+新连接,我们直接使用1000+线程来处理吗!
  2. 如果我们的线程池耗尽了,那么新连接就无法建立了,这个不还是阻塞掉了吗!
  3. 有1000个连接,但是真正活跃的只有十几个,其他非活跃的连接,是不是没有必要在上面耗费资源?
public class BioServiceTest {
    static final ExecutorService execService = new ThreadPoolExecutor(
            1,
            2,
            3,
            TimeUnit.SECONDS, new LinkedBlockingDeque<>(1),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy()
    );// 专门设置的这些参数,用于测试阻塞的情况

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8000);
            while (true) {
                System.out.println("等待socket连接");

                // 阻塞方法 serverSocket.accept()
                Socket accept = serverSocket.accept();
                System.out.println("监测到客户端连接");
                execService.submit(() -> hander(accept));// 直接通过多线程来处理已经建立的连接
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void hander(Socket accept) {
        byte[] bytes = new byte[2048];
        System.out.println("①开始读取数据");

        try {
            while (true){
                int read = accept.getInputStream().read(bytes);
                System.out.println("②结束读取数据");
                if (read != -1) {
                    System.out.println("接收到的数据为:" + new String(bytes, 0, read));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.2,nio(New I/O)

nio与bio有一些更优的逻辑:

  1. 一个服务员可以为多个客人服务
  2. 服务员与新客人的到来没有耦合关系

image-20220408173632767

public class NioServiceTest {
    private static Selector selector;

    public static void main(String[] args) {
        NioServiceTest nioServiceTest = new NioServiceTest();
        nioServiceTest.initServer(8000);
        nioServiceTest.listen();
    }

    public void initServer(int port) {
        try {
            // 获得一个ServerSocket通道
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            // 设置为非阻塞通道, todo ? 如果设置成true会报错
            serverChannel.configureBlocking(false);
            serverChannel.socket().bind(new InetSocketAddress(port));
            selector = Selector.open();
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);// 新连接事件
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void listen() {
        System.out.println("服务端启动成功! ");
        try {
            while (true) {
                // 当注册的事件到达时,方法返回;否则就阻塞
                selector.select();
                // 获得selector中选中的迭代器,选中的项为注册事件
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    handle(key);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void handle(SelectionKey key) {
        if (key.isAcceptable()) {
            handleAccept(key);
        } else if (key.isReadable()) {
            handleRead(key);
        }
    }

    private void handleAccept(SelectionKey key) {
        try {
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            SocketChannel channel = server.accept();
            channel.configureBlocking(false);
            System.out.println("新连接建立");
            channel.register(selector, SelectionKey.OP_READ);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void handleRead(SelectionKey key) {
        try {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            channel.read(buffer);
            byte[] data = buffer.array();
            System.out.println("接收到的消息为:" + new String(data).trim());
            ByteBuffer outBuffer = ByteBuffer.wrap("消息处理完成".getBytes(StandardCharsets.UTF_8));
            channel.write(outBuffer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20220319170316384

nio最为核心:

  • ServerSocketChannel :获得一个ServerSocket通道
  • SocketChannel:
  • Selector:多路复用器
  • SelectionKey:事件key

nio的疑问:

1,上述代码在运行建立连接后,直接关闭telnet客户端,会有报错

  • 报错是因为我们代码会将接收结果会写到socket客户端,而此时客户端已经关闭,所以会写失败
  • 需要改写代码,检测到数据流中没有数据自动关闭

解决方案:

    private void handleRead(SelectionKey key) {
        try {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int read = channel.read(buffer);
            if(read>0){
                byte[] data = buffer.array();
                System.out.println("接收到的消息为:" + new String(data,"GBK").trim());
                ByteBuffer outBuffer = ByteBuffer.wrap((new String(data,"GBK")+"message is received! ").getBytes("GBK"));
                channel.write(outBuffer);
            }else {
                System.out.println("客户端关闭,自动关闭连接");
                key.cancel();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2,selector.select();阻塞,那么为什么说nio是非阻塞的io?

  • 非阻塞是针对新连接的建立和历史连接的数据处理来说是非阻塞的
  • 另外selector.select(1000);//也可以有非阻塞处理
  • selector.wakeup();//也可以唤醒阻塞的selector

3,SelectionKey.OP_WRITE是代表什么意思

  • OP_WRITE表示缓冲区是否有空间,是则响应返回true

2.3,aio(Asynchronous I/O)

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS(系统内核)先完成了再通知服务器应用去启动线程进行处理,每个线程不必亲自处理io,而是委派os来处理,并且也不需要等待io完成了,如果完成后,os会通知的。

img

AIO也被称作是NIO2.0。

区别于传统的BIO(Blocking IO,同步阻塞式模型,JDK1.4之前就存在于JDK中,NIO于JDK1.4版本发布更新)的阻塞式读写,AIO提供了从建立连接到读、写的全异步操作。AIO可用于异步的文件读写和网络通信。

构架师-Java AIO使用

实现一个最简单的AIO socket通信server、client,主要需要这些相关的类和接口:

  • AsynchronousServerSocketChannel服务端Socket通道类,负责服务端Socket的创建和监听;

  • AsynchronousSocketChannel客户端Socket通道类,负责客户端消息读写;

  • CompletionHandler<A,V>消息处理回调接口,是一个负责消费异步IO操作结果的消息处理器;

  • ByteBuffer负责承载通信过程中需要读、写的消息。

服务端代码

public class AioServerTest1 {
    public static void main(String[] args) {
        AioServerTest1.start();
    }

    private static int DEFAULT_PORT = 8000;
    private static AsyncServerHandler serverHandle;
    public volatile static long clientCount = 0;

    public static void start() {
        start(DEFAULT_PORT);
    }

    public static synchronized void start(int port) {
        if (serverHandle != null)
            return;
        serverHandle = new AsyncServerHandler(port);
        new Thread(serverHandle, "Server").start();
    }
}

// 新建服务端监听
class AsyncServerHandler implements Runnable {
    public CountDownLatch latch;
    public AsynchronousServerSocketChannel channel;

    public AsyncServerHandler(int port) {
        try {
            //创建服务端通道
            channel = AsynchronousServerSocketChannel.open();
            //绑定端口
            channel.bind(new InetSocketAddress(port));
            System.out.println("服务器已启动,端口号:" + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //CountDownLatch初始化
        //它的作用:在完成一组正在执行的操作之前,允许当前的现场一直阻塞
        //此处,让现场在此阻塞,防止服务端执行完成后退出
        //也可以使用while(true)+sleep
        //生成环境就不需要担心这个问题,因为服务端是不会退出的
        latch = new CountDownLatch(1);
        //用于接收客户端的连接
        channel.accept(this, new AcceptHandler());
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 处理器逻辑
class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncServerHandler> {
    @Override
    public void completed(AsynchronousSocketChannel channel, AsyncServerHandler serverHandler) {
        //继续接受其他客户端的请求
        AioServerTest1.clientCount++;
        System.out.println("连接的客户端数:" + AioServerTest1.clientCount);
        serverHandler.channel.accept(serverHandler, this);
        //创建新的Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //异步读  第三个参数为接收消息回调的业务Handler
        channel.read(buffer, buffer, new ReadHandler(channel));
    }

    @Override
    public void failed(Throwable exc, AsyncServerHandler serverHandler) {
        exc.printStackTrace();
        serverHandler.latch.countDown();
    }
}

// 数据读取处理器
class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
    //用于读取半包消息和发送应答
    private AsynchronousSocketChannel channel;

    public ReadHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }

    //读取到消息后的处理
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        //flip操作
        attachment.flip();
        //根据
        byte[] message = new byte[attachment.remaining()];
        attachment.get(message);
        try {
            String expression = new String(message, "GBK");
            System.out.println("服务器收到消息: " + expression);
            //向客户端发送消息
            doWrite(expression+"-from server");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    //发送消息
    private void doWrite(String result) {
        byte[] bytes = result.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        //异步写数据 参数与前面的read一样
        channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer buffer) {
                //如果没有发送完,就继续发送直到完成
                if (buffer.hasRemaining())
                    channel.write(buffer, buffer, this);
                else {
                    //创建新的Buffer
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    //异步读  第三个参数为接收消息回调的业务Handler
                    channel.read(readBuffer, readBuffer, new ReadHandler(channel));
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                try {
                    channel.close();
                } catch (IOException e) {
                }
            }
        });
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        try {
            this.channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端代码

public class AioClientTest1 {

    private static String DEFAULT_HOST = "localhost";
    private static int DEFAULT_PORT = 8000;
    private static AsyncClientHandler clientHandle;
    public static void start(){
        start(DEFAULT_HOST,DEFAULT_PORT);
    }
    public static synchronized void start(String ip,int port){
        if(clientHandle!=null)
            return;
        clientHandle = new AsyncClientHandler(ip,port);
        new Thread(clientHandle,"Client").start();
    }
    //向服务器发送消息
    public static boolean sendMsg(String msg) throws Exception{
        if(msg.equals("q")) return false;
        clientHandle.sendMsg(msg);
        return true;
    }
    @SuppressWarnings("resource")
    public static void main(String[] args) throws Exception{
        AioClientTest1.start();
        System.out.println("请输入请求消息:");
        Scanner scanner = new Scanner(System.in);
        while(AioClientTest1.sendMsg(scanner.nextLine()));
    }
}

class AsyncClientHandler implements CompletionHandler<Void, AsyncClientHandler>, Runnable {
    private AsynchronousSocketChannel clientChannel;
    private String host;
    private int port;
    private CountDownLatch latch;
    public AsyncClientHandler(String host, int port) {
        this.host = host;
        this.port = port;
        try {
            //创建异步的客户端通道
            clientChannel = AsynchronousSocketChannel.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        //创建CountDownLatch等待
        latch = new CountDownLatch(1);
        //发起异步连接操作,回调参数就是这个类本身,如果连接成功会回调completed方法
        clientChannel.connect(new InetSocketAddress(host, port), this, this);
        try {
            latch.await();
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        try {
            clientChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //连接服务器成功
    //意味着TCP三次握手完成
    @Override
    public void completed(Void result, AsyncClientHandler attachment) {
        System.out.println("客户端成功连接到服务器...");
    }
    //连接服务器失败
    @Override
    public void failed(Throwable exc, AsyncClientHandler attachment) {
        System.err.println("连接服务器失败...");
        exc.printStackTrace();
        try {
            clientChannel.close();
            latch.countDown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //向服务器发送消息
    public void sendMsg(String msg){
        byte[] req = new byte[0];
        try {
            req = (msg+"-from client").getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        //异步写
        clientChannel.write(writeBuffer, writeBuffer,new WriteHandler(clientChannel, latch));
    }
}

class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel clientChannel;
    private CountDownLatch latch;

    public WriteHandler(AsynchronousSocketChannel clientChannel, CountDownLatch latch) {
        this.clientChannel = clientChannel;
        this.latch = latch;
    }

    @Override
    public void completed(Integer result, ByteBuffer buffer) {
        //完成全部数据的写入
        if (buffer.hasRemaining()) {
            clientChannel.write(buffer, buffer, this);
        } else {
            //读取数据
            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
            clientChannel.read(readBuffer, readBuffer, new ReadHandler1(clientChannel, latch));
        }
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.err.println("数据发送失败...");
        try {
            clientChannel.close();
            latch.countDown();
        } catch (IOException e) {
        }
    }
}

class ReadHandler1 implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel clientChannel;
    private CountDownLatch latch;
    public ReadHandler1(AsynchronousSocketChannel clientChannel,CountDownLatch latch) {
        this.clientChannel = clientChannel;
        this.latch = latch;
    }
    @Override
    public void completed(Integer result,ByteBuffer buffer) {
        buffer.flip();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        String body;
        try {
            body = new String(bytes,"UTF-8");
            System.out.println("客户端收到结果:"+ body);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void failed(Throwable exc,ByteBuffer attachment) {
        System.err.println("数据读取失败...");
        try {
            clientChannel.close();
            latch.countDown();
        } catch (IOException e) {
        }
    }
}

测试代码

public class AioTest {
    //测试主方法
    @SuppressWarnings("resource")
    public static void main(String[] args) throws Exception {
        //运行服务器
        AioServerTest1.start();
        //避免客户端先于服务器启动前执行代码
        Thread.sleep(100);
        //运行客户端
        AioClientTest1.start();
        System.out.println("请输入请求消息:");
        Scanner scanner = new Scanner(System.in);
        while (AioClientTest1.sendMsg(scanner.nextLine())) ;
    }
}

特点:

  • 类似于CallableFuture这种有很多回调操作
  • 由系统内核来处理相关的io请求与数据传输,我们这边被动的接收相关的回调

3,netty

3.1,初级样例

服务端

public class NettyTest {
    private final static String GBK = "GBK";
    public static void main(String[] args) {
        // 服务类
        ServerBootstrap bootstrap = new ServerBootstrap();
        ExecutorService boss = Executors.newCachedThreadPool();
        ExecutorService worker = Executors.newCachedThreadPool();

        // 设置Nio工厂类
        bootstrap.setFactory(new NioServerSocketChannelFactory(boss,worker));

        // 设置管道的工程
        bootstrap.setPipelineFactory(() -> {
            ChannelPipeline pipeline = Channels.pipeline();
            pipeline.addLast("helloHandler",new HelloHandler());
            return pipeline;
        });

        // 绑定端口
        bootstrap.bind(new InetSocketAddress(8000));
    }

    static class HelloHandler extends SimpleChannelHandler{

        @Override
        public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
            ChannelBuffer message = (ChannelBuffer) e.getMessage();
            String s = new String(message.array(),GBK);
            if(s.equals("exception")){
                int i = 1/0;
            }
            System.out.println("接收到新消息:"+new String(message.array(),GBK));

            // 给客户端回写数据
            ChannelBuffer writeMsg =
                    ChannelBuffers.copiedBuffer(("服务端已接收到消息:"+s).getBytes(GBK));
            ctx.getChannel().write(writeMsg);

            super.messageReceived(ctx, e);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
            System.out.println("捕获到异常");
            super.exceptionCaught(ctx, e);
        }

        @Override
        public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
            System.out.println("新连接建立");
            super.channelConnected(ctx, e);
        }

        @Override
        public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
            System.out.println("连接关闭");
            super.channelClosed(ctx, e);
        }
    }
}

输出日志

新连接建立
接收新消息:你好
捕获到异常
四月 09, 2022 12:15:28 下午 org.jboss.netty.channel.SimpleChannelHandler
警告: EXCEPTION, please implement activeclub.netty.netty.NettyTest$HelloHandler.exceptionCaught() for proper handling.
java.lang.ArithmeticException: / by zero
    .....
接收到消息:123
连接关闭

客户端

public class NettyClientTest {
    private final static String GBK = "GBK";
    public static void main(String[] args) {
        try {
            // 服务类
            ClientBootstrap bootstrap = new ClientBootstrap();

            // 线程池
            ExecutorService boss = Executors.newCachedThreadPool();
            ExecutorService worker = Executors.newCachedThreadPool();

            // socket 工厂类
            bootstrap.setFactory(new NioClientSocketChannelFactory(boss, worker));
            // 管道工厂类
            bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
                @Override
                public ChannelPipeline getPipeline() throws Exception {
                    ChannelPipeline pipeline = Channels.pipeline();
                    pipeline.addLast("clientHandler", new ClientHandler());
                    return pipeline;
                }
            });

            // 连接服务端
            ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8000));
            Channel channel = connect.getChannel();
            System.out.println("客户端启动");
            Scanner scanner = new Scanner(System.in);

            while (true) {
                if (scanner.hasNext()) {
                    String s = scanner.next();
                    ChannelBuffer writeMsg = ChannelBuffers.copiedBuffer((s).getBytes(GBK));
                    channel.write(writeMsg);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 消息处理类
    static class ClientHandler extends SimpleChannelHandler {

        public ClientHandler() {
            super();
        }

        @Override
        public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
            ChannelBuffer message = (ChannelBuffer) e.getMessage();
            String s = new String(message.array(), GBK);
            System.out.println(s);
            super.messageReceived(ctx, e);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
            super.exceptionCaught(ctx, e);
        }

        @Override
        public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
            super.channelConnected(ctx, e);
        }

        @Override
        public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
            super.channelDisconnected(ctx, e);
        }
    }
}

输出日志:

客户端启动
123
服务端已接收到消息:123
111
服务端已接收到消息:111
222
服务端已接收到消息:222
333
服务端已接收到消息:333

特点:

  • bootstrap服务引导:
  • boss线程:处理新连接的线程
  • worker线程:处理已建立连接的IO流事件
  • channel:消息通道,可以往这个通道中发消息,或者接收消息
  • SimpleChannelHandler:通道处理类

3.2,C10K问题样例

C10K情景就是,1w客户端连接时的情景(同理有C10M问题)

下面的程序本质就是获取1w个管道连接,然后通过不同的管道发送消息

public class NettyClientTest {
    private final static String GBK = "GBK";
    private static AtomicLong atomicLong1 = new AtomicLong(0);
    private static AtomicLong atomicLong2 = new AtomicLong(0);
    private static AtomicLong atomicLong3 = new AtomicLong(0);
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    private static CountDownLatch countDownLatch = new CountDownLatch(0);
    public static void main(String[] args) {
        try {
            // 服务类
            ClientBootstrap bootstrap = new ClientBootstrap();

            // 线程池
            ExecutorService boss = Executors.newCachedThreadPool();
            ExecutorService worker = Executors.newCachedThreadPool();

            // socket 工厂类
            bootstrap.setFactory(new NioClientSocketChannelFactory(boss, worker));
            // 管道工厂类
            bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
                @Override
                public ChannelPipeline getPipeline() throws Exception {
                    ChannelPipeline pipeline = Channels.pipeline();
                    pipeline.addLast("clientHandler", new ClientHandler());
                    return pipeline;
                }
            });


            System.out.println("客户端启动");
            int num = 10000;
            List<Channel> channelList = new ArrayList<>(num);
            countDownLatch = new CountDownLatch(num);

            long timeIndex1 = System.currentTimeMillis();

            for (int i = 0; i < num; i++) {
                // 连接服务端
                ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8000));
                channelList.add(connect.getChannel());// 检测连接多个会话传输消息
                countDownLatch.countDown();
            }

            long timeIndex2 = System.currentTimeMillis();
            countDownLatch.await();
            for (int i = 0; i < num; i++) {
                ChannelBuffer writeMsg = ChannelBuffers.copiedBuffer((i+"").getBytes(GBK));
                channelList.get(i).write(writeMsg);
            }
            long timeIndex3 = System.currentTimeMillis();

            System.out.println(String.format("创建%d连接耗时:%d",num,timeIndex2-timeIndex1));
            System.out.println(String.format("发送%d消息耗时:%d",num,timeIndex3-timeIndex2));

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

    // 消息处理类
    static class ClientHandler extends SimpleChannelHandler {

        public ClientHandler() {
            super();
        }

        @Override
        public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
            ChannelBuffer message = (ChannelBuffer) e.getMessage();
            String s = new String(message.array(), GBK);

            System.out.println(s);
            super.messageReceived(ctx, e);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
            System.out.println("连接出现异常"+atomicLong2.incrementAndGet());
        }

        @Override
        public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
            System.out.println("创建新连接"+atomicLong2.incrementAndGet());
        }

        @Override
        public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
            System.out.println("连接断开"+atomicLong1.incrementAndGet());
        }
    }
}

/*
创建10000连接耗时:5709
发送10000消息耗时:169
*/

3.3,源码理解

image-20220409161107707

1断点;2打印;3调用栈;4搜索

image-20220409162557754

3.4,netty5样例

服务端

public class Netty5ServerTest {
    public static void main(String[] args) {
        // 设置服务类
        ServerBootstrap bootstrap = new ServerBootstrap();

        // 设置boss和worker
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 设置线程池
            bootstrap.group(boss, worker);

            // 设置socket工厂类
            bootstrap.channel(NioServerSocketChannel.class);

            // 设置管道工厂类
            bootstrap.childHandler(new ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel ch) {
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new StringEncoder());
                    ch.pipeline().addLast(new ServerHandler());
                }
            });

            //设置参数,TCP参数
            bootstrap.option(ChannelOption.SO_BACKLOG, 2048);//serverSocketchannel的设置,链接缓冲池的大小【超过这么大就拒绝连接】
            bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);//socketchannel的设置,维持链接的活跃,清除死链接
            bootstrap.childOption(ChannelOption.TCP_NODELAY, true);//socketchannel的设置,关闭tcp延迟发送

            // 绑定端口
            ChannelFuture future = bootstrap.bind(8000).sync();
            System.out.println("开始监听端口");

            // 等待【未来某时刻】关闭服务
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }

    }

    static class ServerHandler extends SimpleChannelInboundHandler {
        @Override
        protected void messageReceived(ChannelHandlerContext ctx, Object o) {
            System.out.println(o);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            super.exceptionCaught(ctx, cause);
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("新连接建立");
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("历史连接关闭");
        }
    }
}

客户端

public class Netty5ClientTest {
    public static void main(String[] args) {
        // 设置服务类
        Bootstrap bootstrap = new Bootstrap();

        // 设置worker
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 设置线程池
            bootstrap.group(worker);

            // 设置socket工厂类
            bootstrap.channel(NioSocketChannel.class);

            // 设置管道工厂类
            bootstrap.handler(new ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel ch) {
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new StringEncoder());
                    ch.pipeline().addLast(new ServerHandler());
                }
            });

            //设置参数,TCP参数
            bootstrap.option(ChannelOption.SO_BACKLOG, 2048);//serverSocketchannel的设置,链接缓冲池的大小【超过这么大就拒绝连接】

            // 绑定端口
            ChannelFuture future = bootstrap.connect("127.0.0.1",8000).sync();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            while (true){
                String msg = bufferedReader.readLine();
                future.channel().writeAndFlush(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
        }
    }

    static class ServerHandler extends SimpleChannelInboundHandler {
        @Override
        protected void messageReceived(ChannelHandlerContext ctx, Object o) throws UnsupportedEncodingException {
            System.out.println(o);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            super.exceptionCaught(ctx, cause);
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("新连接建立");
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("历史连接关闭");
        }
    }
}

3.5,client多连接

public class Netty5ClientMutiTest {
    // 设置服务类
    private Bootstrap bootstrap = new Bootstrap();
    private AtomicInteger index = new AtomicInteger(0);

    // 设置通道缓存
    private static List<Channel> channelList;

    public Netty5ClientMutiTest(int count) throws InterruptedException {
        channelList = new ArrayList<>(count);
        bootstrap.group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new StringEncoder());
                        ch.pipeline().addLast(new ServerHandler());
                    }
                });
        for (int i = 0; i < count; i++) {
            ChannelFuture future = bootstrap.connect("127.0.0.1", 8000).sync();
        }
    }

    /*
    获取有效连接
     */
    public Channel nextChannel() throws InterruptedException {
        return getFirstActiveChannel(0);
    }

    private Channel getFirstActiveChannel(int count) throws InterruptedException {
        Channel channel = channelList.get(Math.abs(index.incrementAndGet() % channelList.size()));
        if (!channel.isActive()) {
            reconnect(channel);
            if (channelList.size() >= count) {
                return getFirstActiveChannel(count++);
            } else {
                throw new RuntimeException("无有效连接可用! ");
            }
        } else {
            return channel;
        }
    }

    private void reconnect(Channel channel) throws InterruptedException {
        synchronized (channel) {
            int index = channelList.indexOf(channel);
            if (index == -1) {
                return;
            }
            ChannelFuture future = bootstrap.connect("172.20.48.1", 8000).sync();
            Channel newChannel = future.channel();
            channelList.set(index, newChannel);
            future.channel().closeFuture().sync();
        }
    }

    static class ServerHandler extends SimpleChannelInboundHandler {
        @Override
        protected void messageReceived(ChannelHandlerContext ctx, Object o) throws UnsupportedEncodingException {
            System.out.println(o);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            super.exceptionCaught(ctx, cause);
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("新连接建立");
            channelList.add(ctx.channel());
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("历史连接关闭");
        }
    }

    public static void main(String[] args) throws InterruptedException, IOException {
        Netty5ClientMutiTest netty5ClientMutiTest = new Netty5ClientMutiTest(5);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        while (true) {
            String s = bufferedReader.readLine();
            netty5ClientMutiTest.nextChannel().writeAndFlush(s);
        }
    }
}

3.6,心跳检测

本质就是直接

public class Netty5ServiceHeartBeat {
    public static void main(String[] args) {
        // 设置服务类
        ServerBootstrap bootstrap = new ServerBootstrap();

        // 设置boss和worker
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 设置线程池
            bootstrap.group(boss, worker);

            // 设置socket工厂类
            bootstrap.channel(NioServerSocketChannel.class);

            // 设置管道工厂类
            bootstrap.childHandler(new ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel ch) {
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new StringEncoder());
                    ch.pipeline().addLast(new CustomIdleHandler(5,5,5));
                    ch.pipeline().addLast(new CustomServerHandler());
                }
            });

            //设置参数,TCP参数
            bootstrap.option(ChannelOption.SO_BACKLOG, 2048);//serverSocketchannel的设置,链接缓冲池的大小【超过这么大就拒绝连接】
            bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);//socketchannel的设置,维持链接的活跃,清除死链接
            bootstrap.childOption(ChannelOption.TCP_NODELAY, true);//socketchannel的设置,关闭tcp延迟发送

            // 绑定端口
            ChannelFuture future = bootstrap.bind(8000).sync();
            System.out.println("开始监听端口");

            // 等待【未来某时刻】关闭服务
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }

    }

    static class CustomServerHandler extends SimpleChannelInboundHandler {

        @Override
        protected void messageReceived(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
            System.out.println(o);
        }
    }

    static class CustomIdleHandler extends IdleStateHandler {
        public CustomIdleHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
            super(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds);
        }

        public CustomIdleHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) {
            super(readerIdleTime, writerIdleTime, allIdleTime, unit);
        }

        @Override
        protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
            SimpleDateFormat dataFormat = new SimpleDateFormat("ss");
            System.out.println(evt.state()+" "+dataFormat.format(new Date()));
            if(evt.state().equals(IdleState.ALL_IDLE)){
                ctx.channel().writeAndFlush("time out ,you will be close !");
                ctx.channel().close().sync();
            }
            super.channelIdle(ctx, evt);
        }
    }
}

3.7,手写dubbo

3.7.1,RPC与REST通信对比

RPC架构图

看了这个架构图,是不是觉得和netty的CS网络结构很像!!

其实我们知道的dubbo、gRPC也都是使用netty实现的

image-20220411115651797

REST架构图

image-20220411115851719

对比

RPC REST
消息格式 二进制Thrift、protobuf 文本XML、JSON
通信协议 TCP四层网络架构 HTTP、HTTP/2:OSI 7层网络
性能 一般
接口契约IDL Thrift、protobuf swagger 本质就是没有限定
客户端 强类型客户端 HTTP客户端即可
OKHttp、HttpClient、UrlConnection
框架 Dubbo、gRPC、Thrift Spring MVC、Struts2等
开发者友好 需要额外生成存根
二进制解析麻烦
JSON文本可以直接解析内容
应用场景 服务间通信推荐使用RPC
例如protobuf通信字节流节省大约10倍
对外暴露接口推荐使用REST,调用方便

3.7.2,核心功能实现(todo)

利用netty nio的功能,实现类似dubbo的一个注册中心

这个注册中心有如下几个功能

核心功能

  1. 服务注册与发现:接收其他组件的注册信息,同时可以将其注册信息实时分发给已注册的微服务
  2. RPC调用:可以对已注册的微服务进行心跳检测,实时更新不健康微服务并推送

进阶功能

  1. 实现更新已注册微服务的配置信息
  2. 反向代理其他组件
  3. 字节流压缩
  4. 可以进行代理RPC调用
    • 原本服务A-调用服务B 转换为 服务A-调用注册中心-由注册中心去调用服务B,网关

image-20220409231409468

4,netty技术点

4.1,protocol序列化

这个应该不算是netty的技术,但是有相关度!

java序列化是一种协议,是为了在进行数据传输时,将对象序列化成字节数组,或者二进制流数据。对于分布式微服务来说,网络通信是一种需要认真考虑的资源。我们可以将原有的实体对象序列化后进行数据流传输,从而节省网络带宽的耗费!

当前有针对分布式编解码比较优秀的编码协议protocol,该协议是google开发出来的用于网络数据流压缩传输使用。

protocol优点:

  1. 可以大大节省网络带宽,例如使用java自带的序列化功能进行字节流编码与protocol的字节流编码进行比较,protocol可以节省大约10倍左右的带宽

protocol缺点:

  1. 需要额外使用.proto配置文件来生成需要进行数据传输的实体类

这里源码对比使用java自带的序列化编码和protocol进行比较

4.1.1,java自带的序列化编码:

序列化机制只保存对象的类型信息,属性的类型信息和属性值,和方法没有什么关系,你就是给这个类增加10000个方法,序列化内容也不会增加任何东西

/**
 * @Author 59456
 * @Date 2022/4/10
 * @Descrip 普通的java序列化工具,数据流比较大
 * 该方式序列化时,会额外保存java对象的信息:
 * (1)对象的类信息
 * (2)对象的属性类型信息
 * (3)对象属性的值
 * @Version 1.0
 */
public class JavaSerializeUtil {

    public static byte[] toByte(Object obj) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(obj);
        return byteArrayOutputStream.toByteArray();
    }

    public static <T> T toObject(byte[] bytes,Class<T> clazz) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
        return (T)objectInputStream.readObject();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        BaseResponse baseResponse = new BaseResponse();
        baseResponse.setCode("1");
        baseResponse.setMsg("hello");
        baseResponse.setData("1");

        byte[] bytes = toByte(baseResponse);
        BaseResponse newBr = toObject(bytes,BaseResponse.class);
        System.out.println(Arrays.toString(bytes));
    }
}

/*
输出样例(143字节):
[-84, -19, 0, 5, 115, 114, 0, 38, 99, 111, 109, 46, 97, 99, 116, 105, 118, 101, 99, 108, 117, 98, 46, 100, 117, 98, 98, 111, 46, 112, 111, 106, 111, 46, 66, 97, 115, 101, 82, 101, 115, 112, 111, 110, 115, 101, -48, 87, 114, 108, -101, -104, 97, 124, 2, 0, 3, 76, 0, 4, 99, 111, 100, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 4, 100, 97, 116, 97, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 76, 0, 3, 109, 115, 103, 113, 0, 126, 0, 1, 120, 112, 116, 0, 1, 49, 113, 0, 126, 0, 4, 116, 0, 5, 104, 101, 108, 108, 111]
*/

4.1.2,protocol序列化

// protocol的类信息配置
syntax = "proto3";
option java_package = "com.activeclub.dubbo.pojo";
option java_outer_classname = "BaseResponseModel";
message BaseResponse1{
  string code = 1;
  string msg = 2;
  string data = 3;
}

java代码

/**
 * @Author 59456
 * @Date 2022/4/10
 * @Descrip 序列化后的字节流很少,大大节省带宽
 * 1,将一些类信息都保存在本地,传输数据流时不额外重复传输
 * 2,各种数据类型字节数的可动态伸缩;例如int类型,原本是占用4个节点,但是使用动态伸缩后,会依据情况转化为占用1~5个字节(一般情况下,节省很多)
 * @Version 1.0
 */
public class ProtoUtil {

    public static byte[] toBytes(){
        BaseResponseModel.BaseResponse1.Builder builder = BaseResponseModel.BaseResponse1.newBuilder();
        builder.setCode("1")
                .setMsg("hello")
                .setData("100");
        BaseResponseModel.BaseResponse1 br = builder.build();
        byte[] bytes = br.toByteArray();
        return bytes;
    }

    public static Object toObject(byte[] bytes) throws InvalidProtocolBufferException {
        BaseResponseModel.BaseResponse1 baseResponse1 = BaseResponseModel.BaseResponse1.parseFrom(bytes);
        return baseResponse1;
    }

    public static void main(String[] args) throws InvalidProtocolBufferException {
        byte[] bytes = toBytes();
        BaseResponseModel.BaseResponse1 object = (BaseResponseModel.BaseResponse1) toObject(bytes);
        System.out.println(Arrays.toString(bytes));
    }
}

/*
输出样例(15个字节):
[10, 1, 49, 18, 5, 104, 101, 108, 108, 111, 26, 3, 49, 48, 48]
*/

4.2,数据缓冲

内存池

PooledByteBufAllocator是netty中的内存缓冲池

allocator.buffer()可以设置申请大小,默认256,并且申请后的ByteBuf是可以动态扩容的

public class BufferTest {
    public static void main(String[] args) {
        // 内存缓冲池
        PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
        ByteBuf buf = allocator.buffer();
        buf.writeInt(32);

        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        System.out.println(Arrays.toString(bytes));
        buf.release();
    }
}

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer(相当于一个内存块),情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty提供了基于ByteBuf内存池的缓冲区重用机制。需要的时候直接从池子里获取ByteBuf使用即可,使用完毕之后就重新放回到池子里去。

netty的零拷贝、架构设计、ByteBuf扩容机制详解_netty_08

直接(堆外)&堆内存

在这里插入图片描述

PooledByteBufAllocator支持堆内存,也支持直接内存。

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,某些情况下这部分内存也会被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

Java里用DirectByteBuffer可以分配一块直接内存(堆外内存),元空间对应的内存也叫作直接内存,它们对应的都是机器的物理内存。

  • 分配堆内存 ByteBufAllocator.DEFAULT.heapBuffer();

  • 分配直接内存 ByteBufAllocator.DEFAULT.directBuffer();

堆内存的分配效率高,但是读写效率低。直接内存读写效率高;nettty默认采用的是直接内存,也就是上面创建ByteBuf时直接使用的

ByteBufAllocator.DEFAULT.buffer(); 获取的是直接内存。

netty中的ByteBuf支持一种池化的管理,netty默认情况下获取的ByteBuf都是从池中获取的,如果不想要从池中获取需要在jvm启动配置项中加一个-Dio.netty.allocator.type=upooled参数

直接内存-申请

ByteBuffer.allocateDirect(1000)底层通过unsafe.allocateMemory(size)实现,接下去看看在JVM层面是如何实现的:

netty的零拷贝、架构设计、ByteBuf扩容机制详解_netty_02

可以发现,最底层是通过malloc方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe提供了另一个接口freeMemory可以对申请的堆外内存进行释放。

netty的零拷贝、架构设计、ByteBuf扩容机制详解_netty_03

直接内存-释放

直接内存优点

  1. 直接内存访问效率高,访问速度比堆内存快
  2. 不占用堆内存空间,减少了发生GC的可能
  3. java虚拟机实现上,本地IO会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡

直接内存缺点

  1. 初始分配较慢(原因:系统中的很多进程都在不断的申请释放内存,争抢比较激烈;而堆内存也算是已经申请过的内存,只受JVM管理)
  2. 没有JVM直接帮助管理内存,容易发生内存溢出。为了避免一直没有FULL GC,最终导致直接内存把物理内存耗完。我们可以指定直接内存的最大值,通过-XX:MaxDirectMemorySize来指定,当达到阈值的时候,调用system.gc来进行一次FULL GC,间接把那些没有被使用的直接内存回收掉。

ByteBuf

在这里插入图片描述

ByteBuf有读指针和写指针,不像nio中的ByteBuffer公用一个指针 然后还要进行读写模式切换。刚开始,读写两个指针都在0的位置。

Bytebuf如果容量不够了可以动态扩容,但最大容量不能超过int的最大值。

上图中的四块区域分为:废弃区、可读区、可写区、可扩容区

四个属性为:读指针,写指针,容量,最大容量

扩容规则

如果写入的数据没有超过512,则选择的是下一个16的整数倍,例如现在容量是12,进行一次扩容会变为16,如果又不够了就会变为32

而如果写入后的数据大小超过了512,则选择下一个2n。例如写入后的大小为513,库容后的大小就变为1024,因为2^9=512不够

但是扩容不能超过max capacity(int的最大值),否则会报错

netty4+规则有改变

Netty的ByteBuf需要动态扩容来满足需要,扩容过程:

默认门限阈值为4MB(这个阈值是一个经验值,不同场景,可能取值不同),当需要的容量等于门限阈值,使用阈值作为新的缓存区容量 目标容量,

如果大于阈值,采用每次步进4MB的方式进行内存扩张((需要扩容值/4MB)*4MB),扩张后需要和最大内存(maxCapacity)进行比较,大于maxCapacity的话就用maxCapacity,否则使用扩容值 目标容量,如果小于阈值,采用倍增的方式,以64(字节)作为基本数值,每次翻倍增长64-》128-》256,直到倍增后的结果大于或等于需要的容量值。

内存回收

ByteBuf有几种实现方式,针对这几种ByteBuf实现的机制也不同

  1. UnpooleHeapByteBuf 使用的是jvm内存,只需要等待GC回收内存即可
  2. UnPooleDirectByteBuf 使用的是直接内存,需要特殊的方法来回收内存
  3. PooledByteBuf 和它的子类使用了池化机制,需要复杂的规则来回收内存

不同的ByteBuf的实现,内存回收也不一样,还好netty提供了统一的接口ReferenceCounted接口(标记计数法),该接口提供了通用的方法来进行上面几种ByteBuf的内存回收,该接口的工作方式是采用引用计数的规则

每个ByteBuf 对象的初始计数为1
调用release() 方法计数减1,如果计数为0则ByteBuf的内存会被回收
调用retain()方法计数加1,表示调用者没用完之前,其它handler即使调用了release()也不会造成回收
当计数为0时,底层内存会被回收,这时即使ByteBuf对象还在,其各个方法均无法正常使用

4.3,零拷贝

4.3.1,零拷贝

在客户端与服务端交互的过程中

  1. 如果使用传统的JVM堆内存(非零拷贝),有数据发送时,服务端会调用unsafe.read()方法读取socketChannel中的数据,这个read函数是操作系统来执行的。由于JVM无法直接读取socketChannel中的数据,需要操作系统把socketChannel中的ByteBuf数据拷贝一份到操作系统的直接内存中去,然后jvm再从直接内存中拷贝一份到jvm的堆内存中使用。读数据这个过程经历了两次拷贝。
  2. 如果使用直接内存(堆外内存)(零拷贝),在处理客户端与服务端连接时,read函数把socketChannel中的ByteBuf数据拷贝到操作系统的直接内存中去就完事了,不再进行 直接内存 >> jvm堆内存 的拷贝过程。jvm内部通过直接内存的引用 管理着ByteBuf数据。

其中JVM堆内存被称为用户态空间、直接内存又被称为内核态空间

零拷贝并不是说没有拷贝过程,而是减少了用户空间和内核空间的数据相互拷贝,增加了处理效率,netty正是使用零拷贝保证了客户端和服务端交互的高性能。堆内存和直接内存示意图如下:

netty的零拷贝、架构设计、ByteBuf扩容机制详解_netty_06

当前实现零拷贝的几种方式

零,没有使用零拷贝技术

基本操作就是循环的从磁盘读入文件内容到缓冲区,再将缓冲区的内容发送到socket。

img

从上图中可以看出,共产生了四次数据拷贝,即使使用了DMA来处理了与硬件的通讯,CPU仍然需要处理两次数据拷贝,与此同时,在用户态与内核态也发生了2次上下文切换,无疑也加重了CPU负担。

总结:4次拷贝、2次上下文切换

①,用户态直接 I/O

这种方法可以使应用程序或者运行在用户态下的库函数直接访问硬件设备,数据直接跨过内核进行传输,内核在整个数据传输过程除了会进行必要的虚拟存储配置工作之外,不参与其他任何工作,这种方式能够直接绕过内核,极大提高了性能。

img

缺陷:

1)这种方法只能适用于那些不需要内核缓冲区处理的应用程序,这些应用程序通常在进程地址空间有自己的数据缓存机制,称为自缓存应用程序,如数据库管理系统就是一个代表。

2)这种方法直接操作磁盘 I/O,由于 CPU 和磁盘 I/O 之间的执行时间差距,会造成资源的浪费,解决这个问题需要和异步 I/O 结合使用。

总结:2次拷贝,0次上下文切换(todo 待确认)

②,mmap技术(memory map,内存映射)

应用程序调用mmap(),磁盘上的数据会通过DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,这一切都发生在内核态,最后,socket缓冲区再把数据发到网卡去。

img

缺陷:

1)mmap 隐藏着一个陷阱,当 mmap 一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被 SIGBUS 信号终止,SIGBUS 默认会杀死进程并产生一个 coredump,如果服务器被这样终止了,那损失就可能不小了。

解决这个问题通常使用文件的租借锁:首先为文件申请一个租借锁,当其他进程想要截断这个文件时,内核会发送一个实时的 RT_SIGNAL_LEASE 信号,告诉当前进程有进程在试图破坏文件,这样 write 在被 SIGBUS 杀死之前,会被中断,返回已经写入的字节数,并设置 errno 为 success。

通常的做法是在 mmap 之前加锁,操作完之后解锁:

总结:3次拷贝、1次上下文切换

③普通sendfile技术

sendfile 是只发生在内核态的数据传输接口,没有用户态的参与,自然避免了用户态数据拷贝。它指定在 in_fd 和 out_fd 之间传输数据,其中,它规定 in_fd 指向的文件必须是可以 mmap 的,out_fd 必须指向一个套接字,也就是规定数据只能从文件传输到套接字,反之则不行。sendfile 不存在像 mmap 时文件被截获的情况,它自带异常处理机制。

img

缺陷:

1)只能适用于那些不需要用户态处理的应用程序。

总结:3次拷贝、0次上下文切换

④DMA 辅助的 sendfile

常规 sendfile 还有一次内核态的拷贝操作,能不能也把这次拷贝给去掉呢?答案就是这种 DMA 辅助的 sendfile。

这种方法借助硬件的帮助,在数据从内核缓冲区到 socket 缓冲区这一步操作上,并不是拷贝数据,而是拷贝缓冲区描述符,待完成后,DMA 引擎直接将数据从内核缓冲区拷贝到协议引擎中去,避免了最后一次拷贝。

img

缺陷:

1)除了3.4 中的缺陷,还需要硬件以及驱动程序支持。

2)只适用于将数据从文件拷贝到套接字上。(文件->套接字socket)

总结:2次拷贝、0次上下文切换

⑤splice技术

splice 去掉 sendfile 的使用范围限制,可以用于任意两个文件描述符中传输数据。

但是 splice 也有局限,它使用了 Linux 的管道缓冲机制,所以,它的两个文件描述符参数中至少有一个必须是管道设备。

splice 提供了一种流控制的机制,通过预先定义的水印(watermark)来阻塞写请求,有实验表明,利用这种方法将数据从一个磁盘传输到另外一个磁盘会增加 30%-70% 的吞吐量,CPU负责也会减少一半。

缺陷:

1)同样只适用于不需要用户态处理的程序

2)传输描述符至少有一个是管道设备。(channel必须有一个)

总结:2次拷贝、0次上下文切换

⑥写时复制

在某些情况下,内核缓冲区可能被多个进程所共享,如果某个进程想要这个共享区进行 write 操作,由于 write 不提供任何的锁操作,那么就会对共享区中的数据造成破坏,写时复制就是 Linux 引入来保护数据的。

写时复制,就是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中,这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时拷贝。这种方法在某种程度上能够降低系统开销,如果某个进程永远不会对所访问的数据进行更改,那么也就永远不需要拷贝。

缺陷:

需要 MMU 的支持,MMU 需要知道进程地址空间中哪些页面是只读的,当需要往这些页面写数据时,发出一个异常给操作系统内核,内核会分配新的存储空间来供写入的需求。

总结:2次拷贝、0次上下文切换

⑦缓冲区共享

这种方法完全改写 I/O 操作,因为传统 I/O 接口都是基于数据拷贝的,要避免拷贝,就去掉原先的那套接口,重新改写,所以这种方法是比较全面的零拷贝技术,目前比较成熟的一个方案是最先在 Solaris 上实现的 fbuf (Fast Buffer,快速缓冲区)

Fbuf 的思想是每个进程都维护着一个缓冲区池,这个缓冲区池能被同时映射到程序地址空间和内核地址空间,内核和用户共享这个缓冲区池,这样就避免了拷贝。

img

缺陷:

1)管理共享缓冲区池需要应用程序、网络软件、以及设备驱动程序之间的紧密合作

2)改写 API ,尚处于试验阶段。

4.3.2,netty中的零拷贝

从哪里看出netty默认使用的内核空间呢?channelRead方法会接收来自客户端的消息

netty的零拷贝、架构设计、ByteBuf扩容机制详解_netty_07

4.3.3,buffer聚合

Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。

在这里插入图片描述

Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

duplicate() 截取原始ByteBuf的所有内容,而slice()方法只是截取一部分。底层和原始ByteBuf也是使用的同一块内存,也有自己的读写指针。

copeXXXXX 与零拷贝对应的就是一系列以cope开头的方法,也是创建新的ByteBuf,只是会进行数据拷贝,读写都与原始ByteBuf无关

上面是将一个大的ByteBuf切分为几个小的ByteBuf,下面还有将几个小的ByteBuf零拷贝组合成一个大的ByteBuf。

也就是调用compositeBuffer()创建ByteBuf,然后调用addComponents()方法进行添加。

在这里插入图片描述

然后运行,发现数据并没有写入新的ByteBuf中,这是因为addComponents() 可变参数 或者是空参的这个方法默认不会去跳转写指针的位置。需要在该方法的参数1位置加一个boolean类型的参数true就可以了。

buffer.addComponents(true, buf1, buf2); 这个就表示写指针会自动增长。这样就可以正确的将两个ByteBuf组合到一起。

使用这个也需要注意release()的问题。

Unpooled是一个工具类,提供了非池化的 ByteBuf创建、组合、复制等操作。这里有一个关于零拷贝相关的方法

Unpooled.wrappedBuffer(ByteBuf buf...)方法,可以将多个Bytebuf组合成一个Bytebuf,底层使用的是compositeBuffer()方法

4.4,多路复用器

背景知识

一段最基础的网络编程代码,先新建socket对象,依次调用bind、listen、accept,最后调用recv接收数据。recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。

操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态:

  • 运行状态是进程获得cpu使用权,正在执行代码的状态;

  • 等待状态是阻塞状态,比如上述程序运行到recv时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态

操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。

多路复用:netty底层使用NIO多路复用器selector,实现模式为把多个连接(请求)放入集合中,只用一个线程可以处理多个请求(连接),也就是多路复用。不同于BIO的一个连接开启一个线程。

非阻塞:表现为线程不会一直在等待,把连接加入集合后,线程会一直轮询集合中的连接,有则处理,无则继续接受请求。

4.5,Reactor模式

主从:用主线程组boosGroup中的NioEventLoop来【接受新请求】,生成客户端channel,并把客户端channel注册到WorkerGroup中的NioEventLoop中去。从线程组WorkerGroup负责处理所有的【读写请求

Reactor线程模型:基于事件响应的模型,底层使用epoll函数,使用操作系统的硬中断方式判断如果连接有事件响应,就把该连接放入就绪事件列表中,后续只处理就绪事件列表中的连接!避免空轮询

Reactor模型也主要有三种实现方式,单Reactor单线程、单Reactor多线程和主从Reactor多线程。

1、单 Reactor 单线程
该模式和NIO的通信方式一致,单线程处理客户端的连接和读写操作。缺点很明显,就是连接和读写多的情况下,效率低下。(redis使用的是单reactor单线程,这里面的单线程是针对数据处理、那些接收、解析、发送已经可以多线程处理了)

2、单 Reactor 多线程
该模式与NIO的区别在于,通过多线程的方式来处理客户端的读写操作,这样既减轻了Selector的压力,又保证了处理读写操作的能力。这个缺点在于Reactor既要负责客户端的连接,又要负责读写操作,连接操作比较快,但读写可能很多且比较耗时。

image

3、主从 Reactor 多线程

基于单 Reactor 多线程的缺点,于是有了主从Reactor模式,主 Reactor 只负责处理客户端的连接,从 Reactor 负责客户端的读写操作,并且是一主多从的模型。主从模型下,不仅遵循了单一职责的原则,还提高了承受读写负载的能力。(nginx是多reactor多线程模型)

4.6,网络模型

经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。

I/O多路复用底层主要用的Linux内核函数(select、poll、epoll)来实现,windows不支持epoll实现,windows底层不支持epoll实现,window底层是基于winsock

select poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当有IO事件就绪,系统注册的回调函数就会被调用,时间复杂度O(1)
最大连接 有上限1024~2048
默认1024个
无上限 无上限
版本 jdk1.4之前 jdk1.4 jdk1.5

4.6.1,select

先准备一个数组(下面代码中的fds),让fds存放着所有需要监视的socket。然后调用select,如果fds中的所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒进程。用户可以遍历fds,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。

select的实现思路很直接。假如程序同时监视如下图的sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。

当任何一个socket收到数据后,中断程序将唤起进程。下图展示了sock2接收到了数据的处理流程。

所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。如下图所示。

img

经由这些步骤,当进程A被唤醒后,它知道至少有一个socket接收了数据。程序只需遍历一遍socket列表,就可以得到就绪的socket。

缺点:

  • 每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket
  • 进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
int s = socket(AF_INET, SOCK_STREAM, 0);  bind(s, ...)listen(s, ...)
int fds[] =  存放需要监听的socket
while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的数据处理
        }
    }}

4.6.2,poll

和select类似,只是描述fd集合的方式不同,poll使用pollfd结构而非select的fd_set结构。 管理多个描述符也是进行轮询,根据描述符的状态进行处理,但poll没有最大文件描述符数量的限制

poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

  • 它将用户传入的数组拷贝到内核空间

  • 然后查询每个fd对应的设备状态:

    • 如果设备就绪 在设备等待队列中加入一项继续遍历
    • 若遍历完所有fd后,都没发现就绪的设备 挂起当前进程,直到设备就绪或主动超时,被唤醒后它又再次遍历fd。这个过程经历多次无意义的遍历。

没有最大连接数限制,因其基于链表存储,其缺点:

  • 大量fd数组被整体复制于用户态和内核地址空间间,而不管是否有意义
  • 如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd

4.6.2,epoll

epoll模型修改主动轮询为被动通知,当有事件发生时,被动接收通知。所以epoll模型注册套接字后,主程序可做其他事情,当事件发生时,接收到通知后再去处理。

可理解为event poll,epoll会把哪个流发生哪种I/O事件通知我们。所以epoll是事件驱动(每个事件关联fd),此时我们对这些流的操作都是有意义的。复杂度也降到O(1)。

4.7,无锁串行化设计思想

在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。从表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

NIO的多路复用只允许一个线程处理所有请求的读写事件,其实就是一种无锁串行化的设计思想。这种思想也被应用到了 redis 和 netty 的线程模型中,这也是redis 和 netty不仅能保证高并发,且不需要加锁的原因。

Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的自定义Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。

4.8,高可用、可扩展架构

Netty框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来,让你可以专注业务的开发,而不必兼顾网络io层面的问题。在使用时只需要在ChindHandler中嵌入自己的业务Handler即可。netty架构代码解耦、可扩展性强,支持高性能序列化协议(自定义protostuff编解码序列化机制)。

开发者只用关注自定义handler即可!

4.9,灵活的TCP参数配置能力

合理设置TCP参数在某些场景下对于性能的提升可以起到显著的效果,例如接收缓冲区SO_RCVBUF和发送缓冲区SO_SNDBUF。如果设置不当,对性能的影响是非常大的。通常建议值为128K或者256K。

Netty在启动辅助类ChannelOption中可以灵活的配置TCP参数,满足不同的用户场景。

netty的零拷贝、架构设计、ByteBuf扩容机制详解_netty_09

//设置参数,TCP参数
//serverSocketchannel的设置,链接缓冲池的大小【超过这么大就拒绝连接】
bootstrap.option(ChannelOption.SO_BACKLOG, 2048);

//socketchannel的设置,维持链接的活跃,清除死链接
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

//socketchannel的设置,关闭tcp延迟发送
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);

5,实例

5.1,redis网络模型

# 直接下载redis的源码包
wget http://download.redis.io/releases/redis-6.2.6.tar.gz
# 解压
tar zxvf redis-6.2.6.tar.gz redis-6.2.6
cd redis-6.2.6/src/
# 查看源码
root@VM-12-2-ubuntu:/home/activeclub/document/redis-6.2.6/src# vim ae
ae.c         ae_epoll.c   ae_evport.c  ae.h         ae_kqueue.c  ae_select.c

vim ae_epoll.c

redis-网络模型c语言源码

#include <sys/epoll.h>

typedef struct aeApiState {
    int epfd;
    struct epoll_event *events;
} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;
    state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }
    state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
    if (state->epfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }
    anetCloexec(state->epfd);
    eventLoop->apidata = state;
    return 0;
}

// ....
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

// ...
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);
    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}


// state->epfd = epoll_create(1024); 核心调用native方法 
// if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
// retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,

epfd:epoll filename descripter

5.2,kafka

5.3,TIM架构

image-20220320144922332

参考链接

  1. 都问Netty,它真的这么香吗?
  2. win10 解决telnet不是内部或外部命令的方案
  3. 游戏编程干货! 如果这篇文章说不清epoll的本质,那就过来掐死我吧!
  4. netty入门到精通(RPC实现)
  5. IO模型之AIO代码及其实践详解
  6. Netty中的buffer和零拷贝
  7. netty——bytebuf的创建、内存分配与池化、组成、扩容规则、写入读取、内存回收、零拷贝
  8. 【Netty4】直接内存
  9. netty的零拷贝、架构设计、ByteBuf扩容机制详解
  10. netty源码调用栈-导图
  11. Java Reactor通信模型

标签:总结,netty,void,技术,System,内存,new,public,channel
来源: https://www.cnblogs.com/Mufasa/p/16032943.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有