Netty原理与JavaNIO-学

1 Netty介绍与应用场景

1.1 前提条件

多线程编程,IO编程,网络编程,熟悉常用的设计模式

1.2 Netty的介绍

1、Netty是一个异步的,基于事件驱动的网络应用框架
在这里插入图片描述
2、Netty主要针对在TCP协议下,面向客户端的高并发应用,或者P to P 场景下的大量数据持续传输的因应用
在这里插入图片描述
3、Netty本质是一个NIO框架,所以要理解NIO

1.3 Netty的应用场景

  1. 分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架。往往作为基础通信组件被这些RPC框架使用。
  2. 阿里分布式服务框架Dubbo默认使用Netty作为基础通信组件,用于实现个进程节点之间的内部通信。
  3. 网络游戏,Netty提供了TCP/UDP,和HTTP,方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以通过Netty进行高性能的通信
  4. 大数据领域。经典的Hadoop的高性能通信和序列化组件(AVRO 实现数据共享)的RPC框架,默认采用Netty进行跨界点通信。基于Netty进行二次封装
  5. 开源项目。Akka、Flink(内存级的计算框架)、Spark

1.4 Netty的学习资料

在这里插入图片描述
Netty权威指南是针对 5 写的,原理性的东西写的还可以。实战可以学第一本。

2 IO模型

2.1 基本说明

2.1.1简单的理解

用什么样的通道进行数据的发送和接收

2.1.2 BIO

同步并阻塞 一个链接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理

会给服务器端造成压力,而且连接可能是空闲的,造成不必要的开销,同时还给阻塞
在这里插入图片描述

2.1.3 NIO

同步非阻塞
一个线程处理多个请求 ,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
一个线程维护多个连接

在这里插入图片描述
有效利用线程
在这里插入图片描述

2.1.4 AIO

异步非阻塞 JDK1.7引入的,没有得到广泛的应用

2.2 适用场景

  1. BIO适用于连接数目比较小且固定的架构,对服务器资源要求比较高
  2. NIO适用于连接数目多且连接比较短的架构,比如聊天服务器,弹幕系统,服务器间通讯等
  3. AIO方式用于连接数目多且连接比较长的架构,如相册服务器,充分调用操作系统参与并发操作

2.3 BIO基本介绍

传统的java io编程
blocking I/O 同步阻塞
可以通过线程池机制改善,实现多个客户连接服务器
在这里插入图片描述
BIO编程的流程

  1. 服务器端启动一个ServerSocket
  2. 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯
  3. 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待或者拒绝
  4. 如果有响应,客户端线程会等待请求结束后才继续执行

BIO示例

package com.zy.bio;

import javax.swing.text.EditorKit;
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 Exception {

        //线程池机制

        //思路
        //1. 创建一个线程池
        //2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)

        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

        //创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);

        System.out.println("服务器启动了");

        while (true) {

            System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
            //监听,等待客户端连接
            System.out.println("等待连接....");
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");

            //就创建一个线程,与之通讯(单独写一个方法)
            newCachedThreadPool.execute(new Runnable() {
                public void run() { //我们重写
                    //可以和客户端通讯
                    handler(socket);
                }
            });

        }


    }

    //编写一个handler方法,和客户端通讯
    public static void handler(Socket socket) {

        try {
            System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            //通过socket 获取输入流
            InputStream inputStream = socket.getInputStream();

            //循环的读取客户端发送的数据
            while (true) {

                System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());

                System.out.println("read....");
                int read =  inputStream.read(bytes);
                if(read != -1) {
                    System.out.println(new String(bytes, 0, read
                    )); //输出客户端发送的数据
                } else {
                    break;
                }
            }


        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println("关闭和client的连接");
            try {
                socket.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.4 NIO

2.4.1 NIO基本介绍

全程 non-blocking IO,是指JDK提供的新的API 是同步非阻塞的
NIO相关的类被放在java.nio包下
三大核心:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
等待
在这里插入图片描述
NIO是面向缓冲区编程的。数据读取到一个它稍后处理的缓冲区,需要时可以在缓冲区前后移动,增加了处理过程中的灵活性,实现非阻塞的机制。
NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是他仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞。
NIO是可以做到一个线程来处理多个操作的。假设有10000个请求过来,可以分配50或者100个线程来处理

2.4.2 Buffer的使用

简单说明

package com.zy.nio;

import java.nio.IntBuffer;

public class BasicBuffer {
    public static void main(String[] args) {

        //举例说明Buffer 的使用 (简单说明)
        //创建一个Buffer, 大小为 5, 即可以存放5个int
        IntBuffer intBuffer = IntBuffer.allocate(5);

        //向buffer 存放数据
//        intBuffer.put(10);
//        intBuffer.put(11);
//        intBuffer.put(12);
//        intBuffer.put(13);
//        intBuffer.put(14);
        for(int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put( i * 2);
        }

        //如何从buffer读取数据
        //将buffer转换,读写切换(!!!)
        /*
        public final Buffer flip() {
        limit = position; //读数据不能超过5
        position = 0;
        mark = -1;
        return this;
    }
         */
        intBuffer.flip();
        intBuffer.position(1);//1,2
        System.out.println(intBuffer.get());
        intBuffer.limit(3);
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

2.4.3 BIO与NIO的区别

  1. BIO以流的方式处理数据,而NIO以块的方式处理成数据
  2. BIO是阻塞的,NIO是非阻塞的
  3. BIO基于字节流和字符流进行操作,而NIO是基于通道和缓冲区进行操作,Selector用于监听多个通道的事件,可以实现一个线程监听多个客户端

2.4.4 NIO三大核心原理

在这里插入图片描述

  1. 每一个Channel都会对应一个Buffer
  2. Selector都会对应一个线程, 一个线程会对应多个Channel
  3. 改图反映了有三个Channel注册到该selector程序上
  4. 程序切换到那个channel是由事件决定的,Event是一个很重要的概念
  5. Selector会根据不同的事件,在各个通道上切换
  6. Buffer就是一个内存块,底层是由一个数组实现的
  7. 数据的读取通过Buffer,这个和BIO有本质的不同,BIO中要么是输入流,要么是输出流,不能双向,NIO的Buffer可以读也可以写,需要flip进行切换
  8. Channel也是双向的,可以反映底层操作系统的情况。比如Linux,底层的操作系统也是双向的

2.4.5 Buffer的具体介绍

本质是一个可以读写的内存块,理解成一个容器对象,提供了一组方法,可以轻松
地使用内存块。

在这里插入图片描述
Buffer源码分析
在这里插入图片描述

public abstract class Buffer {

    /**
     * The characteristics of Spliterators that traverse and split elements
     * maintained in Buffers.
     */
    static final int SPLITERATOR_CHARACTERISTICS =
        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;

    // Invariants: mark <= position <= limit <= capacity
    
    private int mark = -1;
    //位置,下一个要被读或写的元素的索引,每次读写缓冲区都会改变的元素,为下次读写做准备
    private int position = 0;
    //表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。极限是可以进行修改的
    private int limit;
    //容量,在缓冲区创建时被设定且不能改变
    private int capacity;
    public final Buffer flip() {
    	//当前limit换成pos,读不能超过这个值 
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

方法

	//设置新的 position
	public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw createPositionException(newPosition);
        if (mark > newPosition) mark = -1;
        position = newPosition;
        return this;
    }
    //设置新的limit,不能超过这个limit
    public final Buffer limit(int newLimit) {
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        limit = newLimit;
        if (position > newLimit) position = newLimit;
        if (mark > newLimit) mark = -1;
        return this;
    }
    //清除缓冲区,即将各个标记恢复到初始状态,但是数据并没有被真正的清除
    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
//这是intBuffer的数据成员
    final int[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;   
    //返回底层数组
    public final int[] array() {
        if (hb == null)
            throw new UnsupportedOperationException();
        if (isReadOnly)
            throw new ReadOnlyBufferException();
        return hb;
    }

ByteBuffer
ByteBuffer是最常用的,因为网络都是以字节为基本单位进行传输的,所以此最常用

	//创建直接缓冲区
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
    DirectByteBuffer(int cap) {                   // package-private

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    } 
    public abstract byte get(int index);
      
  public ByteBuffer put(int i, byte x) {

        unsafe.putByte(ix(checkIndex(i)), ((x)));
        return this;



    }

2.4.6 Channel 的具体介绍

类似于流,区别如下:

  1. 通道可以进行读写,流只能读或者写
  2. 通道可以实现异步读写数据
  3. 通道可以从缓冲区读写数据

public interface Channel extends Closeable {

    public boolean isOpen();

    public void close() throws IOException;

}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

	//把数据从通道读取到缓存区中
    public abstract int read(ByteBuffer dst) throws IOException;
	//把数据从缓冲区写入到通道中
    public abstract int write(ByteBuffer src) throws IOException;
    //从另一个通道读取数据到当前通道
    public abstract long transferFrom(ReadableByteChannel src,
                                      long position, long count)
        throws IOException;    
    //从当前通道写数据到另一个通道
    public abstract long transferTo(long position, long count,
                                    WritableByteChannel target)
        throws IOException;    
案例1

在这里插入图片描述

package com.atguigu.nio;

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileChannel01 {
    public static void main(String[] args) throws Exception{

        String str = "hello,尚硅谷";
        //创建一个输出流->channel
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");

        //通过 fileOutputStream 获取 对应的 FileChannel
        //这个 fileChannel 真实 类型是  FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();

        //创建一个缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //将 str 放入 byteBuffer
        byteBuffer.put(str.getBytes());


        //对byteBuffer 进行flip
        byteBuffer.flip();

        //将byteBuffer 数据写入到 fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close();

    }
}

在这里插入图片描述
在这里插入图片描述

案例2 本地文件读数据
package com.atguigu.nio;

import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileChannel02 {
    public static void main(String[] args) throws Exception {

        //创建文件的输入流
        File file = new File("d:\\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(file);

        //通过fileInputStream 获取对应的FileChannel -> 实际类型  FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();

        //创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());

        //将 通道的数据读入到Buffer
        fileChannel.read(byteBuffer);

        //将byteBuffer 的 字节数据 转成String
        System.out.println(new String(byteBuffer.array()));
        fileInputStream.close();

    }
}
案例3 使用一个Buffer完成文件的读取

使用FileChannel完成文件的拷贝
在这里插入图片描述

package com.atguigu.nio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileChannel03 {
    public static void main(String[] args) throws Exception {

        FileInputStream fileInputStream = new FileInputStream("1.txt");
        FileChannel fileChannel01 = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
        FileChannel fileChannel02 = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        while (true) { //循环读取

            //这里有一个重要的操作,一定不要忘了
            /*
             public final Buffer clear() {
                position = 0;
                limit = capacity;
                mark = -1;
                return this;
            }
             */
            byteBuffer.clear(); //清空buffer
            int read = fileChannel01.read(byteBuffer);
            System.out.println("read =" + read);
            if(read == -1) { //表示读完
                break;
            }
            //将buffer 中的数据写入到 fileChannel02 -- 2.txt
            byteBuffer.flip();
            fileChannel02.write(byteBuffer);
        }

        //关闭相关的流
        fileInputStream.close();
        fileOutputStream.close();
    }
}

方法2

public class NIOFileChannel04 {
    public static void main(String[] args)  throws Exception {

        //创建相关流
        FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");

        //获取各个流对应的filechannel
        FileChannel sourceCh = fileInputStream.getChannel();
        FileChannel destCh = fileOutputStream.getChannel();

        //使用transferForm完成拷贝
        destCh.transferFrom(sourceCh,0,sourceCh.size());
        //关闭相关通道和流
        sourceCh.close();
        destCh.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}

2.4.7 注意事项

  1. ByteBuffer put放入的是什么类型,get就应该使用相应的数据类型来取出
  2. 可以将一个普通的Buffer转变为一个只读的Buffer
public class ReadBufferExam {
   public static void main(String[] args) {
       ByteBuffer buffer = ByteBuffer.allocate(1000);
       for (int i = 0; i < 64; i++) {
           buffer.putInt(i);
       }
       buffer.flip();
       ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
       readOnlyBuffer.putInt(12);
       while (readOnlyBuffer.hasRemaining()){
           System.out.println(readOnlyBuffer.getInt());
       }
   }
}

在这里插入图片描述
3. NIO还提供了MapperByteBuffer,可以文件直接在内存中进行修改

 package com.atguigu.nio;

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/*
说明 MappedByteBuffer 可让文件直接在内存(堆外内存)修改, 操作系统不需要拷贝一次
 */
public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception {

        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        //获取对应的通道
        FileChannel channel = randomAccessFile.getChannel();

        /**
         * 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式
         * 参数2: 0 : 可以直接修改的起始位置
         * 参数3:  5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存
         * 可以直接修改的范围就是 0-5
         * 实际类型 DirectByteBuffer
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

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

        randomAccessFile.close();
        System.out.println("修改成功~~");



    }
}

4、前面我们讲的读写操作,都是通过一个Buffer完成的,NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即Scattering和Gathering
在这里插入图片描述

public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
        serverSocketChannel.socket().bind(inetSocketAddress);

        int max_length=8;
        ByteBuffer[] byteBuffers=new ByteBuffer[2];
        byteBuffers[0]=ByteBuffer.allocate(5);
        byteBuffers[1]=ByteBuffer.allocate(3);
        while (true){
            SocketChannel socketChannel = serverSocketChannel.accept();
            long l=0;
            while (l<max_length) {
                long read = socketChannel.read(byteBuffers);
                l+=read;
                Arrays.asList(byteBuffers).stream().map(byteBuffer -> "position:"+byteBuffer.position()+" " +
                        "limit:"+byteBuffer.limit()).forEach(System.out::println);
            }
            Arrays.asList(byteBuffers).stream().forEach(byteBuffer -> byteBuffer.flip());
            long byteWrite=0;
            while(byteWrite<max_length){
                long write = socketChannel.write(byteBuffers);
                byteWrite+=write;
            }
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
            System.out.println("byteRead="+l+" byteWrite="+byteWrite+" length="+max_length);
        }
    }
}

2.4.8 selector

  1. java的NIO,用非阻塞的IO方式,可以用一个线程,处理多个客户端连接,就会使用到selector
  2. selector能够检测多个注册的通道上是否有事件发生。多个channel以事件的形式注册到selector上
    在这里插入图片描述
  3. 只有在通道上真正有事件发生时,才会进行读写,就大大减少了系统开销,并且不必为每一个连接都创建一个线程
  4. 避免了多线程之间的上下文切换导致的开销

在这里插入图片描述

  1. Netty的IO线程NioEventLoop聚合了Selector,可以同时并发处理成百上千个客户端连接
  2. 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
  3. 线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
  4. 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起
  5. 一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了bio一连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升
源码

public abstract class Selector implements Closeable {

    /**
     * Initializes a new instance of this class.
     */
    protected Selector() { }
    //得到一个选择器的对象
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    /**
     * Opens a selector.
     */
    public abstract boolean isOpen();

    /**
     * Returns the provider that created this channel.
     *
     * @return  The provider that created this channel
     */
    public abstract SelectorProvider provider();

    /**
     * Returns this selector's key set.
     
     */
    public abstract Set<SelectionKey> keys();

    /**
     * Returns this selector's selected-key set.
     * 从内部集合中得到所有的SelectionKey
     */
    public abstract Set<SelectionKey> selectedKeys();

    /**
     * Selects a set of keys whose corresponding channels are ready for I/O
     * operations.
     *
     */
    public abstract int selectNow() throws IOException;

    /**
     * Selects a set of keys whose corresponding channels are ready for I/O
     * operations.
     *
    
     */
     //监控所有非注册通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间,即使没有监听到任何事件发生,也会返回
    public abstract int select(long timeout)
        throws IOException;

    /**
     * Selects a set of keys whose corresponding channels are ready for I/O
     * operations.
     *
    
     */
    public abstract int select() throws IOException;

    /**
     * Causes the first selection operation that has not yet returned to return
     * immediately.
     *
    
     */
    public abstract Selector wakeup();

    /**
     * Closes this selector.
     *
    
     */
    public abstract void close() throws IOException;

}


public abstract class SelectorImpl extends AbstractSelector {
    protected Set<SelectionKey> selectedKeys = new HashSet();
    protected HashSet<SelectionKey> keys = new HashSet();
    private Set<SelectionKey> publicKeys;
    private Set<SelectionKey> publicSelectedKeys;

在这里插入图片描述

SelectionKey 与其他的关系 *

在这里插入图片描述

  1. 当客户端连接时,会通过ServerSocketChannel得到SocketChannel
  2. 将SocketChannel注册到Selector上,一个selector可以注册多个SocketChannel
     public final SelectionKey register(Selector sel, int ops)
        throws ClosedChannelException
    {
        return register(sel, ops, null);
    }
    //ops 对应的操作
    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;
  1. 注册后返回一个SelectionKey,会和该Selector进行关联,以Set的形式
  2. Selector监听使用Select方法,返回有事件发生的通道个数
    public int select() throws IOException {
        return this.select(0L);
    }
  1. 进一步得到各个SelectionKey
    public abstract Set<SelectionKey> selectedKeys();
  1. 再通过SelectionKey反向得到各个channel,通过channel方法
    public abstract SelectableChannel channel();
  1. 可以通过得到的Channel,进行业务的处理

2.4.9 NIO快速入门

编写一个入门案例,实现服务器端和客户端之间的数据简单通讯

public class NIOServer {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        Selector selector = Selector.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            int i = selector.select(1000);
            if(i==0){
                System.out.println("没有连接,去做别的事情了");
                continue;
            }
            //返回的是有事件发生的channel对应的selectedKeys
            Set<SelectionKey> selectionKeys = selector.selectedKeys();\
            //这个是selector中所有的注册进来的channel的selectionKey
            Set<SelectionKey> keys = selector.keys();
            System.out.println("有多少个key="+selectionKeys.size());
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                SelectionKey selectionKey = keyIterator.next();
                if (selectionKey.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功"+socketChannel.hashCode());
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                else if(selectionKey.isReadable()){
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    socketChannel.read(buffer);
                    System.out.println("内容为"+new String(buffer.array()));
                }
            }
            keyIterator.remove();
        }
    }
}

public class NIOClient {
    public static void main(String[] args) throws Exception{
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        InetSocketAddress inetSocketAddress = new InetSocketAddress(6666);
        if (!socketChannel.connect(inetSocketAddress)){
            while (!socketChannel.finishConnect()) {
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
            }
        }
        String msg="hello,浙江大学";
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        socketChannel.write(buffer);
        System.in.read();
    }
}

2.4.10 相关API

selectionKey

在这里插入图片描述

	//得到与之关联的Selector对象
    public abstract Selector selector();
    //得到与之关联的共享数据
    public final Object attachment() {
        return attachment;
    }
    //得到与之关联的通道
    public abstract SelectableChannel channel();
    //设置或改变监听事件
    public abstract int interestOps();
    //是否可以被接受等
    public final boolean isAcceptable() {
        return (readyOps() & OP_ACCEPT) != 0;
    }
SocketChannel

主要负责读写等功能


public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
{
//返回一个socketChannel实例
public static SocketChannel open() throws IOException {
        return SelectorProvider.provider().openSocketChannel();
    }
//绑定一个服务器的地址
public abstract SocketChannel bind(SocketAddress local)
        throws IOException;
//连接服务器
public abstract boolean connect(SocketAddress remote) throws IOException;
//将数据写入到buffer中
public abstract long read(ByteBuffer[] dsts, int offset, int length)
//从buffer读出数据
public abstract long write(ByteBuffer[] srcs, int offset, int length)
        throws IOException;
ServerSocketChannel

主要负责连接等功能

public abstract class ServerSocketChannel
    extends AbstractSelectableChannel
    implements NetworkChannel
{
//打开一份channel
public static ServerSocketChannel open() throws IOException {
        return SelectorProvider.provider().openServerSocketChannel();
    }
//绑定服务器的地址
public final ServerSocketChannel bind(SocketAddress local)
        throws IOException
    {
        return bind(local, 0);
    }

3 群聊系统与零拷贝

3.1 系统代码

package com.zy.nio.groupchat;

import com.sun.org.apache.bcel.internal.generic.Select;

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

public class NIOServer {
    private ServerSocketChannel listener = null;
    private Selector selector;
    private int port=6667;

    public NIOServer() {
        try {
            listener = ServerSocketChannel.open();
            selector=Selector.open();
            listener.socket().bind(new InetSocketAddress(port));
            listener.configureBlocking(false);
            listener.register(selector, SelectionKey.OP_ACCEPT);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void listen(){
        System.out.println("监听线程: " + Thread.currentThread().getName());
        while (true){
            try {
                int count = selector.select(2000);
                if (count > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                    while (keyIterator.hasNext()){
                        SelectionKey selectionKey = keyIterator.next();
                        if (selectionKey.isAcceptable()){
                            SocketChannel socketChannel = listener.accept();
                            System.out.println(socketChannel.getRemoteAddress()+"上线了!");
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector,SelectionKey.OP_READ);
                        }
                        else if(selectionKey.isReadable()){
                            readData(selectionKey);
                        }
                    }
                    keyIterator.remove();
                }else {
//                    System.out.println("等待....");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void readData(SelectionKey selectionKey) {
        SocketChannel socketChannel=null;
        try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            socketChannel = (SocketChannel) selectionKey.channel();
            int count = socketChannel.read(buffer);
            if(count>0){
                String msg=new String(buffer.array());
                System.out.println("from 客户端" + msg);
                sendMsgToOthers(msg,socketChannel);
            }
        }catch (Exception e){
            try {
                System.out.println(socketChannel.getRemoteAddress() + " 离线了..");
                //取消注册
                selectionKey.cancel();
                //关闭通道
                socketChannel.close();
            }catch (IOException e2) {
                e2.printStackTrace();;
            }
        }

    }

    private void sendMsgToOthers(String msg, SocketChannel self) {
        System.out.println("服务器转发消息中...");
        System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
        Set<SelectionKey> keys = selector.keys();
//        Iterator<SelectionKey> keyIterator = keys.iterator();
//        while (keyIterator.hasNext()){
//            SelectionKey selectionKey = keyIterator.next();
//            Channel channel = selectionKey.channel();
//            if(channel==self) continue;
//            if (channel instanceof SocketChannel) {
//                if (selectionKey.isReadable()) {
//                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//                    try {
//                        SocketChannel socketChannel = (SocketChannel) channel;
//                        socketChannel.write(buffer);
//                    } catch (Exception e) {
//                        e.printStackTrace();
//                    }
//                }
//            }
//        }
        for (SelectionKey selectionKey : keys) {
            Channel channel = selectionKey.channel();
            if(channel==self) continue;
            if (channel instanceof SocketChannel) {
                if (selectionKey.isReadable()) {
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                    try {
                        SocketChannel socketChannel = (SocketChannel) channel;
                        socketChannel.write(buffer);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        NIOServer nioServer = new NIOServer();
        nioServer.listen();
    }
}
package com.zy.nio.groupchat;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
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;
import java.util.Set;

public class NIOClient {
    private SocketChannel socketChannel;
    private Selector selector;
    private String url="127.0.0.1";
    private int port=6667;
    private String userName;

    public NIOClient() {
        try {
            socketChannel = SocketChannel.open(new InetSocketAddress(url, port));
            socketChannel.configureBlocking(false);
            selector=Selector.open();
            socketChannel.register(selector, SelectionKey.OP_READ);
            userName=socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(userName + " is ok...");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void sendInfo(String msg){
        try {
            msg=userName+" 说:"+msg;
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
            socketChannel.write(buffer);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public void readInfo(){
        try {
            int count = selector.select();
            if (count>0){
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                while (keyIterator.hasNext()){
                    SelectionKey selectionKey = keyIterator.next();
                    if (selectionKey.isReadable()){
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int read = socketChannel.read(byteBuffer);
                        if (read>0){
                            System.out.println(new String(byteBuffer.array()).trim());
                        }
                    }
                }
                keyIterator.remove();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        NIOClient nioClient = new NIOClient();
        new Thread(){
            @Override
            public void run() {
                while (true){
                    nioClient.readInfo();
                    try {
                        Thread.currentThread().sleep(3000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }.start();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
            String s = scanner.nextLine();
            nioClient.sendInfo(s);
        }
    }
}

在这里插入图片描述
在这里插入图片描述

3.2 零拷贝

  1. 零拷贝是网络编程的关键
  2. 常用的零拷贝有mmap(内存映射)和sendFIie
  3. NIO如何使用零拷贝

传统IO

DMA:Direct Memery Access 直接内存拷贝,不使用CPU

mmap

使用内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核控件到用户空间的拷贝次数
在这里插入图片描述
三次拷贝,三次空间的切换

sendFile

Linux2.1版本提供了sendFile函数,其基本远离如下:数据根本不需要经过用户态,直接从内核缓冲区进入到socketBuffer,同时,由于和用户状态无关,就减少了一次上下文的切换
在这里插入图片描述
protocol engine :协议栈
零拷贝:是从OS角度看的,是指没有CPU拷贝,所以没有实现真正的零拷贝

Linux2.4 零拷贝

2.4中,做了一些修改,避免了从内核缓冲区拷贝到SocketBuffer的操作,直接拷贝到了协议栈,从而再次减少了数据拷贝
还是有一次cpu拷贝,kernel buffer -> socket buffer ,但是拷贝的信息很少,比如length,offset,消耗低,可以忽略不计
在这里插入图片描述

零拷贝理解

  1. 是从操作系统的角度来说的,因为内核缓冲区之间,没有数据时重复的
  2. 不仅带来了更少的数据复制 ,还带来其它的性能优势,更少的上下文切换,更少的CPU缓存伪共享,以及无CPU校验和计算

在这里插入图片描述

3.3 零拷贝案例

在这里插入图片描述


public class NewIOServer {
    public static void main(String[] args) throws Exception {

        InetSocketAddress address = new InetSocketAddress(7001);

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        ServerSocket serverSocket = serverSocketChannel.socket();

        serverSocket.bind(address);

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

        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();

            int readcount = 0;
            while (-1 != readcount) {
                try {

                    readcount = socketChannel.read(byteBuffer);

                }catch (Exception ex) {
                   // ex.printStackTrace();
                    break;
                }
                //
                byteBuffer.rewind(); //倒带 position = 0 mark 作废
            }
        }
    }
}
public class NewIOClient {
    public static void main(String[] args) throws Exception {

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 7001));
        String filename = "protoc-3.6.1-win32.zip";

        //得到一个文件channel
        FileChannel fileChannel = new FileInputStream(filename).getChannel();

        //准备发送
        long startTime = System.currentTimeMillis();

        //在linux下一个transferTo 方法就可以完成传输
        //在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件, 而且要主要
        //传输时的位置 =》 课后思考...
        //transferTo 底层使用到零拷贝
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);

        System.out.println("发送的总的字节数 =" + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));

        //关闭
        fileChannel.close();

    }
}

零拷贝,linux支持,直接从文件的缓存发送到了目标的通道
在这里插入图片描述

3.4 Java AIO介绍

  1. 进行IO编程时,常用到两种模式:Reactor和Proactor
  2. 异步不阻塞IO。AIO引入了异步通道的概念,采用了Proactor模式,简化了程序编写。有效的请求才启动线程,他的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时长较长的应用
  3. 目前AIO还没有广泛应用,Netty也是基于NIO

在这里插入图片描述
在这里插入图片描述

4 Netty介绍

4.1 Netty简介

原生IO问题
在这里插入图片描述

是一个异步的基于事件驱动的网络应用框架
在这里插入图片描述

  1. Netty是JBOSS提供的一个Java开源框架
  2. 可以帮助你快速、简单的开发出一个网络应用,相当于简化和流程化了NIO的开发过程
  3. 是目前最流行的NIO框架,在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用。例如ES、Dubbo

在这里插入图片描述
在这里插入图片描述

4.2 线程模型概述

Netty的高性能架构设计
线程模型的基本介绍

  1. 不同的线程模型,对程序的性能有很大影响,为了搞清Netty线程模型,我们来讲解一下各个线程模型

  2. 目前存在的线程模型有:
    传统阻塞I/O服务模型、Reactor模型

  3. 根据Reactor的数量和处理资源池线程的数量不同,有三种典型的实现:
    单Reactor单线程、单Reactor多线程、主从Reactor多线程

  4. Netty线程模型(Netty主要基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程模型有多个Reactor)

4.2.1 传统阻塞IO模型

在这里插入图片描述
黄色的框表示的是对象,蓝色的框表示的是线程,白色的框表示的是API
模型的特点

  1. 每个连接都需要独立的线程完成数据的输入,业务处理,数据返回
  2. 采用阻塞的IO模式获取输入的数据

问题分析

  1. 当并发数很大,就会创建大量的线程,占用很大的系统资源
  2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费

4.2.2 Reactor模式

对应的叫法:反应器模式、分发者模式(Dispatcher)、通知者模式

  1. 基于IO复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
  2. 基于线程池复用线程资源;不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。

在这里插入图片描述

Reactor模式

  1. 通过一个或多个输入同时传递给服务处理器的一种模式,是基于事件驱动的
  2. 服务器端的程序处理传入的多个请求并将他们同步分派到相应的处理线程,因此Reactor模式也叫Dispatcher模式,即分发者模式
  3. Reactor使用了IO多路复用监听事件,收到事件后,分发给某个线程(进程),这点就是网路服务高并发处理的关键

核心组成

在这里插入图片描述

  1. Reactor是一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。他就像公司的电话接线员,他接听来自客户的电话并将线路转移到适当的联系人
  2. Hadnlers:处理程序执行IO事件要完成的实际事件,类似于客户想要阈值交谈的公司中的实际官员。Reactor通过调度适当的处理程序来相应IO事件,处理程序执行非阻塞操作

三种典型的实现:

  1. 单Reactor单线程
  2. 单Reactor多线程
  3. 主从Reactor

4.2.3 单Reactor单线程

群聊系统就是一个示例。
对比群聊的异同点如下:

  1. Select是前面IO复用模型介绍的标准网络编程API,可以实心应用程序通过一个阻塞对象监听多路连接请求。
  2. Reactor对象通过Select监控客户端请求事件,收到事件后通过Dispatch进行分发
  3. 如果是建立连接请求事件,则由Acceptor通过Accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理
  4. 如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应
  5. Handler会完成Read->业务处理->Send的完整业务流程

服务器端用一个线程通过多路复用搞定所有的IO操作,包括连接,读,写等,编码简单,但是如果客户端连接数量较多的话,将无法支撑,群聊的NIO案例就属于这种类型

4.2.4单Reactor多线程

  1. Reactor对象通过select监控客户端请求事件,收到事件后,通过dispatch进行分发
  2. 如果建立连接请求,则由Reactor通过accept处理连接请求,然后创建一个Handler对象处理连接完成后的各种事件
  3. 如果不是连接请求,则由reactor分发调用连接对应的handler来处理
  4. handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池里面的某个线程处理业务
  5. worker线程池会分配独立线程完成真正的业务,并将结果返回给handler
  6. handler收到响应后,通过send返回给client

优缺点:

  1. 可以充分地利用多核CPU的处理能力
  2. 多线程数据共享和访问会比较复杂。Reactor处理了所有的事件的监听和响应。在单线程运行的,在高并发应用场景容易出现性能瓶颈。

在这里插入图片描述

4.2.5主从Reactor多线程模式

在这里插入图片描述

  1. Reactor主线程MainReactor通过select监听连接事件,收到事件后,通过Acceptor处理连接事件
  2. 当Acceptor处理连接事件后,MainReactor将连接分配给SubReactor
  3. subReactor将连接加入到连接队列进行监听,并创建handler进行各种事件的处理
  4. 当有新的事件发生时,subReactor就会调用对应的handler进行处理
  5. handler通过read读取数据,分发给后面的worker线程处理
  6. worker线程池分配独立的worker线程进行业务处理,并返回结果
  7. handler收到相应的结果后,再通过send将结果返回给client
  8. Reactor主线程可以响应多个Reactor子线程,即MainReactor可以关联多个SubReactor

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3 Netty模型

4.3.1简单版

在这里插入图片描述

  1. BossGroup线程维护selector,只关注Accept
  2. 当接收到Accept事件后,获取到对应的SocketChannel,进一步封装成NIOSockerChannel,并注册到Worker的Selector中
  3. 当worker线程监听到有自己感兴趣的事件后,就进行处理(由handler),handler事先已经加入了通道中

4.3.2 进阶版

在这里插入图片描述
在这里插入图片描述

4.3.3完整模型

在这里插入图片描述

  1. Netty抽象出两组线程池。BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络的读写

  2. BossGroup和WorkerGroup类型都是NioEventLoopGroup

  3. NIOEventLoopGroup相当于一个事件循环组,这个组中包含有多个循环,每一个事件循环是NIOEventLoop

  4. NIOEventLoop表示的是一个不断循环的执行处理任务的线程,每个NIOEventLoop都有一个Selector,用于监听绑定在其上的socket 的网络通讯

  5. NIOEventLoopGroup可以含有多个线程,即可以含有多个NIOEventLoop

  6. 每个BossGroup 的NIOEventLoop的执行步骤有三步:
    (1)轮训accept事件
    (2)处理accept事件,与client建立连接,生成NIOSocketChannel,并将其注册到某个wokrer的NIOEventLoop的seletor上
    (3)处理任务队列的任务,即allTasks

  7. 每个Worker 上的NIOEventLoop的执行步骤有三步:
    (1)轮询read和write事件
    (2)处理io事件,即read和write事件,在对应的NIOSocketChannel处理
    (3)处理任务队列的任务,即alltasks

  8. 每一个worker的NIOEventLoop处理业务时,会使用pipeline,pipeline中包含了channel,即通过管道可以获取到对应的通道,管道中维护了很多的处理器

4.4 Netty入门案例

4.4.1代码

NettyServer

public class NettyServer {
    public static void main(String[] args) throws Exception {


        //创建BossGroup 和 WorkerGroup
        //说明
        //1. 创建两个线程组 bossGroup 和 workerGroup
        //2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
        //3. 两个都是无限循环
        //4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
        //   默认实际 cpu核数 * 2
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8



        try {
            //创建服务器端的启动对象,配置参数
            ServerBootstrap bootstrap = new ServerBootstrap();

            //使用链式编程来进行设置
            bootstrap.group(bossGroup, workerGroup) //设置两个线程组
                    .channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
                    .option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
//                    .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
                    .childHandler(  new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
                        //给pipeline 设置处理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    }); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器

            System.out.println(".....服务器 is ready...");

            //绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
            //启动服务器(并绑定端口)
            ChannelFuture cf = bootstrap.bind(6668).sync();

            //给cf 注册监听器,监控我们关心的事件

            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (cf.isSuccess()) {
                        System.out.println("监听端口 6668 成功");
                    } else {
                        System.out.println("监听端口 6668 失败");
                    }
                }
            });


            //对关闭通道进行监听
            cf.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

NettyServerHandler

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    //读取数据实际(这里我们可以读取客户端发送的消息)
    /*
    1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
    2. Object msg: 就是客户端发送的数据 默认Object
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

/*

        //比如这里我们有一个非常耗时长的业务-> 异步执行 -> 提交该channel 对应的
        //NIOEventLoop 的 taskQueue中,

        //解决方案1 用户程序自定义的普通任务

        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
                    System.out.println("channel code=" + ctx.channel().hashCode());
                } catch (Exception ex) {
                    System.out.println("发生异常" + ex.getMessage());
                }
            }
        });

        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵3", CharsetUtil.UTF_8));
                    System.out.println("channel code=" + ctx.channel().hashCode());
                } catch (Exception ex) {
                    System.out.println("发生异常" + ex.getMessage());
                }
            }
        });

        //解决方案2 : 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中

        ctx.channel().eventLoop().schedule(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵4", CharsetUtil.UTF_8));
                    System.out.println("channel code=" + ctx.channel().hashCode());
                } catch (Exception ex) {
                    System.out.println("发生异常" + ex.getMessage());
                }
            }
        }, 5, TimeUnit.SECONDS);



        System.out.println("go on ...");*/


        System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
        System.out.println("server ctx =" + ctx);
        System.out.println("看看channel 和 pipeline的关系");
        Channel channel = ctx.channel();
        ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接, 出站入站


        //将 msg 转成一个 ByteBuf
        //ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客户端地址:" + channel.remoteAddress());
    }

    //数据读取完毕
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {

        //writeAndFlush 是 write + flush
        //将数据写入到缓存,并刷新
        //一般讲,我们对这个发送的数据进行编码
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
    }

    //处理异常, 一般是需要关闭通道

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

NettyClient

public class NettyClient {
    public static void main(String[] args) throws Exception {

        //客户端需要一个事件循环组
        EventLoopGroup group = new NioEventLoopGroup();


        try {
            //创建客户端启动对象
            //注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
            Bootstrap bootstrap = new Bootstrap();

            //设置相关参数
            bootstrap.group(group) //设置线程组
                    .channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
                        }
                    });

            System.out.println("客户端 ok..");

            //启动客户端去连接服务器端
            //关于 ChannelFuture 要分析,涉及到netty的异步模型
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            //给关闭通道进行监听
            channelFuture.channel().closeFuture().sync();
        }finally {

            group.shutdownGracefully();

        }
    }
}

NettyClientHandler

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    //当通道就绪就会触发该方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client " + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
    }

    //当通道有读取事件时,会触发
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf buf = (ByteBuf) msg;
        System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
    }

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

4.4.2任务队列

任务队列中的Task有3种典型使用场景

  1. 用户自定义的普通任务
  2. 用户自定义的定时任务
  3. 非当前Reactor线程调用Channel的各种方法

在这里插入图片描述
在这里插入图片描述

4.5 异步模型

  1. 异步的概念与同步相对。当一个异步的过程调用发生后,调用者不能立刻获得结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
  2. Netty中的IO操作是异步的,包括Bind、Write、Connect等操作会简单的返回一个ChannelFuture
  3. 调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果
  4. Netty的异步模型是建立在future和callBack机制上的,callback就是回调,重点是Future,它的核心思想是假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个future,后续可以通过future去监控方法fun的处理过程

Future说明:

  1. 表示异步的执行结果,可以通过它提供的方法来检测执行是否完成。比如检索计算等。
  2. ChannelFuture是一个接口,我们可以添加监听器,当监听的事件发生时,就会通知到监听器

工作原理说明

  1. 在使用netty进行编程时,拦截操作和转换出入栈数据只需要您提供callback或利用future即可。这使得链式操作简单、高效,并有利于编写可重用的、通用的代码
  2. netty框架的目的就是让你的业务逻辑从网络基础应用中分离出来,解脱出来

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.6HTTP服务

public class TestServer {
    public static void main(String[] args) throws Exception {

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new TestServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(6668).sync();
            
            channelFuture.channel().closeFuture().sync();

        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
/*
说明
 - SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter
 - HttpObject 客户端和服务器端相互通讯的数据被封装成 HttpObject


 */
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {


    //channelRead0 读取客户端数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {


        System.out.println("对应的channel=" + ctx.channel() + " pipeline=" + ctx
        .pipeline() + " 通过pipeline获取channel" + ctx.pipeline().channel());

        System.out.println("当前ctx的handler=" + ctx.handler());

        //判断 msg 是不是 httprequest请求
        if(msg instanceof HttpRequest) {

            System.out.println("ctx 类型="+ctx.getClass());

            System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode());

            System.out.println("msg 类型=" + msg.getClass());
            System.out.println("客户端地址" + ctx.channel().remoteAddress());

            //获取到
            HttpRequest httpRequest = (HttpRequest) msg;
            //获取uri, 过滤指定的资源
            URI uri = new URI(httpRequest.uri());
            if("/favicon.ico".equals(uri.getPath())) {
                System.out.println("请求了 favicon.ico, 不做响应");
                return;
            }
            //回复信息给浏览器 [http协议]

            ByteBuf content = Unpooled.copiedBuffer("hello, 我是服务器", CharsetUtil.UTF_8);

            //构造一个http的相应,即 httpresponse
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);

            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());

            //将构建好 response返回
            ctx.writeAndFlush(response);

        }
    }
}
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {

        //向管道加入处理器

        //得到管道
        ChannelPipeline pipeline = ch.pipeline();

        //加入一个netty 提供的httpServerCodec codec =>[coder - decoder]
        //HttpServerCodec 说明
        //1. HttpServerCodec 是netty 提供的处理http的 编-解码器
        pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
        //2. 增加一个自定义的handler
        pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
        System.out.println("ok~~~~");
    }
}

4.7Netty核心模块梳理

  • NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由IO线程NIOEventLoop负责

  • NIOEventLoopGroup下包含多个NIOEventLoop

  • 每个NIOEventLoop下包含一个selector,一个taskQueue

  • 每个NIOEventLoop的selector可以注册多个NIOChannel

  • 每个NIOChannel都绑定在唯一的NIOEventLoop上

  • 每个NIOChannel上都绑定一个自己的ChannelPipeline

  • Netty中的IO操作都是异步的,包括bind,write,connect等操作会简单的返回一个ChannelFuture

  • 调用者并不能立即得到结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果

  • BootStrap的意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个netty程序,串联各个组件,netty中bootstrap是客户端程序的启动引导类,ServerBootStrap是服务器端启动引导类

  • 在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.7.2 Pipeline

  1. ChannelPipeline是一个Handler的集合,他负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿netty的链。(也可以这样理解,ChannelPipeline是一个保存ChannelHandler的List,用于处理或拦截Channel的入栈事件和出栈操作)
  2. ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及channel中各个channelHandler如何相互交互。
  3. 在netty中每个channel都有且仅有一个channelpipeline与之对应,他们的组成关系如下:

在这里插入图片描述
channel包含了channelpipeline,而channelpipeline中又维护了一个由ChannelHandlerContext组成的双向链表,并且每个channelHandlerContext中又关联了一个ChannelHandler
入栈事件和出栈事件在一个双向链表中,入栈事件会从链表head往后传递到最后一个入栈的handler,出栈事件会从链表tail往前传递到最前一个出栈的handler,两种类型的handler互不干扰
在这里插入图片描述
在这里插入图片描述

4.8ChannelHandlerContext

  1. 保存channel相关的所有上下文信息,同时关联一个ChannelHandler对象
  2. 绑定了相应的pipeline和channel的信息,方便对channelHandler进行调用

在这里插入图片描述

4.9ChannelOption

在这里插入图片描述

4.10EventLoopGroup

  1. 是一组EventLoop的抽象。netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护一个Selector
  2. EventLoopGroup维提供next接口,可以从组里面按照一定规则获取其中一个EventLoop进行处理。
  3. 通常一个服务端口即一个 ServerSocketChannel对应一个Selector 和一个EventLoop线程。BossEventLoop 负责接收客户端的连接并将 SocketChannel 交给 WorkerEventLoopGroup 来进行 IO 处理,如下图所示

在这里插入图片描述

  • BossEventLoopGroup 通常是一个单线程的 EventLoop,EventLoop 维护着一个注册了ServerSocketChannel 的 Selector 实例BossEventLoop 不断轮询 Selector 将连接事件分离出来
  • 通常是 OP_ACCEPT 事件,然后将接收到的 SocketChannel 交给 WorkerEventLoopGroup
  • WorkerEventLoopGroup 会由 next 选择其中一个 EventLoop来将这个 SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理

4.11 Unpooled类

  1. netty提供一个专门用来操作缓冲区的工具类
  2. 在这里插入图片描述

在这里插入图片描述

public class NettyByteBuf02 {
    public static void main(String[] args) {

        //创建ByteBuf
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", Charset.forName("utf-8"));

        //使用相关的方法
        if(byteBuf.hasArray()) { // true

            byte[] content = byteBuf.array();

            //将 content 转成字符串
            System.out.println(new String(content, Charset.forName("utf-8")));

            System.out.println("byteBuf=" + byteBuf);

            System.out.println(byteBuf.arrayOffset()); // 0
            System.out.println(byteBuf.readerIndex()); // 0
            System.out.println(byteBuf.writerIndex()); // 12
            System.out.println(byteBuf.capacity()); // 36

            //System.out.println(byteBuf.readByte()); //
            System.out.println(byteBuf.getByte(0)); // 104

            int len = byteBuf.readableBytes(); //可读的字节数  12
            System.out.println("len=" + len);

            //使用for取出各个字节
            for(int i = 0; i < len; i++) {
                System.out.println((char) byteBuf.getByte(i));
            }

            //按照某个范围读取
            System.out.println(byteBuf.getCharSequence(0, 4, Charset.forName("utf-8")));
            System.out.println(byteBuf.getCharSequence(4, 6, Charset.forName("utf-8")));
        }
    }
}

5.Netty群聊系统

public class GroupChatServer {

    private int port; //监听端口


    public GroupChatServer(int port) {
        this.port = port;
    }

    //编写run方法,处理客户端的请求
    public void run() throws  Exception{

        //创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop

        try {
            ServerBootstrap b = new ServerBootstrap();

            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {

                            //获取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //向pipeline加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
                            //向pipeline加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            //加入自己的业务处理handler
                            pipeline.addLast(new GroupChatServerHandler());

                        }
                    });

            System.out.println("netty 服务器启动");
            ChannelFuture channelFuture = b.bind(port).sync();

            //监听关闭
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws Exception {
        new GroupChatServer(7000).run();
    }
}

public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {

    //public static List<Channel> channels = new ArrayList<Channel>();

    //使用一个hashmap 管理
    //public static Map<String, Channel> channels = new HashMap<String,Channel>();

    //定义一个channle 组,管理所有的channel
    //GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例
    private static ChannelGroup  channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    //handlerAdded 表示连接建立,一旦连接,第一个被执行
    //将当前channel 加入到  channelGroup
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        //将该客户加入聊天的信息推送给其它在线的客户端
        /*
        该方法会将 channelGroup 中所有的channel 遍历,并发送 消息,
        我们不需要自己遍历
         */
        channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 加入聊天" + sdf.format(new java.util.Date()) + " \n");
        channelGroup.add(channel);




    }

    //断开连接, 将xx客户离开信息推送给当前在线的客户
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        Channel channel = ctx.channel();
        channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 离开了\n");
        System.out.println("channelGroup size" + channelGroup.size());

    }

    //表示channel 处于活动状态, 提示 xx上线
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        System.out.println(ctx.channel().remoteAddress() + " 上线了~");
    }

    //表示channel 处于不活动状态, 提示 xx离线了
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + " 离线了~");
    }

    //读取数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

        //获取到当前channel
        Channel channel = ctx.channel();
        //这时我们遍历channelGroup, 根据不同的情况,回送不同的消息

        channelGroup.forEach(ch -> {
            if(channel != ch) { //不是当前的channel,转发消息
                ch.writeAndFlush("[客户]" + channel.remoteAddress() + " 发送了消息" + msg + "\n");
            }else {//回显自己发送的消息给自己
                ch.writeAndFlush("[自己]发送了消息" + msg + "\n");
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //关闭通道
        ctx.close();
    }
}

public class GroupChatClient {

    //属性
    private final String host;
    private final int port;

    public GroupChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void run() throws Exception{
        EventLoopGroup group = new NioEventLoopGroup();

        try {


        Bootstrap bootstrap = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {

                        //得到pipeline
                        ChannelPipeline pipeline = ch.pipeline();
                        //加入相关handler
                        pipeline.addLast("decoder", new StringDecoder());
                        pipeline.addLast("encoder", new StringEncoder());
                        //加入自定义的handler
                        pipeline.addLast(new GroupChatClientHandler());
                    }
                });

        ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
        //得到channel
            Channel channel = channelFuture.channel();
            System.out.println("-------" + channel.localAddress()+ "--------");
            //客户端需要输入信息,创建一个扫描器
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                //通过channel 发送到服务器端
                channel.writeAndFlush(msg + "\r\n");
            }
        }finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new GroupChatClient("127.0.0.1", 7000).run();
    }
}
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg.trim());
    }
}

5.2netty心跳机制

public class Server {
    public static void main(String[] args) throws Exception{
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
       try {
           ServerBootstrap serverBootstrap = new ServerBootstrap();
           serverBootstrap.group(bossGroup,workerGroup)
                   .channel(NioServerSocketChannel.class)
                   .handler(new LoggingHandler(LogLevel.INFO))
                   .childHandler(new ChannelInitializer<SocketChannel>() {
                       @Override
                       protected void initChannel(SocketChannel socketChannel) throws Exception {
                           ChannelPipeline pipeline = socketChannel.pipeline();
                           pipeline.addLast(new IdleStateHandler(3000,7000,10000,TimeUnit.MILLISECONDS));
                           pipeline.addLast(new ServerHandler());
                       }
                   });
           ChannelFuture future = serverBootstrap.bind(7001).sync();
           future.channel().closeFuture().sync();
       }finally {
           bossGroup.shutdownGracefully();
           workerGroup.shutdownGracefully();
       }
    }
}

public class ServerHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent) evt;
            String eventType=null;
            switch (event.state()){
                case READER_IDLE:
                    eventType = "读空闲";
                    break;
                case WRITER_IDLE:
                    eventType = "写空闲";
                    break;
                case ALL_IDLE:
                    eventType = "读写空闲";
                    break;
            }
            System.out.println(ctx.channel().remoteAddress() + "--超时时间--" + eventType);
            System.out.println("服务器做相应处理..");
        }
    }
}

5.3长连接

public class MyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop
        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    pipeline.addLast(new HttpServerCodec());
                    pipeline.addLast(new ChunkedWriteHandler());
                    pipeline.addLast(new HttpObjectAggregator(8192));
                    pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
                    pipeline.addLast(new MyTextWebSocketFrameHandler());
                }
            });
            ChannelFuture future = serverBootstrap.bind(7000).sync();
            future.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        System.out.println("服务器收到消息 " + msg.text());

        //回复消息
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now() + " " + msg.text()));
    }

    //当web客户端连接后, 触发方法
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        //id 表示唯一的值,LongText 是唯一的 ShortText 不是唯一
        System.out.println("handlerAdded 被调用" + ctx.channel().id().asLongText());
        System.out.println("handlerAdded 被调用" + ctx.channel().id().asShortText());
    }


    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        System.out.println("handlerRemoved 被调用" + ctx.channel().id().asLongText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("异常发生 " + cause.getMessage());
        ctx.close(); //关闭连接
    }
}

5.3编解码器

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
protobuf
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

syntax="proto3";
option java_outer_classname="StudentPOJO";
message Student{
  int32 id=1;
  string name=2;
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    //当通道就绪就会触发该方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        //发生一个Student 对象到服务器

        StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("智多星 吴用").build();
        //Teacher , Member ,Message
        ctx.writeAndFlush(student);
    }

    //当通道有读取事件时,会触发
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf buf = (ByteBuf) msg;
        System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
    }

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

.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
                        //给pipeline 设置处理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {


                            ChannelPipeline pipeline = ch.pipeline();
                            //在pipeline加入ProtoBufDecoder
                            //指定对哪种对象进行解码
                            pipeline.addLast("decoder", new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
                            pipeline.addLast(new NettyServerHandler());
                        }
                    }); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
syntax="proto3";
option optimize_for=SPEED;
option java_package="com.zy.netty.codec2";
option java_outer_classname="MyDataInfo";
message MyMessage{
  enum DataType{
    StudentType=0;
    WorkerType=1;
  }
  DataType data_type=1;
  oneof dataBody{
    Student student=2;
    Worker worker=3;
  }
}

message Student{
  int32 id=1;
  string name=2;
}
message Worker{
  string name=1;
  int32 age=2;
}

5.4入栈与出栈逻辑

ChannelHandler充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样,只不过它是用来处理出站数据的

ChannelPipeline提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler,并被这些Handler处理,反之则称为入站的
在这里插入图片描述
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception {
if (in.readableBytes() >= 4) {
out.add(in.readInt());
}
}
}
说明:
这个例子,每次入站从ByteBuf中读取4字节,将其解码为一个int,然后将它添加到下一个List中。当没有更多元素可以被添加到该List中时,它的内容将会被发送给下一个ChannelInboundHandler。int在被添加到List中时,会被自动装箱为Integer。在调用readInt()方法前必须验证所输入的ByteBuf是否具有足够的数据
decode 执行分析图 [示意图]
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast(new MyByteToLongDecoder());
        pipeline.addLast(new MyLongToByteEncoder());
        pipeline.addLast(new MyServerHandler());
    }

public abstract class ReplayingDecoder extends ByteToMessageDecoder

ReplayingDecoder扩展了ByteToMessageDecoder类,使用这个类,我们不必调用readableBytes()方法。参数S指定了用户状态管理的类型,其中Void代表不需要状态管理

应用实例:使用ReplayingDecoder 编写解码器,对前面的案例进行简化 [案例演示]

ReplayingDecoder使用方便,但它也有一些局限性:

并不是所有的 ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个 UnsupportedOperationException。
ReplayingDecoder 在某些情况下可能稍慢于 ByteToMessageDecoder,例如网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢
在这里插入图片描述

5.5TCP粘包和拆包基本介绍

TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的

由于TCP无消息保护边界, 需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题, 看一张图
在这里插入图片描述
在这里插入图片描述

6.核心源码分析

6.1启动过程

7.RPC

RPC是一种远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。
两个或多个应用程序分布在不同的服务器上,他们之间的调用都像是本地方法调用一样。
在这里插入图片描述
常见的 RPC 框架有: 比较知名的如阿里的Dubbo、google的gRPC、Go语言的rpcx、Apache的thrift, Spring 旗下的 Spring Cloud。
在这里插入图片描述
术语说明:在RPC 中, Client 叫服务消费者,Server 叫服务提供者
PRC调用流程说明

服务消费方(client)以本地调用方式调用服务
client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
client stub 将消息进行编码并发送到服务端
server stub 收到消息后进行解码
server stub 根据解码结果调用本地的服务
本地服务执行并将结果返回给 server stub
server stub 将返回导入结果进行编码并发送至消费方
client stub 接收到消息并进行解码
服务消费方(client)得到结果

小结:RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用。
需求说明

dubbo 底层使用了 Netty 作为网络通讯框架,要求用 Netty 实现一个简单的 RPC 框架
模仿 dubbo,消费者和提供者约定接口和协议,消费者远程调用提供者的服务,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用 Netty 4.1.20

设计说明

创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。
创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。
创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 Netty 请求提供者返回数据
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值