如何实现RTMP协议

news2024/11/16 21:41:59

认识rtmp

rtmp是Adobe公司出品的流媒体传输协议,它的全称是Real Time Messaging Protocol,是一个实时消息传输协议,学习RTMP一定要抓住 一个关键点:消息。

rtmp协议的原文可以在Adobe官网下载,内容十分精简,建议读一读原文。

rtmp的核心是消息交换,是一个基于TCP的协议,消息被分成消息块(chunk)使用TCP传输。每个chunk都携带一个id,称为chunk id,接收端根据chunk id将分块重新组装成完整的消息。所有chunk id相同的分块构成一条虚拟的chunk stream(块流),是一条逻辑流。同时每个消息也有一个message stream id,所有message stream id相同的消息构成一条消息流,这是第二条逻辑流。

在这里插入图片描述

message stream和chunk stream之间并不存在一一对应的关系。一条message stream可以通过多条chunk stream传输,不同的message stream也可以复用同一条chunk stream。消息有很多种类型,消息类型和消息流也不是一一对应的关系,一条消息流可以传输不同类型的消息,但一般每一种消息都会独占一条chunk stream。

在这里插入图片描述

rtmp协议的流程也是从握手开始。握手之后就全部是消息交换。

工欲善其事 必先利其器

rtmp协议的目的是流媒体传输,为了验证效果,需要用到ffmpegffplay,这两个程序都可以在ffmpeg官网上找到,直接下载压缩包解压就可以使用。当然,为了使用方便,可以添加到path。有其他的推拉流工具也可以,作为开发,这两个命令就足够了。

推流命令:

ffmpeg -re -stream_loop -1 -i trailer.mp4 -codec copy -f flv rtmp://localhost/live/test

播放命令:

ffplay -autoexit rtmp://localhost/live/test

这两个命令比较长,可以使用make帮助我们简化工作。

push: trailer.mp4
  @ffmpeg -re -stream_loop -1 -i trailer.mp4 -codec copy -f flv rtmp://localhost/live/test

pull:
  @ffplay -autoexit rtmp://localhost/live/test

协议使用golang开发,具体的代码实现详见Github.

握手

rtmp协议从握手开始,由客户端发起。客户端和服务端分别需要发送3个数据块,客户端发送的称为C0、C1和C2;服务端发送的称为S0、S1和S2。

C0和S0有相同的结构,C1和S1有相同的结构,C2和S2有相同的结构。

握手过程如下:

  1. 客户端发送C0和C1
  2. 服务端收到C0(或C1)后,发送S0和S1
  3. 客户端收到S1后发送C2
  4. 服务端收到C1后,发送S2
  5. 客户端收到S2且服务端收到C2,握手完成

数据格式

C0和S0都只有一个字节,内容为协议版本号。

在这里插入图片描述

C0中的是客户端要求的RTMP版本号,S0中是服务端选择的版本号,目前版本号为3。0-2是早期版本,已废弃,4-31是未来版本,32-255不允许使用,因为在ASSIC码中他们是可打印字符,其他协议常会用一个可打印字符作为版本号,RTMP协议为了做出区分,不使用可打印字符作为版本号。服务端无法识别客户端版本号时,响应3,客户端要么降到版本3,要么放弃握手。

C1和S1都是1536字节,格式如下:

在这里插入图片描述

time字段用来协调消息的时间起点,因为rtmp的每个消息都是带有时间戳的。rtmp的主要目的是传输音视频数据,而音视频都是时间相关的信息。

zero字段必须是全零。random bytes是随机数,可以是任意字节内容。

C2和S2也是1536字节,格式如下:

在这里插入图片描述

C2的time来自于S1的time字段,S2的time来自于C1的time字段。

C2的time2来自于C1的time字段,S2的time2来自于S1的time字段。

C2的random echo来自于S1的random bytes字段,S2的random echo来自于C1的random bytes字段。

一个完整的握手流程如下:

在这里插入图片描述

  • Unintialized(未初始化):在此阶段发送协议版本。
  • Version Sent(版本已发送):发送C0和S0后分别进入此状态,客户端等待S1,服务端等待C1。
  • Ack Sent(确认已发送):发送C2和S2后进入此状态。
  • Handshake Done(握手结束):收到C2和S2进入此状态。

复杂握手

上面是rtmp协议中描述的握手过程,被称为简单握手。现在还有一种称为复杂握手的握手方式,没有公开的官方说明,只有网络上流传着它的传说。

如果要实现一个可用的rtmp服务就需要实现复杂握手,因为有些客户端已经采用了复杂握手,并且拒绝简单握手,这其中就有ffplay。

复杂握手和简单握手的区别在于复杂握手的random bytes不只是单纯的随机字节,而是带有校验信息的。C1和S1结构对比如下图所示。

在这里插入图片描述

复杂握手的C1和S1有scheme 0scheme 1两种结构,它们的区别仅仅是keydigest的摆放顺序不同。不管是哪种结构,keydigest的结构都是相同的。另外以前的zero字段现在变成了version,注意和C0、S0的version区分开。在简单握手中,zero字段是全零,而在复杂握手中,zero不是零,我们就是以此来区分要进行简单握手还是复杂握手的。

keydigestoffset字段并不是直接编码的偏移量,计算时需要将各个字节相加来计算。

  • key offset:(offset[0] + offset[1] + offset[2] + offset[3]) % 632
  • digest offset:(offset[0] + offset[1] + offset[2] + offset[3]) % 728

对于key来说,keyoffset要占去132字节,因此偏移量最大是764-132=632字节;而对于digestdigestoffset要占36字节,最大偏移量是764-36=728字节。

服务端需要对C1进行校验,校验的方式是先找到C1中32字节的digest,然后去掉它,对剩余的部分做sha256哈希,最后将哈希结果与digest进行比较。

在这里插入图片描述

这里我们并不知道如何区分scheme 0scheme 1,网上几乎都是说先选一种scheme结构去做校验,如果失败就换另一种scheme去校验,如果成功,说明就是这种scheme。虽然能工作,但是怎么看都不是靠谱的样子,现在的version字段不要求为0,4个字节肯定是会编码一些信息的,将scheme编码到version字段中的确是可行的方案,不过由于没找到关于version字段含义的说明,也只能作为一种猜测,还需要进一步验证。关于C1中128字节的key也没有找到相关的用途说明。

对于S1,服务端需要以相同的方式生成digest供客户端校验。S2的生成要复杂一些,首先要将C1的digest哈希得到一个key,然后用这个key哈希S2的前1504字节得到sign,最后将sign放到S2的最后组成完整的S2。

在这里插入图片描述

以上就是复杂握手的过程,关于哈希用到的key和具体代码实现可用参考handshake.go

chunk

rtmp协议将消息分块后进行传输,分块的目的有两个:

  1. 避免大而不重要的消息阻塞小而重要的消息。
  2. 减少重复发送相同的消息头部。

chunk是rtmp的基本单位,每个chunk必须完整发送,也就是说发送完一个chunk之前,不能发送另一个chunk。

chunk有4个部分组成,分为Chunk HeaderChunk Data,如下图所示:

在这里插入图片描述

基本头部

基本头部中包含两个信息:消息头部的格式(fmt)和chunk stream id(csid)。fmt指示了消息头部的格式,消息头部一共4种类型,需要两个比特来编码;csid标识了该chunk属于哪一路chunk流,接收端需要根据它来组装消息。

本着能省则省的原则,基本头部的长度有1字节,2字节和3字节三种,根据chunk stream id的大小而定。

第一种情况是csid在2到63之间,用一个字节编码。

在这里插入图片描述

第二种情况是csid在64到319之间,使用2字节编码。

在这里插入图片描述

第三种情况是csid在320到65599之间,使用3字节编码。

在这里插入图片描述

注意,此情况下csid的计算方式是第三个字节 × 256 + 第二个字节 + 64,换句话说,csid是以小端序编码的。

csid的范围是2到65599,0和1保留,0表示基本头使用2字节编码,1表示基本头使用3字节编码。2也是一个特殊的csid,专用于协议控制消息和用户控制消息,普通消息的csid都是从3开始。

消息头部

消息头部中记录了消息的相关信息,包括消息的时间戳,长度,类型和所属的消息流id。消息头部有4种类型,由基本头部中的fmt指定。

Type 0

0类型消息头部共11字节,包含完整的头部信息。

在这里插入图片描述

根据能省则省的原则,消息头部中的timestamp只有3字节,如果时间戳超过了0xFFFFFF,需要将它设置为0xFFFFFF,然后将真正的时间戳写入Chunk HeaderExterned Timestamp中。

message length也只有3个字节,所以消息的最大长度不能超过0xFFFFFF

message type id(mtid)表示消息的类型,不同类型的消息携带的负载也不同,这个后面再说。

message stream id(msid)表示消息所属的消息流,是这些字段中唯一以小端序编码的字段。0是一个特殊的msid,专用于协议控制消息和用户控制消息。

Type 1

1类型的消息头部共7字节,相比于类型0,缺少了message stream id

在这里插入图片描述

如果消息属于同一个消息流,那么后面的消息就不用重复发送消息流id了。注意这里的头三个字节不是时间戳了,而是时间戳增量。有些rtmp实现始终将头三个字节解释为时间戳其实是不对的,之所以也能正常工作是因为大部分时候消息时间戳都是从零开始的。如果时间戳增量超过了0xFFFFFF,也需要编码到Externed Timestamp中。

Type 2

2类型的消息头只剩3个字节,用来设置时间戳增量。如果超过了0xFFFFFF,也需要编码到Externed Timestamp中。

在这里插入图片描述

Type 3

3类型消息头部是0字节,这下全都省了。对于固定长度,时间戳成等差数列的消息,第一个分块发送一个0类型消息头,第二个分块发送一个2类型的消息头,之后的消息就可以发送3类型的消息头了,比如音频数据。此外,如果一个大消息被分成多个chunk发送,除了第一个chunk,后面的chunk也可以发送3类型的消息头,比如视频数据。

在读取消息时,对时间戳的处理要格外小心。因为对于音视频消息,时间戳时非常重要的,会影响到播放,如果时间戳错误可能会导致音画不同步。在笔者的实现中,就曾犯过这样的错误。特别是对时间戳增量的处理,如果处理的不对,音画不同步的现象会随着播放的进行逐渐累积。

扩展时间戳

扩展时间戳是一个可选项,只有当消息头部中的时间戳(时间戳增量)大于0xFFFFFF时,才存在扩展时间戳。

分块负载

chunk的负载长度(chunk size)也不是固定的,但是不能小于128字节。在Chunk Header中并没有指定负载长度,它是客户端/服务端的一个状态,默认是128字节,可以通过协议控制消息来修改,并且读和写的chunk size可以单独设置。

消息

rtmp有很多类型的消息,不同类型的消息有不同的格式和作用。

协议控制消息

协议控制消息的message stream id必须是0,chunk stream id是2,主要用于设置chunk stream的相关状态。协议控制消息的时间戳都是0,必须立即生效。

Set Chunk Size

在这里插入图片描述

mtid=1,用来设置分块的负载大小。确切的说是对方的读chunk size,自己的写chunk size,因为读写可以设置不同的chunk size。该消息负载4字节,有效位只有31比特,也就是说chunk的最大负载是0x7FFFFFFF字节。

Abort Message

在这里插入图片描述

mtid=2,大小4字节,内容是csid,用来告诉对方放弃读取所指定chunk stream中的消息。比如某个消息发送了一半不想发送了,就可以使用这个消息来取消。

Acknowledgement

在这里插入图片描述

mtid=3,rtmp也提供了窗口机制,当接收端接收到窗口大小的字节数后,需要发送一个确认消息。注意确认消息中的内容是截至目前为止已接收到的字节数。

Window Acknowledgement Size

在这里插入图片描述

mtid=5,用来设置窗口大小。

Set Peer Bandwidth

在这里插入图片描述

mtid=6,除了设置窗口大小,还会设置带宽模式,共3种:

  • 0:严格,将窗口大小设置为该消息指定的大小。
  • 1:宽松,可以使用该窗口大小,如果之前的窗口更小,也可以继续使用之前。
  • 2:动态,如果之前设置了严格模式,把该消息当作严格模式,否则忽略该消息。

用户控制消息

用户控制消息的message stream id也必须是0,chunk stream id是2,主要用于设置message stream的相关状态。

用户控制消息的消息类型为4,内容包括Event Type和Event Data,共7种类型。

在这里插入图片描述

Stream Begin一般在连接或创建流之后由服务端发给客户端。

命令消息

消息类型17和20都表示命令消息,区别是编码格式不同,17是采用AMF3编码,而20是采用AMF0编码。命令消息主要是控制流媒体的相关状态。

AMF格式与解码参见【Go】FLV文件解析(二)。

命令消息分成两大类:NetConnection Command和NetStream Command。

在这里插入图片描述

无论是推流还是拉流,客户端都会先发送connect命令。接下来对于推流端会发送publish命令,可能还有FCPublish命令,取决于客户端;而对于拉流端,会发送play命令。

所有命令的前两项都是CommandNameTransactionID,之后的结构因命令而异。对于这些命令的具体结构,我的建议是把他们保存到文件里,用二进制查看器亲自看一看,vscode就不错。

connect

connect命令消息结构如下。

在这里插入图片描述

其中Command Name为connect,Transaction ID为1,User Argument是可选项。

服务端收到connect命令后需要发送一个响应,响应也是command消息,结构是一样的,其中Command Name为_result_error,Transaction ID固定为1。

publish

publish命令用来发布流,会携带流名称和流类型两个信息,结构如下。

在这里插入图片描述

Publishing Type有以下三种:

  • live:不将数据写入文件,直播使用此类型。
  • record:将数据写入文件,如果文件已存在,覆盖原文件。
  • append:将数据追加到文件,如果文件不存在则创建。

play

play命令用来播放流,结构如下。

在这里插入图片描述

对于直播来说,重要的是Stream Name,点播会用到Start。

音视频消息

传输音视频数据是我们使用rtmp协议的主要目的,音频消息的消息类型是8,视频消息的消息类型是9。如果你熟悉FLV文件的结构,会发现这些数字很眼熟,都是Adobe出品的,所以定义是一样的。此外还有类型为18的视频元数据Tag,在rtmp中对应的是消息类型为18和15的消息,18是AMF0编码,15是AMF3编码。

对于音视频消息,其负载是FLV文件的Tag Data部分的内容,如果是做直播应用,直接缓存它,然后发送给播放端就可以了,如果是做点播应用,需要提取出Tag Data发送给客户端。

FLV文件中有三个特殊的Tag:Script Tag,Video Tag 0和Audio Tag 0。播放时,必须先将这三个Tag按顺序发送给播放端。

关于FLV文件的格式与解析参见【Go】FLV文件解析(一)。

交互流程

无论是推流还是拉流,都是从connectcreateStream命令开始的。注意,这里的connect命令并不是连接到服务器,而是连接到应用。这里需要说明一下rtmp的地址结构,如下图。

在这里插入图片描述

rtmp的地址由4个部分组成,connect命令会携带application信息,至于streamName则由publishplay命令携带,可以简单的字符串,也可以是带参数的路径,如stream_name?secret=xxx&key=xxx,取决于服务端的实现。

createStream命令创建的是message stream,之后的音视频消息都会在这条message stream上传输。与它对应的另一个命令是deleteStream,用来删除一条message stream。

在这里插入图片描述

上面是connectcreateStream的流程示意,上面的流程并不是强制的,有时候你会发现精简一下,去掉几个过程也能正常工作,但connectcreateStream的response是必不可少的。

推流

推流使用publish命令,有些客户端还会发动FCPublish命令,一般我们会忽略掉后者。推流的大致流程如下。

在这里插入图片描述

这个过程也不是十分严格的,除了publish result是必须的,实际的过程可能有出入,收到音视频数据后如何缓存它们已经超出了协议本身的内容,可以有不同的实现方案。

作为直播的服务端,从抽象的角度来说,流缓存器应该是一个无限长的队列,发布者向队尾写入数据。队列上有一些入口,播放端从入口开始读取数据,但是不删除。注意,入口不一定是队头,这些入口对应的应该是关键帧所在位置。当播放端读到队尾时,需要等待发布者写入数据。

然而实际中我们不可能实现一个无限长的队列,不过我们可以使用环形队列来替代。想象在三维空间中的一个螺旋上升的弹簧,在二维空间就是一个圆。还要注意读写的时候不能加锁,因为不能让读阻塞写,也就是拉流端不能影响推流端。当拉流端读的太慢时,启动丢帧机制。

在这里插入图片描述

播放

播放使用play命令。播放流程如下所示。

在这里插入图片描述

以上流程也不是严格的,比如直播就可以不用发送StreamIsRecorded消息,如果play命令没有带reset标志,服务端也不需要发送reset响应。注意在发送音视频消息之前要先发送Metadata,第一个视频消息必须时video tag 0,其中包含了解码视频需要的SPS和PPS,第二个视频帧要是一个关键帧,否则解码会失败。

示例程序

rtmp协议本身的内容不多,实现起来也不难,我希望向使用http服务那样使用rtmp服务,下面是我实现的一个直播示例。前两个HandleCommand分别处理FCUpublishplay命令,HandleData用来处理音视频数据。

本文只介绍了使用rtmp实现推拉流所涉及的内容,完整的rtmp协议可以阅读协议原文。

package main

import (
  "fmt"
  "log"

  "github.com/chenyj/rtmp"
  "github.com/chenyj/rtmp/encoding/av"
)

func main() {
  // 流缓存器
  streams := map[string]rtmp.Streamer{}
  // 处理unpublished命令
  rtmp.HandleCommand(rtmp.CMD_FCUNPUBLISH, func(w rtmp.MessageWriter, r *rtmp.Request) error {
    s, ok := streams[r.StreamPath]
    if !ok {
      return nil
    }
    s.Write(nil)
    return nil
  })
  // 处理play命令
  rtmp.HandleCommand(rtmp.CMD_PLAY, func(w rtmp.MessageWriter, r *rtmp.Request) error {
    s, ok := streams[r.StreamPath]
    if !ok {
      return rtmp.ResponsePlay(w, false, "stream not found")
    }
    err := rtmp.ResponsePlay(w, true, "")
    if err != nil {
      return err
    }

    go func(it rtmp.Iterator) {
      for {
        p, err := it.Next()
        if err != nil {
          break
        }
        err = w.WriteMessage(rtmp.NewMessage(p))
        if err != nil {
          break
        }
      }
      fmt.Println("播放结束")
    }(s.Iterator())

    return nil
  })
  // 处理音视频数据
  rtmp.HandleData(func(app, path string, p *av.Packet) error {
    s, ok := streams[path]
    if !ok {
      s = rtmp.NewStream(3000)
      streams[path] = s
    }
    s.Write(p)
    return nil
  })
  // 使用默认端口启动rtmp服务
  err := rtmp.ListenAndServe("", nil)
  log.Fatal(err)
}

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

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

相关文章

用户身份管理(CIAM)如何帮助业务持续增长?|身份云研究院

精明的决策者很早就意识到,数字化转型的核心是为用户提供完善的“数字旅程”,这里的用户包括“员工”和“客户”,而“数字旅程”的核心则是持续提供优质的「数字用户体验(DCX)」。本文将主要探讨如何制定完善“客户数字…

window版Docker打包镜像并上传到服务器使用

背景:利用jmeter实现自动化进行线上监视,要部署于多台服务器上监视,为了节省时间,方便使用,最终决定使用docker将自动化脚本打包成镜像,这样只要服务器上安装docker环境,直接下载镜像就可以使用…

2023全新SF授权系统源码 V3.7全开源无加密版本

内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示三、学习资料下载一、详细介绍 SF多应用综合验证授权系统 V4.0更新内容 采用ThinkPHP 6.0 EasyWebAdmin 支持自定义判断规则(默认提供域名QQ机器码规则) 支持在线充值,用户Api授权&…

(免费分享)springboot人事管理系统

基础环境:1. JDK:1.82. MySQL:5.73. Maven3.01. 核心框架:Spring Boot 2.2.13.RELEASE2. ORM框架:MyBatisPlus 3.1.23. 数据库连接池:Druid 1.2.84. 安全框架:Apache Shiro 1.8.05. 日志:SLF4J &#xff0c…

最近邻插值法

文章目录前言一、最近邻插值法二、代码实现总结本章节进入图像处理,利用python语言来实现各种图像处理的方法,从软件角度去理解图像处理方法,为后期的FPGA处理图像做准备。 前言 一、最近邻插值法 最近邻插值就是在目标像素点上插入离对应原…

界面控件DevExpress WinForm中文教程 - 如何应用Windows 11 UI?

DevExpress WinForm拥有180组件和UI库,能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForm能完美构建流畅、美观且易于使用的应用程序,无论是Office风格的界面,还是分析处理大批量的业务数据,它都能轻松胜任…

全网最详细的org.springframework.jdbc.UncategorizedSQLException的多种解决方法

文章目录1. 引出问题2. 分析问题3. 解决问题4. 解决该问题的其他方法4.1 方法14.2 方法24.3 方法34.4 方法4如果你遇到的问题不是我所遇到的问题,可以使用最下面的方法解决你遇到的这个错误。 1. 引出问题 今天在写“Mybatis-Plus中分页插件PaginationInterceptor…

利用Python读取外部数据文件

名字:阿玥的小东东 学习:python、c 主页:阿玥的小东东 目录 一、读取文本文件的数据 二、读取电子表格文件 三、读取统计软件生成的数据文件 不论是数据分析,数据可视化,还是数据挖掘,一切的一切全都是以…

java常用类: Arrays类的常用方法

java常用类型: Ineteger等包装类 String类,StringBuffer类和StringBuilder类 Math类及常用方法 System类及常用方法 Arrays类及常用方法 BigInteger类和BigDecimal类及常用方法 日期类Date类,Calender类和LocalDateTime类 文章目录ArraysArrays常用方法Arrays.sort(…

全排列问题的解题思路

假设有这么个正整数n,要求输出1到n的所有排列?   输入:3 输出:123,132,213,231,312,321 一、无脑循环求解? 拿到这个问题,当然我的第一个想法就…

上下文驱动的图上文案生成

✍🏻 本文作者:持信、弈臻、悟放、积流、孟诸1. 摘要为商品图片上特定位置配上装饰性文案来突出重点在广告业务中有着十分广泛的应用前景。然而,现有的图片文案描述生成系统均生成与图片位置关系无关的文案,无法很好地应用到广告业…

66. Python 类的总结

66. 类的总结 文章目录66. 类的总结1. 类2. 对象3. 类的语法4. 属性5. 方法6. 创建对象7. 调用属性8. 调用方法9. 方法的值的传递第1情况:没有值第2种情况:有值10. __init__方法1. __init__写法2. __init__作用3. 重点关注11. 自定义函数和方法的异同1. …

Unity渲染管线(Render Pipeline)笔记

Rendering是什么 渲染Rendering可以理解为将拿到的3D数据生成一副2D图像的过程。 这些3D数据包含:3D模型本身的点的信息,三角形面的描述信息,模型应用的材质以及摆放的虚拟相机的信息等。Rendering过程会使用全部的数据(物体的几何…

摄像头录像大师推荐?如何录制摄像头,图文教程

现如今,很多笔记本电脑上都会携带摄像头,用来录制摄像头画面,方便小伙伴的时候。可很多小伙伴却表示,自己不知道电脑摄像头画面该如何录制。有什么有什么好用的摄像头录制大师?如何录制摄像头画面?本篇文章…

【10w字】超详细【百分百拿offer】的面试教程,接口测试篇

1.请问你是如何做接口测试的? 大体来说,经历以下过程:接口需求调研、接口测试工具选择、接口测试用例编写、接口测试执行、接口测试回归、接口测试自动化持续集成。 具体来说,接口测试流程分成以下九步: 第一步&…

RabbitMq之发布确认(高级)

一.发送消息到交换机失败 正常情况下生产者只发布消息到交换机,无法确定是否成功把消息发送到交换机当中,由此发布确认的回调函数可以通知生产者消息是否发送到了交换机。 代码如下 1.先创建交换机、队列等信息 package jot.jothot.testMq;import or…

OS 学习笔记(6) 操作系统引导

OS 学习笔记(6) 操作系统引导 这篇笔记对应的王道OS 1.5 操作系统引导,同时参考了 《Operating System Concepts, Ninth Edition》和 俗称ostep的《 Operating Systems: Three Easy Pieces》还有 《Operating Systems: Principles and Practice》 文章目录OS 学习笔…

实战还原--从大黄蜂样本到域控管理员技术解析

0 前言实战案例还原《BumbleBee Roasts Its Way To Domain Admin》一文详细的描述了一次渗透案例,但其文章组织架构建立在ATT&CK框架上,而不是按照时间线逻辑来组织,因此对于渗透人员了解学习其前后过程有些困难,特此梳理一番…

发布微信小程序获取收集用户信息权限

前言在发布微信小程序的时候我们经常会遇到审核不通过的情况,其中一种特别让我头疼就是说小程序收集、使用和储存用户信息。不给予通过。但是他们的接口又不能提供这个功能,并且老是改动,真的特别的麻烦加无语。有时候审核偷一下懒&#xff0…

算法刷题-回文数、找出小于平均值的数、旋转图像(C_C++)

文章目录回文数找出小于平均值的数旋转图像回文数 给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。 回文数是指正序(从左向右)和倒序(从右向左)读都是一样的…