netty基础学习


高并发应用场景

私信/聊天/大规模推送/弹幕/实时定位/在线教育/智能家居/互动游戏/抽奖

io读写基础原理

client ---> 网卡 ---> 内核缓存区 <---> linux内核空间 <---> 内核缓存区 ---> 用户缓存区

四种主要的io模型

1,同步阻塞io(blocking io)
阻塞io,指的是需要内核io操作彻底完成后,才返回到用户空间执行用户的操作.(传统io和java创建的socket都是阻塞的)
同步io是用户空间的线程主动发起io请求
异步io是系统内核主动发起io请求
阻塞时不会占用cpu资源
阻塞io在高并发应用场景下是不可用的

2,同步非阻塞io(non-blocking)
非阻塞io,指的是用户空间的程序不需要等待内核io操作彻底完成,可以立即返回用户空间执行用户的操作,与此同时内核会立即返回给用户一个状态值
linux系统下,可以设置为非阻塞模式
应用程序的线程需要不断地进行io系统调用,轮询数据是否已经准备好
优点:用户线程不阻塞,实时性较好
缺点:是占用大量的cpu时间,效率低下
同步非阻塞io在高并发应用场景下是不可用的

3,io多路复用
经典的reactor反应器设计模式,异步阻塞io,java中的selector选择器和linux中的epoll都是这种模型
避免同步非阻塞io模型中轮询等待的问题
查询io的就绪状态,一个进程可以监视多个文件描述符,一旦某个描述符就绪,内核能够将就绪的状态返回给应用程序
流程:
1,选择器注册(Seletor)
2,就绪状态的查询
3,用户线程活动就绪状态的列表,发起read系统调用
4,复制完成,内核返回结果,用户线程才会解除阻塞的状态
优点:使用select/epoll,一个选择器查询线程可以同时处理成千上万个连接
java NIO使用的就是io多路复用模型,在linux上使用的是epoll系统调用
缺点:select/epoll系统调用是阻塞式的
netty使用的就是io多路复用模型

4,异步io(Aysnchronous IO)
用户空间线程变成被动接受者
彻底解除线程的阻塞
也称为信号驱动io
缺点:需要底层内核提供支持

通过合理配置来支持百万级并发连接

文件句柄,也叫文件描述符
linux系统文件分为:普通文件,目录文件,链接文件和设备文件
所有的io系统调用,都是通过文件描述符完成的
ulimit命令ulimit -n查看打开的最大文件句柄数量
单个进程打开的文件句柄数量超过了系统配置的上限时,就会发出"Socket/File:Can't open so many files"的错误提示
ulimit -n 1000000 建议以root用户来执行(只是临时修改)
编辑/etc/rc.local开机启动文件,添加内容:ulimit -SHn 1000000 (永久的设置)
普通用户可将软极限更改到硬极限的最大值,root用户可以更改硬极限
配置/etc/security/limits.conf(终极解除,es和netty都需要解除)
soft nofile 1000000
hard nofile 1000000

java nio通信基础详解
三个核心组件
Channel
Selector
Buffer
oio是面向流(stream oriented)
nio是面向缓冲区(buffer oriented)
nio 有八种缓存区ByteBuffer(使用最多)/CharBuffer/DoubleBuffer/FloatBuffer/IntBuffer/LongBuffer/ShortBuffer/MappedByteBuffer
三个重要的成员属性:capacity(写入数据对象的数量)/position/limit/mark

nio buffer类的重要方法
allocate()创建缓冲区
put()写入到缓冲区,写入的数据类型要求与缓冲区的类型保持一致
flip()翻转,切换读写模式
get(),读取数据
所有数据读取完成,再读会抛出BufferUnderflowException
读模式改成写模式必须调用Buffer.clear()或Buffer.compact(),才能变成写入模式
rewind()倒带
mark(),将当前的position的值保存起来,放在mark属性中
reset(),将mark值恢复到position中
clear(),清空缓冲区,切换成写模式

Channel的主要类型(实现):FileChannel/SocketChannel/ServerSocketChannel/DatagramChannel
获取FileChannel通道
FileInputStream fis = new FileInputStream(srcFile);
fis.getChannel();
FileOutputStream fos = new FileOuputStream();
fos.getChannel();
RandomAccessFile raf = new RandomAccessFile("filename.txt", "rw");
raf.getChannel();

public static void nioCopyFile(String srcPath, String destPath) {
    
    File srcFile = new File(srcPath);
    File destFile = new File(destPath);
    try {
        if(!destFile.exists()) {
            destFile.createNewFile();
        }
        long startTime = System.currentTimeMillis();
        FileInputStream fis = null;
        FileOutputStream fos = null;
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);
            inChannel = fis.getChannel();
            outChannel = fos.getChannel();
            int length = -1;
            ByteBuffer buf = ByteBuffer.allocate(1024);
            while((length = inChannel.read(buf)) != -1) {
                //变成读模式
                buf.flip();
                int outlength = 0;
                while((outlength = outChannel.write(buf)) != 0) {
                    System.out.println("写入的字节数: " + outlength);
                }
                //第二次切换,清楚buf,编程写入模式
                buf.clear();
            }
            //强制刷新到磁盘
            outchannel.force(true);
        }finally {
            //关闭所有可以关闭对象
            IOUtil.closeQuietly(outChannel);
            IOUtil.closeQuietly(fos);
            IOUtil.closeQuietly(inChannel);
            IOUtil.closeQuietly(fis);
        }
        long endTime = System.currentTimeMillis();
        Logger.info("base复制毫秒数: " + (endTime - startTime));
    }catch(IOException e) {
        e.printStackTrace();
    }
}

更高效的文件复制,调用文件通道的transferFrom方法

SocketChannel套接字通道
SocketChannel负责接连传输
ServerSocketChannel监听通道
socketChannel.configureBlocking(false);//非阻塞

获取SocketChannel传输通道
客户端
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 80));
while(!socketChannel.finishConnect()) {
    
}
服务端
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBloking(false);

读取SocketChannel传输通道
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buf);

写入到SocketChannel传输通道
buffer.flip();
socketChannel.write(buffer);

关闭SocketChannel传输通道
//终止输出方法,向对方发送一个输出的结束标志
socketChannel.shutdownOutput();
//关闭套接字连接
IOUtil.closeQuietly(socketChannel);

DatagramChannel数据包通道
DatagramChannel channel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
channel.socket().bind(new InetSocketAddress(18080));

读取DatagramChannel数据包通道数据
ByteBuffer buf = ByteBuffer.allocate(1024);
SocketAddress clientAddr = datagramChannel.receive(buffer);

写入DatagramChannel数据报通道
buffer.flip();
dChannel.send(buffer, new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP, NioDemoConfig.SOCKET-SERVER_PORT));
buffer.clear();

关闭DatagramChannel数据通道
dChannel.close();

详解NIO Selector选择器

选择器的使命是完成IO的多路复用,选择器和通道的关系,是监控和被监控的关系
Channel.register(Selector selector, int ops);
IO事件(就绪状态)类型包括以下四种:
1,可读,SelectionKey.OP_READ
2,可写,SelectionKey.OP_WRITE
3,连接,SelectionKey.OP_CONNECT
4,接收,SelectionKey.OP_ACCEPT

按位或同时监控可读和可写事件

SelectableChannel可选择通道
继承了抽象类SelectableChannel(可选择通道),提供了实现通道的可选择性所需要的公共方法.

SelectionKey选择键,就是那些被选择器选中的IO事件,不仅仅可以获得通道的IO事件类型,还可以获得发送IO事件所在的通道.

选择器使用流程
1,获取选择器实例.
Selector selector = Selector.open();

2,将通道注册到选择器中.
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

3,轮询感兴趣的IO就绪事件
while(selector.select > 0){
    Set selectedKeys = selector.selectedKey();
    Iterator keyInterator = selectorKeys.iterator();
    while(keyIterator.hasNext()){
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()){
            //IO事件:ServerSocketChannel服务器监听通道有新连接
        }else if(key.isConnectable()){
            //IO事件:传输通道连接成功
        }else if(key.isReadable()){
            //IO事件:传输通道可读
        }else if(key.isWritable()){
            //IO事件:传输通道可写
        }
        //处理完成后,移除选择键
        keyIterator.remove();
    }
}

select()/select(long timeout)/selectNow()
表示io事件的通道数量(选择器感兴趣的IO事件的通道数)

使用NIO实现Discard服务器的实践案例

服务端
public class NioDiscardServer {
    
    public static void startServer() throws IOException {
        //1,获取选择器
        Selector selector = Selector.open();
        //2,获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //3,设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //4,绑定连接
        serverSocketChannel.bind(new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT));
        Logger.info("服务器启动成功");

        //5,将通道注册的"接收新连接"IO事件,注册到选择器上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //6,轮询感兴趣的IO就绪事件(选择键集合)
        while(selector.select()>0){
            //7,获取选择键集合
            Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
            while(selectedKeys.hasNext()) {
                //8,获取单个的选择键,并处理
                SelectionKey selectedKey = selectedKeys.next();
                //9,判断key是具体的什么事件
                if(selectedKey.isAcceptable()) {
                    //10,若选择键的IO事件是"连接就绪事件",就获取客户端连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //11,切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    //12,将改新连接的通道的可读事件,注册到选择器上
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }else if(selectedKey.isReadable()) {
                    //13,若选择键的IO事件是"可读"事件,读取数据
                    SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
                    //14,读取数据,然后丢弃
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int length = 0;
                    while((length = socketChannel.read(byteBuffer)) > 0) {
                        byteBuffer.flip();
                        Logger.info(new String(byteBuffer.array(), 0, length));
                        byteBuffer.clear();
                    }
                    socketChannel.close();
                }
                //15,移除选择键
                selectedKeys.remove();
            }
        }
        //16,关闭连接
        serverSocketChannel.close();
    }
}

客户端
public class NioDiscardClient {
    public static void startClient() throw IOException {
        InetSocketAddress address = new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT);
        //1,获取通道
        SocketChannel socketChannel = SocketChannel.open(address);
        //2,切换成非阻塞模式
        socketChannel.configureBlocking(false);
        //不断地自旋,等待连接完成,或者做一下其他的事情
        while(!socketChannel.finishConnect()){

        }
        Logger.info("客户端连接成功");
        //3,分配指定大小的缓存区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("abc".getBytes());
        byteBuffer.flip();
        //发送到服务器
        socketChannel.write(byteBuffer);
        socketChannel.shutdownOutput();
        socketChannel.close();
    }
}

使用SocketChannel在服务器端接收文件的实践案例

客户端
public class NioSenClient {
    private Charset charset = Charset.forName("UTF-8");
    public void sendFile() throw Exception {
        try {
            String sourcePath = NioDemoConfig.SOCKET_SEND_FILE;
            String srcPath = IOUtil.getResourcePath(sourcePath);
            Logger.info("srcPath= " + srcPath);
            String destFile = NioDemoConfig.SOCKET_RECEIVE_FILE;
            Logger.info("destFile= " + destFile);
            File file = new File(srcPath);
            if(!file.exists()) {
                Logger.info("文件不存在");
                return;
            }
            FileChannel fileChannel = new FileInputStream(file).getChannel();
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.socket().connect(InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP,NioDemoConfig.SOCKET_SERVER_PORT));
            socketChannel.configureBlocking(false);
            while(!socketChannel.finishConnect()) {

            }
            Logger.info("Client成功连接服务器端");
            //发送文件名称
            ByteChannel fileNameByteBuffer = charset.encode(destFile);
            socketChannel.write(fileNameByteBuffer);
            //发送文件长度
            ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_SIZE);
            buffer.putLong(file.length());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
            //发送文件内容
            Logger.info("开始传输文件");
            int length = 0;
            long progress = 0;
            while((length = fileChannel.read(buffer)) > 0) {
                buffer.flip();
                socketChannel.write(buffer);
                buffer.clear();
                progress += length;
                Logger.info("| " + (100 * progress / file.length()) + "% |");
                if(length == -1) {
                    IOUtil.closeQuietly(fileChannel);
                    //在SocketChannel传输通道关闭前,尽量发送一个输出结束标记到对端
                    socketChannel.shutdowmOutput();
                    IOUtil.closeQuietly(socketChannel);
                }
                Logger.info("====== 文件传输成功 ======");
            }
        }catch(Exception e) {
            e.printStackTrace();
        }
    }
}

服务端
public class NioReceiveServer {
    private Charset charset = Charset.forName("UTF-8");
    static class Client {
        //文件名称
        String fileName;
        //长度
        long startTime;
        //开始传输的时间
        long startTime;
        //客户端地址
        InetSocketAddress remoteAddress;
        //输出的文件通道
        FileChannel outChannel;
    }
    private ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SERVER_BUFFER_SIZE);
    //使用Map保存每个文件输出,当OP_READ可读时,根据通道找到对应的对象
    Map<SelectableChannel, Client> clientMap = new HashMap<SelectableChannel, client>();
    public void startServer() throws IOException {
        //1,获取选择器
        Selector selector = Selector.open();
        //2,获取通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverChannel.socket();
        //3,设置非阻塞
        serverChannel.configureBloking(false);
        //4,绑定连接
        InetSocketAddress address = new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT);
        severSocket.bind(address);
        //5,将通道注册到选择器上,并注册的IO事件为:"接收新连接"
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        Print.tcfo("serverChannel is listeing...");
        //6,选择感兴趣的IO就绪事件(选择键集合)
        while(selector.select() > 0) {
            //7,获取选择键集合
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while(it.hasNext()) {
                //8,获取单个的选择键,并处理
                SelectionKey key = it.next();
                //9,判断key是具体的什么事件,是否为新连接事件
                if(key.isAcceptable()) {
                    //10,若接受的事件是"新连接"事件,就获取客户端新连接
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    if(socketChannel == null) {
                        continue;
                    }
                    //11,客户端新连接,切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    //12,将客户端新连接通道注册到选择器上
                    SelectionKey selectionKey = socketChannel.register(selector, SelecionKey.OP_READ);
                    //为每一条传输通道,建立一个Client客户端对象,放入map,供后面使用
                    Client client = new Client();
                    client.remoteAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
                    clientMap.put(socketChannel, client);
                    Logger.info(socketChannel.getRemoteAddress() + "连接成功...");
                } else if (key.isReadable()) {
                    //13,若接收的事件是"数据可读"事件,就读取客户端新连接
                    processData(key);
                }
                //NIO的特点只会累加,已选择的键的集合不会删除
                //如果不删除,下一次又会被select函数选中
                it.remove();
            }
        }
    }

    /**
     * 处理客户端传输过来的数据
     *
     /
    private void processData(SelectionKey key) throws IOException {
        Client client clientMap.get(key.channel());
        SocketChannel socketChannel = (SocketChannel) key.channel();
        int num = 0;
        try {
            buffer,clear();
            while((num = socketChannel.read(buffer)) > 0) {
                buffer.flip();
                if (null == client.fileName) {
                    //客户端发送过来的,首先是文件名
                    //根据文件名,创建服务器端的文件,将文件通道保存到客户端
                    String fileName = charset.decode(buffer).toString();
                    String destPath = IOUtil.getResourcePath(NioDemoConfig.SOCKET_RECEOVE_PATH);
                    File directory = new File(destPath);
                    if(!directory.exists()) {
                        directory.mkdir();
                    }
                    client.fileName = fileName;
                    String fullName = directory.getAbsolutePath() + File.separatorChar + fileName;
                    Logger.inf("NIO 传输目标文件: " + fullName);
                    File file = new File(fullName);
                    FileChannel fileChannel = new FileOutputStream(file).getChannel();
                    client.outChannel = fileChannel;
                }else if (0 == client.fileLength) {
                    //客户端发送过来的,其次是文件长度
                    long fileLength = buffer.getLong();
                    client.fileLength = fileLength;
                    client.startTime = System.currentTimeMillis();
                    Logger.inf("NIO 传输开始");
                } else {
                    //客户端发送过来的,最后是文件内容,写入文件内容
                    client.outChannel.write(buffer);
                }
                buffer.clear();
            }
            key.cancel();
        }catch(IOException e) {
            key.cancel();
            e.printStackTrace();
            return;
        }
        //读取数量 -1 ,标识客户端传输标志到了
        if(num == -1) {
            IOUtil.closeQuitetly(client.outChannel);
            System.out.println("上传完毕");
            key.cancel()
            Logger.info("文件接收成功, File name: " + client.fileName);
            Logger.info("Size: " + IOUtil.getFormatFileSize(client.fileLength));
            long endTime = System.currentTimeMillis();
            Logger.info("NIO IO 传输毫秒数: " + (endTime - client.startTime));
        }
    }
}


reactor 反应器模式
单线程的reactor反应器模式
void attach(Object o) 将对象添加到SelectionKey实例
Object attachment() 取出通过attch(Object o)添加到SelectionKey选择键实例的附件

一个Reactor反应器版本的EchoServer实践案例

class EchoServerReactor implements Runnable {
    Selector selector;
    ServerSocketChannel serverSocket;
    EchoServerReactor() throws IOException {
        //获取选择器,开启serverSocker服务监听通道
        //绑定AcceptorHandler
    }
    //轮询和分发事件
    public void run(){
        try {
            while(!Thread.interrupted()) {
                selector.select();
                Set<SelectionKey> selected = selector.selectedKeys();
                Iterator<SelectionKey> it = selected.iterator();
                while(it.hasNext()) {
                    //反应器负责dispatch收到的实践
                    SelectionKey sk = it.next();
                    dispatch(sk);
                }
                selected.clear();
            }
        }catch(IOException ex) {
             ex.printStackTrace();
        }
    }
    void dispatch(SelectionKey sk) {
        Runnable handler = (Runnable) sk.attachment();
        //调用之前attach绑定到选择键的handler处理对象
        if(handler != null) {
            handler.run();
        }
    }
    //Handler 新连接处理器
    class AcceptorHandler implements Runnable {
        public void run() {
            try {
                SocketChannel channel = serverSocket.accept();
                if(channel != null){
                    new EchoHandler(selector, channel);
                }
            }catch(IOException e) {
                e.printStackTrace();
            }
        }
    }
}

EchoHandler implements Runnable {
    final SocketChannel channel;
    final SelectionKey sk;
    final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    static final int RECIEVING = 0, SENDING = 1;
    int state RECIEVING;
    EchoHandler(Selector selector, SocketChannel c) thows IOException {
        channel = c;
        c.configureBlocking(false);
        //取得选择键,再设置感兴趣的IO事件
        sk = channel.register(selector, 0);
        sk.attach(this);
        //注册Read就绪事件
        sk.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }
    public void run() {
        try {
            if (state == SENDING) {
                //写入通道
                channel.write(byteBuffer);
                //写完后,准备开始从通道读,byteBuffer切换成写入模式
                byteBuffer.clear();
                //写完后,注册read就绪事件
                sk.interestOps(SelectionKey.OP_READ);
                //写完后,进入接收的状态
                state = RECIEVING;
            }else if (state == RECIEVING) {
                //从通道读
                int length = 0;
                while((length = channel.read(byteBuffer)) > 0) {
                    Logger.info(new String(byteBuffer.array(), 0, length));
                }
                //读完后,准备开始写入通道,byteBuffer切换成读取模式
                byteBuffer.flip();
                //读完后,注册write就绪事件
                sk.interestOps(SelectionKey.OP_WRITE);
                //读完后,进入发送状态
                state = SENDING;
            }
            //处理结束了,这里不能关闭selectkey,需要重复使用
            //sk.cancel();
        }catch(IOException ex) {
            ex.printStackTrace();
        }
    }
}

单线程反应器模式实际使用的很少


多线程的Reactor反应器模式
1,将负责输入输出处理的IOHandler处理器的执行,放入独立的线程池中,这样,业务处理线程与负责服务监听和IO事件查询的反应器线程相隔离,
避免服务器的连接监听收到阻塞

2,如果服务器为多核的CPU,可以将反应器线程拆成为多个子反应器(SubReactor)线程,同时,引入多个选择器,每一个SubReactor子线程负责一个选择器.
这样,充分释放了系统资源的能力,也提高了反应器管理大量连接,提升选择大量通道的能力

为了提升效率,建议SubReactor的数量和选择器的数量一致

class MultiThreadEchoServerReactor {
    //声明服务端通道
    ServerSocketChannel serverSocket;
    AtomicInteger next = new AtomicInteger(0);
    //选择器集合,引入多个选择器
    Selector[] selectors = new Selector[2];
    //引入多个子反应器
    SubReactor[] subReactors = null;
    MutiThreadEchoServerReactor() throws IOException {
        //初始化多个选择器
        selectors[0] = Selector.open();
        selectors[1] = Selector.open();
        serverSocket = ServerSocketChannel.open();
        InetSocketAddress address = new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP, NioDemoConfig.SOCKET_SERVER_PORT);
        serverSocket.socket().bind(address);
        //非阻塞
        serverSocket.configureBlocking(false);
        //第一个选择器,负责监控新连接事件
        SelectionKey sk = serverSocekt.register(selectors[0], SelectionKey.OP_ACCEPT);
        //绑定Handler: attch新连接监控handler处理器到SelectionKey
        sk.attach(new AcceptorHandler());
        //第一个子反应器,一子反应器负责一个选择器
        SubReactor subReactor1 = new SubReactor(selectors[0]);
        //第二个子反应器,一子反应器负责一个选择器
        SubReactor subReactor2 = new SubReactor(selectors[1]);
        subReactors = new SubReactor[] {subReactor1, subReactor2};
    }
    private void startService() {
        //一子反应器对应一个线程
        new Thread(subReactors[0]).start();
        new Thread(subReactors[1]).start();
    }
    //子反应器
    class SubReactor implements Runnable {
        //每个线程负责一个选择器的查询和选择
        final Selector selector;
        public SubReactor(Selector selector) {
            this.selector = selector;
        }
        public void run() {
            try {
                while(!Thread.interrupted())  {
                    selector.select();
                    Set<SelectionKey> keySet = selector.selectedKeys();
                    Iterator<SelectionKey> it = keySet.iterator();
                    while(it.hasNext()) {
                        //反应器负责dispatch收到的事件
                        SelectionKey sk = it.next();
                        dispatch(sk);
                    }
                    keySet.clear();
                }
            }catch(IOException ex) {
                ex.printStackTrace();
            }
        }
        void dispatch(SelectionKey sk) {
            Runnable handler = (Runnable) sk.attachment();
            //调用之前attach绑定到选择键的handler处理器对象
            if(handler != null) {
                handler.run();
            }
        }
        //Handler: 新连接处理器
        class AcceptorHandler implements Runnable {
            public void run() {
                try {
                    SocketChannel channel = serverSocket.accept();
                    if(channel != null) {
                        new MultiThreadEchoHandler(selectors[next.get()], channel);
                    }
                }catch(IOException e) {
                    e.printStackTrace();
                }
                if(next.incrementAndGet() == selectors.length){
                    next.set(0);
                }
            }
        }

        public static void main(String [] args) {
            MultiThreadEchoServerReactor server = new MultiThreadEchoServerReactor();
            server.startService();
        }
    }
}

业务处理的代码执行在自己的线程池中,彻底地做到业务处理线程和反应器IO事件线程的完全隔离

class MultiThreadEchoHandler implements Runnable {
    final SocketChannel channel;
    final SelectionKey sk;
    final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    static final int RECIEVING = 0; SENDING = 1;
    int state = RECIEVING;
    //引入线程池
    static ExceptorService pool = Executors.newFixedThreadPool(4);
    MultiThreadEchoHandler(Selector selector, SocketChannel c) throws IOException {
        channel = c;
        c.configureBlocking(false);
        //取得选择键,再设置感兴趣的IO事件
        sk = channel.register(selector, 0);
        //将本Handler作为sk选择键的附件,方便事件分发(dispatch)
        sk.attach(this);
        //向sk选择键注册Read就绪事件
        sk.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }
    public void run() {
        //异步任务,在独立的线程池中执行
        pool.execute(new AsyncTask());
    }
    //业务处理,不在反应器线程中执行
    public synchronized void asyncRun() {
        try {
            if (state == SENDING) {
                //写入通道
                channel.write(byteBuffer);
                //写完后,准备开始从通道读取,byteBuffer切换成写模式
                //byteBuffer.clear();
                //写完后,注册read就绪事件
                sk.interestOps(SelectionKey.OP_READ);
                //写完后,进入接收的状态
                state = RECIEVING;
            }else if(state == RECIEVING) {
                //从通道读
                int length = 0;
                while ((length = channel.read(byteBuffer)) > 0) {
                    Logger.info(new String(byteBuffer.array(), 0, length));
                }
                //读完后,准备开始写入通道,byteBuffer切换成读模式
                byteBuffer.flip();
                //读完后,注册write就绪事件
                sk.interestOps(SelectionKey.OP_WRITE);
                //读完后,进入发送的状态
                state == SENDING;
            }
            //处理结束了,这里不能关闭select key,需要重复使用
            //sk.cancel();
        }catch(IOException ex) {
            ex.printStackTrace();
        }
    }

    class AsyncTask implements Runnable {
        public void run(){
            MultiThreadEchoHandler.this.asyncRun();
        }
    }
}

缺点:同一个Handler业务线程中,如果出现一个长时间的数据读写,会影响这个反应器中其他通道的IO处理,
例如在大文件传输时,IO操作就会影响其他客户端的响应时间.


Future异步回调模式

使用join实现异步泡茶喝的实践案例
public class JoinDemo {
    public static final int SLEEP_GAP = 500;
    public static String getCurTreadName() {
        return Thread.currentThread().getName();
    }
    static class HotWarterThread extends Thread {
        public HotWarterThread() {
            super("---烧水线程---");
        }
        public void run() {
            try {
                Logger.inf("洗好水壶");
                Logger.inf("灌上凉水");
                Logger.inf("放在火上");
                Thread.sleep(SLEEP_GAP);
                Logger.inf("水开了");
            }catch(InterruptedException e) {
                Logger.info("发生异常被中断");
            }
            Logger.info("运行结束");
        }
    }

    static class WashThread extends Thread {
        public WashThread() {
            super("---清洗线程---");
        }
        public void run() {
            try{
                Logger.inf("洗茶壶");
                Logger.inf("洗茶叶");
                Logger.inf("拿茶叶");
                Thread.sleep(SLEEP_GAP);
                Logger.inf("洗完了");
            }catch(InterruptedException e) {
                Logger.inf("发生异常被中断");
            }
            Logger.inf("运行结束");
        }
    }

    public static void main(String args []) {
        Thread hThread = new HotWarterThread();
        Thread wThread = new WashThread();
        hThread.start();
        wThread.start();
        try {
            //合并烧水-线程;
            hThread.join();
            //合并清洗-线程;
            wThread.join();
            Thread.currentThread().setName("主线程");
            Logger.info("泡茶喝");
        }catch(InterruptedException e) {
            Logger.info(Thread.currentThread.getName() + "发生异常");
        }
        Logger.info(getCurTreadName + "运行结束");
    }

}

如果需要获得异步线程的执行结果,可以使用java的FutureTask系列类

FutureTask异步回调
Callable接口, call方法又返回值 (函数式接口)

java提供了在Callable实例和Thread的target成员之间一个搭桥的类---FutureTask

future接口
判断并发任务是否执行完成
获取并发的任务完成后的结果
取消并发执行中的任务
V get() 阻塞
V get(Long timeout, TimeUnit unit) 超时阻塞
boolean isDone() 获取并发任务的执行状态,如果任务执行结束则返回true
boolean isCancelled() 获取并发任务的取消状态,如果任务完成前被取消则返回true
boolean cancel(boolean mayInterruptRunning) 取消并发任务的执行

Future类
Object outcome 用于保存结果

使用futureTask类实现异步泡茶喝的实践案例

public class JavaFutureDemo {
    public static final int SLEEP_GAP = 500;
    public static String getCurThreadName(){
        return Thread.currentThread().getName();
    }

    static class HotWarterJob implements Callable<Boolean> {
        @Override
        public Boolean call() throws Exception {
            try {
                Logger.info("洗好水壶");
                Logger.info("灌上凉水");
                Logger.info("放在火上");
                Thread.sleep(SLEEP_GAP);
                Logger.info("水开了");
            }catch(InterruptedException e) {
                Logger.info("发生异常被中断");
                return false;
            }
            Logger.info("运行结束");
            return true;
        }
    }
    static class WashJob implements Callable<Boolean> {
        @Override
        public Boolean call() throws Exception {
            try {
                Logger.info("洗茶壶");
                Logger.info("洗茶叶");
                Logger.info("拿茶叶");
                Thread.sleep(SLEEP_GAP);
                Logger.info("洗完了");
            }catch(InterruptedException e) {
                Logger.info("清洗工作发送异常被中断");
                return false;
            }
            Logger.info("清洗工作运行结束");
            return true;
        }
    }
    public static void drinkTea(bookean warterOk, boolean cupOk) {
        if(warterOk && cupOk) {
            Logger.info("泡茶喝");
        } else if (!warterOk) {
            Logger.info("烧水失败,没有茶喝了");
        } else if (!cupOk) {
            Logger.info("被子洗不了,没有茶喝了");
        }
    }
    public static void main(String [] args) {
        Callable<Boolean> hjob = new HotWarterJob();
        FutureTask<Boolean> hTask = new FutureTask<>(hjob);
        Thread hThread = new Thread(hTask, "---烧水线程---");
        Callable<Boolean> wJob = new WashJob();
        FutureTask<Boolean> wTask = new FutureTask<>(wJob);
        Thread wThread = new Thread(wTask, "---清洗线程---");
        hThread.start();
        wThread.start();
        Thread.currentThread().setName("主线程");
        try {
            boolean warterOk = hTask.get();
            boolean cupOk = wTask.get();
            drinkTea(warterOk, cupOk);
        } catch (InterruptedException e) {
            Logger.info(getCurThreadName() + "发生异常被中断");
        } catch (ExceptionException e) {
            e.printStackTrace();
        }
        Logger.info(getCurThreadName() + "运行结束");
    }
}

如果需要用到获取异步的结果,则需要引入一些额外的框架
guava的异步回调
1,引入了一个新的接口ListenableFuture, 继承了Java的Future接口,使得Future异步任务,在Guava中能被监控和获得非阻塞异步执行的结果
2,引入了一个新的接口FutureCallback,这是一个独立的新接口,该接口的目的,是在异步任务执行完成后,根据异步结果,完成不同的回调处理,并且
可以处理异步结果.
Futures.addCallback(listenableFuture, new FutureCallback<Boolean>(){
    public void onSuccess(Boolean r){}
    public void onFailure(Throwable t){}
});

第一步:实现java的Callable接口,创建异步执行逻辑,如果不需要返回值,异步执行逻辑也可以实现java的Runnable接口
第二步:创建Guava线程池
第三步:将第一步创建的Callable/Runnable异步执行逻辑的实例,通过submit提交到Guava线程池,从而获取ListenableFuture异步任务实例
第四步:创建FutureCallback回调实例,通过Futures.addCallback将回调实例绑定到ListenableFuture异步任务上

使用Guava实现泡茶喝的实践案例

public class GuavaFutureDemo {
    public static final int SLEEP_GAP = 500;
    public static String getCurThreadName() {
        return Thread.currentThread().getName();
    }

    //业务逻辑:烧水
    static class HotWarterJob implements Callable<Boolean> {
        @Override
        public Boolean call() throws Exception {
            try {
                Logger.info("洗好水壶");
                Logger.info("灌上凉水");
                Logger.info("放在火上");
                Thread.sleep(SLEEP_GAP);
                Logger.info("水开了");
            }catch(InterruptedException e) {
                Logger.info("发生异常被中断");
                return false;
            }
            Logger.info("运行结束");
            return true;
        }
    }
    //业务逻辑:清洗
    static class WashJob implements Callable<Boolean> {
        @Override
        public Boolean call() throws Exception {
            try {
                Logger.info("洗茶壶");
                Logger.info("洗茶叶");
                Logger.info("拿茶叶");
                Thread.sleep(SLEEP_GAP);
                Logger.info("洗完了");
            }catch(InterruptedException e) {
                Logger.info("清洗工作发送异常被中断");
                return false;
            }
            Logger.info("清洗工作运行结束");
            return true;
        }
    }
    //新创建一个异步业务类型,作为泡茶喝主线程类
    static class MainJob implements Runnable {
        boolean warterOk = false;
        boolean cipOk = false;
        int gap = SEELP_GAP / 10;
        @Override
        public void run() {
            while(true) {
                try {
                    Thread.sleep(gap);
                    Logger.info("读书中...");
                }catch(InterruptedException e) {
                    Logger.info(getCurThreadName() + "发生异常被中断");
                }
                if (warterOk && cupOk) {
                    drinkTea(warterOk, cupOk);
                }
            }
        }
        public void drinkTea(Boolean wOk, Boolean cOk) {
            if(wOk && cOk){
                Logger.info("泡茶喝,茶喝完");
                this.warterOk = false;
                this.gap = SLEEP_GAP * 100;
            }else if (!wOk) {
                Logger.info("烧水失败,没有茶喝了");
            }else if(!cOk){
                Logger.info("杯子洗不了,没有茶喝了");
            }
        }
    }

    public static void main(String [] args) {
        MainJob mainJob = new MainJob();
        Thread mainThread = new Thread(mainJob);
        mainThread.setName("主线程");
        mainThread.start();
        //烧水的业务逻辑实例
        Callable<Boolean> hotJob = new HotWarterJob();
        //清洗的业务逻辑实例
        Callable<Boolean> washJob = new WashJob();
        //创建java线程池
        ExecutorService jpool = Executors.newFixedThreadPool(10);
        //包装java线程池,构造Guava线程池
        ListeningExecutorService gpool = MoreExecutors.listeningDecorator(jpool);
        //提交烧水的业务逻辑实例,到Guava线程池获取异步任务
        ListenableFuture<Boolean> hotFuture = gPool.sumit(hotJob);
        //绑定异步回调,烧水完成后,把喝水任务的warterOk标志设置为true
        Futures.addCallback(hotFuture, new FutureCallback<Boolean>(){
            public void onSuccess(Boolean r) {
                if(r) {
                    mainJob.warterOk = true;
                }
            }
            public void onFailure(Throwable t) {
                Logger.info("烧水失败,没有茶喝了");
            }
        });

        //提交清洗的业务逻辑实例,到Guava线程池获取异步任务
        ListenableFuture<Boolean> washFuture = gPool.sumit(washJob);
        //绑定任务执行完成后的回调逻辑到异步任务
        Futures.addCallback(washFuture, new FutureCallback<Boolean>(){
            public void onSuccess(Boolean r) {
                if(r) {
                    mainJob.cupOk = true;
                }
            }
            public void onFailure(Throwable t) {
                Logger.info("杯子洗不了,没有茶喝了");
            }
        });
    }
}

netty的异步回调模式
netty对javaFuture异步任务的扩展如下
1,基础java的Future接口,得到了一个新的属于Netty自己的Future异步任务接口,该接口对原有的接口进行了增强,使得Netty异步任务能够以非阻塞的方式处理回调结果;
注意,Netty没有修改Future的名称,只是调整了所在的包名,Netty的Future类的包名和java的Future接口的包名不同

2,引入了一个新接口GenericFutureListener,用于表示异步执行完成的监听器.这个接口和Guava的FutureCallback回调接口不同.
Netty使用了监听器的模式,异步任务的执行完成后的回调逻辑抽象成了Listener监听器接口.
可以将Netty的GenericFutureListener监听器接口加入Netty异步任务Future中,实现对异步任务执行状态的事件监听.

Netty的future接口,对应Guava的listenableFuture接口
Netty的GenericFutureListener接口,对应Guava的futureCallback接口

详解GenericFutureListrener接口
    GenericFutureListrener拥有一个回调方法:operationComplete,表示异步任务操作完成

详解Netty的Future接口
使用子接口
ChannelFuture
//connect是异步的,仅提交异步任务
ChannelFuture future = bootstrap.connect(new InetSocketAddress("www.manning.com", 80));
//connect的异步任务真正执行完成后,future回调监听器才会执行
future.addListener(new ChannelFutureListener(){
    @Override
    public void operationComplete(ChannelFuture channelFuture) throw Exception {
        if (channelFuture.isSuccess()) {
            System.out.println("Connection established");
        } else {
            System.err.println("Connection attempt failed");
            channelFuture.cause().printStackTrace();
        }
    }
});    

Netty的出战和入站异步回调
//write输出方法,返回的是一个异步任务
ChannelFuture future = ctx.channel().write(msg);

Netty原理与基础

第一个Netty的时间案例DiscardServer
maven 引入依赖
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
</dependency>

第一个Netty服务器端程序

public class NettyDiscardServer {
    private final int serverPort;
    //服务启动类
    ServerBootstrap b = new ServerBootstrap();
    public NettyDiscardServer(int port) {
        this.serverPort = port;
    }
    public void runServer() {
        //创建反应器线程组
        EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1); //多线程的java nio通信的应用场景    
        EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
        try {
            //1,设置反应器线程组
            b.group(bossLoopGroup, workerLoopGroup);
            //2,设置nio类型的通道
            b.channel(NioServerSocketChannel.class);
            //3,设置监听端口
            b.localAddress(serverPort);
            //4,设置通道的参数
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.option(ChannelOption.ALLOCATOR, PooledByBufferAllocator.DEFAULT);
            //5,装配子通道流水线
            b.channelHandler(new ChannelInitializer<SocketChannel>() {
                //有连接到达时会创建一个通道
                protected void initChannel(SocketChannel) throws Exception {
                    //流水线管理子通道中的Handler处理器
                    //向子通道流水线添加一个handler处理器
                    ch.pipeline().addLast(new NettyDiscardHandler());
                }
            });
            //6,开始绑定服务器
            //通过调用sync同步方法阻塞直到绑定成功
            ChannnelFuture channelFuture = b.bind().sync();
            Logger.info("服务器启动成功,监听端口: " + channelFuture.channel().localAddress());
            //7,等待通道关闭的异步任务结束
            //服务监听通道会一直等待通道关闭的异步任务结束
            ChannelFuture closeFuture = channelFuture.channel().closeFuture();
            closeFuture.sync();
        }catch(Exception e) {
            e.printStackTrace();
        }finally {
            //8,关闭EventLoopGroup.shutdownGracefully();
            workerLoopGroup.shutdownGracefully();
            bossLoopGroup.shutdownGracefully();
        }
    }

    public static void main(String [] args) throws InterruptedExcetion {
        int port = NettyDemoConfig.SOCKET_SERVER_PORT;
        new NettyDiscardServer(port).runServer();
    }
}

//继承入站处理器
public class NettyDiscardHandler extends ChannelInboundHandlerAdaper() {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throw Exception {
        //缓冲区
        ByteBuf in = (ByteBuf) msg;
        try {
            Logger.info("收到消息,丢弃如下: ");
            while(in.isReadable()) {
                System.out.print((char) in.readByte());
            }
            System.out.println();
        }finally {
            ReferenceCountUtil.release(msg);
        }
    }
}


解密Netty中的Reactor反应器模式

整个流程大致分为四步
第一步:通道注册,IO和通道是强相关的,将通道注册到选择器,IO事件会被选择器查询到
第二步:查询选择,反应器或者子反应器负责一个线程,不断轮询查询选择器中的IO事件
第三步:事件分发,如果查询到IO事件,分发给IO事件有绑定关系的Handler业务处理器
第四步:完成真正的IO操作和业务处理,这一步由Handler业务处理器负责

netty 中的channel通道组件

对channel通道组件进行了自己的封装
每一种协议的通道,都有nio和oio两个版本
NioSocketChannel: 异步非阻塞TCP Socket传输通道
NioServerSocketChannel: 异步非阻塞TCP Socket服务器监听通道
NioDatagramChannel: 异步非阻塞的UDP传输通道
NioStpChannel: 异步非阻塞Sctp传输通道
NioSctpServerChannel: 异步非阻塞Stcp服务器端监听通道
OioSocketChannel: 同步阻塞式TCP Socket传输通道
OioServerSocketChannel: 同步阻塞式TCP Socket服务监听通道
OioDatagramChannel: 同步阻塞式UDP传输通道
OioSctpChannel: 同步阻塞式Sctp传输通道
OioSctpServerChannel: 同步阻塞式Sctp服务器端监听通道
SelectableChannel: 底层通道

netty 中的reactor反应器
netty的反应器类为: NioEventLoop

Netty中的Handler处理器
第一类是ChannelInboundHandler通道入站处理器
第二类是ChannelOutboundHandler通道出战处理器
ChannelInboundHandlerAdapter通道入站处理适配器
ChannelOutboundHandlerAdapter通道出战处理器适配器

netty的流水线
ChannelPipeline 通道流水线

Bootstrap快速组装通道,EventLoop反应器,Handler处理器

详解Bootstrap启动器类

Bootstrap client专用
ServerBootstrap server专用
父子通道
连接监听类型
传输数据类型
NioServerSocketChannel 负责服务器连接监听和接收
NioSocketChannel 传输类通道
EventLoopGroup事件循环线程组
默认内部线程数为cpu*2

Bootstap的启动流程
1,创建反应器线程组
ServerBootstrao b = new ServerBootstrap();
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
b.group(bossLoopGroup, workerLoopGroup);

2,设置通道的IO类型
b.channel(NioServerSocketChannel.class);

3,设置监听端口
b.localAddress(new InetSocketAddress(port));

4,设置传输通道的配置选项
b.option(ChannelOption.SO_KEEPALIVE,true); //是否开启心跳机制
b.option(ChannelOption.ALLOCATOR,PooledBufAllocator, DEFAULT);

5,装配子通道的pipeline流水线
b.childHandler(new ChannelInitializer<SocketChannel>(){
    protected void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new NettyDiscardHandler());
    }
})

6,开始绑定服务器新连接的监听端口
ChannelFuture channelFuture = b.bind().sync();
logger.info("服务器启动成功, 监听端口: " + channelFuture.channel().localAddress());

7,自我阻塞,直到通道关闭
ChannelFuture closeFuture = channelFuture.channel().closeFuture();
closeFuture.sync();

8,关闭EventLoopGroup
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();

ChannelOption通道选项
1,SO_RCVBUF, SO_SNDBUF
用来设置TCP连接的缓冲区大小

2,TCP_NODELAY
表示立即发送数据,Netty默认为True,操作系统默认为False

3,SO_KEEPALIVE
心跳机制,ture为连接保持心跳,默认值为false,默认心跳间隔是7200s

4,SO_REUSEADDR
TCP参数,设置为true时表示地址复用,默认值为false,四种情况需要用到这个参数

当有一个相同本地地址和端口的socket1处于TIME_WAIT状态,而我们希望启动的程序的socket2要占用该地址和端口,
例如在重启服务且保存先前端口时

有多快网卡或用IP Alias技术的机器在同一端口启动多个进程,但每个进程绑定的本地IP地址不能相同

单个进程绑定相同的端口到多个socket上,但每个socket绑定的IP地址不同

完全相同的地址和端口的重复绑定,但这只用于UDP的多播,不用于TCP

5,SO_LINGER
表示关闭socket的延迟时间,默认值为-1,表示禁用该功能

6.SO_BACKLOG
表示服务器端接收连接的队列长度,连接建立频繁可以适当调大这个参数

7,SO_BRPADCAST
表示设置广播模式

详解Channel通道
专门的单元测试通道
EmbeddedChannel

AbstractChannel
pipeline属性初始化为DefaultChannelPipeline
接口方法
ChannelFuture connect(SocketAdress address)
连接远程服务器,返回ChannelFuture

ChannelFuture bind(SocketAddress address)
绑定监听地址,此方法在服务器的新连接监听和接收通道使用

ChannelFuture close()
关闭通道连接,返回ChannelFuture异步任务
如果需要在连接正式关闭后执行其他操作,需要为异步任务设置回调方法,
或者调用ChannelFuture异步任务的sync()方法来阻塞当前线程,一直等待通道关闭的异步任务执行完毕

Channel read()
读取通道数据,启动入站处理
从内部的java nio channel通道读取数据,然后启动内部的pipeline流水线,开启数据读取的入站处理,返回自身调用

ChannelFuture write(Object o)
启动出战流水处理,把处理后的最终数据写到底层java nio通道,返回出战处理的异步处理任务

Channel flush()
将缓存区中的数据立即写出到对端


EmbeddedChannel嵌入式通道
writeInbound() 入站数据写到通道
readInbound()
writeOutbound()
readOutbound() 读取通道的出站数据
finish()

详解Handler业务处理器
查询到io事件后,分发到handler业务处理,由Handler完成io操作和业务处理
io处理环节,分入站和出站
 (通道读数据包--->数据包解码)--->业务处理--->|目标数据编码--->数据包写到通道--->通道发送到对端

入站处理器 ChannelInboundHandler
1,channelRegistered
通道注册完成后调用fireChannelRegistered,触发通道注册事件,在通道注册过的入站处理器Handler的channelRegistered方法会被调用
2,channelActive
通道激活完成后调用fireChannelActive,触发通道激活事件,在通道注册过的入站处理器Handler的channelActive方法会被调用
3,channelRead
当通道缓冲区可会调用fireChannelRead,触发通道可读事件,在通道注册过的入站处理器Handler的channelRead方法会被调用
4,channelReadComplete
当通道缓冲区读完,会调用fireChannelReadComplete,触发通道读完事件,在通道注册过的入站处理器Handler的channelReadComplete方法会被调用到
5,channelInactive
当连接被断开或者不可用会调用fireChannelInactive,触发连接不可用事件,在通道注册过的入站处理器Handler的channelInactive方法会被调用到
6,exceptionCaught
当通道处理过程发生异常时会调用fireExceptionCaught,触发异常捕获事件,在通道注册过的入站处理器Handler的exceptionCaught方法会被调用到
出战入站处理器都继承到了该方法

开发中只需要继承ChannelInboundHandlerAdapter默认实现即可

ChannelOutboundHandler通道出战处理器
1,bind
用于服务端
2,connect
用于客户端
3,write
完成通道向底层javaio通道的数据写入,并不完成实际的数据写入操作
4,flush
写出到对端
5,read
通道从javaio通道的数据读取
6,disConnect
用于客户端
7,close
主动关闭通道

开发中只需要继承ChannelOutboundHandlerAdapter实现即可

ChannelInitalizer通道初始化处理器
initChannel()方法,拿到新连接通道作为实际参数,往它的流水线中装配Handler业务处理器

ChannelInboundHandler的生命周期的实践案例
InHandlerDemo继承ChannelInboundHandlerAdapter
public class InHandlerDemoTester {
    @Test
    public void testInHandlerLifeCircle() {
        final InHandlerDemo inHandler = new InHandlerDemo();
        //初始化处理器
        ChannelInitializer i = new ChannelInitializer<>(EmbeddedChannel) {
            protected void initChannel(EmbeddedChannel ch) {
                ch.pipeline().addLast(inHandler);
            }
        };
        //创建嵌入式通道
        EmbeddedChannel channel = new EmbeddedChannel(i);
        ByteBuf buf = Unpooled.buffer();
        buf.writeInt(1);
        //模拟入站
        channel.writeInbound(buf);
        channel.flush();
        //通道关闭
        channel.close();
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch(InterruptedException e) {
             e.printStackTrace();
        }
    }
}
调用顺序
handlerAdded()->channelRegistered()->channelActive()->
入站方法回调->channelRead()->channelReadComplete() //每次有ByteBuf数据包入站都会调用到
channelInactive()->channelUnregistered()->handlerRemoved()

通道创建的时候
handlerAdded()
完成ch.pipeline().addLast(handler)语句之后会被回调
channelRegistered()
通道成功绑定一个NioEventLoop线程后,会通过流水线回调所有业务处理器的channelRegistered()方法
channelActive()
通道激活成功后,会通过流水线回调所有业务处理器的channelActive()方法(所有业务处理器添加注册的异步任务完成,并且NioEventLoop线程绑定的异步任务完成)

通道关闭的时候
channelInactive()
底层连接不是ESTABLISH状态,或者底层连接已经关闭时,首先回调所有业务处理器的channelInactive()方法
channelUnregistered()
通道和NioEventLoop线程解除绑定,溢出等于这条通道的事件处理之后,回调所有业务处理器的channelUnregistered()方法
handlerRemoved()
移除通道上所有的业务处理器,并且回调所有的业务处理器的handlerRemoved()方法

channelRead()
有数据包入站,通道可读,入站处理器的channelRead()方法会被依次回调到
channelReadComplete()
流水线完成入站处理后,依次回调每个入站处理器的channelReadComplete方法,表示数据读取完毕

pipeline流水线

pipeline入站处理流程

public class InPipeline {
    static class SimpleInHandlerA extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContent ctx, Object msg) throws Exception {
            Logger.info("入站处理器A被回调");
            super.channelRead(ctx, msg);
        }
    }
    static class SimpleInHandlerB extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContent ctx, Object msg) throws Exception {
            Logger.info("入站处理器B被回调");
            super.channelRead(ctx, msg);
        }
    }
    static class SimpleInHandlerC extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContent ctx, Object msg) throws Exception {
            Logger.info("入站处理器C被回调");
            super.channelRead(ctx, msg);
        }
    }

    @Test
    public void testPipelineInBound() {
        ChannelInitializer i = new ChannelInitializer<EmbeddedChannel>() {
            protected void initChannel(EmbeddedChannel ch) {
                ch.pipeline().addLast(new SimpleInHandlerA());
                ch.pipeline().addLast(new SimpleInHandlerB());
                ch.pipeline().addLast(new SimpleInHandlerC());
            }
        };
        EmbeddedChannel channel = new EmbeddedChannel(i);
        ByteBuf buf = Unpooled.buffer();
        buf.writeInt(1);
        channel.writeInbound(buf);
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}

传递的对象都是同一个信息(msg)


pipeline出战处理流程

public class OutPipeline {
    public class SimpleOutHandlerA extends ChannelOutboundHandlerAdapter {
        @Overide
        public void write(ChannelHandlerContent ctx, Object msg, ChannelPromise promise) throws Excetion {
            Logger.info("出站处理器A被回调");
            super.write(ctx, msg, promise);
        }
    }
    public class SimpleOutHandlerB extends ChannelOutboundHandlerAdapter {
        @Overide
        public void write(ChannelHandlerContent ctx, Object msg, ChannelPromise promise) throws Excetion {
            Logger.info("出站处理器B被回调");
            super.write(ctx, msg, promise);
        }
    }
    public class SimpleOutHandlerC extends ChannelOutboundHandlerAdapter {
        @Overide
        public void write(ChannelHandlerContent ctx, Object msg, ChannelPromise promise) throws Excetion {
            Logger.info("出站处理器C被回调");
            super.write(ctx, msg, promise);
        }
    }
    @Test
    public void testPipelineOutBound() {
        ChannelInitializer i = new ChannelInitializer<EmbeddedChannel>() {
            protected void initChannel(EmbeddedChannel ch) {
                ch.pipeline().addLast(new SimpleOutHandlerA());
                ch.pipeline().addLast(new SimpleOutHandlerB());
                ch.pipeline().addLast(new SimpleOutHandlerC());
            }
        };
        EmbeddedChannel channel = new EmbeddedChannel(i);
        ByteBuf buf = Unpooled.buffer();
        buf.writeInt(1);
        channel.writeInbound(buf);
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ChannelHandlerContext上下文
代表了ChannelHandler通道处理器和ChannelPipeline通道流水线直接的关联
方法分为两类
第一类是获取上下文所关联的netty组件实例
第二类是入站和出站处理方法

ChannelHandlerContext通道处理器上下文进行调用,只会从当前的节点开始执行Handler业务处理器,并传播到同类型处理器的下一站(节点)
Channel通道拥有一条ChannelPipeline通道流水线,每一个流水线节点为一个ChannelHandlerContext通道处理器上下文对象,每一个上下文中包裹了一个
ChannelHandler通道处理器,可以通过Context实例的实参,可以获取ChannelPipeline通道流水线的实例或者Channel通道的实例

截断流水线的处理
1,不调用supper.channelXxx()
2,也不调用ctx.fireChannelXxx()

出站处理流程只要开始执行,就不能被截断,强行截断的话,会抛出异常

Handler业务处理器的热插拔
ChannelPipeline
public interface ChannelPipeline extends Iterable<Entry<String, ChannelHandler>> {
    //在头部增加一个业务处理器,名字由name指定
    ChannelPipeline addFirst(String name, ChannelHandler handler);
    //在尾部增加一个业务处理器,名字由name指定
    ChannelPipeline addLast(String name, ChannelHandler handler);
    //在头baseName处理器的前面增加一个业务处理器,名字由name指定
    ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler);
    //在头baseName处理器的后面增加一个业务处理器,名字由name指定
    ChannelPipeline addAfter(String baseName, String name, ChannelHandler handler);
    ChannelPipeline remove(ChannelHandler handler);
    ChannelPipeline remove(String handler);
    ChannelPipeline removeFirst();
    ChannelPipeline removeLast();
}

public class PipelineHotOperateTester {
    static class SimpleInHandlerA extends ChannelInboundHandlerAdapter {
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            Logger.info("入站处理器A被回调");
            ctx.pipeline().remove(this);
        }
    }
    @Test
    public void testPipelineOutBound() {
        ChannelInitializer i = new ChannelInitializer<EmbeddedChannel>() {
            protected void initChannel(EmbeddedChannel ch) {
                ch.pipeline().addLast(new SimpleInHandlerA());
                ch.pipeline().addLast(new SimpleInHandlerB());
                ch.pipeline().addLast(new SimpleInHandlerC());
            }
        };
        EmbeddedChannel channel = new EmbeddedChannel(i);
        ByteBuf buf = Unpooled.buffer();
        buf.writeInt(1);
        channel.writeInbound(buf);
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}

详解ByteBuf缓冲区
netty提供了bytebuff来替代javanio的bytebuff缓冲区,以操纵内存缓冲区
优势
pooling
复合缓冲区类型,支持零复制
不需要调用flip方法去切换读写模式
扩展性好,stringbuffer
可以自定义缓冲区类型
读取和写入索引分开
方法的链式调用
可以进行引用计数,方便重复使用

逻辑部分
第一部分
废弃 
第二部分
可读 
第三部分
可写 
第四部分
可扩容

readerIndex
writerIndex
maxCapacity

ByteBuf三组方法
容量
capacity()
maxCapacity()

写入
isWritable()
writableBytes()
maxWritableBytes()
writeBytes()
writeTYPE()
setTYPE()
markWriterIndex()
resetWriterIndex()

读取
isReadable()
readableBytes()
readBytes()
readType()
getTYPE()
markReaderIndex()
resetReaderIndex()

ByteBuf基本使用的实践案例
//默认分配器,初试容量为9,最大限制为100个字节
ByteBufAllocator.DEFAULT.buffer(9, 100);

getByte() 不改变指针

ByteBuf的引用计数
Buffer对象池化
retain和release方法应该结对使用
引用计数为0,池化的buffer会放回池中
未池化的如果是堆结构被jvm回收,如果
是direct类型被本地方法释放

ByteBuf的allocator分配器
PoolByteBufAllocator和UnpooledByteBufAllocator
通过System Property的io.netty.allocator.type进行配置
PoolByteBufAllocator  设置为默认的分配器

四种分配方法(一般第一种或第二种)
buffer = ByteBufAllocator.DEFAULT.buffer(9,100);
buffer = ByteBufAllocator.DEFAULT.buffer(); //256, Integer.MAX_VALUE
buffer = UnpooledByteByteBufAllocator.DEFAULT.heapBuffer();
buffer = PooledByteByteBufAllocator.DEFAULT.directBuffer();

ByteBuf缓冲区的类型
Heap ByteBuf
Direct ByteBuf
CompositeBuffer

在读写频繁的情况下,创建Direct Buffer(直接缓冲区)在池化分配器中分配和回收

三类ByteBuf使用的实践案例

public class BufferTypeTest {
    final static Charset UTF_8 = Charset.forName("UTF-8");
    @Test
    public void testHeapBuffer() {
        ByteBuf heapBuf = ByteBufAllocator.DEFAULT.buffer();
        heapBuf.writeBytes("ABC".getBytes(UTF_8));
        if(heapBuf.hasArray()) {
            byte[]array = heapBuf.array();
            int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
            int length = heapBuf.readableBytes();
            Logger.info(new String(array, offset, length, UTF_8));
        }
        heapBuf.release();
    }

    @Test
    public void testDirectBuffer() {
        ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer();
        directBuf.writeBytes("DEF".getBytes(UTF_8);
        if(!directBuf.hasArray()) {
            int length = directBuf.readableBytes();
            byte[] array = new byte[length];
            directBuf.getBytes(directBuf.readerIndex(), array);
            Logger.info(new String(array, UTF_8));
        }
        directBuf.release();
    }
}

public class CompositeBufferTest {
    static Charset utf8 = Charset.forName("UTF-8");
    @Test
    public void byteBufComposite() {
        CompositeByteBuf cbuf = ByteBufAllocator.DEFAULT.compositeBuffer();
        //消息头
        ByteBuf headerBuf = Unpooled.copiedBuffer("ABC", utf8);
        //消息体1
        ByteBuf bodyBuf = Unpooled.copiedBuffer("DEF", utf8);
        cbuf.addComponents(headerBuf, bodyBuf);
        sendMsg(cbuf);
        //在refCnt为0前,retain
        headerBuf.retain();
        cbuf.release();
        cbuf = ByteBufAllocator.DEFAULT.compositeBuffer();
        //消息体2
        bodyBuf = Unpooled.copiedBuffer("DEF", utf8);
        cbuf.addComponents(headerBuf, bodyBuf);
        sendMsg(cbuf);
        cbuc.release();
    }

    private void sendMsg(CompositeByteBufcbuf) {

        for(ByteBuf b: cbuf) {
            int length = b.readableBytes();
            byte[] array = new byte[length];
            b.getBytes(b.readerIndex(), array);
            System.out.print(new String(array, utf8));
        }
        System.out.println();
    }
}


public class CompositeBuffTest {
    @Test
    public void intCompositeBufCompoiset() {
        CompositeByteBuf cbuf = Unpooled.compositeBuff(3);
        cbuf.addComponent(Unpooled.wrappedBuffer(new byte[]{1,2,3}));
        cbuf.addComponent(Unpooled.wrappedBuffer(new byte[]{4});
        cbuf.addComponent(Unpooled.wrappedBuffer(new byte[]{5,6});

        ByteBuffer nioBuffer = cbuf.nioBuffer(0,6);
        bytep[] bytes = nioBuffer.array();
        System.out.print("bytes=");
        for(byte b: bytes) {
            System.out.print(b);
        }
        cbuf.release();
    }
}

ByteBuf的自动释放
方式一: TailHandler自动释放
手动:byteBuf.release();
super.channelRead(ctx, msg);
方式二: SimpleChannelInboundHandler自动释放
手动:byteBuf.release();
继承SimpleChannelInboundHandler

需要确保Bytebuf缓冲区的释放

ByteBuf浅层复制的高级使用方式
分为切片浅层复制和整体浅层复制

ByteBuf的slice方法可以获取到一个ByteBuf的一个切片,一个ByteBuf可以进行多次的切片浅层复制;多次切片后的ByteBuf对象可以共享一个存储区域
1,public ByteBuf slice()
2,public ByteBuf slice(int index, int length)

duplicate整体浅层复制
duplicate()返回的是源ByteBuf的整个对象的一个浅层复制

在调用浅层复制实例时,可以通过调用一次retain()方法来增加引用,表示它们对应的底层内存多了一次引用,在浅层复制实例后,需要调用两次release()方法,将引用计数减一
这样就不影响源ByteBuf的内存释放

实践案例
如果在Handler实例中,没有与特定通道强相关的数据或者状态,建议设计成共享的模式,在Handler类前添加注解@ChannelHandler.Sharable
否则试图将同一个Handler实例添加到多个ChannelPipeline通道流水线时,Netty将会抛出异常

同一个通道上的所有业务处理器,只能被同一个线程处理,所以,不是@Sharable共享类型的业务处理器,在线程的层面是安全的,
不需要进行线程的同步控制.而不同的通道,可能绑定到多个不同的EventLoop反应器线程.因此,加上了@ChannelHandler.Sharable注解后的共享业务处理器的实例,
可能被多个线程并发执行,这样,就会导致一个结果:@Sharable共享实例不是线程层面安全的.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星月IWJ

曾梦想杖键走天涯,如今加班又挨

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值