Netty架构与工作原理

Netty

Java NIO三大组件:Selector,Buffer,Channel

NIO

  1. 每个channel都对应一个Buffer
  2. Selector对应一个线程
  3. Buffer是一个内存块,底部有一个数组

Buffer

//标记
private int mark = -1;
//当前位置
private int position = 0;
//缓冲区的位置
private int limit;
//缓冲区的容量大小
private int capacity; 

Selector

常用方法
//Open方法,返回一个选择器对象
public static Selector open();
//监控注册的通道,当有IO操作时,将对应的SelectionKey加入集合并返回
public abstract int selectNow() 
//监控注册的通道,当有IO操作时,将对应的SelectionKey加入集合并返回,参数用来设置超时时间
public abstract int select(long timeout)
//查看所有SelectionKey
public abstract Set<SelectionKey> selectedKeys();

NIO的简单网络编程

服务端
package com.kklll.leetcode.nio.server;

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

/**
 * @Author DeepBlue
 * @Date 2020/10/11 15:25
 */
public class NioServer {
    public static void main(String[] args) throws Exception {
        //serverSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //先获取Socket然后绑定端口号
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //非阻塞
        serverSocketChannel.configureBlocking(false);

        //创建一个Selector
        Selector selector = Selector.open();

        //把ServerSocketChannel注册到Selector事件为Accept
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            if (selector.select(1000) == 0) {
                System.out.println("我干别的去了");
                continue;
            }
            //获取到Accept发生的集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            SelectionKey key1 = null;
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                key1 = iterator.next();
                if (key1.isAcceptable()) {
                    SocketChannel accept = serverSocketChannel.accept();
                    accept.configureBlocking(false);
                    System.out.println("客户端连接成功!Channel的hashcode是:"+accept.hashCode());
                    //再注册,并关联一个Buffer
                    accept.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (key1.isReadable()) {
                    SocketChannel channel = (SocketChannel) key1.channel();
                    ByteBuffer buffer = (ByteBuffer) key1.attachment();
                    channel.read(buffer);
                    System.out.println("客户端发送的是:" + new String(buffer.array()));
                    buffer.clear();
                    channel.close();
                }
                iterator.remove();
            }
        }


    }
}
客户端
package com.kklll.leetcode.nio.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @Author DeepBlue
 * @Date 2020/10/11 15:25
 */
public class NioClient {
    public static void main(String[] args) throws IOException {
        SocketChannel clientChannel = null;
        try {
            clientChannel = SocketChannel.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            clientChannel.configureBlocking(false);
        } catch (IOException e) {
            e.printStackTrace();
        }
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);

        try {
            if (!clientChannel.connect(inetSocketAddress)) {
                while (!clientChannel.finishConnect()) {
                    System.out.println("连接需要时间,客户端不会阻塞,可以做其他工作!");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //连接成功
        String s = "Hello,Netty And NIO2";
        ByteBuffer buffer = ByteBuffer.wrap(s.getBytes());
        try {
            clientChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            System.in.read();
            clientChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO实现的多人聊天室

服务端
package com.kklll.leetcode.nio.server;

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

/**
 * @Author DeepBlue
 * @Date 2020/10/11 17:17
 */
public class GroupCharServer {
    private Selector selector;
    private ServerSocketChannel listenChannel;
    public static long BLOCK_TIMEOUT = 2000;
    public static int PORT = 6667;


    public GroupCharServer() {
        try {
            selector = Selector.open();
            listenChannel = ServerSocketChannel.open();
            listenChannel.bind(new InetSocketAddress(PORT));
            listenChannel.configureBlocking(false);
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public void listen() {
        try {
            while (true) {
                int count = selector.select(BLOCK_TIMEOUT);
                if (count > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    SelectionKey key = null;
                    while (iterator.hasNext()) {
                        key = iterator.next();
                        handleRequest(key);
                        iterator.remove();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }
    }

    public void handleRequest(SelectionKey key) {
        try {
            //如果是连接
            if (key.isAcceptable()) {
                accept();
            }
            //如果是read方法
            if (key.isReadable()) {
                readData(key);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void accept() {
        try {
            SocketChannel sc = listenChannel.accept();
            sc.configureBlocking(false);
            sc.register(selector, SelectionKey.OP_READ);
            System.out.println(sc.getRemoteAddress() + "上线了");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            if (count > 0) {
                String s = new String(buffer.array());
                System.out.println("FROM 客户端" + s);
                forward(s, channel);
            } else {
                System.out.println(channel.getRemoteAddress());
            }

        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + "离线");
                key.cancel();
                channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    public void forward(String msg, SocketChannel self) throws IOException {
        System.out.println("转发消息中..............");
        for (SelectionKey key : selector.keys()) {
            Channel target = key.channel();
            if (target instanceof SocketChannel && target != self) {
                SocketChannel dest = (SocketChannel) target;
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                dest.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        GroupCharServer server=new GroupCharServer();
        server.listen();
    }
}
客户端
package com.kklll.leetcode.nio.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
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;

/**
 * @Author DeepBlue
 * @Date 2020/10/11 17:17
 */
public class GroupChatClient {
    private static final String HOST = "127.0.0.1";
    private static final int SERVER_PORT = 6667;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username = null;

    public GroupChatClient() {
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress(HOST, SERVER_PORT));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            username = socketChannel.socket().getLocalAddress().getHostAddress()+socketChannel.socket().getPort();
            System.out.println("客户端 IS OK");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void sendInfo(String msg) {
        msg = username + "说:" + msg;
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readInfo() {
        try {
            int select = selector.select();
            if (select > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer allocate = ByteBuffer.allocate(1024);
                        channel.read(allocate);
                        String s = new String(allocate.array());
                        System.out.println(s);
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        GroupChatClient groupChatClient = new GroupChatClient();
        new Thread(() -> {
            while (true) {
                groupChatClient.readInfo();
                try {
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            String next = sc.next();
            groupChatClient.sendInfo(next);
        }
    }
}

Netty架构

img

Netty服务端工作原理

Netty客户端工作原理

重要元素

(1)NioSocketChannel/NioServerSocketChannel

NioSocketChannel/NioServerSocketChannel可以理解成java中的Socket/ServerSocket。既可以输入又可以输出,可以是设置成阻塞和非阻塞。

一个NioSocketChannel对应一个连接,一个NioSocketChannel/NioServerSocketChannel对应一个Pipeline。

(2)NioEventLoopGroup

NioEventLoopGroup相当于是一个线程池,维护一个NioEventLoop[]数组。

NioEventLoopGroup可以设置线程个数,线程默认个数是NettyRuntime.availableProcessors() * 2。

对于Server来说,parent Group一般只有1个线程,child Group有NettyRuntime.availableProcessors() * 2个线程。
对于Client来说,一般也是一个线程。

每次有新Channel注册时,NioEventLoopGroup会按顺序选择一个NioEventLoop来处理这个Channel。对于服务端,NioServerSocketChannel是扔给parent Group 里唯一的线程处理的,而新接入的NioSocketChannel则是由child Group按顺序选择的NioEventLoop来处理。客户端类似。

(3)NioEventLoop

NioEventLoop相当于一个线程,一个NioEventLoop包含一个Selector。

NioEventLoop是用来执行任务的。主要包括3种类型的任务:定时任务、普通任务、IO任务。

在需要执行任务的时候,会启动NioEventLoop线程,NioEventLoop的run()实现如下:

// NioEventLoop
protected void run() {
    while(true) {
        switch(hasTasks() ? selector.selectNow() : -1) {
            case CONTINUE: //-2
                continue;
            case SELECT: //-1
                select(wakenUp.getAndSet(false));
                if(this.wakenUp.get()) {
                    selector.wakeup();
                }
            default:
                processSelectedKeys();
                runAllTasks();
                break;
        }
    }
}

1.select():
轮询是否有到点的定时任务普通任务IO任务可以执行。如果有任一种任务可执行就会跳出循环。如果任何任务都没有,则会调用selector.select(timeoutMillis)等待可执行的IO任务。

定时任务:PriorityQueue scheduledTaskQueue,按timeout时间排序
普通任务:LinkedBlockingQueue taskQueue
IO任务:SelectionKey

2.processSelectedKeys():
处理IO任务OP_CONNECT/OP_ACCEPT/OP_READ/OP_WRITE。

// NioEventLoop
public void processSelectedKeys() {
    for(int i = 0; i < selectedKeys.size; ++i) {
        SelectionKey k = selectedKeys.keys[i];
        AbstractNioChannel ch = k.attachment();
        NioUnsafe unsafe = ch.unsafe();
        
        //处理连接事件
        int readyOps = k.readyOps();
        if ((readyOps & OP_CONNECT) != 0) {
            unsafe.finishConnect();
        }
        
        //处理写事件
        if((readyOps & SelectionKey.OP_WRITE) != 0) {
            unsafe.forceFlush();
        }

        //处理读事件或处理NioServerSocketChannel的ACCEPT事件
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            unsafe.read();
        }
    }
}

3.runAllTasks():
处理到点的定时任务和普通任务。
对于到点的定时任务,会添加到普通任务队列,并执行普通任务队列的任务。

IO任务执行时间比例:ioRatio = 50
其他任务执行时间比例:100 - ioRatio
如果ioRatio设置为0的话,则是顺序执行,先执行完IO任务再执行完其他任务。

(4)Pipeline

Pipeline相当于一个管道,维护一个ChannelHandlerContext的双向链表,头结点是HeadContext,尾结点是TailContext。

HeadContext,既是ChannelInBoundHandler也是ChannelOutBoundHandler,是outBound类型。
TailContext,是ChannelInBoundHandler,是inBound类型。

当进行读操作,即数据从外部读到自己这边的Channel时,数据会先传给Pipeline,然后从HeadContext开始依次传给inBound类型的ChannelInBoundHandler,直到TailContext结束。
当进行写操作,即数据从自己这边的Channel往外部写时,数据会先传给Pipeline,然后从TailContext开始依次传给outBound类型的ChannelOutBoundHandler,直到HeadContext结束。

异常事件是从HeadContext往TailContext传递。

对于Client端:
除HeadContext和TailContext外,Pipeline中的handler还包括Bootstrap.handler()中的handler。

对于Server端:
除HeadContext和TailContext外,NioServerSocketChannel对应的Pipeline中的handler包括ServerBootstrap.handler()中的handler和ServerBootstrapAcceptor。 接入新连接后,NioSocketChannel对应的Pipeline中的handler包括ServerBootstrap.childHandler()中的childHandler

如果使用了ChannelInitializer这个handler的话,由于在它的initChannel()回调方法中又remove了它自己,所以最终对应的handler是initChannel()里的handler。因此ChannelInitializer仅仅起到初始化作用,不参与实际数据处理。

PS: 数据传递过程是一个链式传递,因为之前稍微看过OkHttp源码,有点类似。

(5)ChannelHandlerContext

unsafe:
NioServerSocketChannel对应NioMessageUnsafe NioSocketChannel对应NioByteUnsafe

HeadContext的工作:
1、通过unsafe处理bind/connect/read/write/flush/deregister/disconnect/close等具体事件
2、链式传递exceptionCaught/channelRegistered/channelUnregistered/channelActive/channelInactive/channelRead/channelReadComplete等事件。

TailContext的工作:
负责userEventTriggered/exceptionCaught/channelRead等一些异常捕获或未处理消息的日志提醒和资源释放工作。

用户自定义的ChannelHandlerContext可以选择是否将消息继续往下传递,默认是传递的,如果不传递将ctx.fireChannelXXX();这行代码覆盖掉即可。比如ServerBootstrapAcceptor在channelRead的时候就没有继续往下传。

传递的参数可以是Channel,比如在Server端监听到OP_ACCEPT事件时,ServerSocketChannel.accept()会得到一个NioSocketChannel,这个连接会放到list,然后经由ServerSocketChannel对应的Pipeline的channelRead递归传给ServerBootstrapAcceptor,NioSocketChannel进行一些初始化处理后再交由child Group进行下一步操作。


我的个人博客地址:https://dlddw.xyz/
所有文章均在个人博客中首发。欢迎小伙伴们访问和留言!


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值