从源码全面解析 dubbo 消费端服务调用的来龙去脉

news2024/9/21 12:37:38
  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

在这里插入图片描述

文章目录

    • 一、引言
    • 二、服务调用流程
      • 1、消费端
        • 1.1 动态代理的回调
        • 1.2 过滤器
        • 1.3 路由逻辑
        • 1.4 重试次数
        • 1.5 负载均衡
          • 1.4.1 自定义负载均衡
        • 1.6 调用服务
          • 1.6.1 配置 RPCinvocation
          • 1.6.2 调用 RPC 同步返回结果
          • 1.6.3 等待返回结果
    • 三、流程
    • 四、总结

一、引言

对于 Java 开发者而言,关于 dubbo ,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 dubbo 源码解析系列文章,将带你领略 dubbo 源码的奥秘

本期源码文章吸收了之前 SpringKakfaJUC源码文章的教训,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!

废话不多说,发车!

二、服务调用流程

1、消费端

上一篇文章,讲解了我们的消费端如何订阅我们服务端注册到 Zookeeper 的服务接口:从源码全面解析 dubbo 服务订阅的来龙去脉

既然消费端已经知道了我们的服务信息,那么下一步就要开始正式调用了

我们先从消费端聊聊服务调用的流程

1.1 动态代理的回调

我们聊到消费端订阅服务时,最终创建的代码如下:

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
    return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}

相信看过 动态代理 的小伙伴应该知道,当我们调用 代理 的接口时,实际上走的是 InvokerInvocationHandler 该类的 invoke 方法

public Object invoke(Object proxy, Method method, Object[] args){
    // 获取方法名=getUserById
    String methodName = method.getName();
    // 获取参数
    Class<?>[] parameterTypes = method.getParameterTypes();
    
    // 组装成 RpcInvocation 进行调用
    RpcInvocation rpcInvocation = new RpcInvocation(serviceModel, method.getName(), invoker.getInterface().getName(), protocolServiceKey, method.getParameterTypes(), args);
    
    // 执行调用方法
    return InvocationUtil.invoke(invoker, rpcInvocation);
}

这里我们重点介绍下 RpcInvocation 的几个参数:

  • serviceModel(Consumer):决定了服务的调用方式,包括使用哪种协议、注册中心获取服务列表、负载均衡和容错策略等。
  • method.getNamegetUserById
  • invoker.getInterface().getNamecom.common.service.IUserService
  • protocolServiceKeycom.common.service.IUserService:dubbo
  • method.getParameterTypes:方法的入参类型(Long)
  • args:方法的入参值(2)

我们继续往下看 InvocationUtil.invoke 做了什么

public static Object invoke(Invoker<?> invoker, RpcInvocation rpcInvocation) throws Throwable {
    URL url = invoker.getUrl();
    String serviceKey = url.getServiceKey();
    rpcInvocation.setTargetServiceUniqueName(serviceKey);
    
    return invoker.invoke(rpcInvocation).recreate();
}

// 判断当前的是应用注册还是接口注册
public Result invoke(Invocation invocation) throws RpcException {
    if (currentAvailableInvoker != null) {
        if (step == APPLICATION_FIRST) {
            if (promotion < 100 && ThreadLocalRandom.current().nextDouble(100) > promotion) {
                return invoker.invoke(invocation);
            }
            return decideInvoker().invoke(invocation);
        }
        return currentAvailableInvoker.invoke(invocation);
    }
}

我们继续往下追源码

1.2 过滤器

// 过滤器责任链模式
// 依次遍历,执行顺序:
public interface FilterChainBuilder {
    public Result invoke(Invocation invocation) throws RpcException {
        Result asyncResult;
        InvocationProfilerUtils.enterDetailProfiler(invocation, () -> "Filter " + filter.getClass().getName() + " invoke.");
        asyncResult = filter.invoke(nextNode, invocation);
    }
}

这里会依次遍历所有的 filter

  • ConsumerContextFilter:将消费者端的信息(远程地址、应用名、服务名)传递给服务提供者端
  • ConsumerClassLoaderFilter:将消费者端的ClassLoader传递给服务提供者端,以便服务提供者端可以在调用时使用相同的ClassLoader加载类。
  • FutureFilter:异步调用
  • MonitorFilter:统计服务调用信息(调用次数、平均响应时间、失败次数)
  • RouterSnapshotFilter:动态路由,它可以根据路由规则选择服务提供者,并缓存路由结果,以提高性能。

具体每个过滤器怎么实现的,这里就不展开讲了,后面有机会单独出一章

1.3 路由逻辑

当我们的责任链完成之后,下一步会经过我们的 路由 逻辑

public Result invoke(final Invocation invocation) throws RpcException {
    // 
    List<Invoker<T>> invokers = list(invocation);
    InvocationProfilerUtils.releaseDetailProfiler(invocation);

    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);

    return doInvoke(invocation, invokers, loadbalance);      
}

其中 List<Invoker<T>> invokers = list(invocation) 这里就是我们的路由逻辑:

List<Invoker<T>> invokers = list(invocation);

public List<Invoker<T>> list(Invocation invocation) throws RpcException {
    List<Invoker<T>> routedResult = doList(availableInvokers, invocation);
}

public List<Invoker<T>> doList(BitList<Invoker<T>> invokers, Invocation invocation) {
    // 这里就是我们的路由策略!!!
    List<Invoker<T>> result = routerChain.route(getConsumerUrl(), invokers, invocation);
    return result == null ? BitList.emptyList() : result;
}

这里的路由策略比较多,我举两个比较经典的:

  • simpleRoute(简单路由策略):默认的路由策略

  • routeAndPrint(自定义路由策略):我们可以自定义其路由逻辑

而对于整体路由的流程:

  • 获取可用的服务提供者列表
  • 过滤出符合条件的服务提供者
  • 对过滤后的服务提供者列表进行排序
  • 得到符合规定的服务提供者信息

到这里,我们路由会把符合要求的 服务端 给筛选出来,接下来就进入我们的负载均衡环节了

1.4 重试次数

这里我们设置 retries 为 5

@DubboReference(protocol = "dubbo", timeout = 100, retries = 5)
private IUserService iUserService;

我们看下源码里面有几次调用:根据源码来看,我们会有 5+1 次调用

int len = calculateInvokeTimes(methodName);
for (int i = 0; i < len; i++) {}

private int calculateInvokeTimes(String methodName) {
    // 获取当前的重试次数+1
    int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
    RpcContext rpcContext = RpcContext.getClientAttachment();
    Object retry = rpcContext.getObjectAttachment(RETRIES_KEY);
    if (retry instanceof Number) {
        len = ((Number) retry).intValue() + 1;
        rpcContext.removeAttachment(RETRIES_KEY);
    }
    if (len <= 0) {
        len = 1;
    }

    return len;
}

我们直接 Debug 一下看看:

image-20230612235945732

1.5 负载均衡

这一行 LoadBalance loadbalance = initLoadBalance(invokers, invocation) 得到我们的负载均衡策略,默认情况下如下:

image-20230612223930762

我们可以看到,默认情况下是 RandomLoadBalance 随机负载。

我们继续往下追源码:

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) {
    
    List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
    Set<String> providers = new HashSet<String>(len);
    for (int i = 0; i < len; i++) {
        // 如果是重新调用的,要去更新下Invoker,防止服务端发生了变化
        if (i > 0) {
            checkWhetherDestroyed();
            copyInvokers = list(invocation);
            // 再次校验
            checkInvokers(copyInvokers, invocation);
        }
        // 负载均衡逻辑!!!
        Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
        invoked.add(invoker);
        RpcContext.getServiceContext().setInvokers((List) invoked);
        boolean success = false;
        try {
            Result result = invokeWithContext(invoker, invocation);
            success = true;
            return result;
        } 
    }
}

这里我简单将下负载均衡的逻辑:

Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);

private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected){
    // 如果只有一个服务端,那还负载均衡个屁
    // 直接校验下OK不OK直接返回就好
    if (invokers.size() == 1) {
        Invoker<T> tInvoker = invokers.get(0);
        checkShouldInvalidateInvoker(tInvoker);
        return tInvoker;
    }
    // 如果多个服务端,需要执行负载均衡算法
    Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);
    return invoker;
}

Dubbo 里面的负载均衡算法如下:

image-20230612225319739

这里也就不一介绍了,正常情况下,我们采用的都是 RandomLoadBalance 负载均衡

当然这里博主介绍另外一个写法,也是我们业务中使用的

1.4.1 自定义负载均衡

上面我们看到,通过 LoadBalance loadbalance = initLoadBalance(invokers, invocation) ,我们可以得到一个负载均衡的实现类

在我们的生产场景中,不同的集群上含有不同的合作方,我们需要根据合作方去分发不同集群的调用

这个时候,我们可以重写我们的 LoadBalance ,在里面重写我们 doSelect 的逻辑,而这里的 集群A 也就是我们的 group

image-20230612232552377

1.6 调用服务

当我们完成下面的流程:过滤器 —> 路由 —> 重试 —> 负载均衡,就到了下面这行:

Result result = invokeWithContext(invoker, invocation)

我们继续往下追:

public Result invoke(Invocation invocation) throws RpcException {
    try {
        // 加读写锁
        lock.readLock().lock();
        return invoker.invoke(invocation);
    } finally {
        lock.readLock().unlock();
    }
}

我们直接追到 AbstractInvokerinvoke 方法

public Result invoke(Invocation inv) throws RpcException {
    RpcInvocation invocation = (RpcInvocation) inv;

    // 配置RPCinvocation
    prepareInvocation(invocation);

    // 调用RPC同时同步返回结果
    AsyncRpcResult asyncResult = doInvokeAndReturn(invocation);

    // 等待返回结果
    waitForResultIfSync(asyncResult, invocation);

    return asyncResult;
}

我们可以看到,对于调用服务来说,一共分为一下三步:

  • 配置 RPCinvocation
  • 调用 RPC 同步返回结果
  • 等待返回结果
1.6.1 配置 RPCinvocation

这里主要将 Invocation 转变成 RPCInvocation

  • 设置 RpcInvocationInvoker 属性,指明该调用是由哪个 Invoker 发起的
  • 当前线程的一些状态信息
  • 同步调用、异步调用
  • 异步调用生成一个唯一的调用 ID
  • 选择序列化的类型
private void prepareInvocation(RpcInvocation inv) {
    // 设置 RpcInvocation 的 Invoker 属性,指明该调用是由哪个 Invoker 发起的
    inv.setInvoker(this);
    
	// 当前线程的一些状态信息
    addInvocationAttachments(inv);

    // 同步调用、异步调用
    inv.setInvokeMode(RpcUtils.getInvokeMode(url, inv));

    // 异步调用生成一个唯一的调用 ID
    RpcUtils.attachInvocationIdIfAsync(getUrl(), inv);

    // 选择序列化的类型
    Byte serializationId = CodecSupport.getIDByName(getUrl().getParameter(SERIALIZATION_KEY, DefaultSerializationSelector.getDefaultRemotingSerialization()));
    if (serializationId != null) {
        inv.put(SERIALIZATION_ID_KEY, serializationId);
    }
}
1.6.2 调用 RPC 同步返回结果
private AsyncRpcResult doInvokeAndReturn(RpcInvocation invocation) {
    asyncResult = (AsyncRpcResult) doInvoke(invocation);
}

protected Result doInvoke(final Invocation invocation){
    // 获取超时时间
    int timeout = RpcUtils.calculateTimeout(getUrl(), invocation, methodName, DEFAULT_TIMEOUT);
   
    // 设置超时时间
    invocation.setAttachment(TIMEOUT_KEY, String.valueOf(timeout));
    
    // 从dubbo线程池中拿出一个线程
    ExecutorService executor = getCallbackExecutor(getUrl(), inv);
    // request:进行调用
	CompletableFuture<AppResponse> appResponseFuture = currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
    FutureContext.getContext().setCompatibleFuture(appResponseFuture);
    AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
    result.setExecutor(executor);
    return result;
}

这里的 currentClient.request 进行请求的发送:

public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor){
    return client.request(request, timeout, executor);
}

public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor){
    Request req = new Request();
    req.setVersion(Version.getProtocolVersion());
    req.setTwoWay(true);
    req.setData(request);
    DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout, executor);
    channel.send(req);
    return future;
}

这里的 channel.send(req)dubbo 自己包装的 channel,我们去看看其实现

当然,我们这里如果看过博主 Netty 源码文章的话,实际可以猜到,肯定是封装了 Nettychannel

public void send(Object message, boolean sent) throws RemotingException {
        // 校验当前的Channel是否关闭
        super.send(message, sent);

        boolean success = true;
        int timeout = 0;
        try {
            // channel 写入并刷新
            // channel:io.netty.channel.Channel
            ChannelFuture future = channel.writeAndFlush(message);
            if (sent) {
                // 等待超时的时间
                // 超过时间会报错
                timeout = getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
                success = future.await(timeout);
            }
            // 这里如果报错了,就会走重试的逻辑
            Throwable cause = future.cause();
    }
}
1.6.3 等待返回结果
waitForResultIfSync(asyncResult, invocation);

private void waitForResultIfSync(AsyncRpcResult asyncResult, RpcInvocation invocation) {
    // 判断当前的调用是不是同步调用
    // 异步调用直接返回即可
    if (InvokeMode.SYNC != invocation.getInvokeMode()) {
        return;
    }
    
    // 获取超时时间 
    Object timeoutKey = invocation.getObjectAttachmentWithoutConvert(TIMEOUT_KEY);
    long timeout = RpcUtils.convertToNumber(timeoutKey, Integer.MAX_VALUE);

    // 等待timeout时间
    // 获取失败-直接抛出异常
    asyncResult.get(timeout, TimeUnit.MILLISECONDS);
}

public Result get(long timeout, TimeUnit unit){
    // 获取响应返回的数据-等待timeout时间
    return responseFuture.get(timeout, unit);
}

如果没有异常,如下图所示:

image-20230618160337104

到这里我们的消费端调用服务的整个流程源码剖析就完毕了~

三、流程

高清图片可私聊博主

在这里插入图片描述

四、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

如果你也对 后端架构和中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

往期文章推荐:

  • 美团二面:聊聊ConcurrentHashMap的存储流程
  • 从源码全面解析Java 线程池的来龙去脉
  • 从源码全面解析LinkedBlockingQueue的来龙去脉
  • 从源码全面解析 ArrayBlockingQueue 的来龙去脉
  • 从源码全面解析ReentrantLock的来龙去脉
  • 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
  • 从源码全面解析 ThreadLocal 关键字的来龙去脉
  • 从源码全面解析 synchronized 关键字的来龙去脉
  • 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

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

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

相关文章

3D 顶点着色与Phong 反射模型

Phong 反射模型有时被称为“Phong 照明”或“Phong 照明”。它由环境光照、漫反射&#xff08;朗伯反射&#xff09;、镜面反射三部分组成。 Phong 反射模型提供了一个方程式&#xff0c;用于计算表面上每个点的光照&#xff0c;I_p&#xff1a; 第一部分代表环境光项。在GLSL代…

X书hmac参数

被删重新发送 全文可以查看&#xff1a; 上面一遍unidbg解密shield文章 unidbg - 》 callObjectMethodV方法填写你的小红书路径下s.xml里的值: 或者在抓包响应头中: 查找xy-ter-str hmac 结果都是在&#xff0c;响应头里&#xff0c;所以 hmac 是服务器下发给客户端的. Over…

黑马头条.

文章目录 前言一、项目概述1.1 能收获什么1.2 项目概述1.3 项目术语1.4 业务说明 二、技术栈2.1技术栈整体框架图2.2技术栈简介 三、nacos环境搭建3.1 虚拟机镜像准备3.2 nacos的安装 四、初始工程搭建4.1 开发环境准备 五、实现登录功能5.1 需求分析5.2 表结构分析5.3 思路分析…

22道常见RocketMQ面试题以及答案

面试宝典到手&#xff0c;搞定面试&#xff0c;不再是难题&#xff0c;系列文章传送地址&#xff0c;请点击本链接。 1、RocketMQ是什么? 2、RocketMQ有什么作用&#xff1f; 3、RoctetMQ的架构 4、RoctetMQ的优缺点 8、消息过滤,如何实现&#xff1f; 9、消息去重,如果…

Elasticsearch 基本使用(四)聚合查询

聚合查询 概述单字段聚合查询统计分组后的数量非文档字段分组文档字段分组 其他聚合运算统计平均值统计总金额统计最大值自定义聚合结果排序简单聚合小结 多字段聚合查询 概述 说到聚合查询&#xff0c;马上会想到 SQL 中的 group by&#xff0c;ES中也有类似的功能&#xff0…

编程语言发展历史

文章目录 语言的发展时间轴语言世代时间轴1940年前-机器语言时代1940年后-汇编语言时代1950年-高级语言的初生1960年-高级语言的进一步成熟1980年-各大语言的进一步增强1990年代-飞速发展时代2000年-新时代 高级编程语言的分类解释型与编译型面向过程与面向对象 对语言的评价Ti…

【学习学习】NLP理解层次模型

NLP&#xff08;Neuro-Linguistic Programming&#xff0c;神经语言程序学&#xff09;&#xff0c;由两位美国人理查得.班德勒&#xff08;Richard Bandler&#xff09;与约翰.葛瑞德&#xff08;John Grinder&#xff09;于1976年创办&#xff0c;并在企业培训中广泛使用。美…

PyTorch 深度学习 || 4. 自编码网络 | Ch4.3 卷积自编码网络图像去噪

卷积自编码网络图像去噪 1. 数据的准备 先简单介绍一下训练网络使用到的图像数据集——STL10&#xff0c;该数据集可以通过torchvision.datasets模块中的STL10()函数进行下载&#xff0c;该数据集共包含三种类型数据,分别是带有标签的训练集和验证集&#xff0c;分别包含5000…

Cookie增删改查方法封装(低内存开销版)

本文章中的低内存开销是指在获取cookie的时候不进行字符串—>数组的转变&#xff0c;全程使用sliceindexOf切割字符串&#xff0c;不创建和操作数组&#xff0c;节约内存&#xff0c;本文代码已存放到github中&#xff0c;后续会持续完善功能&#xff0c;传送门&#xff1a;…

二进制方式部署kubernetes集群

二进制方式部署kubernetes集群 1、部署k8s常见的几种方式 1.1 kubeadm Kubeadm 是一个 k8s 部署工具&#xff0c;提供 kubeadm init 和 kubeadm join&#xff0c;用于快速部署 Kubernetes 集群。 Kubeadm 降低部署门槛&#xff0c;但屏蔽了很多细节&#xff0c;遇到问题很难…

掌握Python的X篇_4_开发工具ipython与vscode的安装使用

本篇将会介绍两个工具的安装及使用来提高Python的编程效率。 ipython&#xff1a;比python更好用的交互式开发环境vscode&#xff1a;本身是文本编辑器&#xff0c;通过安装相关的插件vscode可以作为python集中开发环境使用 掌握Python的X篇_4_开发工具ipython与vscode的安装使…

第四章 linux编辑器——vim的使用

第四章 linux编辑器——vim的使用 一、什么是vim&#xff1f;二、vim的基本操作1、模式之间的相互切换2、vim的常见命令集&#xff08;1&#xff09;正常模式的常见命令a. 模式切换b. 光标移动c.删除文字d.复制e.替换f.撤销g.更改 &#xff08;2&#xff09;底行模式的常见命令…

复习之linux的网络配置

一、基本定义 1.IP IP指网际互连协议&#xff0c;Internet Protocol的缩写&#xff0c;是TCP/IP体系中的网络层协议。 电脑之间要实现网络通信&#xff0c;就必须要有一个合法的ip地址。 IP地址网络地址主机地址&#xff08;又称&#xff1a;主机号和网络号组成&#xff09…

【MySQL】MyISAM中的索引方案

介绍 B树索引使用存储引擎如表所示&#xff1a; 索引/存储引擎MyISAMInnoDBMemoryB树索引支持支持支持 多个存储引擎支持同一种类型的索引&#xff0c;但是他们的实现原理是不同的。 InnoDB和MyISAM默认的索引是B树索引&#xff0c;而Memory默认的索引是Hash索引。 MyISAM…

【软件测试】在Windows使用Docker搭建CentOS环境(详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 我们做软件测试在…

租服务器跑代码、pycharm连接服务器跑代码、Xshell连接服务器运行代码

一、服务器 1.1 注册 推荐使用矩池云服务器&#xff0c;按时按量计费&#xff0c;服务器自带镜像可选&#xff0c;可将要运行的项目上传到网盘 注册网址&#xff1a;矩池云 1.2 租用 选择合适的租用 1.3 选择镜像 选择合适的系统镜像 1.4 复制命令 进入租用列表&#xff…

【每日挠头算法题(9)】二叉树的直径|二叉树的层序遍历

文章目录 一、二叉树的直径思路&#xff1a;二叉树的深度优先搜索具体代码如下&#xff1a; 二、二叉树的层序遍历思路&#xff1a;借助队列实现具体代码如下&#xff1a; 总结&#xff1a; 一、二叉树的直径 点我直达~ 思路&#xff1a;二叉树的深度优先搜索 根据题目要求&a…

【计网】第三章 数据链路层

文章目录 数据链路层一、使用点对点信道的数据链路层1.1 数据链路和帧1.2 三个基本问题封装成帧透明传输差错控制 二、点对点协议 PPP2.1 PPP 协议的特点2.2 PPP 协议的帧格式2.3 PPP 协议的工作状态 三、使用广播信道的数据链路层3.1 局域网的数据链路层3.2 CSMA/CD 协议3.3 使…

安规测试简介(二)-常见安规认证测试之CE认证

CE认证&#xff1a; CE是法语的缩写&#xff0c;英文意思为 “European Conformity” 即”欧洲共同体”, 事实上&#xff0c;CE还是欧共体许多国家语种中的"欧共体"这一词组的缩写&#xff0c;原来用英语词组EUROPEAN COMMUNITY 缩写为EC&#xff0c;后因欧共体在法文…

python---字典(1)

字典的创建 字典: 是一种存储键值对的 键值对: 键(key) 值(value) 根据key可以快速的找到value (key和value有一定的映射关系) 在python字典中可以包含很多键值对,但是键是唯一的. 创建一个空的字典 创建字典的同时,设置初始值 推荐写法是如下的字典的初始化: 字典查找ke…