深入剖析Tomcat(九) Session的实现原理

提到Session,相信大家都不陌生,Http协议本身是无状态的,每次请求都是独立的,而当我们想要将多次请求建立某种关系的时候,就会用到Cookie+Session这个组合,也就是常说的“会话”概念,将多次请求当成一次会话来看待。而Tomcat恰恰就是支持这个机制的。

Cookie+Session

Cookie是由客户端来支持的,这个客户端通常是由浏览器来担任,当使用ip或域名访问一个服务时,如果在服务端的响应头中有“Set-Cookie”这个key,证明服务端给了客户端一些Cookie,在接下来的请求中,客户端就要在请求头中带上这些Cookie。在Tomcat的会话机制中,这个Cookie通常叫做“JSESSIONID”。

Session由服务端来管理,准确的说,是由Tomcat中的一个Context容器持有的Session管理器来管理。一个服务端可能与多个客户端建立会话,所以Session管理器要管理多个Session对象,管理的方式其实就是用一个Map来存储所有活跃的Session,key为Session的id,value就是Session对象本身。而“JSESSIONID”其实就是这个Map的key,所以Tomcat通过JSESSIONID就能找到对应的Session对象。

至于为什么是Context容器持有Session管理器,前面文章有讲过,一个Context容器代表一个Web应用,而我们客户端访问的其实就是一个Web应用的接口,与这个Web应用建立会话。Tomcat本身是支持部署多个Web应用的,也就是可能会有多个Context容器,他们分别管理自己的会话;但是基于目前springboot大流行的场景下,一个Tomcat也就只包含了一个Context容器,针对的也就是我们这个springboot项目。

在Tomcat中,Session只存在于内存中吗?关闭Tomcat后,所有Session就会丢失吗?

答案是不会!默认情况下,Tomcat在正常关闭时,会将当前活跃的Session序列化存储到一个本地文件中,这个文件的文件名默认为“SESSIONS.ser”;Tomcat启动时会去找这个文件,如果文件存在的话,就将这个文件中的Session集合反序列化回来,重新放到内存中。当然如果你觉得你的应用中Session非常重要的话,Tomcat还支持你将Session存储到数据库中(这个好像用到的场景不多)。

如果你kill -9杀进程的话,Tomcat没有正常关闭,是来不及将session持久化的。

每个首次请求的Http请求,Tomcat都会为其创建Session吗?

不都会。Tomcat不会为所有请求都创建Session,仅当某个请求需要Session支持时才创建Session。那什么情况下代表它需要Session支持呢?通常情况下是在servlet中显示调用“HttpServletRequest.getSession()”方法后,就代表它需要Session了。如果一个全新的请求打进来后,没有任何逻辑去获取Session,那Tomcat就不会为其创建Session。

Seesion有效期是怎么刷新的呢?如何判断过没过有效期?

每个Session对象都会维护一个“最后一次访问时间”的字段,叫做“lastAccessedTime” ;另外还维护了一个Session有效期的时长字段,叫做“maxInactiveInterval”。

判断Session有没有过期的算法为“ 当前时间- lastAccessedTime > maxInactiveInterval”,为true就是过期了;基于当前会话,如果后续请求打进来后,Session会自动将lastAccessedTime更新为当前时间,也就是刷新了有效期(对应StandardSession#access()方法,该方法是被一个叫AuthenticatorBase的阀调用的)。

Session过期后,Session管理器会将该对象回收利用,下次需要创建Session对象时,如果回收池中有可用对象的话,就直接用了,避免了重复创建对象。什么时候触发这个Session过期的判断呢?有两个触发点:

  1. 每次请求打进来后,如果携带了sessionId,就判断一次   
  2. Tomcat后台维护一个线程,每隔一段时间就检查一遍所有Session,判断有没有过期。

上面大白话说了一堆,下面来看看Tomcat是怎么设计这套Session管理机制的。

Session设计

Session对象

Session在Tomcat中以对象的形式存在。

这里有个概念要提前弄明白,Tomcat是基于servlet规范来设计的,servlet规范中对于容器、Session等有一套定义好的接口,比如Session类就是实现了javax.servlet.http.HttpSession 接口的类,这些servlet规范的接口都在servlet.jar中。再说Catalina,它是servlet容器的一个实现,可以理解为是以一个插件的形式放在Tomcat中,是存在被替换掉的可能性的。Catalina内部为了实现特有的功能,会自己定义一些接口,比如针对Session就设计了org.apache.catalina.Session接口。所以在Catalina中,一个Session的具体实现类要继承两个接口:来自servlet的HttpSession接口与自己yy的Session接口。但是,当Catalina要把做好的Session对象提供给具体的servlet进行使用时,就不能暴露自己在org.apache.catalina.Session中定义的方法了,因为这个接口是它自己yy的,没在servlet规范中,如果某天Tomcat心血来潮替换掉了Catalina这个组件,那这个yy的接口就根本不存在了,所以只能让servlet使用javax.servlet.http.HttpSession这个接口中定义的方法。 但是catalina中的Session实现类是实现了两个接口的,怎么办呢?Tomcat针对这种事件的通用做法就是使用门面类,类名通常就叫做 xxxxFacade。以静态代理的方式,使用门面类只暴露HttpSession接口中的方法即可,将这个门面类给servlet使用就OK了。

在Tomcat中,每种组件的规范,其实都是通过接口来定义的,比如本次讲的Session就是HttpSession接口与Session接口,接口代码就不往这放了,随便找个Web项目你都能看到,这里我仅以Catalina中对Session的默认实现类StandardSession,来简单讲一下Session对象都有啥

下面是删减后的StandardSession类的代码

public class StandardSession implements HttpSession, Session, Serializable {// 该session中的属性集合,通常我们往session中放东西就是放到了这个属性里protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();// session idprotected String id = null;// 当前请求的访问时间protected volatile long thisAccessedTime = creationTime;// 最后一次访问时间protected volatile long lastAccessedTime = creationTime;// 该session关联的session管理器protected transient Manager manager = null;// session会话的超时时间,单位为秒,负值代表永不超时protected volatile int maxInactiveInterval = -1;// 该sesssion会话请求次数计数器protected transient AtomicInteger accessCount = null;// Session的门面类,提供给servlet使用protected transient StandardSessionFacade facade = null;/*** 返回一个实现了HttpSession接口的门面类的对象,供servlet使用*/@Overridepublic HttpSession getSession() {if (facade == null) {if (SecurityUtil.isPackageProtectionEnabled()) {facade = AccessController.doPrivileged(new PrivilegedNewSessionFacade(this));} else {facade = new StandardSessionFacade(this);}}return facade;}/*** 当该Session相关的请求进来时,更新访问时间。这个方法由Context容器自动调用,无需用户手动在servlet中调用*/@Overridepublic void access() {this.thisAccessedTime = System.currentTimeMillis();if (ACTIVITY_CHECK) {accessCount.incrementAndGet();}}/*** 当请求访问结束时(已经执行了相关servlet的方法了),调用此方法,更新lastAccessedTime的值*/@Overridepublic void endAccess() {isNew = false;/*** The servlet spec mandates to ignore request handling time* in lastAccessedTime.*/if (LAST_ACCESS_AT_START) {this.lastAccessedTime = this.thisAccessedTime;this.thisAccessedTime = System.currentTimeMillis();} else {this.thisAccessedTime = System.currentTimeMillis();this.lastAccessedTime = this.thisAccessedTime;}if (ACTIVITY_CHECK) {accessCount.decrementAndGet();}}/*** 当判断出session过期后,调用此方法使session过期*/@Overridepublic void expire() {expire(true);}/*** 使session失效** @param notify 是否通知监听器*/public void expire(boolean notify) {if (notify) {// 通知监听器}// session管理器移除该session// 该将session置为无效// 解绑此session关联的属性对象,即attributes中的值。无用的对象就可以被GC掉了}/*** 回收该session。* 释放所有的对象引用,初始化实例变量的值,准备被再次使用*/@Overridepublic void recycle() {// Reset the instance variables associated with this Sessionattributes.clear();setAuthType(null);creationTime = 0L;expiring = false;id = null;lastAccessedTime = 0L;maxInactiveInterval = -1;notes.clear();setPrincipal(null);isNew = false;isValid = false;manager = null;}}

我们最常用到的应该就是attributes这个属性了,通常我们会在会话中放入一些信息,如验证码、用户信息等,就是放到了这个属性中。类似于下面这种用法

    @GetMapping("/login")public String login(HttpServletRequest request) {//  登录验证的逻辑……HttpSession session = request.getSession();session.setAttribute("userInfo",user);// 其他逻辑……}

其他各项属性都是为了控制Session而存在的,平时用时感知不大。如session有效期的控制,提供门面装饰对象StandardSessionFacade等,它还提供了recycle方法来重置自己,方便被回收利用。

装饰类StandardSessionFacade就比较简单了,它只实现了HttpSession接口,内部代理一个HttpSession对象(通常来说就是StandardSession对象),接口的实现逻辑都是直接调用的代理对象的同名方法。

public class StandardSessionFacade implements HttpSession {public StandardSessionFacade(HttpSession session) {this.session = session;}private final HttpSession session;@Overridepublic long getCreationTime() {return session.getCreationTime();}@Overridepublic String getId() {return session.getId();}// 其他方法省略 …………
}

上面提到的相关类的类图如下

Session管理器

上面展示了Session对象,它代表一个会话,这个对象里存放了该会话的一些属性。Session管理器就是管理多个Session对象的一个存在,catalina制定了Session管理器的规范,封装成了org.apache.catalina.Manager接口,并提供了ManagerBase类(实现了Manager接口)来提供Session管理器的一些统一行为。StandardManager是ManagerBase的子类,是Tomcat默认的Session管理器。下面我综合ManagerBase与StandardManager中的一些属性和方法,来展示下Session管理器的主要作用

首先看这两个属性

// 当前活跃的session,key为session的id
protected Map<String, Session> sessions = new ConcurrentHashMap<>();// 已经被回收的Session集合,可以被再次使用(这个回收利用的机制好似高版本已经不用了,读者注意版本区别,这里是Tomcat4的逻辑)
protected List<Session> recycled = new ArrayList<>();

Session管理器负责创建Session,对应的方法为createSession,HttpServletRequest.getSession()最终也会调到这个createSession方法。createSession中会尝试利用回收池中的Session,减少对象的创建,节省资源。

public Session createSession() {// Recycle or create a Session instanceSession session = null;synchronized (recycled) {int size = recycled.size();if (size > 0) {session = recycled.get(size - 1);recycled.remove(size - 1);}}if (session != null)session.setManager(this);elsesession = new StandardSession(this);// 为新的Session初始化属性值session.setNew(true);session.setValid(true);session.setCreationTime(System.currentTimeMillis());session.setMaxInactiveInterval(this.maxInactiveInterval);String sessionId = generateSessionId();String jvmRoute = getJvmRoute();if (jvmRoute != null) {sessionId += '.' + jvmRoute;session.setId(sessionId);}// setId这个方法中会将这个session加入到sessions这个Map中session.setId(sessionId);return session;
}

Session管理器支持通过sessionId来查找session,其实就是对sessions这个Map做检索, 方法太简单,不再放代码了。

Session管理器支持序列化sessions到“SESSIONS.ser”文件中,也支持从“SESSIONS.ser”文件中反序列化出Session对象集合,放到sessions属性中。对应的方法分别为load()和unload(),这两个方法是在对应的生命周期方法执行时被调用的,比如 Session管理器组件初始化时执行的start()方法中会调用load()方法,组件销毁时调用的stop()方法中会调用unload()方法。代码不在展示了,感兴趣的同学去看下源码吧。

Session管理器还负责销毁那些已经失效的Session对象,Tomcat中会有一个独立线程周期性的调用StandardManager的processExpires方法来巡检所有Session对象,销毁那些已过期的。

private void processExpires() {long timeNow = System.currentTimeMillis();Session sessions[] = findSessions();for (int i = 0; i < sessions.length; i++) {StandardSession session = (StandardSession) sessions[i];if (!session.isValid()) {continue;}int maxInactiveInterval = session.getMaxInactiveInterval();if (maxInactiveInterval < 0) {continue;}// 计算出session已经空闲的时间,与最大空闲时间做对比,如果超时了,就将session进行过期处理(重置并放入回收池)int timeIdle = (int) ((timeNow - session.getLastAccessedTime()) / 1000L);if (timeIdle >= maxInactiveInterval) {try {session.expire();} catch (Throwable t) {log(sm.getString("standardManager.expireException"), t);}}}}

除了StandardManager,Tomcat还提供了PersistentManager来支持将Session持久化到不同的储存库中,可以是文件(FileStore)也可以是数据库(JDBCStore),用的比较少,这里就不详细介绍了。

上面提到的类的类图如下

下面画个流程图来串一下 Session建立、刷新、回收的过程

集群共享Session

线上系统为了高可用,一般都会采用集群部署,并使用负载均衡工具(如Nginx)做负载。这会带来一个问题:Tomcat中的Session管理都是针对单机服务而言的,集群服务如何管理Session呢?通常这就需要一个外部介质来存Session了,而且这个外部介质需要是集群中每个节点都能访问到的。通常来说我们会选择redis来充当这个介质,而spring也提供了spring-session-data-redis包来支持这项工作。

spring-session-data-redis的用法

首先引入maven依赖

<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId><version>2.7.4</version>
</dependency>

然后在springboot的启动类上加上@EnableRedisHttpSession注解,就完事了,用起来非常简单。

@EnableRedisHttpSession注解中有几个属性,方便你做一些个性化设置,如果没有特殊需求,用它的默认值即可。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
public @interface EnableRedisHttpSession {/*** Session的有效期,单位为秒,默认是30分钟*/int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;/*** 为key定义唯一的命名空间。默认为“spring: session:”,用于隔离会话。* 例如,如果你有一个名为“应用A”的应用程序需要与“应用B”保持会话隔离,你可以为应用程序设置两个不同的值,它们可以在同一个Redis实例中运行。*/String redisNamespace() default RedisSessionRepository.DEFAULT_KEY_NAMESPACE;/*** Redis会话的刷新模式。默认是ON_SAVE,在HTTP响应提交之前将Session刷新到redis。* 将该值设置为IMMEDIATE时,对Session有任何更新时,都会立即写入到Redis。*/FlushMode flushMode() default FlushMode.ON_SAVE;/*** 保存session的模式,默认为ON_SET_ATTRIBUTE,只向redis中刷新发生改变的属性。* 设为ON_GET_ATTRIBUTE时,除了session中属性变化时会保存,在从session中读取属性时也会向redis刷新数据。* 设为ALWAYS,代表对session的任何操作,都会向redis中刷新数据*/SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;}

最常使用的属性是maxInactiveIntervalInSeconds,自定义一个session有效期。

其次是redisNamespace,在多个不同的服务都需要往同一个redis实例中放session时,会通过这个属性来做服务区分。

flushMode和saveMode 几乎没用过。甚至我对saveMode这个属性的应用场景都没看懂😂。

spring-session-data-redis的原理

用法很简单,原理才重要。

工作原理
  1. 会话存储替换:Spring Session Data Redis 替换了标准的 HTTP 会话存储机制,通过 Redis 进行会话数据的存储和管理。

  2. 拦截器和过滤器:Spring Session 提供了一个 SessionRepositoryFilter 过滤器,拦截所有进入的 HTTP 请求,替换原有的 HttpSession 实现。

  3. RedisIndexedSessionRepository:该类持有RedisTemplate引用,将会话数据存储在 Redis 中,并负责会话的创建、更新、删除等操作。

主要流程

1.请求拦截SessionRepositoryFilter是一个servlet过滤器,它会拦截所有HTTP 请求,使用自己的requestWrapper与responseWrapper对象将原本的request与response对象包装了一下,然后将包装后的对象扔回了过滤器链中,也就是说当请求到达servlet后,servlet拿到的是这个requestWrapper与responseWrapper对象。 

这个FIlter是设计的精髓,拦截到请求并将request与response偷梁换柱后,自己想特殊实现的逻辑就在xxxWrapper类中重写相关方法,还想使用原逻辑的就还调用原来对象的方法。

2.会话操作与持久化

再看下SessionRepositoryFilter的类结构,在SessionRepositoryRequestWrapper这个类中还有一个HttpSessionWrapper类,这个类就是用来替掉Tomcat原本的HttpSession的。

RedisIndexedSessionRepository类持有一个RedisTemplate对象,负责与redis进行交互。HttpSessionWrapper对session的操作会调用RedisIndexedSessionRepository类中对应方法去redis中存取数据。

redis中存储的session数据

spring-session-data-redis将一个session放入redis时,会创建三个key。Hash结构的key存储的是session的数据,我们编码用到的也是这个数据;另外两个key是与session过期处理相关的,平常我们也不用关心。

前端收到的cookie

使用spring-session-data-redis后,这个框架会将真实的sessionId进行base64编码后再返给前端,如上图的session信息,前端收到的cookie就为  

SESSION=Zjc0MWNmMzYtYTZiMy00YmY2LTgyNDQtM2U3MGYzNmE4NmI0

注意,这时候已经没有“JSESSIONID”了,前后端辨认会话使用“SESSION”。

好,spring-session-data-redis就聊到这里。

下篇文章聊一聊Tomcat的安全性,敬请期待!

源码分享

https://gitee.com/huo-ming-lu/HowTomcatWorks

本篇文章并没有讲原书中的示例,感兴趣的同学可以自己运行观察一下。另外,我在gitee上提供的源码为Tomcat4的代码,写这篇文章时我也参考了Tomcat9的代码,新版和老版的整体思路没变,在一些细节上有些优化,可以先看老版代码,再对比着新版代码去看。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MFC工控项目实例之二添加iPlotx控件

承接专栏《MFC工控项目实例之一主菜单制作》 在WIN10下使用Visual C 6.0 &#xff08;完整绿色版&#xff09;添加iPlotx控件的方法。 1、在资源主对话框界面点击鼠标右键如图选择插入Active控件点击进入。 2、选择iPlotx Contrlolh点击确定。 3、在对话框界面插入iPlotx控件。…

YOLO系列模型 pt文件转化为ONNX导出

文章目录 啥是onnx怎么导出导出之后 啥是onnx Microsoft 和合作伙伴社区创建了 ONNX 作为表示机器学习模型的开放标准。许多框架&#xff08;包括 TensorFlow、PyTorch、scikit-learn、Keras、Chainer、MXNet 和 MATLAB&#xff09;的模型都可以导出或转换为标准 ONNX 格式。 在…

Liunx音频

一. echo -e "\a" echo 通过向控制台喇叭设备发送字符来发声&#xff1a; echo -e "\a"&#xff08;这里的 -e 选项允许解释反斜杠转义的字符&#xff0c;而 \a 是一个响铃(bell)字符&#xff09; 二. beep 下载对应的包 yum -y install beep 发声命令 be…

抖音素材网站有哪些?抖音素材下载网站分享

在如今这个自媒体和短视频成为主流的年代&#xff0c;每位创作者都在努力寻找能够让其内容更具吸引力的素材和工具。尤其是在抖音这种快节奏、多变趋势的平台上&#xff0c;想要脱颖而出&#xff0c;拥有充足的“弹药”是必不可少的。本文将介绍几个国内外优秀的素材网站&#…

《编译原理》期末考试复习手写笔记+真题(一)第一、二、三章

目录 第一章 第二章考试题型&#xff1a; 第三章考试题型【词法分析】&#xff1a; 不会DFA-最小化分割法的看这里&#xff01;&#xff01;&#xff01; 学习完前三章后&#xff0c;期末考试的前面两道大题可以做啦&#xff08;除去第四章消除左递归※&#xff09;&#…

[STM32]定位器与PWM的LED控制

目录 1. 深入了解STM32定时器原理&#xff0c;掌握脉宽调制pwm生成方法。 (1)STM32定时器原理 原理概述 STM32定时器的常见模式 使用步骤 (2)脉宽调制pwm生成方法。 2. 实验 (1)LED亮灭 代码 测试效果 (2)呼吸灯 代码 测试效果 3.总结 1. 深入了解STM32定时器原…

【Linux操作系统】进程状态(1)

&#x1f389;博主首页&#xff1a; 有趣的中国人 &#x1f389;专栏首页&#xff1a; Linux &#x1f389;其它专栏&#xff1a; C初阶 | C进阶 | 初阶数据结构 小伙伴们大家好&#xff0c;本片文章将会讲解 Linux操作系统 进程状态 的相关内容。 如果看到最后您觉得这篇文章…

钓鱼攻击的隐性经济

近年来&#xff0c;网络钓鱼形势发生了重大变化&#xff0c;涵盖了各种类型的攻击。许多公司已经开发了分类法来对不同的网络钓鱼攻击进行分类&#xff0c;类似于BlueVoyant 提出的分类法。该分类法概述了几种类型的网络钓鱼攻击&#xff0c;例如&#xff1a; 1. 电子邮件钓鱼…

无线和移动网络

背景 两个重要的挑战 无线&#xff1a;通过无线链路通信移动&#xff1a;需要网络处理移动&#xff08;不同变换所接入的网络&#xff09;用户 无线网络中的组件 无线主机&#xff08;无线并不总是意味着移动的&#xff09;基站&#xff08;base station 或者叫AP&#xff0…

旭日X3与英伟达Orin NX通过TCP传输图片

观前提醒&#xff1a;本文主要内容为使用Python在局域网内建立TCP连接并传输图片信息&#xff0c;计算机为一块旭日X3和一块英伟达Orin NX。 一、什么是TCP TCP&#xff08;传输控制协议&#xff09;是一种可靠的、面向连接的协议&#xff0c;它确保数据包的顺序传输和完整性…

计算机组成结构—多处理器

目录 一、SISD、SIMD、MIMD 和向量处理器 1. 费林分类法 2. SIMD 和向量处理器 二、硬件多线程 三、多核处理器和 SMP 1. 多核处理器 2. 共享内存多处理器&#xff08;SMP&#xff09; 3. MPP 和集群 一、SISD、SIMD、MIMD 和向量处理器 通过改进系统结构&#xff0c;可…

java:reactor的Mono和Reactor的简单例子

【pom.xml】 <dependency><groupId>io.projectreactor</groupId><artifactId>reactor-core</artifactId><version>3.3.17.RELEASE</version> </dependency>【MyReactorTest2.java】 package com.chz.myReactor;import react…

十年数据分析经验分享

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

WPF入门--多种方式设置样式(Style)

前言 在上篇文章中&#xff0c;介绍了WPF九种布局方式。本篇文章通过多种方式设置样式&#xff08;Style&#xff09;以控制UI元素的外观和行为。下面来具体介绍一下。 传送门 WPF入门--常用布局方式 目录 前言 一、直接在XAML中设置属性&#xff08;内联样式&#xff09…

【蓝桥杯2025备赛】分巧克力

【蓝桥杯2025备赛】分巧克力 [蓝桥杯 2017 省 AB] 分巧克力 题目描述 儿童节那天有 K K K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。 小明一共有 N N N 块巧克力&#xff0c;其中第 i i i 块是 H i W i H_i \times W_i Hi​Wi​ 的方格组成的长方形…

C++ | Leetcode C++题解之第135题分发糖果

题目&#xff1a; 题解&#xff1a; class Solution { public:int candy(vector<int>& ratings) {int n ratings.size();int ret 1;int inc 1, dec 0, pre 1;for (int i 1; i < n; i) {if (ratings[i] > ratings[i - 1]) {dec 0;pre ratings[i] rati…

c++ 里函数选择的优先级:普通函数、模板函数、万能引用,编译器选择哪个执行呢?

看大师写的代码时&#xff0c;除了在类里定义了 copy 构造函数&#xff0c;移动构造函数&#xff0c;还定义了对形参采取万能引用的构造函数&#xff0c;因此有个疑问&#xff0c;这时候的构造函数优先级是什么样的呢&#xff1f;简化逻辑测试一下&#xff0c;如下图&#xff0…

计算机网络 —— 数据链路层(以太网)

计算机网络 —— 数据链路层&#xff08;以太网&#xff09; 什么是以太网以太网传输介质和拓扑结构的发展传输介质的发展&#xff1a;拓扑结构的发展&#xff1a; 10BASE-T 以太网适配器和MAC地址适配器&#xff08;Adapter&#xff09;MAC地址适配器与MAC地址的关系 MAC帧以太…

GLM-4-9B领先!伯克利函数调用榜单BFCL的Function Calling评测方法解析与梳理

智谱公布的GLM-4-9B基于BFCL榜单的工具调用能力测试结果 ©作者|格林 来源|神州问学 在智谱最新开源的GLM-4-9B-Chat中&#xff0c;其工具调用能力在BFCL&#xff08;伯克利函数调用排行榜&#xff09;榜上获得了超高的总BFCL分&#xff0c;和gpt-4-turbo-2024-04-09几乎不…

举个栗子!Quick BI 技巧(8):柱形图的制作及应用

众所周知&#xff0c;在数据分析中&#xff0c;柱形图是利用率非常高的一种图&#xff0c;主要是用于比较各组数据之间的差别&#xff0c;并且可以显示一段时间内的数据变化情况。那么在 Quick BI 中要如何来制作柱形图呢&#xff1f; 今天的栗子&#xff0c;我们就来分享如何…