• 音视频学习(十四)——rtsp详解


    概念

    rtsp(Real Time Streaming Protocol,RFC2326),实时流传输协议,是TCP/IP协议体系中的一个应用层协议。

    该协议主要规定了一对多应用程序如何有效地通过IP网络传送多媒体数据。RTSP体系结位于RTP和RTCP之上(RTCP用于控制传输,RTP用于数据传输),使用TCP或UDP完成数据传输!

    基本流程

    在这里插入图片描述

    • OPTIONS

      • C—>S:客户端向服务器端发现OPTIONS,请求可用的方法;
      • S—>C:服务器端回复客户端,消息中包含当前可用的方法;
    • DESCRIBE

      • C—>S:客户端向服务器请求媒体描述文件,一般通过rtsp开头的url来发起请求,格式为sdp;
      • S—>C:服务器回复客户端sdp文件,该文件告诉客户端服务器有哪些音视频流,有什么属性,如编解码器信息,帧率等;
    • SETUP

      • C—>S:客户端向服务器端发起建立连接请求,请求建立会话连接,准备开始接收音视频数据,请求信息描述了期望音视频数据包基于UDP还是TCP传输,指定了RTP,RTCP端口,以及是单播还是组播等信息;
      • S—>C:服务器端收到客户端请求后,根据客户端请求的端口号确定发送控制数据的端口以及音视频数据的端口;
    • PLAY

      • C—>S:客户端向服务端请求播放;
      • S—>C:服务器回复客户端200 OK! 之后开始通过SETUP中指定的端口开始发送数据;
    • TEARDOWN

      • C—>S:结束播放的时候,客户端向服务器端发起结束请求;
      • S—>C:服务端收到消息后,向客户端发送200 OK,之后断开连接;

    消息格式

    请求格式

    在这里插入图片描述

    响应格式

    在这里插入图片描述

    SDP格式

    简介

    sdp(Session Description Protocol,会话描述协议)对应RFC2327。RTSP协议中使用sdp进行媒体信息的描述。 sdp的目的就是在媒体会话中,传递媒体流信息,允许会话描述的接收者去参与会话,定义了会话描述的统一格式。

    格式

    sdp信息由多行"="组成,其中是一个字符串,是一个字符串,type表示类型,value的格式视type而定,整个协议区分大小写,"="两侧不允许有空格!

    sdp会话描述包含一个会话级描述(session_level_description)和多个媒体级描述(media_level description)组成!会话级描述的作用域是整个会话,其位置从"v="行开始到第一个媒体描述为止;媒体级描述是对单个的媒体流进行描述,如传输过程中的视频流信息,从m=开始到下一个媒体描述为止,如下图所示:

    在这里插入图片描述

    字段描述

    version

    必选。

    • 格式: v=
    • 描述: 表示sdp的版本号,不包含次版本号

    origin

    必选。

    • 格式:o=
    • 描述:o=选项对会话的发起者进行了描述;
      • username:是用户的登录名, 如果主机不支持,则用"-"代替, 不能包含空格;
      • sessionid:是一个数字串,在整个会话中,必须是唯一的,建议使用个NTP 时间戳;
      • version:该会话公告的版本,建议使用NTP时间戳;
      • network type:网络类型,一般为"IN",表示internet;
      • address type:地址类型,一般为IP4;
      • address:地址;

    Session Name

    必选。

    • 格式:s=
    • 描述: 会话名称,在整个会话中有且只有1个"s="

    Connection Data

    可选。

    • 格式: c=
    • 描述: 表示媒体连接信息;一个会话级描述中必须有"c="或者在每个媒体级描述中有一个"c="选项,也可能在会话级描述和媒体级描述中都有"c="选项;
      • network type:网络类型,一般为IN,表示internet;
      • address type:地址类型,一般为IP4;
      • connection address,地址,可能为域名或ip地址两种形式;

    Bandwidth

    可选。

    • 格式:b=:
    • 描述:该选项描述了建议的带宽,单位 kbs/s,可选,modifier包括两种类型,CT和AS,CT表示总带宽,AS表示单个媒体带宽的最大值;bandwidth-value表示具体的带宽;

    Times

    必选。

    • 格式:t=
    • 描述:t字段描述了会话的开始时间和结束时间, NTP时间,单位是秒;如果 为0表示过了之后,会话一直持续;当 和 都为0的时候,表示持久会话,如拉实时流;

    email

    可选。

    • 格式:e=
    • 描述:用来描述邮件地址;

    phone number

    可选。

    • 格式:p=
    • 描述:用来描述电话号码;

    URI

    可选。

    • 格式:u=
    • 描述:url值;

    a=(*)

    可选。

    • 格式:a=<*>
    • 描述:表示一个会话级别或媒体级别下的0个或多个属性;

    media information

    必选。

    • 格式:m=
    • 描述:表示一个会话的媒体信息;
      • media:媒体类型。有"audio",“video”,“application”,“data”(不向用户显示的数据),“control”(描述额外的控制通道);
      • port:表示媒体流发往传输层的端口,对于RTP,偶数端口用来传输数据,奇数端口用来传输信令;
      • transport type:表示传输协议,与"c="一行相关联,一般用RTP/AVP表示,即 Realtime Transport Protocol using the Audio/Video profile over udp,即我们常说的RTP over udp;
      • fmt list:表示媒体格式,分为静态绑定和动态绑定;
        • 静态绑定:媒体编码方式与RTP负载类型有确定的一一对应关系,如: m=audio 0 RTP/AVP 8;
        • 动态绑定:媒体编码方式没有完全确定,需要使用rtpmap进行进一步的说明;
    m=video 0 RTP/AVP 96
    a=rtpmap:96 H264/90000
    
    • 1
    • 2

    rtpmap

    可选。

    • 格式:a=rtpmap: /
    • 描述:会话的媒体信息;
      • payload type:表示动态负载类型,如 98表示h264;
      • encoding name:表示编码名称,如H.264;
      • clock rate:表示时钟频率,如90000;

    命令说明

    OPTION

    功能

    一般为RTSP客户端发起的第一条请求指令,该指令的目的是得到服务端提供了哪些方法!(OPTIONS, DESCRIBE, PLAY, PAUSE, SETUP, TEARDOWN, SET_PARAMETER, GET_PARAMETER等)。

    请求

    # 请求报文
    OPTIONS rtsp://192.168.0.110:554/Streaming/Channels/101 RTSP/1.0
    CSeq: 2
    User-Agent: LibVLC/3.0.17.4 (LIVE555 Streaming Media v2016.11.28)
    
    • 1
    • 2
    • 3
    • 4
    • OPTIONS:标识请求命令的类型;
    • URI:请求的服务端的URI,以rtsp://开头的地址,一般为rtsp://ip:554(rtsp默认端口号);
    • VER:标识RTSP 版本号,一般常见RTSP/1.0;
    • CSeq:数据包序列号,由于OPTIONS一般而言为RTSP请求的第一条指令,一般而言,针对OPTIONS,该值为1;
    • User-Agent:用户代理;

    响应

    # 响应报文
    RTSP/1.0 200 OK
    CSeq: 2
    Public: OPTIONS, DESCRIBE, GET_PARAMETER, PAUSE, PLAY, SETUP, SET_PARAMETER, TEARDOWN
    Date:  Mon, Dec 05 2022 17:00:10 GMT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • Public:服务端支持的方法;
    • Date:响应时间;

    注意:OPTION响应中的序列号与请求中的序列号相同。

    DESCRIBE

    功能

    客户端发起OPTION请求后,得到了RTSP服务器支持的指令。在此之后,客户端会继续向服务器发送DESCRIBE消息,来获取会话描述信息(sdp)。

    请求

    # 第一次发送describe请求
    DESCRIBE rtsp://192.168.0.110:554/Streaming/Channels/101 RTSP/1.0
    CSeq: 3
    User-Agent: LibVLC/3.0.17.4 (LIVE555 Streaming Media v2016.11.28)
    Accept: application/sdp
    
    # 服务端返回401---未认证
    RTSP/1.0 401 Unauthorized
    CSeq: 3
    WWW-Authenticate: Digest realm="IP Camera(J4640)", nonce="a1c588527e4e05c101acf26a8ce7bea6", stale="FALSE"
    Date:  Mon, Dec 05 2022 17:00:10 GMT
    
    # 第二次发送describe请求
    DESCRIBE rtsp://192.168.0.110:554/Streaming/Channels/101 RTSP/1.0
    CSeq: 4
    Authorization: Digest username="admin", realm="IP Camera(J4640)", nonce="a1c588527e4e05c101acf26a8ce7bea6", uri="rtsp://192.168.0.110:554/Streaming/Channels/101", response="6e0f28a1a40865ab098d748eb306be92"
    User-Agent: LibVLC/3.0.17.4 (LIVE555 Streaming Media v2016.11.28)
    Accept: application/sdp
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • Accept:指明接收数据的格式,如application/sdp表示接收sdp信息,之后加入\r\n表示此条目结束;
    • CSeq:RTSP序列号,一般DESCRIBE包在RTSP请求过程中的序列号为2(此例中CSeq为3),之后加入\r\n表示此条目结束;
    • UserAgent:指明用户代理,由于是最后一个条目,加入两组\r\n表示结束;
    DESCRIBE消息,有两种返回结果:
    1)服务端需要认证,则首先返回401,并要求客户端认证,客户端再次发送包含认证信息的DESCRIBE指令,服务端收到带认证信息的DESCRIBE请求,返回sdp信息给客户端;
    2)服务端不需要认证,则直接返回sdp;
    
    • 1
    • 2
    • 3

    响应

    # 响应报文
    RTSP/1.0 200 OK
    CSeq: 4
    Content-Type: application/sdp
    Content-Base: rtsp://192.168.0.110:554/Streaming/Channels/101/
    Content-Length: 788
    
    v=0
    o=- 1670259610794238 1670259610794238 IN IP4 192.168.0.110
    s=Media Presentation
    e=NONE
    b=AS:5100
    t=0 0
    a=control:rtsp://192.168.0.110:554/Streaming/Channels/101/
    m=video 0 RTP/AVP 96
    c=IN IP4 0.0.0.0
    b=AS:5000
    a=recvonly
    a=x-dimensions:2560,1440
    a=control:rtsp://192.168.0.110:554/Streaming/Channels/101/trackID=1
    a=rtpmap:96 H265/90000
    a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqABQCAFoWNrkk5TNwEBAQQAADhAAAV+QoQ=; sprop-pps=RAHA8vAiQA==
    m=audio 0 RTP/AVP 11
    c=IN IP4 0.0.0.0
    b=AS:50
    a=recvonly
    a=control:rtsp://192.168.0.110:554/Streaming/Channels/101/trackID=2
    a=rtpmap:11 PCM/16000
    a=Media_header:MEDIAINFO=494D4B480103003E0000007D000000000000000000000000000000000000;
    a=appversion:1.0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • Content-type:回复内容类型,值为application/sdp;
    • Content-Base:一般用RTSP URI表示;
    • Content-length:返回的sdp信息的长度 ;

    ANNOUNCE

    功能

    • C—>S:将请求URL标识的演示文稿或媒体对象的描述发布到服务器;
    • S—>C:实时更新会话描述;
    • 如果新的媒体流被添加到演示文稿中(例如,在实时演示中),则应该再次发送整个演示文稿描述,而不仅仅是附加的组件,以便可以删除组件;

    请求

    ANNOUNCE rtsp://192.168.0.110:554/Streaming/Channels/101 RTSP/1.0 
    CSeq:312
    Date:23Jan 199715:35:06GMT
    Session:47112344Content-Type:application/sdp
    Content-Length:332
    文本内容
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    响应

    RTSP/1.0 200 OK
    
    • 1

    SETUP

    功能

    • 指明媒体流该以什么方式传输;
    • 每个流PLAY之前必须执行SETUP操作;
    • 发送SETUP请求时,客户端会指定两个端口,一个端口用于接收RTP数据;另一个端口接收RTCP数据,偶数端口用来接收RTP数据,相邻的奇数端口用于接收RTCP数据;

    请求

    SETUP rtsp://192.168.0.110:554/Streaming/Channels/101/trackID=1 RTSP/1.0
    CSeq: 5
    Authorization: Digest username="admin", realm="IP Camera(J4640)", nonce="a1c588527e4e05c101acf26a8ce7bea6", uri="rtsp://192.168.0.110:554/Streaming/Channels/101/", response="512cc1939bddf54e217bdb9ca51cdfc7"
    User-Agent: LibVLC/3.0.17.4 (LIVE555 Streaming Media v2016.11.28)
    Transport: RTP/AVP/TCP;unicast;interleaved=0-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • URI:请求的RTSP服务器的地址;
    • VER:RTSP的版本;
    • Transport:媒体流的传输方式,具体包括传输协议如RTP/UDP;指出是单播,组播还是广播;声明两个端口,一个奇数,用于接收RTCP数据,一个偶数,用于接收RTP数据;
    • CSeq:数据包请求序列号;
    • User-Agent:用户代理;
    • Authorization:认证信息;

    响应

    RTSP/1.0 200 OK
    CSeq: 5
    Session: 366292562;timeout=60
    Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=224b8452;mode="play"
    Date:  Mon, Dec 05 2022 17:00:10 GMT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • mode:当前rtsp连接的模式;
    • Session:会话ID;

    PLAY

    功能

    客户端发送的播放请求,发送播放请求的时候可以指定播放区间。发起播放请求后,如果连接正常,则服务端开始播放,即开始向客户端按照之前在TRASPORT中约定好的方式发送音视频数据包。

    请求

    PLAY rtsp://192.168.0.110:554/Streaming/Channels/101/ RTSP/1.0
    CSeq: 6
    Authorization: Digest username="admin", realm="IP Camera(J4640)", nonce="a1c588527e4e05c1cf26a8ce7bea6", uri="rtsp://192.168.0.110:554/Streaming/Channels/101/", response="0365b758c23e959fa5964cb47098b"
    User-Agent: LibVLC/3.0.17.4 (LIVE555 Streaming Media v2016.11.28)
    Session: 366292562
    Range: npt=0.000-
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • URI:请求的RTSP 地址;
    • Version:版本号;
    • CSeq:请求的序列号;
    • User-Agent:用户代理;
    • Session:会话id,与SETUP请求返回id对应;
    • Authorizatiuon:认证信息;
    • Range:PLAY消息特有的,代表请求播放的时间段,使用ntp时间来表示。“Range: npt=0.000-”表示拉实时流。

    响应

    RTSP/1.0 200 OK
    CSeq: 6
    Session: 366292562
    RTP-Info: url=rtsp://192.168.0.110:554/Streaming/Channels/101/trackID=1;seq=16946;rtptime=2502369666
    Date:  Mon, Dec 05 2022 17:00:10 GMT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • URI:请求的RTSP 地址;
    • Version:版本号;
    • CSeq:请求的序列号;
    • User-Agent:用户代理;
    • Session:会话id,与SETUP请求返回id对应;
    • RTP-Info:RT;
    • Date:日期;

    PAUSE

    功能

    暂停请求会使得流传输暂时中断,主要用于回放或录制

    请求

    PAUSE rtsp://192.168.0.110:554/Streaming/Channels/101/ RTSP/1.0
    CSeq:12
    Session:366292562
    
    • 1
    • 2
    • 3
    • URI:请求的RTSP 地址;
    • Version:版本号;
    • CSeq:请求的序列号;
    • Session:会话id,与SETUP请求返回id对应;

    响应

    RTSP/1.0  200 OK
    CSeq:12
    Session:366292562
    
    • 1
    • 2
    • 3

    TEARDOWN

    功能

    结束流。

    请求

    TEARDOWN rtsp://192.168.0.110:554/Streaming/Channels/101/ RTSP/1.0
    CSeq: 7
    Authorization: Digest username="admin", realm="IP Camera", nonce="a1c588527e401acf26a8ce7bea6", uri="rtsp://192.168.0.110:554/Streaming/Channels/101/", response="d57351cf4606ef70ef80e4709b"
    User-Agent: LibVLC/3.0.17.4 (LIVE555 Streaming Media v2016.11.28)
    Session: 366292562
    
    • 1
    • 2
    • 3
    • 4
    • 5

    字段基本与其他命令含义相同。

    响应

    RTSP/1.0 200 OK
    CSeq: 7
    Session: 366292562
    Date:  Mon, Dec 05 2022 17:00:13 GMT
    
    • 1
    • 2
    • 3
    • 4

    GET_PARAMETER

    功能

    向服务器获取参数,一般用于获取时间范围。当发送的请求中没有相关请求参数时,则用作保持RTSP连接**(tcp方式的心跳)**。

    请求

    GET_PARAMETER rtsp://192.168.0.110:554/Streaming/Channels/101/?transportmode=unicast&profile=Profile_12 RTSP/1.0
    CSeq: 17
    Authorization: Digest username="admin", realm="bcad28138995", nonce="a1a5b9d3865180dc1cb2eb2a27", uri="rtsp://192.168.0.110:554/Streaming/Channels/101/", response="4764a1f2772821f5528e18c3f9"
    User-Agent: LibVLC/3.0.11 (LIVE555 Streaming Media v2016.11.28)
    Session: 366292562
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • URI:请求的RTSP 地址;
    • Version:版本号;
    • CSeq:请求的序列号;
    • User-Agent:用户代理;
    • Session:会话id,与SETUP请求返回id对应;
    • Authorizatiuon:认证信息;

    响应

    RTSP/1.0 200 OK
    CSeq: 17
    Date: Thu, Aug 27 2022 18:29:00 GMT
    
    • 1
    • 2
    • 3

    SET_PARAMETER

    功能

    给URI指定的流地址设置参数。

    请求

    可跟一个或多个参数。

    响应

    RTP

    简介

    RTP是一种应用层协议,传输层协议可以是TCP或者UDP(UDP多一些)。

    RTP数据包由两部分组成,一部分是RTP Heaeder,一部分是RTP body,RTP Header占用最少12个字节,最多72个字节;另一部分是RTP Payload,用来封装实际的数据负载,如封装h264编码的视频数据!

    格式

    在这里插入图片描述

    RTP Header

    结构

      0               1                 2               3             4
        
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |V=2|P|X|  CC |M|     PT          | sequence number             |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       | timestamp                                                     |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       | synchronization source (SSRC) identifier                      |
       +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
       | contributing source (CSRC) identifiers                        |
       | ....                                                          |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 版本号(V):2比特,用来标志使用的RTP版本,固定为2;
    • 填充位(P):1比特,如果该位置位,则该RTP包的尾部就包含附加的填充字节;
    • 扩展位(X):1比特,如果该位置位的话,RTP固定头部后面就跟有一个扩展头部;
    • CSRC计数器(CC):4比特,含有固定头部后面跟着的CSRC的数目;
    • 标记位(M):1比特,该位的解释由配置文档(Profile)来承担;
    • 载荷类型(PT):7比特,标识了RTP载荷的类型;
    • 序列号(SN):16比特,发送方在每发送完一个RTP包后就将该域的值增加1,接收方可以由该域检测包的丢失及恢复包序列。序列号的初始值是随机的;
    • 时间戳:32比特,记录了该包中数据的第一个字节的采样时刻。在一次会话开始时,时间戳初始化成一个初始值。即使在没有信号发送时,时间戳的数值也要随时间而不断地增加(时间在流逝嘛)。时间戳是去除抖动和实现同步不可缺少的;
    • 同步源标识符(SSRC):32比特,同步源就是指RTP包流的来源。在同一个RTP会话中不能有两个相同的SSRC值。该标识符是随机选取的 RFC1889推荐了MD5随机算法,就是ssrc;
    • 贡献源列表(CSRC List):0~15项,每项32比特,用来标志对一个RTP混合器产生的新包有贡献的所有RTP包的源。由混合器将这些有贡献的SSRC标识符插入表中。SSRC标识符都被列出来,以便接收端能正确指出交谈双方的身份;

    对照表

    有效载荷类型表

    PT*Encoding Name**Audio/Video (A/V)**Clock Rate (Hz)**Channels**Reference*
    0PCMUA80001[RFC3551]
    1Reserved
    2Reserved
    3GSMA80001[RFC3551]
    4G723A80001[Vineet_Kumar][RFC3551]
    5DVI4A80001[RFC3551]
    6DVI4A160001[RFC3551]
    7LPCA80001[RFC3551]
    8PCMAA80001[RFC3551]
    9G722A80001[RFC3551]
    10L16A441002[RFC3551]
    11L16A441001[RFC3551]
    12QCELPA80001[RFC3551]
    13CNA80001[RFC3389]
    14MPAA90000[RFC3551][RFC2250]
    15G728A80001[RFC3551]
    16DVI4A110251[Joseph_Di_Pol]
    17DVI4A220501[Joseph_Di_Pol]
    18G729A80001[RFC3551]
    19ReservedA
    20UnassignedA
    21UnassignedA
    22UnassignedA
    23UnassignedA
    24UnassignedV
    25CelBV90000[RFC2029]
    26JPEGV90000[RFC2435]
    27UnassignedV
    28nvV90000[RFC3551]
    29UnassignedV
    30UnassignedV
    31H261V90000[RFC4587]
    32MPVV90000[RFC2250]
    33MP2TAV90000[RFC2250]
    34H263V90000[Chunrong_Zhu]
    35-71Unassigned?
    72-76Reserved for RTCP conflict avoidance[RFC3551]
    77-95Unassigned?
    96-127dynamic?[RFC3551]

    GB28181中对payload的定义

    负载类型编码名称时钟频率通道数SDD描述中m字段的media项
    4G.7238k HZ1audio
    8PCMA(G.711 A)8k HZ1audio
    9G7228k HZ1audio
    18G.7298k HZ1audio
    20SVACA(SVAC音频)8k HZ1audio
    96PS90k HZvideo
    97MPEG-4video
    98H.264
    99SAVC(SVAC视频)

    示例

    在这里插入图片描述

    RTP Body

    载荷数据。

  • 相关阅读:
    【数据结构】手撕双向链表
    阿里云服务器(Ubuntu22)上的MySQL8更改为大小写不敏感
    Dialog show的源码分析
    如何修复老照片?这三个方法建议收藏
    【kali-权限提升】(4.2.6)社会工程学工具包(中):中间人攻击工具Ettercap
    故障分析 | MySQL 无监听端口故障排查
    2023-11-09 LeetCode每日一题(逃离火灾)
    医药行业投资公司都有哪些?医药企业项目投资分析实用工具
    代码随想录算法训练营Day34 (Day33休息) | 贪心算法(3/6) LeetCode 1005.K次取反后最大化的数组和 134. 加油站 135. 分发糖果
    御神楽的学习记录之基于FPGA的AHT10温湿度数据采集
  • 原文地址:https://blog.csdn.net/www_dong/article/details/128192195