Netty 源码分析系列(十八)一行简单的writeAndFlush都做了哪些事?

news2024/9/22 5:23:51

文章目录

    • 前言
    • 源码分析
      • ctx.writeAndFlush 的逻辑
      • writeAndFlush 源码
      • ChannelOutBoundBuff 类
      • addMessage 方法
      • addFlush 方法
      • AbstractNioByteChannel 类
    • 小结


前言

对于使用netty的小伙伴来说,我们想通过服务端往客户端发送数据,通常我们会调用ctx.writeAndFlush(数据)的方式。那么它都执行了那些行为呢,是怎么将消息发送出去的呢。

源码分析

下面的这个方法是用来接收客户端发送过来的数据,通常会使用ctx.writeAndFlush(数据)来向客户端发送数据。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    System.out.println(" 接收到消息:" + msg);
    String str = "服务端收到:" + new Date() + msg;
    ctx.writeAndFlush(str);
}

ctx.writeAndFlush 的逻辑

private void write(Object msg, boolean flush, ChannelPromise promise) {

    //...

    AbstractChannelHandlerContext next = this.findContextOutbound(flush ? 98304 : '耀');
    Object m = this.pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }
    } else {
        Object task;
        if (flush) {
            task = AbstractChannelHandlerContext.WriteAndFlushTask.newInstance(next, m, promise);
        } else {
            task = AbstractChannelHandlerContext.WriteTask.newInstance(next, m, promise);
        }

        if (!safeExecute(executor, (Runnable)task, promise, m)) {
            ((AbstractChannelHandlerContext.AbstractWriteTask)task).cancel();
        }
    }

}

从上述源码我们可以知道,WriteAndFlush()相对于Write(),它的flush字段是true。

write:将需要写的 ByteBuff 存储到 ChannelOutboundBuffer中。

flush:从ChannelOutboundBuffer中将需要发送的数据读出来,并通过 Channel 发送出去。

writeAndFlush 源码

public ChannelFuture writeAndFlush(Object msg) {
    return this.writeAndFlush(msg, this.newPromise());
}

public ChannelPromise newPromise() {
    return new DefaultChannelPromise(this.channel(), this.executor());
}

writeAndFlush方法里提供了一个默认的 newPromise()作为参数传递。在Netty中发送消息是一个异步操作,那么可以通过往hannelPromise中注册回调监听listener来得到该操作是否成功。

在发送消息时添加监听

ctx.writeAndFlush(str,ctx.newPromise().addListener(new ChannelFutureListener(){
    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception{
        channelFuture.isSuccess();
    }
}));

继续向下一层跟进代码,AbstractChannelHandlerContext中的invokeWriteAndFlush的源码。

private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
    if (this.invokeHandler()) {
        this.invokeWrite0(msg, promise);
        this.invokeFlush0();
    } else {
        this.writeAndFlush(msg, promise);
    }

}

从上述源码我们可以能够知道:

1、首先通过invokeHandler()判断通道处理器是否已添加到管道中。

2、执行消息处理 invokeWrite0方法:

  • 首先将消息内容放入输出缓冲区中 invokeFlush0;
  • 然后将输出缓冲区中的数据通过socket发送到网络中。

分析invokeWrite0执行的内容,源码如下:

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler)this.handler()).write(this, msg, promise);
    } catch (Throwable var4) {
        notifyOutboundHandlerException(var4, promise);
    }

}

((ChannelOutboundHandler)this.handler()).write是一个出站事件ChannelOutboundHandler,会由ChannelOutboundHandlerAdapter处理。

@Skip
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ctx.write(msg, promise);
}

接下来会走到ChannelPipeline中,来执行网络数据发送;我们来看DefaultChannelPipeline 中HeadContextwrite方法源码

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    this.unsafe.write(msg, promise);
}

unsafe是构建NioServerSocketChannelNioSocketChannel对象时,一并构建一个成员属性,它会完成底层真正的网络操作等。

我们跟进HenderContext的write() ,而HenderContext的中依赖的是unsafe.wirte()。所以直接去 AbstractChannel的Unsafe 源码如下:

public final void write(Object msg, ChannelPromise promise) {
    this.assertEventLoop();
    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {// 缓存 写进来的 buffer
        this.safeSetFailure(promise, this.newWriteException(AbstractChannel.this.initialCloseCause));
        ReferenceCountUtil.release(msg);
    } else {
        int size;
        try {
            // buffer Dirct化 , (我们查看 AbstractNioByteBuf的实现)
            msg = AbstractChannel.this.filterOutboundMessage(msg);
            size = AbstractChannel.this.pipeline.estimatorHandle().size(msg);
            if (size < 0) {
                size = 0;
            }
        } catch (Throwable var6) {
            this.safeSetFailure(promise, var6);
            ReferenceCountUtil.release(msg);
            return;
        }
        //  插入写队列  将 msg 插入到 outboundBuffer
        //  outboundBuffer 这个对象是 ChannelOutBoundBuff 类型的,它的作用就是起到一个容器的作用
        //  下面看, 是如何将 msg 添加进 ChannelOutBoundBuff 中的
        outboundBuffer.addMessage(msg, size, promise);
    }
}

从上述源码中,我们可以看出,首先调用 assertEventLoop 确保该方法的调用是在reactor线程中;然后,调用 filterOutboundMessage() 方法,将待写入的对象过滤。下面我们来看看filterOutboundMessage方法的源码。

protected final Object filterOutboundMessage(Object msg) {
    if (msg instanceof ByteBuf) {
        ByteBuf buf = (ByteBuf)msg;
        return buf.isDirect() ? msg : this.newDirectBuffer(buf);
    } else if (msg instanceof FileRegion) {
        return msg;
    } else {
        throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg) + 																											EXPECTED_TYPES);
    }
}

从上述源码可以看出,只有ByteBuf以及 FileRegion可以进行最终的Socket网络传输,其他类型的数据是不支持的,会抛UnsupportedOperationException异常。并且会把堆 ByteBuf 转换为一个非堆的 ByteBuf 返回。也就说,最后会通过socket传输的对象时非堆的 ByteBuf 和 FileRegion。

在发送数据时,我们需要估算出需要写入的 ByteBuf 的size,我们来看看 DefaultMessageSizeEstimator 的HandleImpl类中的size()方法。

public final class DefaultMessageSizeEstimator implements MessageSizeEstimator {

    private static final class HandleImpl implements Handle {
        private final int unknownSize;

        private HandleImpl(int unknownSize) {
            this.unknownSize = unknownSize;
        }

        public int size(Object msg) {
            if (msg instanceof ByteBuf) {
                return ((ByteBuf)msg).readableBytes();
            } else if (msg instanceof ByteBufHolder) {
                return ((ByteBufHolder)msg).content().readableBytes();
            } else {
                return msg instanceof FileRegion ? 0 : this.unknownSize;
            }
        }
    }
}

通过ByteBuf.readableBytes()判断消息内容大小,估计待发送消息数据的大小,如果是FileRegion的话直接返回0,否则返回ByteBuf中可读取字节数。

接下来我们来看看是如何将 msg 添加进 ChannelOutBoundBuff 中的。

ChannelOutBoundBuff 类

ChannelOutboundBuffer类主要用于存储其待处理的出站写请求的内部数据。当 Netty 调用 write时数据不会真正地去发送而是写入到ChannelOutboundBuffer 缓存队列,直到调用 flush方法 Netty 才会从ChannelOutboundBuffer取数据发送。每个 Unsafe 都会绑定一个ChannelOutboundBuffer,也就是说每个客户端连接上服务端都会创建一个 ChannelOutboundBuffer 绑定客户端 Channel。

观察 ChannelOutBoundBuff 源码,可以看到以下四个属性:

public final class ChannelOutboundBuffer {

    //...
    
    private ChannelOutboundBuffer.Entry flushedEntry;
    private ChannelOutboundBuffer.Entry unflushedEntry;
    private ChannelOutboundBuffer.Entry tailEntry;
    private int flushed;
    
    //...
    
}
  1. flushedEntry :指针表示第一个被写到操作系统Socket缓冲区中的节点;

  2. unFlushedEntry:指针表示第一个未被写入到操作系统Socket缓冲区中的节点;

  3. tailEntry:指针表示ChannelOutboundBuffer缓冲区的最后一个节点。

  4. flushed:表示待发送数据个数。

下面分别是三个指针的作用,示意图如下:

image-20210922154632809

  1. flushedEntry 指针表示第一个被写到操作系统Socket缓冲区中的节点;
  2. unFlushedEntry指针表示第一个未被写入到操作系统Socket缓冲区中的节点;
  3. tailEntry指针表示ChannelOutboundBuffer缓冲区的最后一个节点。

初次调用 addMessage 之后,各个指针的情况为:

image-20210922155035351

fushedEntry指向空,unFushedEntrytailEntry 都指向新加入的节点。第二次调用 addMessage之后,各个指针的情况为:

image-20210922155156086

第n次调用 addMessage之后,各个指针的情况为:

image-20210922155428422

可以看到,调用n次addMessageflushedEntry指针一直指向NULL,表示现在还未有节点需要写出到Socket缓冲区。

ChannelOutboundBuffer 主要提供了以下方法:

  • addMessage方法:添加数据到对列的队尾;
  • addFlush方法:准备待发送的数据,在 flush 前需要调用;
  • nioBuffers方法:用于获取待发送的数据。在发送数据的时候,需要调用该方法以便拿到数据;
  • removeBytes方法:发送完成后需要调用该方法来删除已经成功写入TCP缓存的数据。

addMessage 方法

addMessage 方法是系统调用write方法时调用,源码如下。

public void addMessage(Object msg, int size, ChannelPromise promise) {
    ChannelOutboundBuffer.Entry entry = ChannelOutboundBuffer.Entry.newInstance(msg, size, total(msg), promise);
    if (this.tailEntry == null) {
        this.flushedEntry = null;
    } else {
        ChannelOutboundBuffer.Entry tail = this.tailEntry;
        tail.next = entry;
    }

    this.tailEntry = entry;
    if (this.unflushedEntry == null) {
        this.unflushedEntry = entry;
    }

    this.incrementPendingOutboundBytes((long)entry.pendingSize, false);
}

上述源码流程如下:

  • 将消息数据包装成 Entry 对象;
  • 如果对列为空,直接设置尾结点为当前节点,否则将新节点放尾部;
  • unflushedEntry为空说明不存在暂时不需要发送的节点,当前节点就是第一个暂时不需要发送的节点;
  • 将消息添加到未刷新的数组后,增加挂起的节点。

这里需要重点看看第一步将消息数据包装成 Entry 对象的方法。

static final class Entry {
    private static final Recycler<ChannelOutboundBuffer.Entry> RECYCLER = new Recycler<ChannelOutboundBuffer.Entry>() {
        protected ChannelOutboundBuffer.Entry newObject(Handle<ChannelOutboundBuffer.Entry> handle) {
            return new ChannelOutboundBuffer.Entry(handle);
        }
    };

    // ...

    static ChannelOutboundBuffer.Entry newInstance(Object msg, int size, long total, ChannelPromise promise) {
        ChannelOutboundBuffer.Entry entry = (ChannelOutboundBuffer.Entry)RECYCLER.get();
        entry.msg = msg;
        entry.pendingSize = size + ChannelOutboundBuffer.CHANNEL_OUTBOUND_BUFFER_ENTRY_OVERHEAD;
        entry.total = total;
        entry.promise = promise;
        return entry;
    }

    // ...

}

其中Recycler类是基于线程本地堆栈的轻量级对象池。这意味着调用newInstance方法时 ,并不是直接创建了一个 Entry 实例,而是通过对象池获取的。

下面我们看看incrementPendingOutboundBytes方法的源码。

private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
    if (size != 0L) {
        // TOTAL_PENDING_SIZE_UPDATER 当前缓存中 存在的代写的 字节
        // 累加
        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
        // 判断 新的将被写的 buffer的容量不能超过  getWriteBufferHighWaterMark() 默认是 64*1024  64字节
        if (newWriteBufferSize > (long)this.channel.config().getWriteBufferHighWaterMark()) {
            // 超过64 字节,进入这个方法
            this.setUnwritable(invokeLater);
        }

    }
}

在每次添加新的节点后都调用incrementPendingOutboundBytes((long)entry.pendingSize, false)方法,这个方法的作用是设置写状态,设置怎样的状态呢?我们看它的源码,可以看到,它会记录下累计的ByteBuf的容量,一旦超出了阈值,就会传播channel不可写的事件。

addFlush 方法

addFlush 方法是在系统调用 flush 方法时调用的,addFlush 方法的源码如下。

public void addFlush() {
    ChannelOutboundBuffer.Entry entry = this.unflushedEntry;
    if (entry != null) {
        if (this.flushedEntry == null) {
            this.flushedEntry = entry;
        }

        do {
            ++this.flushed;
            if (!entry.promise.setUncancellable()) {
                int pending = entry.cancel();
                this.decrementPendingOutboundBytes((long)pending, false, true);
            }

            entry = entry.next;
        } while(entry != null);

        this.unflushedEntry = null;
    }

}

以上方法的主要功能就是暂存数据节点变成待发送节点,即flushedEntry 指向的节点到unFlushedEntry指向的节点(不包含 unFlushedEntry)之间的数据。

上述源码的流程如下:

  • 先获取unFlushedEntry指向的暂存数据的起始节点;
  • 将待发送数据起始指针flushedEntry 指向暂存起始节点;
  • 通过promise.setUncancellable()锁定待发送数据,并在发送过程中取消,如果锁定过程中发现其节点已经取消,则调用entry.cancel()取消节点发送,并减少待发送的总字节数。

下面我们看看decrementPendingOutboundBytes方法的源码。

private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) {
    if (size != 0L) {
        // 每次 减去 -size
        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
            //  默认 getWriteBufferLowWaterMark() -32kb
    		//  newWriteBufferSize<32 就把不可写状态改为可写状态
        if (notifyWritability && newWriteBufferSize < (long)this.channel.config().getWriteBufferLowWaterMark()) {
            this.setWritable(invokeLater);
        }
    }
}

AbstractNioByteChannel 类

在这个类中,我们主要看doWrite(ChannelOutboundBuffer in)方法,源码如下。

protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    int writeSpinCount = this.config().getWriteSpinCount();

    do {
        Object msg = in.current();
        if (msg == null) {
            this.clearOpWrite();
            return;
        }

        writeSpinCount -= this.doWriteInternal(in, msg);
    } while(writeSpinCount > 0);

    this.incompleteWrite(writeSpinCount < 0);
}

通过一个无限循环,保证可以拿到所有的节点上的ByteBuf,通过这个函数获取节点,Object msg = in.current();
我们进一步看它的实现,如下,它只会取出我们标记的节点。

public Object current() {
    ChannelOutboundBuffer.Entry entry = this.flushedEntry;
    return entry == null ? null : entry.msg;
}

下面我们看下doWriteInternal(in, msg)的方法源码。

private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        ByteBuf buf = (ByteBuf)msg;
        if (!buf.isReadable()) {
            in.remove();
            return 0;
        }

        int localFlushedAmount = this.doWriteBytes(buf);
        if (localFlushedAmount > 0) {
            in.progress((long)localFlushedAmount);
            if (!buf.isReadable()) {
                in.remove();
            }

            return 1;
        }
    } else {
        if (!(msg instanceof FileRegion)) {
            throw new Error();
        }

        FileRegion region = (FileRegion)msg;
        if (region.transferred() >= region.count()) {
            in.remove();
            return 0;
        }

        long localFlushedAmount = this.doWriteFileRegion(region);
        if (localFlushedAmount > 0L) {
            in.progress(localFlushedAmount);
            if (region.transferred() >= region.count()) {
                in.remove();
            }

            return 1;
        }
    }

    return 2147483647;
}

使用 jdk 的自旋锁,循环16次,尝试往 jdk 底层的ByteBuffer中写数据,调用函数doWriteBytes(buf);他具体的实现是客户端 channel 的封装类NioSocketChannel实现的源码如下:

protected int doWriteBytes(ByteBuf buf) throws Exception {
    int expectedWrittenBytes = buf.readableBytes();
    // 将字节数据, 写入到 java 原生的 channel中
    return buf.readBytes(this.javaChannel(), expectedWrittenBytes);
}

这个readBytes()依然是抽象方法,因为前面我们曾经把从 ByteBuf 转化成了 Dirct 类型的,所以它的实现类是PooledDirctByteBuf 继续跟进如下:

public int readBytes(GatheringByteChannel out, int length) throws IOException {
    this.checkReadableBytes(length);
    // 关键的就是 getBytes()  跟进去
    int readBytes = this.getBytes(this.readerIndex, out, length, true);
    this.readerIndex += readBytes;
    return readBytes;
}


private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException {
    this.checkIndex(index, length);
    if (length == 0) {
        return 0;
    } else {
        ByteBuffer tmpBuf;
        if (internal) {
            tmpBuf = this.internalNioBuffer();
        } else {
            tmpBuf = ((ByteBuffer)this.memory).duplicate();
        }

        index = this.idx(index);
        // 将netty 的 ByteBuf 塞进 jdk的 ByteBuffer tmpBuf;
        tmpBuf.clear().position(index).limit(index + length);
        // 调用jdk的write()方法
        return out.write(tmpBuf);
    }
}

被使用过的节点会被remove()掉, 源码如下。

private void removeEntry(ChannelOutboundBuffer.Entry e) {
    if (--this.flushed == 0) { // 如果是最后一个节点,把所有的指针全部设为null
        this.flushedEntry = null;
        if (e == this.tailEntry) {
            this.tailEntry = null;
            this.unflushedEntry = null;
        }
    } else { // 如果不是最后一个节点, 把当前节点,移动到最后的节点
        this.flushedEntry = e.next;
    }

}

小结

1、调用write方法并没有将数据写到 Socket 缓冲区中,而是写到了一个单向链表的数据结构中,flush才是真正的写出。

2、writeAndFlush等价于先将数据写到netty的缓冲区,再将netty缓冲区中的数据写到Socket缓冲区中,写的过程与并发编程类似,用自旋锁保证写成功。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/564482.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SURF算法详解

Speeded Up Robust Features&#xff08;SURF&#xff0c;加速稳健特征&#xff09; 一&#xff0e;积分图像 1.什么是积分图像 积分图像是输入的灰度图像经过一种像素间的累加运算得到种新的图像媒介。对于一幅灰度的图像&#xff0c;积分图像中的任意一点&#xff08;x,y&…

【投毒情报】PyPI中 colorara 等组件包泄漏主机截屏等敏感信息

漏洞描述 PyPI仓库中受影响版本的 colorara 和 libida组件在安装过程中会根据不同操作系统分别执行恶意逻辑&#xff0c;针对Windows执行White Snake远控木马&#xff0c;针对Linux收集系统截屏、主机名、用户名、IP等主机敏感信息发送至telegram。 漏洞名称PyPI中 colorara …

大数据可视化大屏电子沙盘合集

大数据可视化电子沙盘 使用HTML、CSS、JavaScript&#xff0c;实现的可视化大数据电子沙盘 如果觉得对你有用&#xff0c;随手点个 &#x1f31f; Star &#x1f31f; 支持下&#xff0c;这样才有持续下去的动力&#xff0c;谢谢&#xff01;&#xff5e; 体验地址&#xff0…

一文告诉你黑盒测试、白盒测试、集成测试和系统测试的区别与联系

于开发人员来说&#xff0c;往往对各种测试方法感到疑惑。特别是在整合代码的时候&#xff0c;我们就能深刻感觉受到测试的重要性。很多开发人员只注重写代码&#xff0c;轻视测试的重要性。总是代码一写完提交然后就交给测试组测试了&#xff0c;没多久测试组发回测试报告。然…

java 在线音乐网站系统Myeclipse开发mysql数据库struts2结构java编程计算机网页项目

一、源码特点 java 在线音乐网站系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助struts2开发技术&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mys…

二叉树初阶和堆的详解

前言&#xff1a;二叉树是一种基础数据结构&#xff0c;它由节点和边构成&#xff0c;其中每个节点最多只有两个子节点&#xff0c;称为左子节点和右子节点。二叉树具有许多应用&#xff0c;例如搜索算法和排序算法&#xff0c;还可以用于创建堆等高级数据结构。 堆是一种基于完…

一次完整的性能测试,测试人员需要做哪些工作?

今天和大家讲一下完成性能测试&#xff0c;测试人员需要做哪些工作&#xff1f;接下来一菲用四个步骤妥妥的教会你&#xff0c;啥叫完整的性能测试&#xff0c;请看好了呀&#xff01; 一.流程概述 1.规范流程的意义 规范的性能测试实施流程能够加强测试工作流程控制&#x…

Unity3D安装:从命令行安装 Unity

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 从命令行安装 Unity 如果要在组织中自动部署 Unity&#xff0c;可以从命令行安装 Editor 和其他组件。这些组件是普通的安装程序可执行程序和软件包&#xff0c;可以给用来自动部署…

【MySQL新手到通关】第四章 排序与分页

文章目录 &#x1f43c;1. 排序数据&#x1fa82;&#x1fa82;1.1 排序规则&#x1fa82;&#x1fa82;1.2 单列排序&#x1fa82;&#x1fa82;1.3 多列排序 &#x1f43c;2. 分页&#x1fa82;&#x1fa82;2.1 背景&#x1fa82;&#x1fa82;2.2 实现规则&#x1fa82;&am…

实用交互设计工具大盘点

近年来&#xff0c;页面交互设计有了很好的发展&#xff0c;越来越受到人们的重视。如果你想成为一名页面交互设计师&#xff0c;除了对平面设计和产品设计有一定的了解外&#xff0c;更重要的是要知道哪个软件适合页面交互设计。本文将带您了解5款流行的页面交互设计软件。 1…

一题都看不懂,大厂面试真的变态......

最近我的一个读者朋友去了字节面试&#xff0c;来给我发信息吐槽&#xff0c;说字节的面试太困难了&#xff0c;像他这种三年经验的测试员&#xff0c;在技术面&#xff0c;居然一题都答不上来&#xff0c;这要多高的水平才能有资格去面试字节的测试岗位。 确实&#xff0c;字…

Hudi(三)集成Flink

1、环境准备 将编译好的jar包放到Flink的lib目录下。 cp hudi-flink1.13-bundle-0.12.0.jar /opt/module/flink-1.13.2/lib 2、sql-client方式 2.1、修改flink-conf.yaml配置 vim /opt/module/flink-1.13.2/conf/flink-conf.yamlstate.backend: rocksdb execution.checkpoi…

SpringCloud Gateway高级应用

目录 1 SpringCloud技术栈1.1 SpringCloud技术栈1.2 SpringCloud经典技术介绍1.3 SpringCloud项目场景 2 SpringCloud Gateway2.1 Gateway工作原理2.2 Gateway路由2.2.1 业务说明2.2.2 基于配置路由设置2.2.3 基于代码路由配置2.2.4 Gateway-Predicate2.2.5 断言源码剖析 2.3 G…

Settings apk进行系统签名覆盖安装

由于AndroidStudio中Settings编译出来的包是未签名的,不能将设备覆盖安装替换原先签名的包,故需要将AndroidStudio打包出来的apk进行签名 一.拷贝未签名的apk 注意签名过程需要在ubuntu中进行,所以需要将未签名的apk拷贝到ubuntu中,如下: 二.拷贝libconscrypt_openjd…

Sketch文件用什么软件打开

现在&#xff0c;想要在线打开 Sketch 文件只需要 2 步就能搞定了&#xff01; 第一步&#xff0c;访问Windows 也能用的「协作版 Sketch」——即时设计官网并点击免费使用&#xff0c;即可进入即时设计工作台。 第二步&#xff0c;进入即时设计工作台后&#xff0c;点击【文件…

【软件分析/静态分析】学习笔记01——Introduction

&#x1f517; 课程链接&#xff1a;李樾老师和谭天老师的&#xff1a;南京大学《软件分析》课程01&#xff08;Introduction&#xff09;_哔哩哔哩_bilibili 目录 一、静态程序分析介绍 1.1 PL and Static Analysis 程序语言和静态分析 1.2 为什么要学 Static Analysis? …

JavaScript 基础 DOM (三)

日期对象 实例化 获得当前时间 const date new Date() 获得指定时间 const date1 new Date( 指定时间) 方法 // 1. 实例化const date new Date();// 2. 调用时间对象方法// 通过方法分别获取年、月、日&#xff0c;时、分、秒const year date.getFullYear(); // 四位年份 时…

JDK8以后接口的新特性

JDK8以前&#xff0c;接口内只能定义抽象方法&#xff1b; JDK8&#xff0c;接口内允许定义默认方法、静态方法&#xff1b; JDK9&#xff0c;接口内允许定义私有方法 default&#xff1a;默认方法 public interface Essay01 {/*** 在接口内定义默认方法*/public default v…

CMU - FarPlanning 代码速读

https://github.com/MichaelFYang/far_planner https://www.cmu-exploration.com/ 系统结构 Far Planner 属于 High-level planning module&#xff0c;进行全局规划&#xff0c;找到可行路径&#xff1b;将 way_point发布给 Local planner和 path following KeyPoint Local-la…

帮公司面了个要21K的测试,结果.....

深耕IT行业多年&#xff0c;我们发现&#xff0c;对于一个程序员而言&#xff0c;能去到一线互联网公司&#xff0c;会给我们以后的发展带来多大的影响。 很多人想说&#xff0c;这个我也知道&#xff0c;但是进大厂实在是太难了&#xff0c;简历投出去基本石沉大海&#xff0…