Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

实时通信协议族

9
Sep
2017

实时通信协议族

By Alex
/ in C++,Network
/ tags Multimedia
0 Comments
网络和多媒体
实时性问题

因特网最初设计用于数据的传输,例如文件、电子邮件。那时的语音传输由专门的电话网络负责。随着节点数量的增加、网络带宽的提高,基于因特网的多媒体需求逐渐出现,包括在线影视、在线视频会议。为了响应这类需求,研究人员开发出了专门的协议,包括:

  1. 实时传输协议(Realtime Transmission Protocol,RTP),用于传输媒体
  2. RTP的控制部分:实时传输控制协议(Realtime Transmission Control Protocol,RTCP)
  3. 实时流协议(Realtime Streaming Protocol)

注意:RTP这个术语有时候指RTP协议标准,有时候则指RTP协议标准中的RTP部分(不包含RTCP)。

我们都知道,TCP/IP协议族的基础是IP协议,此协议能很好的处理包的路由递送,但是却无法防止丢包、控制延迟。但是此协议让路由器逻辑很简单、网络易于扩容,这是因特网能繁荣的基础。

为了增强端到端的可靠性,TCP协议被引入,这是因特网上最广泛使用的协议。TCP能够自动重发丢失的包,并且保证包的顺序,TCP还提供了拥塞控制机制。

TCP的某些特性,在用来传递多媒体时,反而成为了障碍,原因是:

  1. 很多多媒体应用,例如视频监控,对延迟非常敏感
  2. 多媒体传输可以容忍某些丢包情况,其质量不会受到影响

因此,很多多媒体传输都是在UDP协议之上进行的。

多媒体应用程序可以分为两个类别:

  1. 交互式应用。例如视频会议、VoIP
  2. 非交互式应用。又可以细分为:
    1. 实时流媒体,例如视频监控预览、网络赛事直播
    2. 非实时流媒体,例如视频点播网站、歌曲点播应用、视频监控回放

除了非实时流媒体应用之外,多媒体应用不能容忍过长时间的缓冲以避免抖动,也不允许延迟的出现。

互操作性问题

大量多媒体应用程序,使用了不同的编码方式,这些编码方式在媒体质量、带宽消耗、计算资源消耗之间做了不同的权衡。

不同的多媒体应用(例如两个独立开发的VoIP应用)要能够相互通信,就必须以某种双方都能理解的编码方式进行媒体的交换。

实时传输协议

直接使用UDP传输流媒体数据包不能满足需求,因为UDP没有任何关于延迟、抖动的语义,或者它不理解何为“实时”。

实时传输协议(RTP)是一种专门处理实时需求的传输协议标准,主要用于处理音频、视频数据。 RTP允许接受者知晓接收到的数据包们在时间维上的关系,这样:

  1. 在缓冲媒体流并播放时不会出现顺序混乱的情况
  2. 多个媒体流(例如音频、视频)能够在播放时保持同步关系

此外RTP协议:

  1. 允许通信双方进行协商(Negotiation),选取两者都认可的编码方式。这解决互操作性问题
  2. 具有识别包丢失的问题,这样端点应用程序能够进行适当的处理
  3. 允许进行拥塞控制,媒体接收者能够向发送者进行网络拥塞状况的反馈(Feedback),这样发送者可以对码流质量进行调整,以改变带宽占用
  4. 支持帧指示(Frame Indication),例如媒体接收者需要知道那些数据包是属于相同的视频场景(Video Scene,帧)的,这样才能进行合适的处理
RTP的历史

通过网络进行音频传输的尝试从1970年代就开始了,在70-80年代多个语音包传输、时间戳、序列号相关的专利被批准。在1991年DARTnet成功完成了一系列语音传输的尝试,DARTnet使用的音频会议工具最终成为RTP版本0。

1992年RTP版本1发布,包含了若干因特网草案,此操作最终在1995年成为RTP版本2,包含:

  1. RFC1889,RTP
  2. RFC1890,RTP Profile —— AVP(用于音视频会议的RTP profile,最小化控制)

1996年,网景基于RTP和其他协议发布了Netscape LiveMidea。微软的NetMeeting软件也支持RTP。

RTP的设计确定了一个后续被广泛认同的原则 —— 应用层分帧原则(Application Level Framing ALF )。ALF认为应用程序更了解自己的需要,网络协议应该尽可能保持简单。例如MPEG解码器明白怎么样处理丢帧,如何从I帧、B帧丢失中恢复。

RTP支持大量种类的应用程序,对于每一类应用程序,RTP定义了一种Profile。Profile可以是:

  1. 一种对RTP协议头结构的约定
  2. 定义对RTP协议的扩展或者修改

RTP的载荷格式规定,则解释了RTP头之后的数据的结构。

RTP组成

RTP作为一个标准,实际上定义了一对协议:

  1. RTP,用于交换媒体数据
  2. RTCP,用于传输的控制,例如周期性的获得数据流传输质量的反馈信息、负责多个媒体流的同步。RTCP也负责传输组会话(Group Session)的参与者信息

尽管RTP协议相对于传输层是独立的,但是它通常在UDP/IP之上运行。当基于UDP/IP传输时,RTP和RTCP使用连续的两个端口号。

为了发起一个RTP会话,应用程序需要定义一对特定的目的传输地址(Destination Transport Addresses,DTA)—— 一个网络地址加上两个端口(分别用于RTP、RTCP)。在一个多媒体(eg,音频 + 视频 + 文本)会话中,每个媒体都在单独的RTP会话中传输。

视频会议的例子

在视频会议中,音频、视频媒体在不同的RTP会话中传输,这些会话使用的DTA是不同的,也就是它们使用两对不同的UDP端口。

视频会议也可能基于组播技术实现,这种情况下,需要使用两对多播地址的DTA。

音频、视频的RTP会话没有直接的关联,这允许接收者仅仅接收音频或者视频流。为了实现一个源的音视频同步,接收者可以使用RTCP包中的时序信息。

每个会议参与者都使用视频、音频应用程序,以块的方式发送数据。这些数据作为RTP包的载荷,RTP头中有专门的字段识别这些数据是如何编码的。

RTP头包含时序信息、序列号,接收者可以用这些信息重新构造音视频流的时序,不同源(音视频)时序信息都是单独构建的。

RTP结构

RTP数据包的整体结构、在网络和应用中的传递方式,如下图所示:

rpt-package

关于此图的说明如下:

  1. RTP协议通常运行在UDP之上,这意味着数据都是无状态、推送的方式传递
  2. RTP数据包是一个瘦协议,对需要持续传递数据(流式)的应用程序提供支持:
    1. 时序重构(Timing Reconstruction)
    2. 丢帧检测(Frame Loss Detection)
    3. 数据安全(Data Security)
    4. 内容识别(Content Identification)
  3. RTP协议不负责处理带宽保留(Reserve Bandwidth)和保证QoS
  4. RTP数据包的载荷部分是数字化(由编码器负责)的媒体流
转换器/混合器

除了发送者、接收者角色之外,RTP协议还定义了另外两个参与此协议处理的角色 —— 转换器(Translater)、混合器(Mixer)它们位于发送者、接收者角色之间,对经过(Passthrough)它们的RTP包做出处理:

  1. 转换器:对经过的RTP载荷进行转换,例如可以降低视频码流的比特率,降低带宽需求
  2. 混合器:用于混合来自多个媒体源的流,例如可以混合多个视频会议参与者的视频流,形成一个单独的流

注意,仅仅当若干RTP流经过混合器,混合器才起作用。例如在一个电话会议的应用场景中,多个音频流通常会经过混合器混合为一个流,以节约带宽占用。

RTP协议头

RTP协议的头格式如下图所示:

rtp-header

  1. 最前面的12字节(到SSRC为止)总是存在(上图中1、2、3……表示bit)
    1. V:2bit的版本号,一般取值2
    2. P:1bit的补白标记,如果此标记被设置,RTP包的尾部会包含1-N个补白字节。这些字节不属于载荷。补白的最后一个字节记录了补白的总数(包含它自己)。之所以需要补白,是为了满足某些加密算法对块(Block)长度的规定
    3. X:1bit的扩展标记,如果此标记被设置,则在标准头后面包含1个扩展头
    4. CC:4bit的CSRC标识符的计数器。如果载荷包含来自多个源的数据,则此计数大于1
    5. M:1bit的Marker标记,此标记的意义由Profile定义,此标记通常用于提示重要事件的发生,例如帧边界
    6. PT:7bit的载荷类型,指示载荷的数据类型。支持的类型包括PCM、MPEG1、MPEG2、JPEG视频、H.261等等。更多载荷类型可以通过Profile规范、载荷格式规范添加
    7. 16bit序列号:每当会话发送一个新的RTP包后,此序列号增加1。接收者可以基于此序列号进行丢包检测。此序列号的初始值是随机的,这样RTP包被加密后,尝试破解变得更加困难。当丢包出现后RTP协议层不做任何操作,应用程序负责对丢包事件做出响应,例如:某些视频应用可能在丢包时自动重放前一帧;另一些视频应用可能因为丢包而降低比特率
    8. 32bit时间戳:记录载荷中第一个字节的采样发生的时间。此字段的用途包括:让接收者可以按照适当的时间间隔来播放采样;允许多个媒体流保持同步;在计算抖动平滑(Jitter smoothing)时使用。时间戳使用的时钟解析度必须足够高,以满足同步精度、抖动度量精度。时间戳的初始值也是随机的,RTP没有规定时间戳的计量单位 —— 时间戳仅仅是时钟的tick计数,两个tick之间对应真实时间是多少,也是和应用程序相关的,这些仍然由Profile、载荷格式规定。频率表示每秒内有多少tick,因而tick数量 / 频率即得到对应真实世界的时间
    9. 32bit的SSRC标识符:用于识别同步源的标识符,此标识符被随机的生成,确保同一媒体会话中,任何两个同步源的标识符都不同。但是即使随机生成也有一定的概率出现重复,因此RTP实现必须有能力识别、解决冲突。当一个信号源改变自己的传递地址后,SSRC标识符必须也更改
  2.  32bit的CSRC标识符,此标识符最多有15个(取决于CC),用于识别此包的载荷部分由哪些(Contributing )源构成。CSRC标识符由混合器(Mixer)插入到包头中,其值就是Contributing源的SSRC头
  3. 后面是可选的扩展头
  4. 在RTP后面,是1-N个音视频帧,作为RTP载荷
实时传输控制协议

RTCP协议专门用于配合RTP协议使用。

在RTP会话中,参与者定期向RTP会话的所有参与者通过组播发送RTCP包。RTCP包中包含媒体发送者/接收者的报告,其内容包括发送数据包的数量、丢失数据包的数量、抖动信息(Jitter)。

应用程序可能使用RTCP中的信息,来自适应的改变媒体流的质量,以适应可用网络带宽。

RTCP为来自同一个发送者的不同媒体流提供了一种协作、同步的机制。例如,当SSRC取值冲突时,需要某个流改变SSRC,这就是通过RTCP完成的。

对于牵涉到多个单独的多媒体流的应用程序,它们之间的同步基于一个通用的系统时钟完成。最初发起会话的那个系统提供此时钟,RTCP消息可以保证会话的所有参与者都使用相同的时钟。

RTCP还用于传输会话中各成员之间的关系。

当会话参与者越来越多时,RTCP数据报的总量会变多。为了防止影响网络,RTCP包占据会话总数据量不会超过5%,这意味着随着参与者的增加RTCP包发送频率会降低

RTCP头

rtcp-header

  1. 2bit的版本号,使用的RTP协议的版本
  2. 1bit的补白标记,RTP包的最后是否具有补白
  3. 5bit的接收报告计数(Reception Report Count ),此包中包含的接收报告块的数量
  4. 8bit的消息类型
  5. 16bit的长度,指示此包的总长度
  6. 32bit的SSRC,同步源标识
RTCP消息类型

RFC 3550定义了五种类型的RTCP报文:

报文类型 说明
RR 接收者报告(Receiver Report),由不作为活动发送者的会话参与者生成,包含接收质量反馈信息。具体内容包括:接收到的最高包序列号、丢包数量、抖动情况、用于计算收发者之间延迟的时间戳信息
SR 发送者报告(Sender Report),由活动的发送者生成,除了RR中的信息外,SR还包含一个发送者信息段。此段提供媒体间(Inter-midea)同步需要的信息、累计发包数量、立即发送字节数
SDES 源描述条目(Source Description Items),包含对源的描述信息。在RTP包中源由32bit的一个头字段标识,但是这个名字不适合人类阅读,SDES则提供了一个所谓规范化名称(Canonical Names)作为会话参与者的唯一性标识。规范化名称可能包括用户名、电话号码、电子邮件地址或者其他信息
BYE 提示发送者结束会话的参与
APP 应用特定功能(Application Specific Functions),主要用于新应用、新特性开发时的实验性功能
实时流协议

RTSP由RFC2326定义,他是一个应用层多媒体展现协议,支持对实时媒体流进行控制 —— 例如暂停播放、Seek、快进、倒放,这些控制行为类似于DVD播放器。RTSP协议本身通常不进行媒体流的传递。

RTSP服务器为客户端维护一个会话,此会话由一个标识符来识别。RTSP协议支持TCP或者UDP传输,在一个RTSP会话中,客户端可能打开、关闭多个传输连接,以发送RTSP请求。

RTSP需要和低层的RTP或者RSVP之类的协议协同,才能在因特网上提供完整的流媒体服务。RTSP在RTP的基础上提供了选择传输通道(TCP/单播UDP/组播UDP)、传输机制的方法。RTSP报文独立于媒体流发送。

RTSP在客户端和流媒体服务器之间创建、控制音视频媒体流。服务器负责提供回放、录制等服务。

RTSP中的每个展现(Presentation)和媒体流都通过一个RTSP URL来识别,整体的展现信息和媒体属性在一个展现描述文件(Presenttation Description File)中记录,此文件中的信息可能包括编码方式、语言、RTSP URLs、目的地址/端口以及其它参数。客户端可以通过HTTP、电子邮件等方式获得展现描述文件。

RTSP协议有意的模仿HTTP协议的设计,但是两者有以下重要的不同:

  1. RTSP是有状态的,它必须维护会话状态,让RTSP请求和某个流关联
  2. RTSP是对称的,媒体服务器和客户端都可以发起请求。例如服务器可以发起请求,来设置流的回放参数

RTSP支持以下方法:

方法 说明
OPTIONS

返回服务器接收的请求类型。报文示例:

1
2
3
4
5
6
7
8
9
10
# 客户端请求
C->S:  OPTIONS rtsp://gmem.cc/media.mp4 RTSP/1.0
       CSeq: 1
       Require: implicit-play
       Proxy-Require: gzipped-messages
# 服务器应答
S->C:  RTSP/1.0 200 OK
       CSeq: 1
       # 支持的请求类型列表
       Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE
DESCRIBE

客户端发起此报文,或者RTSP URL所代表的展现/媒体对象的描述信息。报文示例:

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
# 客户端请求
C->S: DESCRIBE rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 2
# 服务器应答
S->C: RTSP/1.0 200 OK
      CSeq: 2
      Content-Base: rtsp://gmem.cc/media.mp4
      Content-Type: application/sdp
      Content-Length: 460
      # 下面是一个媒体行 —— 基于AVP Profile的RTP传输
      m=video 0 RTP/AVP 96
      a=control:streamid=0
      a=range:npt=0-7.741000
      a=length:npt=7.741000
      # 视频编码方式
      a=rtpmap:96 MP4V-ES/5544
      a=mimetype:string;"video/MP4V-ES"
      a=AvgBitRate:integer;304018
      a=StreamName:string;"hinted video track"
      # 下面是一个媒体行,音频部分
      m=audio 0 RTP/AVP 97
      a=control:streamid=1
      a=range:npt=0-7.712000
      a=length:npt=7.712000
      a=rtpmap:97 mpeg4-generic/32000/2
      a=mimetype:string;"audio/mpeg4-generic"
      a=AvgBitRate:integer;65790
      a=StreamName:string;"hinted audio track"
ANNOUNCE

当由客户端发起时,更新RTSP URL所代表的展现/媒体对象的描述信息

当由服务器发起时,实时的更新会话描述

SETUP

客户端请求服务器为某个流分配资源,并启动一个RTSP会话。报文示例:

1
2
3
4
5
6
7
8
9
10
11
C->S: SETUP rtsp://gmem.cc/media.mp4/streamid=0 RTSP/1.0
      CSeq: 3
      # 基于AVP Profile的RTP,使用UDP单播,RTP/RTCP端口
      Transport: RTP/AVP;unicast;client_port=8000-8001
 
S->C: RTSP/1.0 200 OK
      CSeq: 3
      # 附加服务器端口信息,媒体源唯一标识
      Transport: RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001;ssrc=1234ABCD
      # 分配会话标识符
      Session: 12345678
PLAY

客户端请求服务器通过SETUP分配的流推送数据。报文示例:

1
2
3
4
5
6
7
8
9
C->S: PLAY rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 4
      Range: npt=5-20
      Session: 12345678
 
S->C: RTSP/1.0 200 OK
      CSeq: 4
      Session: 12345678
      RTP-Info: url=rtsp://gmem.cc/media.mp4/streamid=0;seq=9810092;rtptime=3450012
PAUSE 客户端临时停止流的递送,但是不释放服务器资源
TEARDOWN 客户端请求服务器停止流的递送,并释放分配的资源
GET_PARAMETER

获取RTSP URL所代表的展现/流的某个参数的值。报文示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
S->C: GET_PARAMETER rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 9
      Content-Type: text/parameters
      Session: 12345678
      Content-Length: 15
      # 获取两个参数
      packets_received
      jitter
 
C->S: RTSP/1.0 200 OK
      CSeq: 9
      Content-Length: 46
      Content-Type: text/parameters
 
      packets_received: 10
      jitter: 0.3838 
SET_PARAMETER

设置RTSP URL所代表的展现/流的某个参数的值。报文示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
C->S: SET_PARAMETER rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 10
      Content-length: 20
      Content-type: text/parameters
      # 设置的参数名、值
      barparam: barstuff
 
S->C: RTSP/1.0 451 Invalid Parameter
      CSeq: 10
      Content-length: 10
      Content-type: text/parameters
 
      barparam 
REDIRECT

服务器发起,通知客户端,必须重新连接到一个媒体位置,此报文的location头指示媒体的新位置。报文示例:

1
2
3
4
5
S->C: REDIRECT rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 11
      # 新的位置
      Location: rtsp://cast.gmem.cc.com:8001
      Range: clock=19960213T143205Z-
RECORD 客户端基于展现描述,发起媒体数据某个范围的录制请求
Live555

Live555项目提供了一套C++库,用于RTP/RTCP、RTSP、SIP等标准协议下的多媒体应用开发。Live555库被用来实现Live555媒体服务器、Live555代理服务器。

Live555还可以用来流化、接收、处理MPEG, H.265, H.264, H.263+, DV等视频编码格式以及若干音频编码格式。要支持其它音视频编码格式,你只需要简单的扩展。

组件

下载Live555后,你得到以下几个组件,它们被位于不同的子目录:

组件 说明
UsageEnvironment

包含类UsageEnvironment、TaskScheduler用于延迟事件的调度,例如异步读事件、输出错误/警告消息

包含类HashTable,一个哈希表实现

groupsock 此组件中的类封装了网络接口和套接字。Groupsock对组播的收发行为进行了封装
liveMedia 定义了一个类层次,其根是Medium。提供对多种流媒体类型、编码方式的支持
BasicUsageEnvironment 定义了UsageEnvironment的一个具体化子类,主要在简单的、基于控制台的应用程序中使用。读事件和延迟操作在一个select()循环中处理
testProgs 基于BasicUsageEnvironment实现了一些样例
安装

Linux下安装Live555库的步骤如下:

Shell
1
2
3
4
5
6
wget http://www.live555.com/liveMedia/public/live.2017.07.18.tar.gz
tar xzf live.2017.07.18.tar.gz
pushd live
# 如果需要保留调试信息,可以修改config.linux文件,添加编译参数-O0 -g3
./genMakefiles linux
make && make install PREFIX=/home/alex/CPP/lib/live555
RTSP客户端
DESCRIBE示例
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "liveMedia.hh"
#include "BasicUsageEnvironment.hh"
#include <iostream>
 
using namespace std;
 
volatile char eventLoopWatchVariable = 0;
 
int main() {
    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    UsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    const char *url = "rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream";
    RTSPClient *client = RTSPClient::createNew( *env, url, 0 );  // 参数3传入1则控制台会打印调试信息
    // 发送RTSP DESCRIBE命令,注意所有RTSP命令都是异步发送的,其应答后续在事件循环中被处理
    client->sendDescribeCommand( []( RTSPClient *rtspClient, int resultCode, char *resultString ) -> void {
        // 如果resultCode不为0说明失败
        // 打印SDP
        cout << resultString << endl;
    } );
 
    // 直到eventLoopWatchVariable变为非零之前,下面的事件循环不会停止
    env->taskScheduler().doEventLoop( &eventLoopWatchVariable );
    return 0;
}

公司的IP摄像头给出如下应答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
v=0
o=- 1505140878681876 1505140878681876 IN IP4 192.168.0.196
s=Media Presentation
e=NONE
b=AS:5050
t=0 0
a=control:rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream/
# 视频基于RTP/VAP传输类型,编码方式H264
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:5000
a=recvonly
a=control:rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream/trackID=1
# 90000表示时钟频率,即每秒内,有多少个时间戳tick,或者说每秒RTP时间戳增加多少
a=rtpmap:96 H264/90000
# H.264 Profile: baseline , constraints 0 , level-idc 4.1 
# level-idc,用于提示自己的解码能力 —— 最大多大的分辨率、帧率、码率
a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z00AFJWoWCWm4CAgIEA=,aO48gA==
a=Media_header:MEDIAINFO=494D4B48010100000400000100000000000000000000000000000000000000000000000000000000;
a=appversion:1.0

上述内容和WebRTC的SDP Offer类似,都属于会话描述协议。SDP消息可以划分为三个主要的段,分别对会话、时间、媒体进行描述。SDP各字段的含义如下:

SDP字段 说明
会话描述
v  协议版本号,总是0
o 发起者以及会话标识符
s 会话的名称 
i 会话的描述和简短信息 
u 

描述(Description)的URI 

e  0-N个电子邮件地址,附加联系人名称 
p 0-N个电话号码,附加联系人名称  
c  连接信息
b 0-N个带宽信息行 
z 时区调整
k 加密密钥
a 0-N个会话属性行
时间描述(1-N)
t

会话活动时间

其中的绝对时间基于网络时间协议(NTP)格式,即1900年到目前的秒数

开始时间为0表示会话是永久的;结束时间为零表示会话持续时间不限制

r 0-N个repeat times
媒体描述(0-N)
m

媒体名称、传输地址,以及传输协议。示例:

1
2
3
4
# 音频,在49170端口,基于Profile AVP、载荷类型0(PCMU)的RTP协议传输
m=audio 49170 RTP/AVP 0
# 视频,在51372端口,基于Profile AVP、载荷类型96的RTP协议传输
m=video 51372 RTP/AVP 96

载荷类型如果是96-127之间,则表示载荷类型是动态分配的,后面会出现a=rtpmap行来映射此载荷类型:

1
a=rtpmap:96 H264/90000

载荷类型可以声明若干个,表示这些类型在会话中都可能使用

i 媒体的标题或者信息
c 连接信息
b 带宽信息
k 加密密钥
a 0-N个媒体属性行,可以覆盖会话属性行同名属性
客户端封装

为简化开发,下面给出一个live555的RTSP客户端封装。

创建如下CMake项目:

CMakeLists.txt
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
31
32
33
cmake_minimum_required(VERSION 3.6)
project(live5555)
 
set(CMAKE_CXX_STANDARD 11)
 
set(LIVE555_HOME /home/alex/CPP/lib/live555)
 
include_directories(${LIVE555_HOME}/include/UsageEnvironment)
include_directories(${LIVE555_HOME}/include/BasicUsageEnvironment)
include_directories(${LIVE555_HOME}/include/liveMedia)
include_directories(${LIVE555_HOME}/include/groupsock)
 
include_directories(/home/alex/CPP/lib/spdlog/include)
 
set(CMAKE_CXX_FLAGS "-w -pthread")
 
set(LIVE5555_SRC SinkBase.cpp RTSPClientBase.cpp)
 
add_library(live5555 ${LIVE5555_SRC})
target_link_libraries(
        live5555
        ${LIVE555_HOME}/lib/libliveMedia.a
        ${LIVE555_HOME}/lib/libgroupsock.a
        ${LIVE555_HOME}/lib/libBasicUsageEnvironment.a
        ${LIVE555_HOME}/lib/libUsageEnvironment.a
)
 
set(WS_PUSH_SRC wspush.cpp)
add_executable(wspush ${WS_PUSH_SRC})
target_link_libraries(
        wspush
        live5555
)

基础头文件:

live5555/common.h
C++
1
2
3
4
5
6
7
#ifndef LIVE5555_COMMON_H
#define LIVE5555_COMMON_H
 
#include "liveMedia.hh"
#include "BasicUsageEnvironment.hh"
 
#endif //LIVE5555_COMMON_H

类RTSPClientBase,对RTSPClient进行扩展,将RTSP命令回调函数指针转换为成员函数,实现基本的取流逻辑:

live5555/RTSPClientBase.h
C++
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#ifndef LIVE5555_RTSPCLIENTBASE_H
#define LIVE5555_RTSPCLIENTBASE_H
 
#include "common.h"
 
class RTSPClientBase : public RTSPClient {
private:
    MediaSession *session;
    // 事件循环监控变量,不为零时事件循环退出
    volatile char eventLoopWatchVariable;
    char *rtspURL;
    volatile int acceptedSubSessionCount;
    volatile int preparedSubSessionCount;
 
    static void onDescribeResponse( RTSPClient *client, int resultCode, char *resultString );
 
    static void onSetupResponse( RTSPClient *client, int resultCode, char *resultString );
 
    static void onPlayResponse( RTSPClient *client, int resultCode, char *resultString );
 
    static void onSubSessionClose( void *clientData );
 
protected:
    RTSPClientBase( UsageEnvironment &env, const char *rtspURL );
 
    // 处理RTSP命令DESCRIBE的响应
    virtual void onDescribeResponse( int resultCode, const char *sdp );
 
    // 处理RTSP命令SETUP的响应
    virtual void onSetupResponse( int resultCode, const char *resultString );
 
    virtual void onPlayResponse( int resultCode, char *resultString );
 
    // 是否初始化指定的子会话
    virtual bool acceptSubSession( const char *mediumName, const char *codec )=0;
 
    virtual MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession )=0;
 
    // 处理子会话关闭事件
    virtual void onSubSessionClose( MediaSubsession *subsess );
 
public:
    virtual void start();
 
    virtual void stop();
};
 
#endif //LIVE5555_RTSPCLIENTBASE_H

RTSPClientBase.cpp
C++
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#include "live5555/RTSPClientBase.h"
#include "spdlog/spdlog.h"
 
static auto LOGGER = spdlog::stdout_color_st( "RTSPClientBase" );
 
static const char *getResultString( char *resultString ) {
    return resultString ? resultString : "N/A";
}
 
// 父构造函数的第三个参数是调试信息冗余级别
RTSPClientBase::RTSPClientBase( UsageEnvironment &env, const char *rtspURL ) :
    RTSPClient( env, rtspURL, 0, NULL, 0, -1 ) {
}
 
void RTSPClientBase::start() {
    LOGGER->trace( "Starting RTSP client..." );
    this->rtspURL = rtspURL;
    LOGGER->trace( "Send RTSP command: DESCRIBE" );
    sendDescribeCommand( onDescribeResponse );
    LOGGER->trace( "Startup live555 eventloop" );
    envir().taskScheduler().doEventLoop( &eventLoopWatchVariable );
}
 
 
void RTSPClientBase::onDescribeResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER->trace( "DESCRIBE response received, resultCode: {}", resultCode );
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    bool ok = false;
    if ( resultCode == 0 ) {
        clientBase->onDescribeResponse( resultCode, resultString );
    } else {
        LOGGER->trace( "Stopping due to DESCRIBE failure" );
        clientBase->stop();
    };
    delete[] resultString;
}
 
void RTSPClientBase::onDescribeResponse( int resultCode, const char *sdp ) {
    LOGGER->debug( "SDP received: \n{}", sdp );
    UsageEnvironment &env = envir();
    LOGGER->trace( "Create new media session according to SDP" );
    session = MediaSession::createNew( env, sdp );
    if ( session && session->hasSubsessions()) {
        MediaSubsessionIterator *it = new MediaSubsessionIterator( *session );
        // 遍历子会话,SDP中的每一个媒体行(m=***)对应一个子会话
        while ( MediaSubsession *subsess = it->next()) {
            const char *mediumName = subsess->mediumName();
            // 初始化子会话,导致相应的RTPSource被创建
            LOGGER->trace( "Initialize sub session {}", mediumName );
            if ( !acceptSubSession( mediumName, subsess->codecName())) {
                continue;
            }
            acceptedSubSessionCount++;
            bool ok = subsess->initiate();
            if ( !ok ) {
                LOGGER->error( "Failed to initialize sub session: {}", mediumName );
                stop();
                break;
            }
            const Boolean muxed = subsess->rtcpIsMuxed();
            const char *codec = subsess->codecName();
            const int port = subsess->clientPortNum();
            LOGGER->debug( "Initialized sub session... \nRTCP Muxed: {}\nPort: {}\nMedium : {}\nCodec: {}", muxed, port, mediumName, codec );
 
            LOGGER->trace( "Send RTSP command: SETUP for subsession {}", mediumName );
            sendSetupCommand( *subsess, onSetupResponse, False, False );
        }
    } else {
        stop();
    }
}
 
void RTSPClientBase::onSetupResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER->trace( "SETUP response received, resultCode: {}, resultString: {}", resultCode, getResultString( resultString ));
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    if ( resultCode == 0 ) {
        clientBase->preparedSubSessionCount++;
        clientBase->onSetupResponse( resultCode, resultString );
    } else {
        LOGGER->trace( "Stopping due to SETUP failure" );
        clientBase->stop();
    }
    delete[] resultString;
}
 
void RTSPClientBase::onSetupResponse( int resultCode, const char *resultString ) {
    if ( preparedSubSessionCount == acceptedSubSessionCount ) {
        MediaSubsessionIterator *it = new MediaSubsessionIterator( *session );
        while ( MediaSubsession *subsess = it->next()) {
            const char *mediumName = subsess->mediumName();
            const char *codec = subsess->codecName();
            if ( acceptSubSession( mediumName, codec )) {
                MediaSink *sink = createSink( mediumName, codec, subsess );
                // 让Sink回调能够感知Client对象
                subsess->miscPtr = this;
                // 导致Sink的continuePlaying被调用,准备接受数据推送
                sink->startPlaying( *subsess->readSource(), NULL, subsess );
                // 此时数据推送不会立即开始,直到调用STSP命令PLAY
                RTCPInstance *rtcp = subsess->rtcpInstance();
                if ( rtcp ) {
                    // 正确处理针对此子会话的RTCP命令
                    rtcp->setByeHandler( onSubSessionClose, subsess );
                }
                LOGGER->trace( "Send RTSP command: PLAY" );
                // PLAY命令可以针对整个会话,也可以针对每个子会话
                sendPlayCommand( *session, onPlayResponse );
            }
        }
    }
}
 
void RTSPClientBase::onPlayResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER->trace( "PLAY response received, resultCode: {}, resultString: {}", resultCode, getResultString( resultString ));
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    if ( resultCode == 0 ) {
        clientBase->onPlayResponse( resultCode, resultString );
    } else {
        LOGGER->trace( "Stopping due to PLAY failure" );
        clientBase->stop();
    }
    delete[] resultString;
}
 
void RTSPClientBase::onPlayResponse( int resultCode, char *resultString ) {
    // 此时服务器应该开始推送流过来
    // 如果播放的是定长的录像,这里应该注册回调,在时间到达后关闭客户端
    double &startTime = session->playStartTime();
    double &endTime = session->playEndTime();
    LOGGER->debug_if( startTime == endTime, "Session is infinite" );
}
 
void RTSPClientBase::onSubSessionClose( void *clientData ) {
    MediaSubsession *subsess = (MediaSubsession *) clientData;
    RTSPClientBase *clientBase = (RTSPClientBase *) subsess->miscPtr;
    clientBase->onSubSessionClose( subsess );
}
 
void RTSPClientBase::onSubSessionClose( MediaSubsession *subsess ) {
    LOGGER->debug( "Stopping subsession..." );
    // 首先关闭子会话的SINK
    Medium::close( subsess->sink );
    subsess->sink = NULL;
 
    // 检查是否所有兄弟子会话均已经结束
    MediaSession &session = subsess->parentSession();
    MediaSubsessionIterator iter( session );
    while (( subsess = iter.next()) != NULL ) {
        // 存在未结束的子会话,不能关闭当前客户端
        if ( subsess->sink != NULL ) return;
    }
    // 关闭客户端
    LOGGER->debug( "All subsession closed" );
    stop();
}
 
void RTSPClientBase::stop() {
    LOGGER->debug( "Stopping RTSP client..." );
    // 修改事件循环监控变量
    eventLoopWatchVariable = 0;
    UsageEnvironment &env = envir();
    if ( session != NULL ) {
        Boolean someSubsessionsWereActive = False;
        MediaSubsessionIterator iter( *session );
        MediaSubsession *subsession;
        // 检查是否存在需要处理的子会话
        while (( subsession = iter.next()) != NULL ) {
            if ( subsession->sink != NULL ) {
                // 强制关闭子会话的SINK
                Medium::close( subsession->sink );
                subsession->sink = NULL;
                if ( subsession->rtcpInstance() != NULL ) {
                    // 服务器可能在处理TEARDOWN时发来RTCP包BYE
                    subsession->rtcpInstance()->setByeHandler( NULL, NULL );
                }
                someSubsessionsWereActive = True;
            }
        }
 
        if ( someSubsessionsWereActive ) {
            // 向服务器发送TEARDOWN命令,让服务器关闭输入流
            sendTeardownCommand( *session, NULL );
        }
    }
    // 关闭客户端
    Medium::close( this );
}

类SinkBase,一个基础的Sink实现,从流中获取帧:

live5555/SinkBase.h
C++
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
#ifndef LIVE5555_SINKBASE_H
#define LIVE5555_SINKBASE_H
 
#include "common.h"
 
class SinkBase : public MediaSink {
 
private:
    static void afterGettingFrame( void *clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime,
        unsigned /*durationInMicroseconds*/ );
 
protected:
    unsigned recvBufSize;
 
    unsigned char *recvBuf;
 
    SinkBase( UsageEnvironment &env, unsigned recvBufSize );
 
    // sink->startPlaying会调用continuePlaying,实现播放逻辑
    virtual Boolean continuePlaying();
 
    virtual ~SinkBase();
 
public:
    virtual void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime );
};
 
#endif //LIVE5555_SINKBASE_H

SinkBase.cpp
C++
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
31
32
33
#include "live5555/SinkBase.h"
#include "spdlog/spdlog.h"
 
static auto LOGGER = spdlog::stdout_color_st( "SinkBase" );
 
SinkBase::SinkBase( UsageEnvironment &env, unsigned recvBufSize ) : MediaSink( env ) {
    this->recvBufSize = recvBufSize;
    this->recvBuf = new unsigned char[recvBufSize];
}
 
SinkBase::~SinkBase() {
    delete[] this->recvBuf;
}
 
// 缺省实现:保存已分帧源的下一帧到缓冲区中,然后执行回调
Boolean SinkBase::continuePlaying() {
    if ( fSource == NULL ) return False;
    fSource->getNextFrame( recvBuf, recvBufSize, afterGettingFrame, this, onSourceClosure, this );
    return True;
};
 
// 由于getNextFrame需要的是一个函数指针,因此这里用静态函数。此函数简单的转调对应的成员函数
void SinkBase::afterGettingFrame( void *clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime,
    unsigned /*durationInMicroseconds*/ ) {
    SinkBase *sink = (SinkBase *) clientData;
    sink->afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
}
 
// 缺省实现:递归获取下一帧
void SinkBase::afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) {
    LOGGER->trace( "Frame of {} bytes received",frameSize );
    fSource->getNextFrame( recvBuf, recvBufSize, afterGettingFrame, this, onSourceClosure, this );
} 
封装应用示例

下面的客户端基于上节的封装:

live5555/client.h
1
2
3
4
5
6
7
#ifndef LIVE5555_LIVE5555_H
#define LIVE5555_LIVE5555_H
 
#include "RTSPClientBase.h"
#include "SinkBase.h"
 
#endif //LIVE5555_LIVE5555_H

wspush.cpp
C++
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include "live5555/client.h"
#include "spdlog/spdlog.h"
 
static auto LOGGER = spdlog::stdout_color_st( "wspush" );
 
class VideoSink : public SinkBase {
public:
    VideoSink( UsageEnvironment &env, unsigned int recvBufSize ) : SinkBase( env, recvBufSize ) {}
 
    void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) override {
        unsigned naluHead = recvBuf[ 0 ];
        unsigned nri = naluHead >> 5;
        unsigned f = nri >> 2;
        unsigned type = naluHead & 0b00011111;
        LOGGER->trace( "NALU info: nri {} type {}", nri, type );
        SinkBase::afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
    }
};
 
class H264RTSPClient : public RTSPClientBase {
private:
    MediaSink *videoSink;
public:
    H264RTSPClient( UsageEnvironment &env, const char *rtspURL, MediaSink *videoSink ) :
        RTSPClientBase( env, rtspURL ), videoSink( videoSink ) {}
 
protected:
    // 测试用的摄像头(RTSP源)仅仅有一个子会话,因此这里简化了实现:
    bool acceptSubSession( const char *mediumName, const char *codec ) override {
        return true;
    }
 
    MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession) override {
        return videoSink;
    }
};
 
int main() {
    spdlog::set_pattern( "%Y-%m-%d %H:%M:%S.%e [%l] [%n] %v" );
    spdlog::set_level( spdlog::level::trace );
 
    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    BasicUsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    VideoSink *sink = new VideoSink( *env, 1024 * 1024 );
    H264RTSPClient *client = new H264RTSPClient( *env, "rtsp://admin:kingsmart123@192.168.0.196:554/ch1/sub/av_stream", sink );
    client->start();
    return 0;
}

此客户端很简单,它建立RTSP会话,然后依次执行DESCRIBE、SETUP、PLAY命令,最终建立RTP会话,开始视频流的传输。

live555的RTSP客户端中的所有网络事件 —— 包括RTSP请求响应、RTP推送 —— 都由BasicTaskScheduler的事件循环处理,该事件循环会不断的执行select()系统调用,监听网络事件的到达。事件循环由RTSPClientBase.start()启动。

在SETUP响应回调中,发送PLAY命令之前,我们调用了VideoSink.startPlaying()方法,此方法会转调VideoSink.continuePlaying()方法,后者则调用H264RTPSource.getNextFrame()注册下一帧的处理回调。getNextFrame()会将解析得到的帧存放到你指定的缓冲区中,测试时打印前几帧:

67 42 00 1F 95 A8 14 01 6E 9B 80 80 80 81
68 CE 3C 80
61 E4 A0 4F F3 7A 06 B9 36 39 80 07 4C 9A ...

可以看到,这些帧都是标准的H264 NALU格式(不带起始码),第一个是SPS,第二个是PPS,后续是普通切片。

H264RTPSource

RTP包到达后,事件循环委托H264RTPSource进行如下处理:

  1. 检查RTP包头的合法性
  2. 剔除RTP包头和Padding
  3. 抽取H264帧,然后针对自己调用afterGetting(this),并导致之前通过getNextFrame()注册的回调函数被调用
术语列表
术语 说明
Jitter

抖动(Jitter)是TCP/IP网络和组件天生具有的一种不被期望的“倾向”。它是数据报被接收到的延迟(Delay)时间的变化性(Variation)

发送方以固定的频率发送数据报,但是由于网络拥塞、不适当的数据报排队或者配置错误,接收者接收到数据报的频率可能会在较大范围变动

抖动会影响流媒体的回放体验,在等待延迟到达的数据报时,可能出现gap

NPT

正常播放时间(Normal Play Time),指示相对于展现(presentation)开始点的,流的当前位置(时间偏移)

NPT使用一个浮点数表示,整数部分可能按秒数、或者小时+分钟+秒数来解释,小数部分则进行一秒内度量(例如.500表示当前秒过了一半)

展现的开始点的NPT定义为0.0,负数的意义没有定义

当x倍速播放时,NPT的增长速度增加为x倍

Presentation

展现,一个或者多个被渲染到客户端流,它们共同组成了一个Media feed

 

← Spring Boot学习笔记
C++日志组件spdlog →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 基于Broadway的HTML5视频监控
  • HTML5视频监控技术预研
  • 基于MinGW的海康视频监控开发
  • 基于Kurento搭建WebRTC服务器
  • 基于C/C++的WebSocket库

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2