【IO】Netty初探

6 篇文章 1 订阅


前言

在了解BIO和NIO后开始初探Netty的实现;Netty是基于NIO实现的高性能服务器,在多路复用器的基础上能够提供多client的快速响应。
Netty 是基于 Java NIO 的异步事件驱动的网络应用框架,使用 Netty 可以快速开发网络应用,Netty 提供了高层次的抽象来简化 TCP 和 UDP 服务器的编程


一、多线程下的多路复用

1.1 基础代码

场景描述
在server 和 client的链接当中,可以存在多个selector,对于每一个client或者server需要考虑将事件注册到哪个selector中
为什么使用多线程?
即使是NIO的场景,在selecotr()阻塞结束后的其他操作也都是顺序执行的,因此一旦某一次链接中执行的任务量过多导致长时间无法处理结束,那么整个selector便会受到影响,其他链接的消息也无法正常执行。
在这里插入图片描述
代码实现
既然是多线程环境,需要多个selector,因此使用Runnable创建线程
在这里插入图片描述

public class SelectorThread implements Runnable{

    // 多路复用的线程
    Selector selector = null;

    SelectorThread() {
        try {
            selector = Selector.open();   // 初始化一个selector
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            while (true) {
                int num = selector.select();  // 阻塞, 等待绑定事件

                if (num > 0) {  // 处理事件
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();

                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {  // 线性处理
                        SelectionKey key = iter.next();
                        iter.remove();  // 删除监听
                        if (key.isAcceptable()) {  // 多线程环境clien注册到哪个客户端
                            handleAcceptable(key);
                        } else if (key.isReadable()) {
                            handleReadable(key);
                        }
                    }
                }

                // 可以处理其他task
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void handleReadable(SelectionKey key) {
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        SocketChannel client = (SocketChannel) key.channel();  // 获取客户端的channel

        // 读数据
        buffer.clear();

        while (true) {
            try {
                int num = client.read(buffer);
                if (num > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                    buffer.clear();
                } else if (num == 0) {
                    break;
                } else {
                    System.out.println(client.getRemoteAddress() + " 链接关闭了");
                    key.cancel();  // 取消事件
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void handleAcceptable(SelectionKey key) {

        ServerSocketChannel server = (ServerSocketChannel) key.channel();

        try {
            SocketChannel client = server.accept();
            client.configureBlocking(false);  // 配置非阻塞

            // chooose a select and register



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

考虑:想要拥有一个多线程版本的selector集合,使用多线程创建即可;但是多路复用中涉及事件的触发,需要绑定到一个selector等其监听,因此对于每一个连接下的fd我们应该绑定到哪个selector上呢?

我们需要一个集合用来管理创建的selectors,可以通过轮询的方式为每一个fd分配selector

public class ThreadGroup {

    SelectorThread[] selectorThreads = null;   // selector的集合
    AtomicInteger ac = new AtomicInteger();  // 用来取模获取集合中的具体下标

    ThreadGroup(int num) {
        selectorThreads = new SelectorThread[num];

        for (int i = 0; i < num; i++) {
            selectorThreads[i] = new SelectorThread();  // 创建新的线程

            new Thread(selectorThreads[i]).start();  // 启动线程
        }

    }

    public SelectorThread next() {   // 选择一个selector
        // 分配一个Selector
        int index = ac.incrementAndGet() % selectorThreads.length;
        return selectorThreads[index];
    }

    public void bind(int port) {  // 用于主线程中绑定端口

        ServerSocketChannel server = null;
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));  // 绑定一个server端口

            System.out.println("server already run ...");

            // 分配server一个selector进行监听
            nextSelector(server);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    // 由于会为client, server都会分配selector,因此这里可以使用多态,父类:Channel
    private void nextSelector(Channel channel) {

        SelectorThread selectorThread = next();  // 分配server一个selector

        if (channel instanceof ServerSocketChannel) {  // 此时为serverSocketChannel,监听Acceptable事件
            ServerSocketChannel ss = (ServerSocketChannel) channel;
            try {

                selectorThread.selector.wakeup();  // 唤醒selector,避免阻塞
                ss.register(selectorThread.selector, SelectionKey.OP_ACCEPT);  // 为选定的selector上面注册接受链接的事件
                System.out.println("事件绑定结束");

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


    }
}

创建一个server用来等待链接请求, 主进程

public static void main(String[] args) {
        // 主线程,用来生产SelectorThread
        ThreadGroup group = new ThreadGroup(1);  // 定义一个容量num的线程组

        group.bind(9999);
    }

以上就是多路复用的代码结构


代码中wakeup的作用:当创建一个SelectorThread,在run()中会走到select()方法进行阻塞,只有当事件的数据准备完毕才会继续往后走。
不同于单线程版本,我们需要在选择一个selector后才会去考虑注册事件的操作,因此需要想办法先打破阻塞状态,然后为选定的selector绑定一个Acceptable事件。

selectorThread.selector.wakeup();  // 唤醒selector,避免阻塞

在这里插入图片描述
SelectorThread中的run()方法, 这里使用比较low的方式,用Thread.sleep的方式保证事件一定注册成功

while (true) {
  System.out.println(Thread.currentThread().getName() + " before connect ..." + selector.keys().size());
    int num = selector.select();  // 阻塞, 等待绑定事件
    Thread.sleep(1000);
    System.out.println(Thread.currentThread().getName() + " after connect ..." + selector.keys().size());

    if (num > 0) {  // 处理事件
        Set<SelectionKey> selectionKeys = selector.selectedKeys();

        Iterator<SelectionKey> iter = selectionKeys.iterator();
        while (iter.hasNext()) {  // 线性处理
            SelectionKey key = iter.next();
            iter.remove();  // 删除监听
            if (key.isAcceptable()) {  // 多线程环境clien注册到哪个客户端
                handleAcceptable(key);
            } else if (key.isReadable()) {
                handleReadable(key);
            }
        }
    }

    // 可以处理其他task
}

运行结果

在这里插入图片描述
这里使用Linux命令模拟客户端连接:

// 如果nc没有找到,先使用yum进行安装
yum install nc

// 模拟客户端
nc ip port   // 如: nc 192.168.xxx.xxx 9999

在这里插入图片描述


分割线,明天继续

1.2 划分角色的多路复用

场景 上述的Group角色划分不明确,每一个Group都可以接受Acceptable或读写操作,比较混杂;现在想分为Boss和Worker,Boss名下会有Worker,连接到Boss下的读写操作将交付给Worker处理

代码实现
首先会为每一个ThreadGroup定义角色,初始每一个ThreadGroup都标识Boss;
增加的代码:next1(), nextSelector1()

public class ThreadGroup {

    SelectorThread[] selectorThreads = null;
    AtomicInteger ac = new AtomicInteger();

    ThreadGroup worker = this;   // 工作的线程组

    ThreadGroup(int num) {
        selectorThreads = new SelectorThread[num];

        for (int i = 0; i < num; i++) {
            selectorThreads[i] = new SelectorThread(this);  // 创建新的线程

            new Thread(selectorThreads[i]).start();  // 启动线程
        }

    }

    public SelectorThread next() {
        // 分配一个Selector
        int index = ac.incrementAndGet() % selectorThreads.length;
        return selectorThreads[index];
    }

    // 由工作组进行分配进程,当处理的Channel为SocketChannel的时候由worker处理
    public SelectorThread next1() {
        // 分配一个Selector
        int index = worker.ac.incrementAndGet() % worker.selectorThreads.length;
        return worker.selectorThreads[index];
    }

    public void bind(int port) {

        ServerSocketChannel server = null;
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);  // 非阻塞
            server.bind(new InetSocketAddress(port));

            System.out.println("server already run ...");  // boss负责连接

            // 分配server一个selector进行监听
            nextSelector1(server);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    // 由于会为client, server都会分配selector,因此这里可以使用多态,父类:Channel
    public void nextSelector1(Channel channel) {

        if (channel instanceof ServerSocketChannel) {
            SelectorThread st = this.next();
            st.setWorker(worker);
            st.lbq.add(channel);
            st.selector.wakeup();
        } else if (channel instanceof  SocketChannel) {
            SelectorThread st = next1();
            st.lbq.add(channel);
            st.selector.wakeup();
        }


    }
    public void setWorker(ThreadGroup worker) {
        this.worker = worker;
    }
}

SelectorThread中的修改

在设计SelectorThread的时候,对于读事件的绑定会在监听到Acceptable的时候触发,因此需要重新定义一个成员变量ThearGroup,在这个里面指定selector

public class SelectorThread implements Runnable{

    // 多路复用的线程
    Selector selector = null;
    LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();
    ThreadGroup threadGroup = null;

    SelectorThread(ThreadGroup threadGroup) {
        try {
            this.threadGroup = threadGroup;
            selector = Selector.open();   // 初始化一个selector
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // 。。。
    }

    private void handleReadable(SelectionKey key) {
        // 。。。。
    }

    private void handleAcceptable(SelectionKey key) {

        ServerSocketChannel server = (ServerSocketChannel) key.channel();

        try {
            SocketChannel client = server.accept();
            client.configureBlocking(false);  // 配置非阻塞

            // chooose a select and register
            // 给客户端绑定一个selector
            this.threadGroup.nextSelector(client);  // 这里根据ThreadGroup进行划分


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

    public void setWorker(ThreadGroup worker) {
        this.threadGroup = worker;
    }
}

二、Netty的API使用【模拟S/C】

2.1 ByteBuf

相比于NIO中的ByteBuffer,Netty的ByteBuf的读写操纵更加方便,不需要进行指针的filp(), 使用单独的方法便可以直接操作。
使用默认分配的时候需要指定初始大小和最大上限;当初始大小满的时候会进行二倍扩容,直到达到最大上限。

@Test
public void testByteBuffer() {
    ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(8, 20);
    print(buffer);
    buffer.writeBytes(new byte[] {1, 2, 3, 4});
    print(buffer);
    buffer.writeBytes(new byte[] {1, 2, 3, 4});
    print(buffer);
    buffer.writeBytes(new byte[] {1, 2, 3, 4});
    print(buffer);
    buffer.writeBytes(new byte[] {1, 2, 3, 4});
    print(buffer);
    buffer.writeBytes(new byte[] {1, 2, 3, 4});
    print(buffer);

}

public void print(ByteBuf buffer) {
    System.out.println("buffer.isReadable():" + buffer.isReadable());
    System.out.println("buffer.readerIndex():" + buffer.readerIndex());
    System.out.println("buffer.readableBytes()" + buffer.readableBytes());
    System.out.println("buffer.isWritable():" + buffer.isWritable());
    System.out.println("buffer.writerIndex():" + buffer.writerIndex());
    System.out.println("buffer.capacity():" + buffer.capacity());
    System.out.println("buffer.maxCapacity():" + buffer.maxCapacity());
    System.out.println("buffer.isDirect()" + buffer.isDirect());
    System.out.println("-------------------------------------");
}

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

2.2 模拟Netty客户端

public void testNettyClient() {
    // 创建一个SocketChannel
    NioSocketChannel client = new NioSocketChannel();
    // 创建一个连接
    client.connect(new InetSocketAddress("192.168.189.130", 9090));
    // 生成一个指定字符串大小的buffer
    ByteBuf buffer = Unpooled.copiedBuffer("hello world".getBytes(StandardCharsets.UTF_8));
    client.writeAndFlush(buffer);
}

在这里插入图片描述
netty中的消息处理属于异步通信,因此为了保证连接,消息到成功处理,需要在某些地方等待数据回馈,即阻塞进程。

@Test
public void testNettyClient() throws InterruptedException {
    // 创建一个事件循环
    NioEventLoopGroup eventExecutors = new NioEventLoopGroup();

    // 创建一个SocketChannel
    NioSocketChannel client = new NioSocketChannel();

    // 在事件循环器上注册channel
    eventExecutors.register(client);

    // 绑定一个异步事件处理器,可以反馈服务端的响应消息
    ChannelPipeline pipeline = client.pipeline();
    // Inserts {@link ChannelHandler}s at the last position of this pipeline.
    pipeline.addLast(new ChannelInboundHandlerAdapter() {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf info = (ByteBuf) msg;
            // netty中没有NIO中的flip的方法,使用readable会把指针搜索,这样在没有翻转后的写操作便无法写入
//                CharSequence charSequence = info.readCharSequence(info.readableBytes(), StandardCharsets.UTF_8);
            CharSequence charSequence = info.getCharSequence(0, info.readableBytes(), StandardCharsets.UTF_8);
            System.out.println("server:" + charSequence);
            ByteBuf buffer = Unpooled.copiedBuffer("pong".getBytes(StandardCharsets.UTF_8));
            ctx.writeAndFlush(buffer);

        }
    });

    // 创建一个连接
    ChannelFuture connect = client.connect(new InetSocketAddress("192.168.189.130", 9090));
    // 阻塞等待异步连接建立
    connect.sync();
    System.out.println("connect already is created");
    // 生成一个指定字符串大小的buffer
    ByteBuf buffer = Unpooled.copiedBuffer("hello world".getBytes(StandardCharsets.UTF_8));
    ChannelFuture channelFuture = client.writeAndFlush(buffer);
    // 阻塞等待消息发送
    channelFuture.sync();

    System.out.println("infomation was send");

    connect.channel().closeFuture().sync();  // 可以在发送消息后处理一些其他操作,阻塞然后去执行
    System.out.println("client closed");
}

在这里插入图片描述

2.3 模拟Netty服务端

场景:模拟Netty的服务端接受,注意,服务端应该适用多连接

基础代码

@Test
public void testNettyServer() throws InterruptedException {
    // 4. 发生报错,没有event loop
    NioEventLoopGroup eventExecutors = new NioEventLoopGroup(1);

    // 1.创建一个serverSocket
    NioServerSocketChannel server = new NioServerSocketChannel();

    // 5. 将server注册到eventExecutor上,分配一个线程处理
    eventExecutors.register(server);

    // 7. 定义一个异步处理器
    ChannelPipeline pipeline = server.pipeline();
    pipeline.addLast(new ServerInHandler(eventExecutors));

    // 2.绑定一个端口
    ChannelFuture connect = server.bind(new InetSocketAddress(9090));
    // 3. 等待连接建立
    connect.sync();
    System.out.println("server connect");

    // 6. 响应式的可以阻塞在这里,然后server可以通过popline()处理一些其他操作,比如通信
    // 当客户端发送消息的时候,应该像写客户端的时候配置一个pipline(),定义一个处理器
    connect.channel().closeFuture().sync();
}

自定义类

class ServerInHandler extends ChannelInboundHandlerAdapter {


    private final NioEventLoopGroup evenExecutors;

    public ServerInHandler(NioEventLoopGroup eventExecutors) {
        this.evenExecutors = eventExecutors;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 8. 当一个连接建立的时候,客户端发送过来的msg应该是他自己的Channel,可以从这里面获取信息
        /*
            之前写NIO的时候,写过:SocketChannel scChnnel = ssChannel.accept(); 用于接受客户端的连接
            那么在netty的时候,什么时候处理的accept, 非阻塞怎么设置的
         */
        System.out.println("channelRead ...");
        SocketChannel client = (SocketChannel) msg;

        // 9. 模仿客户端的写法,在获取client的channel后,我们应该为这个连接添加一些处理器

        /*
            写完这步之后,发现连接建立后,客户端不能正常通信?
            问题:
                1. 我是用eventExecutor给server注册了accept事件,能够正常监听;到那时我在handler中获取的client是
                哪个selector监听的 -》 没有注册
                2. 然后我们目前注册了一个eventExecutor,所以应该成员变量获取
         */
        // 10. 给client注册一个selector
        this.evenExecutors.register(client);

        ChannelPipeline pipeline = client.pipeline();
        pipeline.addLast(new MyInHandler());
    }
}


class MyInHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf info = (ByteBuf) msg;
        /* netty中没有NIO中的flip的方法,使用readable会把指针搜索,这样在没有翻转后的写操作便无法写入
            CharSequence charSequence = info.readCharSequence(info.readableBytes(), StandardCharsets.UTF_8);
         */
        CharSequence charSequence = info.getCharSequence(0, info.readableBytes(), StandardCharsets.UTF_8);
        System.out.println("server:" + charSequence);
//        ByteBuf buffer = Unpooled.copiedBuffer("pong".getBytes(StandardCharsets.UTF_8));
        ctx.writeAndFlush(info);

    }
}

在这里插入图片描述
在这里插入图片描述
错误模拟:实现MyInHandler的共享错误问题,将MyInHandler的创建通过传参决定

在这里插入图片描述

class ServerInHandler extends ChannelInboundHandlerAdapter {


    private final NioEventLoopGroup evenExecutors;
    private final ChannelHandler handler;


    public ServerInHandler(NioEventLoopGroup eventExecutors, MyInHandler myInHandler) {
        this.evenExecutors = eventExecutors;
        this.handler = myInHandler;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 8. 当一个连接建立的时候,客户端发送过来的msg应该是他自己的Channel,可以从这里面获取信息
        /*
            之前写NIO的时候,写过:SocketChannel scChnnel = ssChannel.accept(); 用于接受客户端的连接
            那么在netty的时候,什么时候处理的accept, 非阻塞怎么设置的
         */
        System.out.println("channelRead ...");
        SocketChannel client = (SocketChannel) msg;

        // 9. 模仿客户端的写法,在获取client的channel后,我们应该为这个连接添加一些处理器

        /*
            写完这步之后,发现连接建立后,客户端不能正常通信?
            问题:
                1. 我是用eventExecutor给server注册了accept事件,能够正常监听;到那时我在handler中获取的client是
                哪个selector监听的 -》 没有注册
                2. 然后我们目前注册了一个eventExecutor,所以应该成员变量获取
         */
        // 10. 给client注册一个selector
        this.evenExecutors.register(client);

        ChannelPipeline pipeline = client.pipeline();
        pipeline.addLast(handler);

        super.channelRead(ctx, msg);
    }
}

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

思考:MyInHandler是用来处理读写操作的,那一定是用户自定义的,当业务场景复杂的时候,我可以在读写操作中添加一些计数器等成员变量,但是这玩意为什么能被共享呢? 并且我总不能要求用户定义的时候不准定义变量吧。。。

所以呢,我们可以把他进一步封装,像我最开始没有演示出错误那样,由用户自定义才好;但是我上面自定义的时候是写到 ServerInHandler 里了,因此有了下面的逻辑

在这里插入图片描述

@ChannelHandler.Sharable
class ChannelInit extends  ChannelInboundHandlerAdapter {

    private final NioEventLoopGroup evenExecutors;

    public ChannelInit(NioEventLoopGroup eventExecutors) {
        this.evenExecutors = eventExecutors;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 			{
        System.out.println("channelRead ...");
        SocketChannel client = (SocketChannel) msg;

        this.evenExecutors.register(client);

        ChannelPipeline pipeline = client.pipeline();
        pipeline.addLast(new MyInHandler());
        pipeline.remove(this);
    }
}

在这里插入图片描述

三、Netty的API使用【Netty的Boostrap】

3.1 客户端实现

可以看前面模拟的客户端,整体思路都是一样的;这里的ChannelInitializer就是上面实现的ChannelInit类,都是起到中间者的作用,避免共享hanler问题

/**
*  Netty API情况下的client实现
*/
@Test
public void testNettyClient() throws InterruptedException {
   Bootstrap bs = new Bootstrap();
   NioEventLoopGroup group = new NioEventLoopGroup();  // 1.创建一个事件循环 event loop
   ChannelFuture connect = bs.group(group)
           .channel(NioSocketChannel.class)  // 指定监听的channel类型,这里是客户端使用SocketChannel
           .handler(new ChannelInitializer<SocketChannel>() {  // 绑定Handler
               @Override
               protected void initChannel(SocketChannel ch) throws Exception {
                   ChannelPipeline pipeline = ch.pipeline();
                   pipeline.addLast(new MyInHandler());
               }
           })
           .connect(new InetSocketAddress("192.168.189.130", 9090));  // 绑定端口

   Channel client = connect.sync().channel();
   ByteBuf buffer = Unpooled.copiedBuffer("hello server".getBytes(StandardCharsets.UTF_8));
   ChannelFuture channelFuture = client.writeAndFlush(buffer);
   channelFuture.sync();

   connect.channel().closeFuture().sync();
}
class MyInHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf info = (ByteBuf) msg;
        CharSequence charSequence = info.getCharSequence(0, info.readableBytes(), StandardCharsets.UTF_8);
        System.out.println("server:" + charSequence);
        ctx.writeAndFlush(info);

    }
}

3.2 服务端实现

@Test
public void testNettyServer() throws InterruptedException {
    ServerBootstrap bs = new ServerBootstrap();
    NioEventLoopGroup group = new NioEventLoopGroup();
    ChannelFuture connect = bs.group(group)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {

                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    System.out.println(ch.remoteAddress().getAddress() + ":" + ch.remoteAddress().getPort() + " connect...");
                    ChannelPipeline pipeline = ch.pipeline();
                    pipeline.addLast(new MyInHandler());
                }
            })
            .bind(new InetSocketAddress("192.168.189.1", 9090));
    ChannelFuture sync = connect.sync();
    System.out.println("connect already created");
    connect.channel().closeFuture().sync();
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值