Netty学习系列一

Netty学习系列

lyowish
回顾一下传统的HTTP服务器的原理 :
1、创建一个ServerSocket,监听并绑定一个端口
2、一系列客户端来请求这个端口
3、服务器使用Accept,获得一个来自客户端的Socket连接对象
4、启动一个新线程处理连接
 4.1、读Socket,得到字节流
 4.2、解码协议,得到Http请求对象
 4.3、处理Http请求,得到一个结果,封装成一个HttpResponse对象
 4.4、编码协议,将结果序列化字节流写Socket,将字节流发给客户端
5、继续循环步骤3

  HTTP服务器之所以称为HTTP服务器,是因为编码解码协议是HTTP协议,如果协议是Redis协议,那它就成了Redis服务器,如果协议是WebSocket,那它就成了WebSocket服务器,等等。使用Netty你就可以定制编解码协议,实现自己的特定协议的服务器

  NIO代表的一个词汇叫着IO多路复用Netty就是基于Java NIO技术封装的一套框架。为什么要封装呢?原生的Java NIO使用没那么方便。Netty把它封装后,提供一个易于操作的使用模式和接口,用户使用起来就更加容易了。

  为什么使用Netty?

  1. 使用JDK原生API比较复杂。
  2. 需要要掌握更多的基础知识,比如NIO涉及到的Reactor模式。
  3. 高可用的话,需要出路断路重连,半包读写,失败缓存等问题。
  4. JDK NIO的bug
  5. 对Netty来说,它的API简单,性能高而且社区活跃。

NIO基础了解

  在BIO模式下,Socket通信从头到尾都是阻塞的,这些线程只能等着,占用系统cpu资源,什么事也不做。

  NIO如何做到非阻塞呢?NIO使用事件驱动机制。可以用一个线程把accept,读写操作,请求处理逻辑都给干了。如果什么事都没有,它也不会死循环,它会将线程休眠起来,知道下一个事情来了再继续干活。

  NIO的特点:

  1. 一个线程可以处理多个通道,减少创建线程数量。
  2. 读写非阻塞,节约资源。没有可读/可写数据时,不会发生阻塞导致线程资源的浪费。

阻塞与非阻塞

阻塞:系统调用内核发现数据还没有准备好,就一直阻塞等待,不会去做其他事情。
非阻塞:系统调用内核发现数据还没有准备好,不会等待。如果数据已经准备好,也直接返回。

同步与异步

同步:应用程序直接参与IO读写的操作
异步:IO读写交给操作系统处理,应用程序只需要等待通知

Java Nio中主要包含三个概念。缓存区(Buffer),通道(channel),选择器(selector)

缓存区(Buffer)

  缓存区概念是对java原生数组对象的封装,它除了包含数组外,还包含四个描述缓冲区特征属性以及一组用来操作缓冲区的API。

四个属性
  容量:缓存区能够容纳的数据元素的最大数量。初始化后不能更改。
  上界:缓存区中第一个不能被读或者写的元素位置。或者说,缓存区内现存元素的上界。
  位置:缓存区内下一个将要被读或写的元素位置。在进行读写缓存区时,位置会自动更新。
  标记:一个备忘录。初始时为"未定义",调用mark时mark = position,调用reset时,position=mark。

四个属性关系:mark <= position <= limit <= capacity

写入数据
  position表示下一个要写入的坐标
读数据
  读数据前先调用flip方法,flip方法完成两件事
(1)将limit设置为当前的position值
(2)把position设置为0

Clear()方法能够把所有的状态变化设置为初始化时的值。
remaining方法是返回缓冲区中目前存储的元素个数。
hasRemaining()的含义是查询缓存区中是否还有元素。
compact()压缩方法是为了将读取了一部分的buffer,其剩下的部分整体移动到buffer的头部。
duplicate复制缓冲区,两个缓冲区对象实际上指向了同一个内部数组,但分别管理各自的属性。
lice缓冲区切片,缓冲区切片,将一个大缓冲区的一部分切出来,作为一个单独的缓冲区,但是它们公用同一个内部数组。切片从原缓冲区的position位置开始,至limit为止。

  直接缓冲区 DirectByteBuffer。使用的内存是直接调用了操作系统api分配的,绕过了JVM堆栈。直接缓冲区通过ByteBuffer.allocateDirect()方法创建,并可以调用isDirect()来查询一个缓冲区是否为直接缓冲区。

Channel

  通常来说NIO中的所有IO都是从Channel(通道)开始的。

  1. 从通道进行数据读取。创建一个缓存区(Buffer),然后请求通道数据到缓存区。
  2. 从通道进行数据写入。创建一个缓存区(Buffer),填充数据,然后请求通道将缓存区数据写入。

  与流的区别:
  1. 通道可以读也可以写。流是单向的(只能读或者写,所以之前我们用流进行IO操作的时候,需要分别创建一个输入流和一个输出流)。
  2. 通道可以异步读写。
  3. 通道总是基于缓存区(Buffer)来读写。

  Java NIO 中最重要的几个Channel的实现:
  1. FileChannel,用于文件的数据读写。
  2. DatagramChannel,用于UDP的数据读写。
  3. SocketChannel,用与TCP的数据读写,一般是客户端实现。
  4. ServerSocketChannel,允许我们监听TCP链接请求,每个请求会创建一个SocketChannel,一般是服务器实现。

  SocketChannel的使用:
  1. 通过SocketChannel链接到远程服务器。
  2. 创建读数据/写数据缓冲区对象来读取服务端数据或向服务端写入数据。
  3. 关闭SocketChannel。

public class WebClient {
    public static void main(String[] args) throws IOException {
        //1.通过SocketChannel的open()方法创建一个SocketChannel对象
        SocketChannel socketChannel = SocketChannel.open();
        //2.连接到远程服务器(连接此通道的socket)
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 3333));
        // 3.创建写数据缓存区对象
        ByteBuffer writeBuffer = ByteBuffer.allocate(128);
        writeBuffer.put("hello WebServer this is from WebClient".getBytes());
        writeBuffer.flip();
        socketChannel.write(writeBuffer);
        //创建读数据缓存区对象
        ByteBuffer readBuffer = ByteBuffer.allocate(128);
        socketChannel.read(readBuffer);
        //String 字符串常量,不可变;StringBuffer 字符串变量(线程安全),可变;StringBuilder 字符串变量(非线程安全),可变
        StringBuilder stringBuffer=new StringBuilder();
        //4.将Buffer从写模式变为可读模式
        readBuffer.flip();
        while (readBuffer.hasRemaining()) {
            stringBuffer.append((char) readBuffer.get());
        }
        System.out.println("从服务端接收到的数据:"+stringBuffer);

        socketChannel.close();
    }
}

  ServerSocketChannel的实现:
  1. 通过ServerSocketChannel 绑定ip地址和端口号。
  2. 通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据。
  3. 创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据。
  4. 关闭SocketChannel和ServerSocketChannel。

public class WebServer {
    public static void main(String args[]) throws IOException {
        try {
            //1.通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象,open方法的作用:打开套接字通道
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //2.通过ServerSocketChannel绑定ip地址和port(端口号)
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 3333));
            //通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
            SocketChannel socketChannel = ssc.accept();
            //3.创建写数据的缓存区对象
            ByteBuffer writeBuffer = ByteBuffer.allocate(128);
            writeBuffer.put("hello WebClient this is from WebServer".getBytes());
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
            //创建读数据的缓存区对象
            ByteBuffer readBuffer = ByteBuffer.allocate(128);
            //读取缓存区数据
            socketChannel.read(readBuffer);
            StringBuilder stringBuffer=new StringBuilder();
            //4.将Buffer从写模式变为可读模式
            readBuffer.flip();
            while (readBuffer.hasRemaining()) {
                stringBuffer.append((char) readBuffer.get());
            }
            System.out.println("从客户端接收到的数据:"+stringBuffer);
            socketChannel.close();
            ssc.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }}

  在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel

  1. transferFrom() :transferFrom方法把数据从通道源传输到FileChannel
  2. transferTo() :transferTo方法把FileChannel数据传输到另一个channel

Selector

  使用Selector来实现单线程控制多路非阻塞IO。Selector需要其他两种对象配合使用。即SelectionKey(选择key) 和 SelectableChannel(可选择的通道)。
在这里插入图片描述

  SelectableChannel是一类可以与Selector进行配合的通道,例如Socket相关通道以及Pipe产生的通道都属于SelectableChannel。这类通道可以将自己感兴趣的操作(例如read、write、accept和connect)注册到一个Selector上,并在Selector的控制下进行IO相关操作。

  Selector是一个控制器,它负责管理已注册的多个SelectableChannel,当这些通道的某些状态改变时,Selector会被唤醒(从select()方法的阻塞中),并对所有就绪的通道进行轮询操作。

  SelectionKey是一个用来记录SelectableChannelSelector之间关系的对象,它由SelectableChannelregister()方法返回,并存储在Selector的多个集合中。它不仅记录了两个对象的引用,还包含了SelectableChannel感兴趣的操作,即OP_READOP_WRITEOP_ACCEPTOP_CONNECT

  1. Register方法,SelectalbeChannel的register方法
SelectionKey register(Selector sel, int ops)
  1. Selector的三个集合
    (1)keys集合,存储了所有与Selector关联的SelectionKey对象;
    (2)selectedKeys集合,存储了在一次select()方法调用后,所有状态改变的通道关联的SelectionKey对象;
    (3)cancelledKeys集合,存储了一轮select()方法调用过程中,所有被取消但还未从keys中删除的SelectionKey对象。

  2. select 方法
      Selector类的select()方法是一个阻塞方法,它有两种形式:
      int select()。不带参数的方法会一直阻塞,直到至少有一个注册的通道状态改变,才会被唤醒。
      int select(long timeout),带有timeout参数的方法会一直阻塞,直到时间耗尽,或者有通道的状态改变。

  3. 轮询处理
      在一次select()方法返回后,应对selectedKeys集合中的所有SelectionKey对象进行轮询操作,并在操作完成后手动将SelectionKey对象从selectedKeys集合中删除。

在这里插入图片描述
服务端代码

public class SelectorServer {
    private static final int PORT = 1234;
    private static ByteBuffer buffer = ByteBuffer.allocate(1024);

    public static void main(String[] args) {
        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.bind(new InetSocketAddress(PORT));
            ssc.configureBlocking(false);
            //1.register()
            Selector selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("REGISTER CHANNEL , CHANNEL NUMBER IS:" + selector.keys().size());

            while (true) {
                //2.select()
                int n = selector.select();
                if (n == 0) {
                    continue;
                }
                //3.轮询SelectionKey
                Iterator<SelectionKey> iterator = (Iterator) selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    //如果满足Acceptable条件,则必定是一个ServerSocketChannel
                    if (key.isAcceptable()) {
                        ServerSocketChannel sscTemp = (ServerSocketChannel) key.channel();
                        //得到一个连接好的SocketChannel,并把它注册到Selector上,兴趣操作为READ
                        SocketChannel socketChannel = sscTemp.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("REGISTER CHANNEL , CHANNEL NUMBER IS:" + selector.keys().size());
                    }
                    //如果满足Readable条件,则必定是一个SocketChannel
                    if (key.isReadable()) {
                        //读取通道中的数据
                        SocketChannel channel = (SocketChannel) key.channel();
                        readFromChannel(channel);
                    }
                    //4.remove SelectionKey
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void readFromChannel(SocketChannel channel) {
        buffer.clear();
        try {
            while (channel.read(buffer) > 0) {
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                System.out.println("READ FROM CLIENT:" + new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }}

客户端代码

public class SelectorClient {
    static class Client extends Thread {
        private String name;
        private Random random = new Random(47);

        Client(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                SocketChannel channel = SocketChannel.open();
                channel.configureBlocking(false);
                channel.connect(new InetSocketAddress(1234));
                while (!channel.finishConnect()) {
                    TimeUnit.MILLISECONDS.sleep(100);
                }
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                for (int i = 0; i < 5; i++) {
                    TimeUnit.MILLISECONDS.sleep(100 * random.nextInt(10));
                    String str = "Message from " + name + ", number:" + i;
                    buffer.put(str.getBytes());
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        channel.write(buffer);
                    }
                    buffer.clear();
                }
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(new Client("Client-1"));
        executorService.submit(new Client("Client-2"));
        executorService.submit(new Client("Client-3"));
        executorService.shutdown();
    }}

Netty整体认识

  Netty是一个优秀的网络编程框架。

整体结构

  1. core核心层。提供底层网络交互的抽象和实现。事件模型,通用通信api,支持零拷贝的buffer等。
  2. protocol support 协议支持层。覆盖主流协议的编码,解码实现。
  3. transport service 传输服务层。提供了网络传输的定义和实现方法。

在这里插入图片描述

网络通信层

  封装了底层socket操作。它的职责是执行网络I/O操作,支持多种网络协议和I/O模型的连接操作。 支持提供了bootstrap,serverbootstrap,channel等组件。

  bootstrap,serverbootstrap分别负责客户端,服务端启动。

  channel是网络通信的载体,提供了与底层socket交互的能力。如register,bind,connect,read,write,flush等。Netty自己实现的channle是以JDK NIO Channel为基础的。

  Channel会有多种状态,如连接建立,连接注册,数据读写,连接销毁等。

事件调度层

  通过Reactor线程模型对个各类事件进行聚合处理。通过Selector主循环线程集成多种事件。

  事件调度层的核心组件是:EventLoopGroup,EventLooop。

  EventLoopGroup是Netty Reactor线程模型的具体实现方式。
在这里插入图片描述

单线程模型

  EventLoopGroup只包含一个EventLoop,Boss 和 Worker使用同一个EventLoopGroup。

多线程模型

  EventLoopGroup包含多个EventLoop,Boss 和 Worker使用同一个EventLoopGroup。

主从多线程模型

  EventLoopGroup 包含多个EventLoop,Boss是主Reactor,Worker是从Reactor。主Reactor负责新的网络连接Channel创建,然后Channel注册到从Reactor。

服务编排层

  负责组装各类服务,用以实现网络事件的动态编排和有序传播。

  服务编排层的核心组件有,ChannelPipeline,ChannelHandler,ChannelHandlerContext。

ChannelPipeline,ChannelHandlerContext,ChannelHandler

  ChannelPipeline负责组装各种ChannelHandler。实际数据的编解码以及加工处理操作由ChannelHandler完成。

  当I/O读写事件触发时,ChannelPipeline会一次调用ChannelHandler列表对Channel的数据进行拦截和处理。

  ChannelPipeline是线程安全的。每一个Channel会对应绑定一个新的ChannelPipeline。

  一个ChannelPipeline关联一个EventLoop,一个EventLoop仅会绑定一个线程。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  每个ChannelHandler绑定ChannelHandlerContext的作用是什么呢?
(1)ChannelHandleContext保存ChannelHandler上下文。
(2)实现ChannelHandler之间的交互
(3)包含ChannelHandler声明周期的所有事件,如Connet,bind,read,flush,write,close等

  每个ChannelHanler的通用的逻辑,如果没有ChannelContext抽象,会造成代码的重复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值