netty 学习

普通阻塞式 socket (BIO)

  • client

通过 java.net.Socket 来获取流

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class SocketClient {

    public static void main(String[] args) throws IOException {
        Socket sock = new Socket("localhost", 6666); // 连接指定服务器和端口
        try (InputStream input = sock.getInputStream();
                OutputStream output = sock.getOutputStream()) {
            handle(input, output);
        }
        sock.close();
        System.out.println("disconnected.");
    }

    private static void handle(InputStream input, OutputStream output) throws IOException {
        BufferedWriter writer =
                new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
        BufferedReader reader =
                new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        Scanner scanner = new Scanner(System.in);
        System.out.println("[server] " + reader.readLine());
        for (; ; ) {
            System.out.print(">>> "); // 打印提示
            String s = scanner.nextLine(); // 读取一行输入
            writer.write(s);
            writer.newLine();
            writer.flush();
            String resp = reader.readLine();
            System.out.println("<<< " + resp);
            if (resp.equals("bye")) {
                break;
            }
        }
    }
}
  • server
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

@SuppressWarnings("all")
public class SocketServer {

    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(6666); // 监听指定端口
        System.out.println("server is running...");
        for (; ; ) {
            Socket sock = ss.accept();
            System.out.println("connected from " + sock.getRemoteSocketAddress());
            Thread t = new Handler(sock);
            t.start();
        }
    }

    static class Handler extends Thread {
        Socket sock;

        public Handler(Socket sock) {
            this.sock = sock;
        }

        @Override
        public void run() {
            try (InputStream input = this.sock.getInputStream()) {
                try (OutputStream output = this.sock.getOutputStream()) {
                    handle(input, output);
                }
            } catch (Exception e) {
                try {
                    this.sock.close();
                } catch (IOException ignored) {
                }
            }
            System.out.println("client disconnected.");
        }

        private void handle(InputStream input, OutputStream output) throws IOException {
            BufferedWriter writer =
                    new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
            BufferedReader reader =
                    new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
            writer.write("hello\n");
            writer.flush();
            for (; ; ) {
                String s = reader.readLine();
                if (s.equals("bye")) {
                    writer.write("bye\n");
                    writer.flush();
                    break;
                }
                writer.write("ok: " + s + "\n");
                writer.flush();
            }
        }
    }
}

Buffer

@Slf4j
public class TestFileChannelTransferTo {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        try (final FileChannel from = new FileInputStream("from.txt").getChannel();
                final FileChannel to = new FileOutputStream("to.txt").getChannel()) {
            // 剩余 未传输的 字节数
            long size = from.size();
            // Question: 上限:最大一次传输 2G 数据,超出不会被传输

            // from.transferTo(0, from.size(), to); // before: FileChannel#transferTo

            for (long remainder = size; remainder > 0; ) { // after: use for when bigger 2 GB
                long position = size - remainder; // 全部大小 - 剩余字节树
                System.out.println("position:  " + position + " remainder:" + remainder);
                // 比 输入输出流 效率高,底层使用操作系统的 零拷贝 进行优化
                remainder -= from.transferTo(position, remainder, to); // transferTo 返回实际传输的字节数
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        log.info("---time cost: " + (System.currentTimeMillis() - start) + " 毫秒---");
    }

		@Test
    public void traverse() throws IOException {
        // 这里不能使用 局部变量 int, 因为 内部类使用局部变量 必须都是 final 的,java1.8后可以不写final
        final AtomicInteger dirCount = new AtomicInteger();
        final AtomicInteger fileCount = new AtomicInteger();
        // SimpleFileVisitor uses the visitor pattern.
        Files.walkFileTree(
                Paths.get("E:\\aio1"),
                new SimpleFileVisitor<Path>() {
                    @Override
                    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                            throws IOException {
                        System.out.println(">>> 进入 目录 " + dir);
                        dirCount.incrementAndGet();
                        return super.preVisitDirectory(dir, attrs);
                    }
                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                            throws IOException {
                        // if (file.toString().endsWith(".jar")) // 加判断
                        System.out.println(file);
                        // Files.delete(file); // =================== 删除文件
                        fileCount.incrementAndGet();
                        return super.visitFile(file, attrs);
                    }
                    @Override
                    public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                            throws IOException {
                        // Files.delete(dir); // =================== 删除文件夹
                        System.out.println("<<< 退出 目录 " + dir);
                        return super.postVisitDirectory(dir, exc);
                    }
                });
        System.out.println("文件夹:" + dirCount);
        System.out.println("文件数:" + fileCount);
    }
  
    @Test
    public void testCopyDirAll() throws IOException {
        String source = "E:\\aio1";
        String target = "E:\\aio2";
        // 同样也是遍历
        try (Stream<Path> traverseStream = Files.walk(Paths.get(source))) {
            traverseStream.forEach(
                    path -> {
                        String targetName = path.toString().replace(source, target);
                        try {
                            Path currentPath = Paths.get(targetName);
                            if (Files.isDirectory(path)) { // 如果是 目录 创建
                                Files.createDirectories(currentPath);

                            } else if (Files.isRegularFile(path)) { // 如果是 文件 复制
                                Files.copy(path, currentPath);
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    });
        }
    }
}

阻塞vs非阻塞

  1. 阻塞:

    1. 单线程下, 阻塞方法相互影响, 不能正常工作
    2. 多线程下, 线程数多了, cpu 占用高, 上下文切换成本高; 线程池虽然减少了线程数但只能用短连接. 都是治标不治本;
  2. 非阻塞:

    1. 在 ServerSocketChannel.accept 没有连接建立时返回 null , 继续运行
    2. SocketChannel.read 在没有数据可读时, 会返回 0, 会一直循环, 做的只是等待而已.
    3. 但线程不断运行的背后, 白白浪费了 cpu 资源.
    4. 数据复制的过程中, 线程实际上还是阻塞的
  3. 多路复用

    1. 单线程配合 Selector 完成对多个 Channel 可读写事件的监控.
      1. 在非阻塞的基础上引入事件的概念. 表现在未分配时阻塞, 有事件就放行
      2. 多路复用仅针对网络IO、普通文件IO 没办法利用多路复用

select 何时不阻塞

  • 事件发生时
    • 客服端发起连接: accept
    • client send data, normal close & exception close 都会触发: > read; when data > buffer缓冲区, 会触发多次读取事件
    • channel 可写, 触发 write 事件
    • in linux, nio bug 发生时也不阻塞
  • 调用 selector.wakeup()
  • 调用 selector.close()
  • selector 所在线程 interrupt
  1. 阻塞
    在这里插入图片描述
    在这里插入图片描述

  2. 非阻塞
    在这里插入图片描述

  3. 多路复用
    在这里插入图片描述
    在这里插入图片描述

  4. 信号驱动

以上都是同步: 线程自己去获取结果

  1. 异步非阻塞(异步没有阻塞的) 通过回调来通知后续操作, 推送结果 (至少两个线程)

Netty

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

1、什么是Netty

Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients

Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端

注意netty的异步还是基于多路复用的,并没有实现真正意义上的异步IO

2、Netty的优势

如果使用传统NIO,其工作量大,bug 多

  • 需要自己构建协议
  • 解决 TCP 传输问题,如粘包、半包
  • 因为bug的存在,epoll 空轮询导致 CPU 100%

Netty 对 API 进行增强,使之更易用,如

  • FastThreadLocal => ThreadLocal
  • ByteBuf => ByteBuffer

二、案例

1、服务器端代码

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;

/**
 * Server
 *
 * @author howeres
 * @version 2022/6/3
 */
@Slf4j
public class Server {

    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup(2);
        try {
            ServerBootstrap server = new ServerBootstrap();
            server.channel(NioServerSocketChannel.class);
            server.group(boss, worker);
            server.childHandler(new ChannelInitial());
            ChannelFuture future = server.bind(8080).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

    static class ChannelInitial extends ChannelInitializer<NioSocketChannel> {
        @Override
        protected void initChannel(NioSocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
            pipeline.addLast(new HttpServerCodec());
            pipeline.addLast(
                    new SimpleChannelInboundHandler<HttpRequest>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg)
                                throws Exception {
                            log.info("uri is {}", msg.uri());
                            DefaultFullHttpResponse response =
                                    new DefaultFullHttpResponse(
                                            msg.protocolVersion(), HttpResponseStatus.OK);
                            byte[] bytes = "<h1>Hello world/~</h1>".getBytes();
                            response.headers().setInt(CONTENT_LENGTH, bytes.length);
                            response.content().writeBytes(bytes);
                            ctx.writeAndFlush(response);
                        }
                    });
        }
    }
}

2、客户端代码

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

/**
 * Client
 *
 * @author howeres
 * @version 2022/6/3
 */
@Slf4j
public class Client {

    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap client = new Bootstrap();
            client.channel(NioSocketChannel.class);
            client.group(worker);
            client.handler(new ChannelInitial());
            ChannelFuture future = client.connect("localhost", 8080).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
        }
    }

    static class ChannelInitial extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(new LoggingHandler());
            pipeline.addLast(
                    new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            byte[] LINE = {'\n', '\r'};
                            ByteBuf buffer = ctx.alloc().buffer(); // allocate 分配 调拨
                            buffer.writeBytes("*3".getBytes()).writeBytes(LINE);
                            buffer.writeBytes("$3".getBytes()).writeBytes(LINE);
                            buffer.writeBytes("set".getBytes()).writeBytes(LINE);
                            buffer.writeBytes("$4".getBytes()).writeBytes(LINE);
                            buffer.writeBytes("name".getBytes()).writeBytes(LINE);
                            buffer.writeBytes("$7".getBytes()).writeBytes(LINE);
                            buffer.writeBytes("howeres".getBytes()).writeBytes(LINE);
                            ctx.writeAndFlush(buffer);
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg)
                                throws Exception {
                            ByteBuf buf = (ByteBuf) msg;
                            log.info("msg: {}", buf.toString(StandardCharsets.UTF_8));
                            super.channelRead(ctx, msg);
                        }
                    });
        }
    }
}

源码

启动流程

Selector selector = Selector.open();

  1. AbstractBootstrap#doBind 步骤一: initAndRegister() #1 并切换线程返回了 regFuture 处理

    1. init main线程
      • 创建 NioServerSocketChannel, 在构造器里初始化 mian线程
        ServerSocketChannel ssc = ServerSocketChannel.open();
      • 添加 NioServerSocketChannel 初始化 handler main线程
        • 初始化 handler 等待调用 (nio-thread线程 调用)
          向 nio ssc 加入了 acceptor handler 此处仅添加未调用 (在 accept 事件发生后建立连接)
    2. register (切换线程) initAndRegister() #2 // config().group().register(channel);
      • 启动 nio boss 线程 main线程
      • 原生 java ssc 注册至 selector (未关注事件) AbstractNioChannel#doRegister nio-thread线程
        SelectionKey selectionKey = ssc.register(selector, 0, nettySsc);
      • AbstractUnsafe#register0 执行 NioServerSocketChannel 初始化 handler // pipeline.invokeHandlerAddedIfNeeded(); nio-thread线程
      • safeSetSuccess(promise); 塞回结果 promise == regFuture
    3. 得到结果后 AbstractBootstrap#doBind 设置监听函数进行回调 (新开了一个Nio线程执行NioServerSocketChannel 的 channel#bind )
  2. regFuture 的回调 doBind0 nio-thread线程

  • NioServerSocketChannel#doBind 判断jdk版本是否大于7, 再让 原生ServerSocketChannel 绑定端口 nio-thread线程
     ssc.bind(new InetSocketAddress(8080, backlog));
  • 触发 NioServerSocketChannel active 事件 pipeline.fireChannelActive(); nio-thread线程
    断点 HeadContext#channelActive 关注下一步, 主要目的是注册 ACCEPT 事件
     selectionKey.interestOps(SelectionKey.OP_ACCEPT);

经过初始化的 channel 处于激活状态, 包含的pipeline 为:

head -> acceptor -> tail

channel 不具备业务功能, 真正干活的都是流水线上的 handler;

accept 流程

  1. selector.select() 阻塞直到事件发生
  2. 遍历处理 selectedKeys
  3. 拿到一个 key, 判断事件类型是否为 accept
  4. 创建 SocketChannel, 设置非阻塞 // unsafe.read() -> NioMessageUnsafe#read
    NioServerSocketChannel#doReadMessages(List<Object> readBuf) 将 accept 得到的SocketChannel 初始化成 new NioSocketChannel(this, ch)//并设置为非阻塞, 最后当成一个消息放进 readBuf
    然后使用父 NioServerSocketChannelpipeline.fireChannelRead(readBuf.get(i)); 挨个去调用;
    ServerBootstrapAcceptor#channelRead 方法, 为刚刚新建的 NioSocketChannel 去设置 option参数, 再执行到 group.register() // 让 Group 里面的 selector 对新创建的 channel 进行监听
    判断当前线程是不是inEventLoop(是EventLoop线程就直接注册, 不是就新开一个) 保证是在新channel 的 loop里面操作
  5. 将 SocketChannel 注册至 selector
    sc.register(eventLoop的选择器, 0 , NioSocketChannel)
    调用 NioSocketChannel 上的初始化器
  6. 关注 selectionKey 的 read 事件

read 流程

  1. selector.select() 阻塞直到事件发生
  2. 遍历处理 selectedKeys
  3. 拿到一个 key, 判断事件类型是否为 read NioEventLoop#697 AbstractNioByteChannel#139
  4. 读取操作 AbstractNioByteChannel#148 #163

NioEventLoop

NioEventLoop 重要组成部分: selector、线程 Thread & Executor、**任务队列 ** Queue<Runnable> (在祖父类 SingleThreadEventExecutor 里面)、定时任务 PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue; (在曾祖父类 AbstractScheduledEventExecutor.class 里面)

NioEventLoop 即会处理 io 事件, 也会处理普通任务和定时任务

  1. selector 何时创建 NioEventLoop 构造器里面 openSelector(); // 放在 unwrappedSelector

    • 1.1 selector 为何有两个 selector 成员
      // 替换 selectedKeys 的 Set 集合实现为数组(SelectedSelectionKeySet 继承抽象的 AbstractSet 再组合一个 SelectionKey[] keys), 为了在遍历 selectedKeys 时提高性能
  2. nio线程在何时启动
    首次调用 execute 方法时
    通过 状态位STATE_UPDATER 控制线程只启动一次

  3. 提交普通任务会结束selector阻塞 // 会wakeup(bool inEventLoop) 判断提交线程和 NIO线程是不是同一个. 不是才会下一步

    1. wakeup 方法中的代码如何理解 !inEventLoop // 只有其他线程提交才会wakeup
    2. wakeUp变量的作用是什么 // 原子变量, 只能通过CAS的方式设置值
      因为selector.wakeup() 是一个重量级操作, 避免多个线程频繁调用(只要其中一个调用成功就行)
  4. 每次循环, 什么时候会进入 SelectStrategy.SELECT 分支 NioEventLoop#run
    没有任务时, 才会进入 SelectStrategy.SELECT
    当有任务时, selectSupplier.get() 会 selectNow() (没拿到任务会返回0), 拿到任务连同任务一起交给下面的逻辑运行

    1. 何时会select阻塞, 阻塞多久? NioEventLoop#select
      timeoutMillis-超时时间, selectDeadLineNanos-截止时间=当前时间+1s + 0.5ms 再转化为毫秒 (除开定时任务)
  5. nio空轮询bug在哪里体现, 如何解决 (bug 出在 jdk 在 linux 下的 selector)
    重新创建了一个selector selectRebuildSelector 要把原来的selectionKey都复制回去

  6. ioRatio控制什么, 设置为 100 有何作用?
    ioRatio控制处理io事件所占用的时间比例, 默认 50%
    ioRatio=80 ioTime=8s 运行时间=ioTime* (100-80)/80 = 2s
    如果 ioRatio 设为100, 会特殊处理, 两者全部执行,
    try { processSelectedKeys } finally { runAllTasks }

  7. selecedKeys 优化是怎么回事?
    processSelectedKeys ->
    processSelectedKeysOptimized else processSelectedKeysPlain;
    XXOptimized 使用下标的方式获取 selectedKeys.keys[i] 并置为空
    通过 attachment 拿到 channel

  8. 在哪里区分不同事件类型
    processSelectedKeysOptimized 调用的 processSelectedKey 方法

EventLoop 是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)

结构中有两个线程:

  1. 一个负责程序本身的运行,称为"主线程";
  2. 另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")

任务队列

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
  2. 主线程之外,存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 不断重复第三步

“任务队列” EventQueue是一个事件的队列, FIFO的数据结构, 只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

回调函数"(callback),就是那些会被主线程挂起来的代码

只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

  • javascript 中的 EventLoop
    在这里插入图片描述

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行

  • 以下是借用 node.js 的 EventLoop概念
    在这里插入图片描述

优化

  • MASK

@Skip 注解的使用, 利用注解的继承, 得知重写的注解不会附带父类的注解, 也就不会被跳过. 相反没有被重写的 handler 就会被快速跳过, 不会被 ChannelPipeline 对象调用.

  • 数组

在 NioEventLoop 构造器中对 selector.selectedKeys() 进行了优化, 为了遍历把 Set 优化成 数组. 并在 processSelectedKeysOptimized 方法中, 使用下标的方式获取 selectedKeys.keys[i] 再置为空.

Reference

Essential Technologies for Java Developers: I/O and Netty domestic
Essential Technologies for Java Developers: I/O and Netty overseas
Imooc wiki

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值