Go语言并发编程-Context上下文

Context上下文

Context概述

Go 1.7 标准库引入 context,译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。

context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如 database/sql 包。context 几乎成为了并发控制和超时控制的标准做法。

在一组goroutine 之间传递共享的值、取消信号、deadline是Context的作用

以典型的HTTPServer为例:

image.png

我们以 Context II为例,若没有上下文信号,当其中一个goroutine出现问题时,其他的goroutine不知道,还会继续工作。这样的无效的goroutine积攒起来,就会导致goroutine雪崩,进而导致服务宕机!

没有同步信号:

image.png

增加同步信号:

image.png

参考:Context传递取消信号 小结。

Context 核心结构

context.Context 是 Go 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法:

 type Context interface {// 返回被取消的时间Deadline() (deadline time.Time, ok bool)// 返回用于通知Context完结的channel// 当这个 channel 被关闭时,说明 context 被取消了// 在子协程里读这个 channel,除非被关闭,否则读不出来任何东西Done() <-chan struct{}// 返回Context取消的错误Err() error// 返回key对应的valueValue(key any) any}

除了Context接口,还存在一个canceler接口,用于实现Context可以被取消:

 type canceler interface {cancel(removeFromParent bool, err error)Done() <-chan struct{}}

除了以上两个接口,还有4个预定义的Context类型:

 // 空Contexttype emptyCtx int​// 取消Contexttype cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     atomic.Value          // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel call}​// 定时取消Contexttype timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.​deadline time.Time}​// KV值Contexttype valueCtx struct {Contextkey, val any}​

默认(空)Context的使用

context 包中最常用的方法是 context.Backgroundcontext.TODO,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用:

  • context.Background, 是上下文的默认值,所有其他的上下文都应该从它衍生出来,在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。

  • context.TODO,是一个备用,一个context占位,通常用在并不知道传递什么 context的情形。

使用示例,database/sql包中的执行:

 func (db *DB) PingContext(ctx context.Context) errorfunc (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row

方法,其中第一个参数就是context.Context。

例如:操作时:

 db, _ := sql.Open("", "")query := "DELETE FROM `table_name` WHERE `id` = ?"db.ExecContext(context.Background(), query, 42)

当然,单独 database.sql包中,也支持不传递context.Context的方法。功能一致,但缺失了context.Context相关功能。

 func (db *DB) Exec(query string, args ...any) (Result, error)

context.Background 和 context.TODO 返回的都是预定义好的 emptyCtx 类型数据,其结构如下:

 // 创建方法func Background() Context {return background}func TODO() Context {return todo}​// 预定义变量var (background = new(emptyCtx)todo       = new(emptyCtx))​// emptyCtx 定义type emptyCtx int​func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return}​func (*emptyCtx) Done() <-chan struct{} {return nil}​func (*emptyCtx) Err() error {return nil}​func (*emptyCtx) Value(key any) any {return nil}​func (e *emptyCtx) String() string {switch e {case background:return "context.Background"case todo:return "context.TODO"}return "unknown empty Context"}

可见,emptyCtx 是不具备取消、KV值和Deadline的相关功能的,称为空Context,没有任何功能。

Context传递取消信号

context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。取消操作通常分为主动取消,定时取消两类。

主动取消

需要的操作为:

  • 创建带有cancel函数的Context,func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

  • 接收cancel的Channel,ctx.Done()

  • 主动Cancel的函数,cancel CancelFunc

image.png

示例代码:

 func ContextCancelCall() {// 1. 创建cancelContextctx, cancel := context.WithCancel(context.Background())​wg := sync.WaitGroup{}wg.Add(4)// 2. 启动goroutine,携带cancelCtxfor i := 0; i < 4; i++ {// 启动goroutine,携带ctx参数go func(c context.Context, n int) {defer wg.Done()// 监听context的取消完成channel,来确定是否执行了主动cancel操作for {select {// 等待接收c.Done()这个channelcase <-c.Done():fmt.Println("Cancel")returndefault:​}fmt.Println(strings.Repeat("  ", n), n)time.Sleep(300 * time.Millisecond)}}(ctx, i)}​// 3. 主动取消 cancel()// 3s后取消select {case <-time.NewTimer(2 * time.Second).C:cancel() // ctx.Done() <- struct{}}​select {case <-ctx.Done():fmt.Println("main Cancel")}​wg.Wait()​}​// ======> go test -run TestContextCancelCall31  0  2132  0  01  32  2130013221033012main CancelCancelCancelCancelCancelPASSok      goConcurrency   2.219s​

当调用cancel()时,全部的goroutine会从 ctx.Done() 接收到内容,进而完成后续控制操作。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 函数返回的Context是 context.cancelCtx 结构体对象,以及一个CancelFunc。

其中 context.cancelCtx 结构如下:

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     atomic.Value          // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel call
}

其中:

  • Context,上级Context对象

  • mu, 互斥锁

  • done,用于处理cancel通知信号的channel。懒惰模式创建,调用cancel时关闭。

  • children,以该context为parent的可cancel的context们

  • err,error

Deadline和Timeout定时取消

与主动调用 CancelFunc 的差异在于,定时取消,增加了一个到时自动取消的机制:

  • Deadline,某个时间点后,使用 func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)创建

  • Timeout,某个时间段后,使用 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 创建

示例代码如下,与主动cancel的代码类似:

// 1s后cancel
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)// 每天 20:30 cancel
curr := time.Now()
t := time.Date(curr.Year(), curr.Month(), curr.Day(), 20, 30, 0, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), t)

其他代码一致,当时间到时,ctx.Done() 可以接收内容,进而控制goroutine停止。

不论WithDeadline和WithTimeout都会构建 *timerCtx 类型的Context,结构如下:

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}

其中:

  • cancelCtx,基于parent构建的cancelCtx

  • deadline,cancel时间

  • timer,定时器,用于自动cancel

Cancel操作的向下传递

当父上下文被取消时,子上下文也会被取消。Context 结构如下:

ctxOne|    \
ctxTwo    ctxThree|
ctxFour

示例代码:

func ContextCancelDeep() {ctxOne, cancel := context.WithCancel(context.Background())ctxTwo, _ := context.WithCancel(ctxOne)ctxThree, _ := context.WithCancel(ctxOne)ctxFour, _ := context.WithCancel(ctxTwo)// 带有timeout的cancel//ctxOne, _ := context.WithTimeout(context.Background(), 1*time.Second)//ctxTwo, cancel := context.WithTimeout(ctxOne, 1*time.Second)//ctxThree, _ := context.WithTimeout(ctxOne, 1*time.Second)//ctxFour, _ := context.WithTimeout(ctxTwo, 1*time.Second)cancel()wg := sync.WaitGroup{}wg.Add(4)go func() {defer wg.Done()select {case <-ctxOne.Done():fmt.Println("one cancel")}}()go func() {defer wg.Done()select {case <-ctxTwo.Done():fmt.Println("two cancel")}}()go func() {defer wg.Done()select {case <-ctxThree.Done():fmt.Println("three cancel")}}()go func() {defer wg.Done()select {case <-ctxFour.Done():fmt.Println("four cancel")}}()wg.Wait()
}

我们调用 ctxOne 的 cancel, 其后续的context都会接收到取消的信号。

如果调用了其他的cancel,例如ctxTwo,那么ctxOne和ctxThree是不会接收到信号的。

取消操作流程

创建cancelCtx的流程

使用 context.WithCancel, context.WithDeadlime, context.WithTimeout 创建cancelCtx或timerCtx的核心过程基本一致,以 context.WithCancel 为例:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {if parent == nil {panic("cannot create context from nil parent")}// 构建cancelCtx对象c := newCancelCtx(parent)// 传播Cancel操作propagateCancel(parent, &c)// 返回值,注意第二个cancel函数的实现return &c, func() { c.cancel(true, Canceled) }
}func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}

由此可见,核心过程有两个:

  • newCancelCtx, 使用 parent 构建 cancelCtx

  • propagateCancel, 传播Cancel操作,用来构建父子Context的关联,用于保证在父级Context取消时可以同步取消子级Context

核心的propagateCancel 的实现如下:

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {// parent不会触发cancel操作done := parent.Done()if done == nil {return // parent is never canceled}// parent已经触发了cancel操作select {case <-done:// parent is already canceledchild.cancel(false, parent.Err())returndefault:}// parent还没有触发cancel操作if p, ok := parentCancelCtx(parent); ok {// 内置cancelCtx类型p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err)} else {if p.children == nil {p.children = make(map[canceler]struct{})}// 将当前context放入parent.children中p.children[child] = struct{}{}}p.mu.Unlock()} else {// 非内置cancelCtx类型atomic.AddInt32(&goroutines, +1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}

以上代码在建立child和parent的cancelCtx联系时,处理了下面情况:

  • parent不会触发cancel操作,不做任何操作,直接返回

  • parent已经触发了cancel操作,执行child的cancel操作,返回

  • parent还没有触发cancel操作,child 会被加入 parentchildren 列表中,等待 parent 释放取消信号

  • 如果是自定义Context实现了可用的Done(),那么开启goroutine来监听parent.Done()和child.Done(),同样在parent.Done()时取消child。

如果是WithDeadline构建的timerCtx,构建的过程多了两步:

  • 对截至时间的判定,判定是否已经截至

  • 设置定时器

示例代码:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {if parent == nil {panic("cannot create context from nil parent")}if cur, ok := parent.Deadline(); ok && cur.Before(d) {// The current deadline is already sooner than the new one.return WithCancel(parent)}c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}propagateCancel(parent, c)dur := time.Until(d)// 已过时if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}c.mu.Lock()defer c.mu.Unlock()// 设置定时器if c.err == nil {c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}
ctx.Done() 初始信号channel流程

以 cancelCtx 为例:

func (c *cancelCtx) Done() <-chan struct{} {// 加载已经存在的d := c.done.Load()if d != nil {return d.(chan struct{})}c.mu.Lock()defer c.mu.Unlock()// 初始化新的d = c.done.Load()if d == nil {d = make(chan struct{})c.done.Store(d)}return d.(chan struct{})
}

其中两个步骤:

  1. 先尝试加载已经存在的

  2. 后初始化新的

核心要点是,当调用Done()时,初始化chan struct{}, 而不是在上限文cancelCtx创建时,就初始化完成了。称为懒惰初始化。

cancel()操作流程

取消流程,我们以 cancelCtx 的主动取消函数cancel的实现为例:

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {if err == nil {panic("context: internal error: missing cancel error")}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}// 设置 errc.err = err// 关闭channeld, _ := c.done.Load().(chan struct{})if d == nil {c.done.Store(closedchan)} else {close(d)}// 遍历全部可取消的子contextfor child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err)}c.children = nilc.mu.Unlock()// 从parent的children删除自己if removeFromParent {removeChild(c.Context, c)}
}

以上流程的核心操作:

  • 关闭channel,用来通知全部使用该ctx的goroutine

  • 遍历全部可取消的子context,执行child的取消操作

  • 从parent的children删除自己

Context传值

image.png

若希望在使用context时,携带额外的Key-Value数据,可以使用 context.WithValue 方法,构建带有值的context。并使用 Value(key any) any 方法获取值。带有值

对应方法的签名如下:

func WithValue(parent Context, key, val any) Contexttype Context interface {Value(key any) any
}

需要三个参数:

  • 上级 Context

  • key 要求是comparable的(可比较的),实操时,推荐使用特定的Key类型,避免直接使用string或其他内置类型而带来package之间的冲突。

  • val any

示例代码

type MyContextKey stringfunc ContextValue() {wg := sync.WaitGroup{}ctx := context.WithValue(context.Background(), MyContextKey("title"), "Go")wg.Add(1)go func(c context.Context) {defer wg.Done()if v := c.Value(MyContextKey("title")); v != nil {fmt.Println("found value:", v)return}fmt.Println("key not found:", MyContextKey("title"))}(ctx)wg.Wait()
}

context.WithValue 方法返回 context.valueCtx 结构体类型。context.valueCtx 结构体包含了上级Context和key、value:

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {Contextkey, val any
}func (c *valueCtx) Value(key any) any {if c.key == key {return c.val}return value(c.Context, key)
}

也就是除了 value 功能,其他Contenxt功能都由parent Context实现。

如果 context.valueCtx.Value 方法查询的 key 不存在于当前 valueCtx 中,就会从父上下文中查找该键对应的值直到某个父上下文中返回 nil 或者查找到对应的值。例如:

func ContextValueDeep() {wgOne := sync.WaitGroup{}ctxOne := context.WithValue(context.Background(), MyContextKey("title"), "One")//ctxOne := context.WithValue(context.Background(), MyContextKey("key"), "Value")//ctxTwo := context.WithValue(ctxOne, MyContextKey("title"), "Two")ctxTwo := context.WithValue(ctxOne, MyContextKey("key"), "Value")//ctxThree := context.WithValue(ctxTwo, MyContextKey("title"), "Three")ctxThree := context.WithValue(ctxTwo, MyContextKey("key"), "Value")wgOne.Add(1)go func(c context.Context) {defer wgOne.Done()if v := c.Value(MyContextKey("title")); v != nil {fmt.Println("found value:", v)return}fmt.Println("key not found:", MyContextKey("title"))}(ctxThree)wgOne.Wait()
}

小结

特定的结构体类型:

  • emptyCtx,函数 context.Background, context.TODO

  • cancelCtx,函数 context.WithCancel

  • timerCtx, 函数 context.WithDeadline, context.WithTimeout

  • valueCtx, 函数 context.WithValue

官方博客对Context使用的建议:

  • 直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。

  • 如果你实在不知道传什么,标准库给你准备好了一个 context.TODO。

  • context 存储的应该是一些goroutine共同的数据。

  • context 是并发安全的。

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

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

相关文章

python爬虫实现简单的代理ip池

python爬虫实现简单的代理ip池 我们在普通的爬虫过程中经常遇到一些网站对ip进行封锁的 下面演示一下普通的爬虫程序 使用requests.get爬取数据 这段代码是爬取豆瓣排行榜的数据&#xff0c;使用f12来查看请求的url和数据格式 代码 def requestData():# 爬取数据的urlur…

数学基础【俗说矩阵】:矩阵相乘

矩阵乘法 矩阵乘法推导过程 一、两个线性方程复合代入 二、X1和X2合并同类项 三、复合后方程组结果 四、线性方程组矩阵表示 五、线性方程组矩阵映射表示 复合映射表示 六、矩阵乘法导出 矩阵乘法法则 1、规则一推导过程 左取行&#xff0c;右取列&#xff0c;对应相乘后…

【中项】系统集成项目管理工程师-第2章 信息技术发展-2.1信息技术及其发展-2.1.4信息安全

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

「Vue组件化」封装i18n国际化

前言 在Vue应用中实现国际化(i18n),通常需要使用一些专门的库,比如vue-i18n。本文主要介绍自定义封装i18n,支持Vue、uniapp等版本。 设计思路 一、预期效果 二、核心代码 2.1 i18n.xlsx文件准备 2.2 脚本执行 根目录main.js根目录locali18n.xlsxnode main.jsmain.js 文件…

【ARM】MDK-解决CMSIS_DAP.DLL missing报错

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 记录解决CMSIS_DAP.DLL missing的报错情况&#xff0c;对应相关报错信息&#xff0c;供后续客户参考&#xff0c;快速解决客户问题。 2、 问题场景 客户进行硬件调试时&#xff0c;发现Target设置内有CMSIS_DAP.DL…

全时守护,无死角监测:重点海域渔港视频AI智能监管方案

一、方案背景 随着海洋经济的快速发展和海洋资源的日益紧缺&#xff0c;对重点海域渔港进行有效监控和管理显得尤为重要。视频监控作为一种高效、实时的管理手段&#xff0c;已成为渔港管理中不可或缺的一部分。当前&#xff0c;我国海域面积广阔&#xff0c;渔港众多&#xf…

uni-app学习HBuilderX学习-微信开发者工具配置

HBuilderX官网&#xff1a;简介 - HBuilderX 文档 (dcloud.net.cn)https://hx.dcloud.net.cn/ uni-app官网&#xff1a; uni-app官网 (dcloud.net.cn)https://uniapp.dcloud.net.cn/quickstart-hx.htmlHBuilder下载安装&#xff1a;打开官网 uni-app项目的微信开发者工具配置…

41 QOS技术(服务质量)

1 QOS 产生背景 对于网络业务&#xff0c;影响服务质量的因素包括传输的带宽、传送的时延、数据的丢包率等。网络资源总是有限的&#xff0c;只要存在抢夺网络资源的情况&#xff0c;就会出现服务质量的要求网络总带宽固定的情况下&#xff0c;如果某类业务占用的带宽越多&am…

Mysql-错误处理: Found option without preceding group in config file

1、问题描述 安装MYSQL时&#xff0c;在cmd中“初始化”数据库时&#xff0c;输入命令&#xff1a; mysqld --initialize --consolecmd报错&#xff1a; D:\mysql-5.7.36-winx64\bin>mysql --initialize --console mysql: [ERROR] Found option without preceding group …

Web开发:ASP.NET CORE的后端小结(基础)

1.后端重定向到指定路由 public IActionResult Index(){return RedirectToAction("Index", "Main");//重定向>Main/Index} 【备注】如果在MainController的Index方法中return View();本质是 return View("Index")&#xff0c;返回和方法同名的…

vscode通过ssh链接远程服务器上的docker

目录 1 编译docker image1.1 编译镜像1.2 启动镜像 2 在docker container中启动ssh服务2.1 确认是否安装ssh server2.2 修改配置文件2.3 启动ssh服务 3 生成ssh key4 添加ssh公钥到docker container中5 vscode安装插件Remote - SSH6 在vscode中配置 1 编译docker image 一般来…

二叉树的前、中、后序遍历(递归法、迭代法)leetcode144/94/145

leetcode144、二叉树的前序遍历 给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,2,3] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[] 示例 3&#xff1a;…

【golang-ent】go-zero框架 整合 ent orm框架 | 解决left join未关联报错的问题

一、场景 1、子表&#xff1a;cp_member_point_history cp_member_point_history表中字段&#xff1a;cp_point_reward_id 是cp_point_reward的主键id 当本表中的cp_point_reward_id字段为0&#xff08;即&#xff1a;没有可关联主表的&#xff09; CREATE TABLE cp_member_poi…

数据结构(Java):优先级队列(堆)堆的模拟实现

目录 1、优先级队列 1.1 概念 1.2 PriorityQueue底层结构 2、 堆 2.1 堆的概念 2.2 堆的存储结构 3、优先级队列&#xff08;堆&#xff09;的模拟实现 3.1 堆的创建 3.1.1 向下调整算法建完整堆 3.2 堆的插入 3.2.1 向上调整算法 3.3 堆的删除 3.4 堆排序 1、优先…

CH552的bootload程序IAP直接对ROM-flash修改数据(未尝试)

手动写bootload程序的可能 1&#xff0c;根据ch552g的使用手册内容查看到 2&#xff0c;在下面的参考文件的IAP文件夹中看到IAP文件 参考 下面程序中并没有跳转到厂家bootload的过程&#xff0c;这是直接通过控制有关的寄存器对FLSH进行直接写入和修改&#xff0c;这样可以认…

如何学习Hadoop:糙快猛的大数据之路(利用GPT 学习)

目录 引言Hadoop是什么&#xff1f;学习Hadoop的"糙快猛"之道1. 不要追求完美&#xff0c;先动手再说2. 从简单的MapReduce开始3. 利用大模型加速学习4. 循序渐进&#xff0c;建立知识体系 构建您的Hadoop技能树1. 夯实基础&#xff1a;Linux和Java2. 深入理解HDFS3.…

AI 应用还没有大量出现,缺什么?缺聊天机器人编程语言 | Chatopera

只有帮助人发挥创意的才是大市场 现在是需要大量的 AI 应用了。如何产生大量的 AI 应用呢&#xff1f;当年乔布斯说&#xff0c;他看到了个人电脑的两个趋势&#xff0c;一个是图形化用户界面&#xff0c;一个是面向对象编程语言。今天&#xff0c;AI 应用也是新的【图形用户界…

【QT】label中添加QImage图片并旋转(水平翻转、垂直翻转、顺时针旋转、逆时针旋转)

目录 0.简介 1.详细代码及解释 1&#xff09;原label显示在界面上 2&#xff09;水平翻转 3&#xff09;垂直翻转 4&#xff09;顺时针旋转45度 5&#xff09;逆时针旋转 0.简介 环境&#xff1a;windows11 QtCreator 背景&#xff1a;demo&#xff0c;父类为QWidget&a…

Cisco 路由重发布 —— 实现路由信息在不同路由选择域间的传递

一、技术背景 在实际的组网中&#xff0c;可能会遇到这样一个场景&#xff1a;在一个网络中同时存在两种或者两种以上的路由协议。例如客户的网络原先是纯 Cisco 的设备&#xff0c;使用 EIGRP 协议将网络的路由打通。但是后来网络扩容&#xff0c;增加了一批华为的设备&#…

应用层——HTTP

像我们电脑和手机使用的应用软件就是在应用层写的&#xff0c;当我们的数据需要传输的时候换将数据传递到传输层。 应用层专门给用户提供应用功能&#xff0c;比如HTTP,FTP… 我们程序员写的一个个解决我们实际的问题都在应用层&#xff0c;我们今天来聊一聊HTTP。 协议 协议…