m3u8 文件格式详解

简介

M3U8 是 Unicode 版本的 M3U,用 UTF-8 编码。”M3U” 和 “M3U8” 文件都是苹果公司使用的 HTTP Live Streaming(HLS) 协议格式的基础,这种协议格式可以在 iPhone 和 Macbook 等设备播放。

上述文字定义来自于维基百科。可以看到,m3u8 文件其实是 HTTP Live Streaming(缩写为 HLS) 协议的部分内容,而 HLS 是一个由苹果公司提出的基于 HTTP流媒体网络传输协议

HLS 的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的 extended M3U (m3u8) playlist文件,用于寻找可用的媒体流。
HLS 只请求基本的 HTTP 报文,与实时传输协议(RTP)不同,HLS 可以穿过任何允许 HTTP 数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。

简而言之,HLS 是新一代流媒体传输协议,其基本实现原理为将一个大的媒体文件进行分片,将该分片文件资源路径记录于 m3u8 文件(即 playlist)内,其中附带一些额外描述(比如该资源的多带宽信息···)用于提供给客户端。客户端依据该 m3u8 文件即可获取对应的媒体资源,进行播放。

因此,客户端获取 HLS 流文件,主要就是对 m3u8 文件进行解析操作。

那么,下面就简单介绍下 m3u8 文件。

M3U8 文件简介

m3u8 文件实质是一个播放列表(playlist),其可能是一个媒体播放列表(Media Playlist),或者是一个主列表(Master Playlist)。但无论是哪种播放列表,其内部文字使用的都是 utf-8 编码。

当 m3u8 文件作为媒体播放列表(Meida Playlist)时,其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。其格式如下所示:

#EXTM3U
#EXT-X-TARGETDURATION:10

#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts

对于点播来说,客户端只需按顺序下载上述片段资源,依次进行播放即可。而对于直播来说,客户端需要 定时重新请求 该 m3u8 文件,看下是否有新的片段数据需要进行下载并播放。

当 m3u8 作为主播放列表(Master Playlist)时,其内部提供的是同一份媒体资源的多份流列表资源(Variant Stream)。其格式如下所示:

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/lo_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/hi_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8

该备用流资源指定了多种不同码率,不同格式的媒体播放列表,并且,该备用流资源也可同时提供不同版本的资源内容,比如不同语言的音频文件,不同角度拍摄的视屏文件等等。客户可以根据不同的网络状态选取合适码流的资源,并且最好根据用户喜好选择合适的资源内容。

更多详细内容,可查看:

以上,就是 m3u8 文件的大概内容。下面,我们就对 m3u8 内容格式进行讲解。

m3u8 文件格式简解

m3u8 的文件格式主要包含三方面内容:

  1. 文件播放列表格式定义:播放列表(Playlist,也即 m3u8 文件) 内容需严格满足规范定义所提要求。下面罗列一些主要遵循的条件:
  • m3u8 文件必须以 utf-8 进行编码,不能使用 Byte Order Mark(BOM)字节序, 不能包含 utf-8 控制字符(U+0000 ~ U_001F 和 U+007F ~ u+009F)。
  • m3u8 文件的每一行要么是一个 URI,要么是空行,要么就是以 # 开头的字符串。不能出现空白字符,除了显示声明的元素。
  • m3u8 文件中以 # 开头的字符串要么是注释,要么就是标签。标签以 #EXT 开头,大小写敏感。
  1. 属性列表(Attribute Lists):某些特定的标签的值为属性列表。标签后面的属性列表以 逗号 作为分隔符,分离出多组不带空格的 属性/值 对。
    属性/值 对的语法格式如下:

AttributeName=AttributeValue

其中:

  • 属性AttributeName是由 [A..Z],[0..9] 和 - 组成的不带引号的字符串。因此,属性AttributeName只能使用大写字母,不能使用小写字母,并且AttributeName=中间不能有空格,同理,=AttributeValue之间也不能有空格。
  • AttributeValue的只能取以下类型:
    • 十进制整型(decimal-interger):由 [0..9] 之间组成的十进制不带引号的字符串,范围为 0 ~ 2^{64}(18446744073709551615),字符长度为 1 ~ 20 之间。
    • 十六进制序列:由 [0..9] 和 [A..F] 且前缀为 0x 或 0X 组合成的不带引号的字符串。其序列的最大长度取决于他的属性名AttributeNames
    • 带符号十进制浮点型(signed-decimal-floating-point):由 [0..9],-.组合成的不带引号的字符串。
    • 字符串(quoted-string):由双引号包裹表示的字符串。其中,0xA,0xD 和 双引号"不能出现在该字符串中。该字符串区分大小写。
    • 可枚举字符串(enumerated-string):由AttributeName显示定义的一系列不带引号的字符串。该字符串不能包含双引号",逗号,和空白字符。
    • decimal-resolution:由字符x进行隔离的两个十进制整型数。第一个整型表示水平宽度大小,第二个整型数表示垂直方向高度大小(单位:像素)。
  1. 标签:标签用于指定 m3u8 文件的全局参数或在其后面的切片文件/媒体播放列表的一些信息。

标签的类型可分为五种类型:基础标签(Basic Tags)媒体片段类型标签(Media Segment Tags)媒体播放列表类型标签主播放列表类型标签播放列表类型标签。其具体内容如下所示:

  • 基础标签(Basic Tags):可同时适用于媒体播放列表(Media Playlist)和主播放列表(Master Playlist)。具体标签如下:
    • EXTM3U:表明该文件是一个 m3u8 文件。每个 M3U 文件必须将该标签放置在第一行。
    • EXT-X-VERSION:表示 HLS 的协议版本号,该标签与流媒体的兼容性相关。该标签为全局作用域,使能整个 m3u8 文件;每个 m3u8 文件内最多只能出现一个该标签定义。如果 m3u8 文件不包含该标签,则默认为协议的第一个版本。
  • 媒体片段类型标签(Media Segment Tags):每个切片 URI 前面都有一系列媒体片段标签对其进行描述。有些片段标签只对其后切片资源有效;有些片段标签对其后所有切片都有效,直到后续遇到另一个该标签描述。媒体片段类型标签不能出现在主播放列表(Master Playlist)中。具体标签如下:
    • EXTINF:表示其后 URL 指定的媒体片段时长(单位为秒)。每个 URL 媒体片段之前必须指定该标签。该标签的使用格式为:

      #EXTINF:<duration>,[<title>]
      

      其中:

      • duration:可以为十进制的整型或者浮点型,其值必须小于或等于 EXT-X-TARGETDURATION 指定的值。
        注:建议始终使用浮点型指定时长,这可以让客户端在定位流时,减少四舍五入错误。但是如果兼容版本号 EXT-X-VERSION 小于 3,那么必须使用整型。
    • EXT-X-BYTERANGE:该标签表示接下来的切片资源是其后 URI 指定的媒体片段资源的局部范围(即截取 URI 媒体资源部分内容作为下一个切片)。该标签只对其后一个 URI 起作用。其格式为:

      #EXT-X-BYTERANGE:<n>[@<o>]
      

      其中:

      • n是一个十进制整型,表示截取片段大小(单位:字节)。
      • 可选参数o也是一个十进制整型,指示截取起始位置(以字节表示,在 URI 指定的资源开头移动该字节位置后进行截取)。
        如果o未指定,则截取起始位置从上一个该标签截取完成的下一个字节(即上一个n+o+1)开始截取。
        如果没有指定该标签,则表明的切分范围为整个 URI 资源片段。
        注:使用 EXT-X-BYTERANGE 标签要求兼容版本号 EXT-X-VERSION 大于等于 4。
    • EXT-X-DISCONTINUITY:该标签表明其前一个切片与下一个切片之间存在中断。其格式为:

      #EXT-X-DISCONTINUITY
      

      当以下任一情况变化时,必须使用该标签:

      • 文件格式(file format)
      • 数字(number),类型(type),媒体标识符(identifiers of tracks)
      • 时间戳序列(timestamp sequence)

      当以下任一情况变化时,应当使用该标签:

      • 编码参数(encoding parameters)
      • 编码序列(encoding sequence)

      注:EXT-X-DISCONTINUITY 的一个经典使用场景就是在视屏流中插入广告,由于视屏流与广告视屏流不是同一份资源,因此在这两种流切换时使用 EXT-X-DISCONTINUITY 进行指明,客户端看到该标签后,就会处理这种切换中断问题,让体验更佳。
      更多详细内容,请查看:Incorporating Ads into a Playlist

    • EXT-X-KEY:媒体片段可以进行加密,而该标签可以指定解密方法。
      该标签对所有 媒体片段 和 由标签 EXT-X-MAP 声明的围绕其间的所有 媒体初始化块(Meida Initialization Section) 都起作用,直到遇到下一个 EXT-X-KEY(若 m3u8 文件只有一个 EXT-X-KEY 标签,则其作用于所有媒体片段)。
      多个 EXT-X-KEY 标签如果最终生成的是同样的秘钥,则他们都可作用于同一个媒体片段。
      该标签使用格式为:

      #EXT-X-KEY:<attribute-list>
      

      属性列表可以包含如下几个键:

      • METHOD:该值是一个可枚举的字符串,指定了加密方法。
        该键是必须参数。其值可为NONEAES-128SAMPLE-AES当中的一个。
        其中:
        NONE:表示切片未进行加密(此时其他属性不能出现);
        AES-128:表示表示使用 AES-128 进行加密。
        SAMPLE-AES:意味着媒体片段当中包含样本媒体,比如音频或视频,它们使用 AES-128 进行加密。这种情况下 IV 属性可以出现也可以不出现。
      • URI:指定密钥路径。
        该密钥是一个 16 字节的数据。
        该键是必须参数,除非 METHODNONE
      • IV:该值是一个 128 位的十六进制数值。
        AES-128 要求使用相同的 16字节 IV 值进行加密和解密。使用不同的 IV 值可以增强密码强度。
        如果属性列表出现 IV,则使用该值;如果未出现,则默认使用媒体片段序列号(即 EXT-X-MEDIA-SEQUENCE)作为其 IV 值,使用大端字节序,往左填充 0 直到序列号满足 16 字节(128 位)。
      • KEYFORMAT:由双引号包裹的字符串,标识密钥在密钥文件中的存储方式(密钥文件中的 AES-128 密钥是以二进制方式存储的16个字节的密钥)。
        该属性为可选参数,其默认值为"identity"
        使用该属性要求兼容版本号 EXT-X-VERSION 大于等于 5。
      • KEYFORMATVERSIONS:由一个或多个被/分割的正整型数值构成的带引号的字符串(比如:"1""1/2""1/2/5")。
        如果有一个或多特定的 KEYFORMT 版本被定义了,则可使用该属性指示具体版本进行编译。
        该属性为可选参数,其默认值为"1"
        使用该属性要求兼容版本号 EXT-X-VERSION 大于等于 5。
    • EXT-X-MAP:该标签指明了获取媒体初始化块(Meida Initialization Section)的方法。
      该标签对其后所有媒体片段生效,直至遇到另一个 EXT-X-MAP 标签。
      其格式为:

      #EXT-X-MAP:<attribute-list>
      

      其属性列表取值范围如下:

      • URI:由引号包裹的字符串,指定了包含媒体初始化块的资源的路径。该属性为必选参数。
      • BYTERANGE:由引号包裹的字符串,指定了媒体初始化块在 URI 指定的资源的位置(片段)。
        该属性指定的范围应当只包含媒体初始化块。
        该属性为可选参数,如果未指定,则表示 URI 指定的资源就是全部的媒体初始化块。
    • EXT-X-PROGRAM-DATE-TIME:该标签使用一个绝对日期/时间表明第一个样本片段的取样时间。
      其格式为:

      #EXT-X-PROGRAM-DATE-TIME:<date-time-msec>
      

      其中,date-time-msec是一个 ISO/IEC 8601:2004 规定的日期格式,形如:YYYY-MM-DDThh:mm:ss.SSSZ。

    • EXT-X-DATERANGE:该标签定义了一系列由属性/值对组成的日期范围。
      其格式为:

      #EXT-X-DATERANGE:<attribute-list>
      

      其属性列表取值如下:

      • ID:双引号包裹的唯一指明日期范围的标识。
        该属性为必选参数。
      • CLASS:双引号包裹的由客户定义的一系列属性与与之对应的语意值。
        所有拥有同一 CLASS 属性的日期范围必须遵守对应的语意。
        该属性为可选参数。
      • START-DATE:双引号包裹的日期范围起始值。
        该属性为必选参数。
      • END-DATE:双引号包裹的日期范围结束值。
        该属性值必须大于或等于 START-DATE
        该属性为可选参数。
      • DURATION:日期范围的持续时间是一个十进制浮点型数值类型(单位:秒)。
        该属性值不能为负数。
        当表达立即时间时,将该属性值设为 0 即可。
        该属性为可选参数。
      • PLANNED-DURATION:该属性为日期范围的期望持续时长。
        其值为一个十进制浮点数值类型(单位:秒)。
        该属性值不能为负数。
        在预先无法得知真实持续时长的情况下,可使用该属性作为日期范围的期望预估时长。
        该属性为可选参数。
    • X-<client-attribute>X-前缀是预留给客户端自定义属性的命名空间。
      客户端自定义属性名时,应当使用反向 DNS(reverse-DNS)语法来避免冲突。
      自定义属性值必须是使用双引号包裹的字符串,或者是十六进制序列,或者是十进制浮点数,比如:X-COM-EXAMPLE-AD-ID="XYZ123"
      该属性为可选参数。
    • SCTE35-CMD, SCTE35-OUT, SCTE35-IN:用于携带 SCET-35 数据。
      该属性为可选参数。
    • END-ON-NEXT:该属性值为一个可枚举字符串,其值必须为YES
      该属性表明达到该范围末尾,也即等于后续范围的起始位置 START-DATE。后续范围是指具有相同 CLASS 的,在该标签 START-DATE 之后的具有最早 START-DATE 值的日期范围。
      该属性时可选参数。
  • 媒体播放列表类型标签:媒体播放列表标签为 m3u8 文件的全局参数信息。
    这些标签只能在 m3u8 文件中至多出现一次。
    媒体播放列表(Media Playlist)标签不能出现在主播放列表(Master Playlist)中。
    媒体播放列表具体标签如下所示:

    • EXT-X-TARGETDURATION:表示每个视频分段最大的时长(单位秒)。
      该标签为必选标签。
      其格式为:

      #EXT-X-TARGETDURATION:<s>
      

      其中:参数s表示目标时长(单位:秒)。

    • EXT-X-MEDIA-SEQUENCE:表示播放列表第一个 URL 片段文件的序列号。
      每个媒体片段 URL 都拥有一个唯一的整型序列号。
      每个媒体片段序列号按出现顺序依次加 1。
      如果该标签未指定,则默认序列号从 0 开始。
      媒体片段序列号与片段文件名无关。
      其格式为:

      #EXT-X-MEDIA-SEQUENCE:<number>
      

      其中:参数number即为切片序列号。

    • EXT-X-DISCONTINUITY-SEQUENCE:该标签使能同步相同流的不同 Rendition 和 具备 EXT-X-DISCONTINUITY 标签的不同备份流。
      其格式为:

      #EXT-X-DISCONTINUITY-SEQUENCE:<number>
      

      其中:参数number为一个十进制整型数值。
      如果播放列表未设置 EXT-X-DISCONTINUITY-SEQUENCE 标签,那么对于第一个切片的中断序列号应当为 0。

    • EXT-X-ENDLIST:表明 m3u8 文件的结束。
      该标签可出现在 m3u8 文件任意位置,一般是结尾。
      其格式为:

      #EXT-X-ENDLIST
      
    • EXT-X-PLAYLIST-TYPE:表明流媒体类型。全局生效。
      该标签为可选标签。
      其格式为:

      #EXT-X-PLAYLIST-TYPE:<type-enum>
      

      其中:type-enum可选值如下:

      • VOD:即 Video on Demand,表示该视屏流为点播源,因此服务器不能更改该 m3u8 文件;
      • EVENT:表示该视频流为直播源,因此服务器不能更改或删除该文件任意部分内容(但是可以在文件末尾添加新内容)。
        注:VOD 文件通常带有 EXT-X-ENDLIST 标签,因为其为点播源,不会改变;而 EVEVT 文件初始化时一般不会有 EXT-X-ENDLIST 标签,暗示有新的文件会添加到播放列表末尾,因此也需要客户端定时获取该 m3u8 文件,以获取新的媒体片段资源,直到访问到 EXT-X-ENDLIST 标签才停止)。
    • EXT-X-I-FRAMES-ONLY:该标签表示每个媒体片段都是一个 I-frameI-frames 帧视屏编码不依赖于其他帧数,因此可以通过 I-frame 进行快速播放,急速翻转等操作。
      该标签全局生效。
      其格式为:

      #EXT-X-I-FRAMES-ONLY
      

      如果播放列表设置了 EXT-X-I-FRAMES-ONLY,那么切片的时长(EXTINF 标签的值)即为当前切片 I-frame 帧开始到下一个 I-frame 帧出现的时长。
      媒体资源如果包含 I-frame 切片,那么必须提供媒体初始化块或者通过 EXT-X-MAP 标签提供媒体初始化块的获取途径,这样客户端就能通过这些 I-frame 切片以任意顺序进行加载和解码。
      如果 I-frame 切片设置了 EXT-BYTERANGE,那么就绝对不能提供媒体初始化块。
      使用 EXT-X-I-FRAMES-ONLY 要求的兼容版本号 EXT-X-VERSION 大于等于 4。

  • 主播放列表类型标签:主播放列表(Master Playlist)定义了备份流,多语言翻译流和其他全局参数。
    主播放列表标签绝不能出现在媒体播放列表(Media Playlist)中。
    其具体标签如下:

    • EXT-X-MEDIA:用于指定相同内容的可替换的多语言翻译播放媒体列表资源。
      比如,通过三个 EXT-X-MEIDA 标签,可以提供包含英文,法语和西班牙语版本的相同内容的音频资源,或者通过两个 EXT-X-MEDIA 提供两个不同拍摄角度的视屏资源。
      其格式为:

      #EXT-X-MEDIA:<attribute-list>
      

      其中,属性列表取值范围如下:

      • TYPE:该属性值为一个可枚举字符串。
        其值有如下四种:AUDIOVIDEOSUBTITLESCLOSED-CAPTIONS
        通常使用的都是CLOSED-CAPTIONS
        该属性为必选参数。
      • URI:双引号包裹的媒体资源播放列表路径。
        如果 TYPE 属性值为 CLOSED-CAPTIONS,那么则不能提供 URI
        该属性为可选参数。
      • GROUP-ID:双引号包裹的字符串,表示多语言翻译流所属组。
        该属性为必选参数。
      • LANGUAGE:双引号包裹的字符串,用于指定流主要使用的语言。
        该属性为可选参数。
      • ASSOC-LANGUAGE:双引号包裹的字符串,其内包含一个语言标签,用于提供多语言流的其中一种语言版本。
        该参数为可选参数。
      • NAME:双引号包裹的字符串,用于为翻译流提供可读的描述信息。
        如果设置了 LANGUAGE 属性,那么也应当设置 NAME 属性。
        该属性为必选参数。
      • DEFAULT:该属性值为一个可枚举字符串。
        可选值为YESNO
        该属性未指定时默认值为NO
        如果该属性设为YES,那么客户端在缺乏其他可选信息时应当播放该翻译流。
        该属性为可选参数。
      • AUTOSELECT:该属性值为一个可枚举字符串。
        其有效值为YESNO
        未指定时,默认设为NO
        如果该属性设置YES,那么客户端在用户没有显示进行设置时,可以选择播放该翻译流,因为其能配置当前播放环境,比如系统语言选择。
        如果设置了该属性,那么当 DEFAULT 设置YES时,该属性也必须设置为YES
        该属性为可选参数。
      • FORCED:该属性值为一个可枚举字符串。
        其有效值为YESNO
        未指定时,默认设为NO
        只有在设置了 TYPESUBTITLES 时,才可以设置该属性。
        当该属性设为YES时,则暗示该翻译流包含重要内容。当设置了该属性,客户端应当选择播放匹配当前播放环境最佳的翻译流。
        当该属性设为NO时,则表示该翻译流内容意图用于回复用户显示进行请求。
        该属性为可选参数。
      • INSTREAM-ID:由双引号包裹的字符串,用于指示切片的语言(Rendition)版本。
        TYPE 设为 CLOSED-CAPTIONS 时,必须设置该属性。
        其可选值为:"CC1", "CC2", "CC3", "CC4""SERVICEn"n的值为 1~63)。
        对于其他 TYPE 值,该属性绝不能进行设置。
      • CHARACTERISTICS:由双引号包裹的由一个或多个由逗号分隔的 UTI 构成的字符串。
        每个 UTI 表示一种翻译流的特征。
        该属性可包含私有 UTI。
        该属性为可选参数。
      • CHANNELS:由双引号包裹的有序,由反斜杠/分隔的参数列表组成的字符串。
        所有音频 EXT-X-MEDIA 标签应当都设置 CHANNELS 属性。
        如果主播放列表包含两个相同编码但是具有不同数目 channed 的翻译流,则必须设置 CHANNELS 属性;否则,CHANNELS 属性为可选参数。
    • EXT-X-STREAM-INF:该属性指定了一个备份源。该属性值提供了该备份源的相关信息。
      其格式为:

      #EXT-X-STREAM-INF:<attribute-list>
      <URI>
      

      其中:

      • URI 指定的媒体播放列表携带了该标签指定的翻译备份源。
        URI 为必选参数。
      • EXT-X-STREAM-INF 标签的参数属性列表有如下选项:
        • BANDWIDTH:该属性为每秒传输的比特数,也即带宽。代表该备份流的巅峰速率。
          该属性为必选参数。
        • AVERAGE-BANDWIDTH:该属性为备份流的平均切片传输速率。
          该属性为可选参数。
        • CODECS:双引号包裹的包含由逗号分隔的格式列表组成的字符串。
          每个 EXT-X-STREAM-INF 标签都应当携带 CODECS 属性。
        • RESOLUTION:该属性描述备份流视屏源的最佳像素方案。
          该属性为可选参数,但对于包含视屏源的备份流建议增加该属性设置。
        • FRAME-RATE:该属性用一个十进制浮点型数值作为描述备份流所有视屏最大帧率。
          对于备份流中任意视屏源帧数超过每秒 30 帧的,应当增加该属性设置。
          该属性为可选参数,但对于包含视屏源的备份流建议增加该属性设置。
        • HDCP-LEVEL:该属性值为一个可枚举字符串。
          其有效值为TYPE-0NONE
          值为TYPE-0表示该备份流可能会播放失败,除非输出被高带宽数字内容保护(HDCP)。
          值为NONE表示流内容无需输出拷贝保护。
          使用不同程度的 HDCP 加密备份流应当使用不同的媒体加密密钥。
          该属性为可选参数。在缺乏 HDCP 可能存在播放失败的情况下,应当提供该属性。
        • AUDIO:属性值由双引号包裹,其值必须与定义在主播放列表某处的设置了 TYPE 属性值为 AUDIOEXT-X-MEDIA 标签的 GROUP-ID 属性值相匹配。
          该属性为可选参数。
        • VIDEO:属性值由双引号包裹,其值必须与定义在主播放列表某处的设置了 TYPE 属性值为 VIDEOEXT-X-MEDIA 标签的 GROUP-ID 属性值相匹配。
          该属性为可选参数。
        • SUBTITLES:属性值由双引号包裹,其值必须与定义在主播放列表某处的设置了 TYPE 属性值为 SUBTITLESEXT-X-MEDIA 标签的 GROUP-ID 属性值相匹配。
          该属性为可选参数。
        • CLOSED-CAPTIONS:该属性值可以是一个双引号包裹的字符串或NONE
          如果其值为一个字符串,则必须与定义在主播放列表某处的设置了 TYPE 属性值为 CLOSED-CAPTIONSEXT-X-MEDIA 标签的 GROUP-ID 属性值相匹配。
          如果其值为NONE,则所有的 ext-x-stream-inf 标签必须同样将该属性设置NONE,表示主播放列表备份流均没有关闭的标题。对于某个备份流具备关闭标题,另一个备份流不具备关闭标题可能会触发播放中断。
          该属性为可选参数。
    • EXT-X-I-FRAME-STREAM-INF:该标签表明媒体播放列表文件包含多种媒体资源的 I-frame 帧。
      其格式为:

      #EXT-X-I-FRAME-STREAM-INF:<attribute-list>
      

      该标签的属性列表包含了 EXT-X-I-FRAME-STREAM-INF 标签同样的属性列表选项,除了 FRAME-RATEAUDIOSUBTITLESCLOSED-CAPTIONS。除此之外,其他的属性还有:

      • URI:该属性值由双引号包裹的字符串,指示了 I-frame 媒体播放列表文件的路径,该媒体播放列表文件必须包含 EXT-X-I-FRAMES-ONLY 标签。
    • EXT-X-SESSION-DATA:该标签允许主播放列表携带任意 session 数据。
      该标签为可选参数。
      其格式为:

      #EXT-X-SESSION-DATA:<attribute-list>
      

      其中,其参数属性列表值如下可选项:

      • DATA-ID:由双引号包裹的字符串,代表一个特定的数据值。
        该属性应当使用反向 DNS 进行命名,如"com.example.movie.title"。然而,由于没有中央注册机构,所以可能出现冲突情况。
        该属性为必选参数。
      • VALUE:该属性值为一个双引号包裹的字符串,其包含 DATA-ID 指定的值。
        如果设置了 LANGUAGE,则 VALUE 应当包含一个用该语言书写的可读字符串。
      • URI:由双引号包裹的 URI 字符串。由该 URI 指示的资源必选使用 JSON 格式,否则,客户端可能会解析失败。
      • LANGUAGE:由双引号包裹的,包含一个语言标签的字符串。指示了 VALUE 所使用的语言。
  • EXT-X-SESSION-KEY:该标签允许主播放列表(Master Playlist)指定媒体播放列表(Meida Playlist)的加密密钥。这使得客户端可以预先加载这些密钥,而无需从媒体播放列表中获取。
    该标签为可选参数。
    其格式为:

    #EXT-X-SESSION-KEY:<attribute-list>
    

    其属性列表与 EXT-X-KEY 相同,除了 METHOD 属性的值必须不为NONE

  • 播放列表类型标签:以下标签可同时设置于主播放列表(Master Playlist)和媒体播放列表(Media Playlist)中。
    但是对于在主播放列表中设置了的标签,不应当再次设置在主播放列表指向的媒体播放列表中。
    同时出现在两者播放列表的相同标签必须具备相同的值。这些标签在播放列表中不能出现多次(只能使用一次)。具体标签如下所示:

    • EXT-X-INDEPENDENT-SEGMENTS:该标签表明对于一个媒体片段中的所有媒体样本均可独立进行解码,而无须依赖其他媒体片段信息。
      该标签对列表内所有媒体片段均有效。
      其格式为:

      #EXT-X-INDEPENDENT-SEGMENTS
      

      如果该标签出现在主播放列表中,则其对所有媒体播放列表的所有媒体片段都生效。

    • EXT-X-START:该标签表示播放列表播放起始位置。
      默认情况下,客户端开启一个播放会话时,应当使用该标签指定的位置进行播放。
      该标签为可选标签。
      其格式为:

      #EXT-X-START:<attribute-list>
      

      其参数属性列表的取值范围如下:

      • TIME-OFFSET:该属性值为一个带符号十进制浮点数(单位:秒)。
        一个正数表示以播放列表起始位置开始的时间偏移量。
        一个负数表示播放列表上一个媒体片段最后位置往前的时间偏移量。
        该属性的绝对值应当不超过播放列表的时长。如果超过,则表示到达文件结尾(数值为正数),或者达到文件起始(数值为负数)。
        如果播放列表不包含 EXT-X-ENDLIST 标签,那么 TIME-OFFSET 属性值不应当在播放文件末尾三个切片时长之内。
      • PRECISE:该值为一个可枚举字符串。
        有效的取值为YESNO
        如果值为YES,客户端应当播放包含 TIME-OFFSET 的媒体片段,但不要渲染该块内优先于 TIME-OFFSET 的样本块。
        如果值为NO,客户端应当尝试渲染在媒体片段内的所有样本块。
        该属性为可选参数,未指定则认为NO

到此,m3u8 相关的标签我们已经完全介绍完毕。

下面我们再简单介绍下资源文件的获取具体操作。

上文提到,m3u8 文件要么是媒体播放列表(Meida Playlist),要么是主播放列表(Master Playlist)。但无论是哪种列表,其有效内容均由两部分结构组成:

  • #EXT 开头的为标签信息,作为对媒体资源的进一步描述;
  • 剩余的为资源信息,要么是片段资源(Media Playlist)路径,要么是 m3u8 资源(Master Playlist)路径;

我们先简单介绍下 m3u8 文件媒体片段的表示方法:

  • m3u8 文件中,媒体片段可以采用全路径表示。如下所示:

#EXTINF:10.0,
http://example.com/movie1/fileSequenceA.ts

这样,获取资源片段的路径就是 m3u8 文件内指定的路径,即:http://example.com/movie1/fileSequenceA.ts

  • m3u8 文件中,媒体片段还可以使用相对路径表示。如下所示:

#EXTINF:10.0,
fileSequenceA.ts

这表示片段文件的路径是相对于 m3u8 文件路径的,即假设当前 m3u8 的路径为:https://127.0.0.1/hls/m3u8,那么,片段文件 fileSequenceA.ts 的路径即为:https://127.0.0.1/hls/fileSequenceA.ts

尽管可以在 m3u8 文件中使用绝对路径指定媒体片段资源路径,但是更好的选择是使用相对路径。相对路径相较于绝对路径更轻便,同时是相对于 m3u8 文件的 URL。相比之下,绝对路径增加了 m3u8 文件内容(更多字符),增大了文件内容,同时也增大了网络传输量。

其余一些注意事项

  • 有两种请求 m3u8 播放列表的方法:一是通过 m3u8 的 URI 进行请求,则该文件必须以 .m3u8 或 .m3u 结尾;
    二是通过 HTTP 进行请求,则请求头Content-Type必须设置为 application/vnd.apple.mpegurl或者audio/mpegurl
  • 空行和注释行在解析时都忽略。
  • 媒体播放列表(Media Playlist)的流资源总时长就是各切片资源的时长之和。
  • 每个切片的码率(bit rate)就是切片的大小除以它对应的时长(EXTINF 指定的时长)。
  • 一个标签的属性列表的同一个属性AttributeName只能出现一次。
  • EXT-X-TARGETDURATION 指定的时长绝对不能进行更改。通常该值指定的时长为 10 秒。
  • 对于指定了 EXT-X-I-FRAMES-ONLY 且 第一个媒体片段(或者第一个尾随 EXT-X-DISCONTINUITY 的片段)其资源没有立即携带媒体初始化块的切片,应当增加使用标签 EXT-X-MAP 指定媒体初始化块获取途径。
  • 使用 EXT-X-MAP 标签内含标签 EXT-X-I-FRAMES-ONLY 要求的兼容版本号 EXT-X-VERSION 要大于等于 5;只使用 EXT-X-MAP 要求的兼容版本号要大于等于 6。
  • 由标签 EXT-X-MAP 声明的媒体初始化块可使用 AES-128 方法进行加密,此时,作用于 EXT-X-MAP 标签的 EXT-X-KEY 标签必须设置 IV 属性。
  • 带有属性 END-ON-NEXT=YES 的标签 EXT-X-DATERANGE 必须携带 CLASS 属性,但不能携带 DURATIONEND-DATE 属性。其余带有相同 CLASS 的标签 EXT-X-DATERANGE 不能指定重叠的日期范围。
  • 日期范围如果未指明 DURATIONEND_DATE,END-ON-NEXT=YES 属性时,则其时长(duration)未知,即使其设置了 PLANNED-DURATION 属性。
  • 如果播放列表设置了 EXT-X-DATERANGE 标签,则必须同时设置 EXT-X-PROGRAM-DATE-TIME 标签。
  • 如果播放列表设置了拥有相同 ID 属性值的两个 EXT-X-DATERANGE 标签,则对于相同的属性名,在这两个 EXT-X-DATERANGE 中对应的值必须一致。
  • 如果 EXT-X-DATERANGE 同时设置了 DURATIONEND-DATE 属性,则 END-DATE 属性值必须等于 START-DATE 属性值加上 DURATION 属性值。
  • EXT-X-MEDIA-SEQUENCE 标签必须出现在播放列表第一个切片之前。
  • EXT-X-DISCONTINUITY-DEQUENCE 标签必须出现在播放列表第一个切片之前。
  • EXT-X-DISCONTINUITY-DEQUENCE 标签必须出现在任意 EXT-X-DISCONTINUITY 标签之前。
  • m3u8 文件如果没有设置 EXT-X-PLAYLIST-TYPE 标签,那么播放列表可以随时进行更改。比如,可以更新或删除播放列表中的媒体片段。
  • 每个 EXT-X-I-FRAME-STREAM-INF 标签必须包含一个 BANDWIDTHURI 属性。
  • 每个 EXT-X-SESSION-DATA 标签都必须包含一个 VALUEURI 属性,但不能同时包含两者。
  • 一个播放列表可以包含多个携带相同 DATA-ID 属性的 EXT-X-SESSION-DATA 标签。但是不能包含多个携带相同 DATA-ID 和相同 LANGUAGE 属性的 EXT-X-SESSION-DATA 标签。
  • 如果设置了 EXT-X-SESSION-KEY,那么其 METHODKEYFORMATKEYFORMATVERSIONS 属性值必须与任意相同 URIEXT-X-KEY 标签值相同。
  • 如果多份备用流或者多语言流使用相同的加密密钥和格式,则应当设置 EXT-X-SESSION-KEY 标签。
  • 主播放列表必须不能设置多个具有相同 METHODURIIVKEYFORMATKEYFORMATVERSIONS 属性值得 EXT-X-SESSION-KEY 标签。

附录

作者:Whyn
链接:https://www.jianshu.com/p/e97f6555a070
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

hugegraph图数据库索引详解

前言

在《技术文章之二 hugegraph图数据库概念详解》中我们介绍过IndexLabel,它是索引的元数据,用来描述对顶点/边的属性建立的索引。本文将对hugegraph中的索引做一个较为深入的介绍,并给出每一种索引的适用场景和使用方法的样例。

hugegraph的查询系统是以索引为基础建立的,如果没有索引,hugegraph只能做一些非常简单的查询,比如不带任何条件的查询:g.V()g.E();或者是能够优化为Id查询的条件查询,比如顶点Id策略为PRIMARY_KEY时的主属性(主索引)查询,查询种类十分有限。一旦创建了足够的索引,hugegraph就可以做各种各样复杂的查询了。

hugegraph包含三种索引,分别是:二级索引、范围索引、全文索引,二级索引又可分为单列索引和组合索引,还有一种能联合多种索引的联合索引。给了这么多名次,大家肯定有些头大,不用担心,大家不用太在乎这些索引的名称,暂时知道有多种索引即可,后面阅读完全文,了解这些索引的适用场景和用法即可。

二级索引

二级索引是最常见的索引类型,当用户对某个属性的查询总是“相等”判断时可以使用二级索引二级索引适合建立在大多数普通的属性上,比如:“姓名”、“职业”、“品牌”等容易准确描述的离散值。注意这里的两个措辞:“容易准确描述”和“离散值”并非专业描述,而是用于区别后面的范围索引和全文索引的。

  • “容易准确描述”指用户通常能完整的给出要查找的属性的值,而不是部分的,模糊不清的,比如某人的地址,一大串经常记不住,只能给出“北京市”、“某某小区”这样的部分信息;
  • “离散值”指的是属性的值是一个一个可以罗列的,就像一个一个整数值一样,而不是可以无限细分的浮点数,比如权重、得分这样值。

注意:上面的两个措辞只是一种一般性的描述,可能有些场景下,大的地址已经被限定在“北京市海淀区”里,那地址值就是一些简短的词也不足为奇了,此时也变成了“容易准确描述”的;“离散值”在不同场景下也有不同的定义,所以大家要根据具体的业务场景区分,切勿认死理。

hugegraph中二级索引的逻辑存储结构如下:

field_values | index_label_id | element_ids
  • field_values: 属性的值,可以是单个属性,也可以是多个属性拼接而成
  • index_label_id: 索引标签的Id
  • element_ids: 顶点或边的Id

各种后端具体的“表结构”都按照该逻辑结构设计。

二级索引允许在多个属性上建立索引,在单个属性上建立的索引称为单列索引,在多个属性上建立的索引称之为组合索引,下面分别介绍。

单列索引

先创建一个单列索引标签(元数据):

  1. // 创建索引类型:”personByName”,可以按“name”属性的值快速查询对应的“person”顶点
  2. schema.indexLabel(“personByName”)
  3. .onV(“person”)
  4. .by(“name”)
  5. .secondary()
  6. .create();

再插入几个顶点,有两个顶点的“name”相同:

  1. graph.addVertex(T.label, “person”, T.id, “p1”, “name”, “marko”);
  2. graph.addVertex(T.label, “person”, T.id, “p2”, “name”, “marko”);
  3. graph.addVertex(T.label, “person”, T.id, “p3”, “name”, “josh”);

我们以cassandra后端为例(下同)展示二级索引表的存储结构:

field_values | index_label_id | element_ids
————-±—————±————
marko | 1 | Sp1
marko | 1 | Sp2
josh | 1 | Sp3

表结构中,field_values作为primary key列,index_label_idelement_ids都作为cluster key,这样才能依次存储相同属性的element_id

当用户的查询语句中包含“name”=“marko”这样的条件时,就会命中前面两条记录得到两个element_id,然后再用这两个element_id去顶点表做Id查询得到顶点实体。

在真正的查询流程中,肯定是会先找到index_label_id的,然后用field_valuesindex_label_id一起定位,但是找index_label_id不是这里的重点,一笔带过。

组合索引

先创建一个组合索引标签:

  1. // 创建索引类型:”personByNameAndAge”,可以按“name”和”age”属性的值快速查询对应的“person”顶点
  2. schema.indexLabel(“personByNameAndAge”)
  3. .onV(“person”)
  4. .by(“name”, “age”)
  5. .secondary()
  6. .ifNotExist()
  7. .create();

再插入几个顶点:

  1. graph.addVertex(T.label, “person”, T.id, “p1”, “name”, “marko”, “age”, 29);
  2. graph.addVertex(T.label, “person”, T.id, “p2”, “name”, “vadas”, “age”, 29);
  3. graph.addVertex(T.label, “person”, T.id, “p3”, “name”, “josh”, “age”, 32);

然后再看存储结构:

field_values | index_label_id | element_ids
————-±—————±————
marko | 1 | Sp1
marko | 1 | Sp2
marko!29 | 1 | Sp1
josh!32 | 1 | Sp3
josh | 1 | Sp3
marko!27 | 1 | Sp2

这时我们发现:我只插入了3个顶点,但是一共有6条索引记录,这是为什么呢?

因为组合索引存储时会把多个属性值按顺序拼接在一起作为新的属性值,并且会将该属性值的所有前缀也都存储一份。以第一个顶点为例,它的“name”为“marko”,“age”为29,拼接在一起的新属性值是“marko!29”,会作为一条索引记录存储;然后它的前缀是“marko”,也会存储一份。

以此类推,如果这里创建的属性标签是建立在三个属性上的:“name”、“age”、“city”,假设“city”的值为“Beijing”,那一个顶点会对应三条索引,其field_values分别是“marko”、“marko!29”和“marko!29!Beijing”。

在像rocksdb和hbase这样的后端中,由于rowkey支持前缀匹配,则不会存储这么多的索引记录。

这样的存储结构只提供了前缀匹配的能力,所以当用户查询条件只包含“name”=“marko”时,可以命中前面两条记录;当查询条件包含“name”=“marko”和“age”=29时,可以命中第三条记录;但当查询条件中只包含“age”=29时,则无法命中任何一条记录。所以:创建索引标签时,组合索引中的属性顺序是很重要的。

范围索引

当用户的查询语义是:某属性值大于、小于、大于等于、小于等于、等于某个界限,或者属性值属于某个区间时,适合使用范围索引。比如:“年龄”、“价格”、“得分”等取值比较连续的属性。

要注意的是:该属性必须可以做比较,在Java中也就是实现了Comparable接口,所以Number类型的属性自不必说,像Date这样的属性也是可以创建范围索引的。

hugegraph中二级索引的逻辑存储结构如下:

index_label_id | field_values | element_ids
  • index_label_id: 索引标签的Id
  • field_values: 属性的值,可以是单个属性,也可以是多个属性拼接而成
  • element_ids: 顶点或边的Id

各种后端具体的“表结构”都按照该逻辑结构设计,在cassandra后端中,index_label_id作为primary key列,field_valueselement_ids都作为cluster key。其实相比于二级索引,范围索引表只是将field_valuesindex_label_id调换了一下顺序,这样做是因为在cassandra中,cluster key列是按顺序存储的,而primary key列是哈希存储的。

下面给出一个例子

先创建一个范围索引标签:

  1. // 创建索引类型:”personByAge”,可以按“age”属性的范围快速查询对应的“person”顶点
  2. schema.indexLabel(“personByAge”)
  3. .onV(“person”)
  4. .by(“age”)
  5. .range()
  6. .create();

再插入几个顶点:

  1. graph.addVertex(T.label, “person”, T.id, “p1”, “age”, 29);
  2. graph.addVertex(T.label, “person”, T.id, “p2”, “age”, 27);
  3. graph.addVertex(T.label, “person”, T.id, “p3”, “age”, 32);

然后再看存储结构:

index_label_id | field_values | element_ids
—————±————-±————
1 | 27 | Sp2
1 | 29 | Sp1
1 | 32 | Sp3

可以看到,field_values列的值是按顺序存储的,当用户的查询条件中为“age”>=27 && “age”<30时,可以定位到第一条到第三条记录之间(左闭右开);当查询条件为“age”<29时,可以定位到第一条记录。

全文索引

全文索引用于全文检索,或者说模糊检索,就是用于检索那些“不容易准确描述”的属性值的。比如:“地址”、“描述”等内容很长的属性,全文索引只能对文本类型的属性创建。

hugegraph会使用第三方的分词器将文本切分成多个短词,每个短词分别存储为一条索引记录。查询的时候也会将条件中的词语切分为多个短词,然后每个短词去索引表中查询,最后把每个短词的查询结果取并集就得到了最终的element_ids。全文索引相比于二级索引,只是多个一个分词和将结果合并的步骤,其表结构与二级索引完全相同,这里便不再赘述。

下面给出一个例子

先创建一个全文索引标签:

  1. // 创建索引类型:”personByAddress”,可以根据“address”属性的部分快速查询对应的“person”顶点
  2. schema.indexLabel(“personByAddress”)
  3. .onV(“person”)
  4. .by(“address”)
  5. .search()
  6. .ifNotExist()
  7. .create();

插入几个顶点:

  1. graph.addVertex(T.label, “person”, T.id, “p1”, “address”, “Beijing Haidian Xierqi”);
  2. graph.addVertex(T.label, “person”, T.id, “p2”, “address”, “Shanghai Xuhui”);
  3. graph.addVertex(T.label, “person”, T.id, “p3”, “address”, “Beijing Chaoyang”);

cassandra中的存储结构:

field_values | index_label_id | element_ids
————-±—————±————
shanghai | 1 | Sp2
xuhui | 1 | Sp2
chaoyang | 1 | Sp3
xierqi | 1 | Sp1
haidian | 1 | Sp1
beijing | 1 | Sp1
beijing | 1 | Sp3

可以看到,每个“person”的“address”值都被拆分为了多个部分,比如:“Beijing Haidian Xierqi”被拆分成了“beijing”、“haidian”和“xierqi”,每个单词的首字母还被转换成了小写,但是不影响查询,因为查询的时候也会做同样的转换。

当用户这样查询时:g.V().hasLabel("person").has("address", Text.contains("Beijing"));,分词器会把“Beijing”拆分(转换)为“beijing”,然后命中两条记录;当使用g.V().hasLabel("person").has("address", Text.contains("Beijing Shanghai"));查询时,分词器会把“Beijing Haidian”拆分为“beijing”和“shanghai”,其中“beijing”能命中两条记录“Sp1”和“Sp3”,“shanghai”能命中“Sp2”,然后取并集得到“Sp1”、“Sp2”和“Sp3”。

联合索引

上面提到的索引和查询都是独立工作的,就是说一个查询只会涉及到一个索引类型(IndexLabel),比如示例中:根据“name”=“xxx”查“person”时只会涉及到“personByName”这一个二级索引;根据“age”>xxx查“person”时只会涉及到“personByAge”这一个范围索引;即使是在传入多个条件的二级索引(组合索引)和通过分词产生多条件的全文索引中,也只会涉及到各自的单个索引,即“personByNameAndAge”和“personByAddress”。

相信看到这里,大家会有两个疑惑:

  • 单独创建多个单列索引和创建一个组合索引有什么区别?比如创建两个单列索引:“personByName”、“personByAge”,和创建一个组合索引:“personByNameAndAge”会对查询产生怎样的影响。
  • 似乎只有组合索引支持多个条件的查询,而且组合索引只提供了前缀匹配的能力,那岂不是要将所有的属性排列组合都创建为索引,才能支持任意属性的查询?

其实这两个问题是有关联的,可以合并为一个问题。单独创建多个单列索引和创建一个组合索引的区别在于:单独创建多个单列索引既支持单个属性的查询,也支持多个属性的组合查询,而组合索引只支持前缀属性的查询。

就以“personByName”、“personByAge”和“personByNameAndAge”为例,“personByName”、“personByAge”支持通过“name”查,支持通过“age”查,也支持通过“name”和“age”一起查;但是“personByNameAndAge”只支持通过“name”查和通过“name”和“age”一起查,不支持作为后缀的“age”查。

那多个单列索引是如何支持多个属性的组合查询的呢?很简单,依次查询然后将结果取交集,如果交集为空,则停止查询(这里所说的查询是指查询索引表),这种索引模式称为联合索引。

那又有人问了,既然组合索引能做的联合索引都能做,组合索引的意义何在呢?其实稍微一下就能知道:组合索引提供了前缀匹配的能力,在多属性查询时,一次查询就能命中,而联合查询则需要做最多N(属性个数)次查询,明显组合索引效率更高。所以,大家要根据具体的业务场景创建好索引,如果某些属性经常要组合在一起查询,最好把它创建为组合索引。

下面给出一个例子

先创建几个索引标签:

  1. // 创建组合索引:”personByNameAndAge”,可以根据“name”、“name”+“age”快速查询对应的“person”顶点
  2. schema.indexLabel(“personByNameAndAge”)
  3. .onV(“person”)
  4. .by(“name”, “age”)
  5. .secondary()
  6. .create();
  7. // 创建范围索引:”personByAge”,可以根据“age”属性的范围查询对应的“person”顶点
  8. schema.indexLabel(“personByAge”)
  9. .onV(“person”)
  10. .by(“age”)
  11. .range()
  12. .create();
  13. // 创建全文索引:”personByAddress”,可以根据“address”属性的部分快速查询对应的“person”顶点
  14. schema.indexLabel(“personByAddress”)
  15. .onV(“person”)
  16. .by(“address”)
  17. .search()
  18. .ifNotExist()
  19. .create();

插入几个顶点:

  1. graph.addVertex(T.label, “person”, T.id, “p1”, “name”, “marko”, “age”, 29, “address”, “Beijing Haidian Xierqi”);
  2. graph.addVertex(T.label, “person”, T.id, “p2”, “name”, “marko”, “age”, 27, “address”, “Shanghai Xuhui”);
  3. graph.addVertex(T.label, “person”, T.id, “p3”, “name”, “josh”, “age”, 32, “address”, “Beijing Chaoyang”);

存储结构就不看了,我们直接来做查询:

  1. // 通过“personByNameAndAge”做前缀匹配,命中“p1”和“p2”
  2. g.V().hasLabel(“person”).has(“name”, “marko”).toList();
  3. // 通过“personByNameAndAge”做前缀匹配(全匹配),命中“p1”
  4. g.V().hasLabel(“person”).has(“name”, “marko”).has(“age”, 29).toList();
  5. // 通过“personByNameAndAge”做前缀匹配(全匹配),命中“p1” ,从这里看出,查询时属性的顺序无影响
  6. g.V().hasLabel(“person”).has(“age”, 29).has(“name”, “marko”).toList();
  7. // 通过“personByAge”做范围查询,命中“p1”和“p2”
  8. g.V().hasLabel(“person”).has(“age”, P.between(27, 30)).toList();
  9. // 通过“personByAddress”做全文检索,命中“p1”和“p3”
  10. g.V().hasLabel(“person”).has(“address”, Text.contains(“Beijing”)).toList();
  11. // 通过“personByNameAndAge”和“personByAddress”做联合索引查询,命中“p1”
  12. g.V().hasLabel(“person”).has(“name”, “marko”).has(“address”, Text.contains(“Beijing”)).toList();
  13. // 通过“personByAge”和“personByAddress”做联合索引查询,命中“p1”
  14. g.V().hasLabel(“person”).has(“age”, P.between(27, 30)).has(“address”, Text.contains(“Beijing”)).toList();

 

 

授予每个自然月内发布4篇或4篇以上原创或翻译IT博文的用户。不积跬步无以至千里,不积小流无以成江海,程序人生的精彩需要坚持不懈地积累!

解决git alias权限问题

git aliases causing “Permission denied” error

 

The correct answer to this was actually different. Before git runs the aliases it checks the $PATH. In case the directory does not exist, or lacks permissions, git produces the "fatal: cannot exec 'git-co': Permission denied". It does not ever comes to check the aliases so git foobar will produce the same error.

Good people from the git mailing list also reminded me of an strace tool, that can help finding the entry that is returning EACCES, as in: strace -f -e execve git foobar

 

简单来说就行执行 strace -f -e execve git foobar

看看哪个路径在PATH中,并且没有权限,直接去掉就好了。

echo $PATH 可以查看当前的环境变量

PATH=“xxxx”可以进行赋值

禁用代理APP抓包方法

目前在ios/android上很多app选择禁止使用代理,这就让目前主流的fiddler,charles等无法抓包,但是通过流量采集的wireshark还是可以继续抓包的,目前摘抄一个可用的方法,原文地址:

 

参考文档

www.wireshark.org

Mac OS X上使用Wireshark抓包 (抓取手机网络)
使用wireshark以及filddler配合抓去手机端的TCP以及HTTP请求
Wireshark基本介绍和学习TCP三次握手
wireshark使用教程
Mac下使用Wireshark抓iphone手机数据包
Wireshark抓包分析/TCP/Http/Https及代理IP的识别

Fiddler 教程

一、Mac抓iPhone数据包

原理:使用 Mac 抓取 iPhone 数据包可通过共享和代理两种方式:
  • 使用 Mac 的网络共享功能将 Mac 的网络通过 WiFi 共享给 iPhone 连接;
  • 使用代理软件(例如 Charles)在Mac上建立HTTP代理服务器。
1.1、使用 Mac 的网络共享功能将 Mac 的网络通过 ‘WiFi共享’ 给iPhone连接
1.1.1、原理:

手机->运营商->服务器
手机->PC网卡->运营商->服务器

1.1.2、亲手操作:
  • 1、打开 系统偏好设置 -> 共享
  • 2、我设置了 Wi-Fi 为网络来源,共享给 iPhone USB 端口(因为只有一个网卡的原因,不能以 Wi-Fi 端口共享出去!);
  • 3、关闭 iPhone 上一切网络,用数据线连接到 mac USB 接口;
  • 4、神奇的事情发生了,没有WiFi和4G情况下,手机能正常上网;
开启Mac网络共享
开启Mac网络共享
wireshark监控刚刚我们共享网络的端口
wireshark监控刚刚我们共享网络的端口
wireshark抓到了iPhone的数据包(打开自己的APP请求)
wireshark抓到了iPhone的包

1.2、使用代理软件(例如 Charles)在Mac上建立HTTP代理服务器

1.3、Remote Virtual Interface,RVI

1.3.1、RVI介绍

苹果在 iOS 5 中新引入了“远程虚拟接口(Remote Virtual Interface,RVI)”的特性,可以在 Mac 中建立一个虚拟网络接口来作为 iOS 设备的网络栈,这样所有经过 iOS 设备的流量都会经过此虚拟接口。此虚拟接口只是监听 iOS 设备本身的协议栈(但并没有将网络流量中转到 Mac 本身的网络连接上),所有网络连接都是 iOS 设备本身的,与 Mac 电脑本身联不联网或者联网类型无关。iOS设备本身可以为任意网络类型(WiFi/xG),这样在 Mac 电脑上使用任意抓包工具(tcpdump、Wireshark、CPA)抓取 RVI 接口上的数据包就实现了对 iPhone 的抓包。

1.3.2、终端查看RVI
终端查看RVI
1.4.3、获取UUID
  • 首先,通过 USB 数据线将 iPhone 连接到安装了 Mac 上(老旧的设备可能不行),抓包过程中必须保持连接;
  • 然后,通过 iTunes->Summary 或者 Xcode->Organizer->Devices 获取 iPhone 的 UDID。
1.3.4、终端开启RVI

打开终端,使用“rvictl -s UUID”命令创建 RVI 接口,使用 iPhone 的 UDID 作为参数。

根据UUID开启RVI,并查看list
根据UUID开启RVI
wireshark监控RVI端口
wireshark监控RVI端口
wireshark抓到了iPhone的数据包(某车帝APP请求)
wireshark抓到了iPhone的数据包

二、牛刀小试 – 尝试抓某车帝的数据包

监控iPhone USB端口,打开某车帝APP某个页面。

设置快捷查找表达式查找HTTP请求
某接口
使用Firefox 的 RESTClient进行HTTP模拟请求
模拟HTTP请求

作者:lionsom_lin
链接:https://www.jianshu.com/p/82bcdb1decf7
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Golang程序员开发效率神器汇总

一. 开发工具

1)sql2go
用于将 sql 语句转换为 golang 的 struct. 使用 ddl 语句即可。
例如对于创建表的语句: show create table xxx. 将输出的语句,直接粘贴进去就行。
http://stming.cn/tool/sql2go.html

2)toml2go
用于将编码后的 toml 文本转换问 golang 的 struct.
https://xuri.me/toml-to-go/

3)curl2go
用来将 curl 命令转化为具体的 golang 代码.
https://mholt.github.io/curl-to-go/

4)json2go
用于将 json 文本转换为 struct.
https://mholt.github.io/json-to-go/

5)mysql 转 ES 工具
http://www.ischoolbar.com/EsParser/

6)golang
模拟模板的工具,在支持泛型之前,可以考虑使用。
https://github.com/cheekybits/genny

7)查看某一个库的依赖情况,类似于 go list 功能
https://github.com/KyleBanks/depth

8)一个好用的文件压缩和解压工具,集成了 zip,tar 等多种功能,主要还有跨平台。
https://github.com/mholt/archiver

9)go 内置命令
go list 可以查看某一个包的依赖关系.
go vet 可以检查代码不符合 golang 规范的地方。

10)热编译工具
https://github.com/silenceper/gowatch

11)revive
golang 代码质量检测工具
https://github.com/mgechev/revive

12)Go Callvis
golang 的代码调用链图工具
https://github.com/TrueFurby/go-callvis

13)Realize
开发流程改进工具
https://github.com/oxequa/realize

14)Gotests
自动生成测试用例工具
https://github.com/cweill/gotests

二.调试工具

1)perf
代理工具,支持内存,cpu,堆栈查看,并支持火焰图.
perf 工具和 go-torch 工具,快捷定位程序问题.
https://github.com/uber-archive/go-torch
https://github.com/google/gops

2)dlv 远程调试
基于 goland+dlv 可以实现远程调式的能力.
https://github.com/go-delve/delve
提供了对 golang 原生的支持,相比 gdb 调试,简单太多。

3)网络代理工具
goproxy 代理,支持多种协议,支持 ssh 穿透和 kcp 协议.
https://github.com/snail007/goproxy

4)抓包工具
go-sniffer 工具,可扩展的抓包工具,可以开发自定义协议的工具包. 现在只支持了 http,mysql,redis,mongodb.
基于这个工具,我们开发了 qapp 协议的抓包。
https://github.com/40t/go-sniffer

5)反向代理工具,快捷开放内网端口供外部使用。
ngrok 可以让内网服务外部调用
https://ngrok.com/
https://github.com/inconshreveable/ngrok

6)配置化生成证书
从根证书,到业务侧证书一键生成.
https://github.com/cloudflare/cfssl

7)免费的证书获取工具
基于 acme 协议,从 letsencrypt 生成免费的证书,有效期 1 年,可自动续期。
https://github.com/Neilpang/acme.sh

8)开发环境管理工具,单机搭建可移植工具的利器。支持多种虚拟机后端。
vagrant常被拿来同 docker 相比,值得拥有。
https://github.com/hashicorp/vagrant

9)轻量级容器调度工具
nomad 可以非常方便的管理容器和传统应用,相比 k8s 来说,简单不要太多.
https://github.com/hashicorp/nomad

10)敏感信息和密钥管理工具
https://github.com/hashicorp/vault

11)高度可配置化的 http 转发工具,基于 etcd 配置。
https://github.com/gojek/weaver

12)进程监控工具 supervisor
https://www.jianshu.com/p/39b476e808d8

13)基于procFile进程管理工具. 相比 supervisor 更加简单。
https://github.com/ddollar/foreman

14)基于 http,https,websocket 的调试代理工具,配置功能丰富。在线教育的 nohost web 调试工具,基于此开发.
https://github.com/avwo/whistle

15)分布式调度工具
https://github.com/shunfei/cronsun/blob/master/README_ZH.md
https://github.com/ouqiang/gocron

16)自动化运维平台 Gaia
https://github.com/gaia-pipeline/gaia

三. 网络工具

四. 常用网站

go 百科全书: https://awesome-go.com/

json 解析: https://www.json.cn/

出口 IP: https://ipinfo.io/

redis 命令: http://doc.redisfans.com/

ES 命令首页:

https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html

UrlEncode: http://tool.chinaz.com/Tools/urlencode.aspx

Base64: https://tool.oschina.net/encrypt?type=3

Guid: https://www.guidgen.com/

常用工具: http://www.ofmonkey.com/

五. golang 常用库

日志
https://github.com/Sirupsen/logrus
https://github.com/uber-go/zap

配置
兼容 json,toml,yaml,hcl 等格式的日志库.
https://github.com/spf13/viper

存储
mysql: https://github.com/go-xorm/xorm
es: https://github.com/elastic/elasticsearch
redis: https://github.com/gomodule/redigo
mongo: https://github.com/mongodb/mongo-go-driver
kafka: https://github.com/Shopify/sarama

数据结构
https://github.com/emirpasic/gods

命令行
https://github.com/spf13/cobra

框架
https://github.com/grpc/grpc-go
https://github.com/gin-gonic/gin

并发
https://github.com/Jeffail/tunny
https://github.com/benmanns/goworker
现在我们框架在用的,虽然 star 不多,但是确实好用,当然还可以更好用.
https://github.com/rafaeldias/async

工具
定义了实用的判定类,以及针对结构体的校验逻辑,避免业务侧写复杂的代码.
https://github.com/asaskevich/govalidator
https://github.com/bytedance/go-tagexpr

protobuf 文件动态解析的接口,可以实现反射相关的能力。
https://github.com/jhump/protoreflect

表达式引擎工具
https://github.com/Knetic/govaluate
https://github.com/google/cel-go

字符串处理
https://github.com/huandu/xstrings

ratelimit 工具
https://github.com/uber-go/ratelimit
https://blog.csdn.net/chenchongg/article/details/85342086
https://github.com/juju/ratelimit

golang 熔断的库
熔断除了考虑频率限制,还要考虑 qps,出错率等其他东西.
https://github.com/afex/hystrix-go
https://github.com/sony/gobreaker

表格
https://github.com/chenjiandongx/go-echarts

tail 工具库
https://github.com/hpcloud/taglshi

作者:Gundy
链接:https://juejin.im/post/5de082a95188256f9a25384f
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

golang调试工具Delve

Devle是一个非常棒的golang 调试工具,支持多种调试方式,直接运行调试,或者attach到一个正在运行中的golang程序,进行调试。

线上golang服务出现问题时,Devle是必不少的在线调试工具,如果使用docker,也可以把Devle打进docker镜像里,调试代码。

  安装Devle

安装Devle非常简单,直接运行go  get 即可:

go get -u github.com/derekparker/delve/cmd/dlv

如果你的go版本为1.5请先设置环境变量GO15VENDOREXPERIMENT=1再运行go get。我的go版本为1.10,不用设置。

   使用Devle调试golang服务

先写一个简单的web服务,然后使用Devle来进行调试。

在$GOPATH/src/github.com/mytest 文件夹下创建main.go

复制代码
 1 package main
 2 
 3 import (
 4     "fmt"
 5     "log"
 6     "net/http"
 7     "os"
 8 )
 9 
10 const port  = "8000"
11 
12 func main() {
13     http.HandleFunc("/hi", hi)
14 
15     fmt.Println("runing on port: " + port)
16     log.Fatal(http.ListenAndServe(":" + port, nil))
17 }
18 
19 func hi(w http.ResponseWriter, r *http.Request) {
20     hostName, _ := os.Hostname()
21     fmt.Fprintf(w, "HostName: %s", hostName)
22 }
复制代码

简单吧,一个运行在8000端口上的web服务,访问 hi会返回机器的名称。上面代码的行号是很有用的,等会我们打断点的时候会用到。

使用Delve运行我们的main.go

dlv debug ./main.go

 

可以输入help来看一下帮助文档

很简单的一些命令

我们先打在main方法上打一个断点:

b main.main

然后运行c 来运行到断点,

 

 

在func li  里打一个断点,我们可以使用

b main.hi

或者使用   “文件:行号”来打断点

b /home/goworkspace/src/github.com/mytest/main.go:20

 

 

现在执行continue 让服务跑起来。访问一下我们的服务,看hi方法会不会停下来。

curl localhost:8000/hi

看到了没,在19号停下来了。

输入 n 回车,执行到下一行

输入s 回车,单步执行

输入 print(别名p)输出变量信息

输入 args 打印出所有的方法参数信息

输入 locals 打印所有的本地变量

 

其他的命令我就不在这里给大家演示了,自己动动手试一下。

  使用Delve附加到运行的golang服务进行调试

先编译一下我们的main.go然后去行main

go build main.go

./main

 

然后使用Delve附加到我们的项目上,先看一下我们的项目的pid

ps aux|grep main
dlv attach 29260

 

在hi方法里打断点,然后执行c来等待断点的执行。

b /home/goworkspace/src/github.com/mytest/main.go:20

 

访问我们的服务器,看一下断点会不会被执行

curl localhost:8000/hi

 

断点执行了。然后调试你的代码吧!

[转]golang 实现延迟消息原理与方法

实现延迟消息最主要的两个结构:

环形队列:通过golang中的数组实现,分成3600个slot。

任务集合:通过map[key]*Task,每个slot一个map,map的值就是我们要执行的任务。

原理图如下:

140030-20170726142613250-1793759044

实现代码如下:

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
package main;
import (
    "time"
    "errors"
    "fmt"
)
//延迟消息
type DelayMessage struct {
    //当前下标
    curIndex int;
    //环形槽
    slots [3600]map[string]*Task;
    //关闭
    closed chan bool;
    //任务关闭
    taskClose chan bool;
    //时间关闭
    timeClose chan bool;
    //启动时间
    startTime time.Time;
}
//执行的任务函数
type TaskFunc func(args ...interface{});
//任务
type Task struct {
    //循环次数
    cycleNum int;
    //执行的函数
    exec   TaskFunc;
    params []interface{};
}
//创建一个延迟消息
func NewDelayMessage() *DelayMessage {
    dm := &DelayMessage{
        curIndex:  0,
        closed:    make(chan bool),
        taskClose: make(chan bool),
        timeClose: make(chan bool),
        startTime: time.Now(),
    };
    for i := 0; i < 3600; i++ {
        dm.slots[i] = make(map[string]*Task);
    }
    return dm;
}
//启动延迟消息
func (dm *DelayMessage) Start() {
    go dm.taskLoop();
    go dm.timeLoop();
    select {
    case <-dm.closed:
        {
            dm.taskClose <- true;
            dm.timeClose <- true;
            break;
        }
    };
}
//关闭延迟消息
func (dm *DelayMessage) Close() {
    dm.closed <- true;
}
//处理每1秒的任务
func (dm *DelayMessage) taskLoop() {
    defer func() {
        fmt.Println("taskLoop exit");
    }();
    for {
        select {
        case <-dm.taskClose:
            {
                return;
            }
        default:
            {
                //取出当前的槽的任务
                tasks := dm.slots[dm.curIndex];
                if len(tasks) > 0 {
                    //遍历任务,判断任务循环次数等于0,则运行任务
                    //否则任务循环次数减1
                    for k, v := range tasks {
                        if v.cycleNum == 0 {
                            go v.exec(v.params...);
                            //删除运行过的任务
                            delete(tasks, k);
                        else {
                            v.cycleNum--;
                        }
                    }
                }
            }
        }
    }
}
//处理每1秒移动下标
func (dm *DelayMessage) timeLoop() {
    defer func() {
        fmt.Println("timeLoop exit");
    }();
    tick := time.NewTicker(time.Second);
    for {
        select {
        case <-dm.timeClose:
            {
                return;
            }
        case <-tick.C:
            {
                fmt.Println(time.Now().Format("2006-01-02 15:04:05"));
                //判断当前下标,如果等于3599则重置为0,否则加1
                if dm.curIndex == 3599 {
                    dm.curIndex = 0;
                else {
                    dm.curIndex++;
                }
            }
        }
    }
}
//添加任务
func (dm *DelayMessage) AddTask(t time.Time, key string, exec TaskFunc, params []interface{}) error {
    if dm.startTime.After(t) {
        return errors.New("时间错误");
    }
    //当前时间与指定时间相差秒数
    subSecond := t.Unix() - dm.startTime.Unix();
    //计算循环次数
    cycleNum := int(subSecond / 3600);
    //计算任务所在的slots的下标
    ix := subSecond % 3600;
    //把任务加入tasks中
    tasks := dm.slots[ix];
    if _, ok := tasks[key]; ok {
        return errors.New("该slots中已存在key为" + key + "的任务");
    }
    tasks[key] = &Task{
        cycleNum: cycleNum,
        exec:     exec,
        params:   params,
    };
    return nil;
}
func main() {
    //创建延迟消息
    dm := NewDelayMessage();
    //添加任务
    dm.AddTask(time.Now().Add(time.Second*10), "test1"func(args ...interface{}) {
        fmt.Println(args...);
    }, []interface{}{1, 2, 3});
    dm.AddTask(time.Now().Add(time.Second*10), "test2"func(args ...interface{}) {
        fmt.Println(args...);
    }, []interface{}{4, 5, 6});
    dm.AddTask(time.Now().Add(time.Second*20), "test3"func(args ...interface{}) {
        fmt.Println(args...);
    }, []interface{}{"hello""world""test"});
    dm.AddTask(time.Now().Add(time.Second*30), "test4"func(args ...interface{}) {
        sum := 0;
        for arg := range args {
            sum += arg;
        }
        fmt.Println("sum : ", sum);
    }, []interface{}{1, 2, 3});
    //40秒后关闭
    time.AfterFunc(time.Second*40, func() {
        dm.Close();
    });
    dm.Start();
}

测试结果如下:

docker安装jenkins最新版本

1.pull一个jenkins镜像 docker pull jenkins/jenkins:lts;
这个是安装最新版的jenkins,如果安装旧版本,很多插件安装不上,docker环境下升级又比较麻烦。

image.png

2.查看已经安装的jenkins镜像 docker images;

image.png

查看是否是最新版 docker inspect ba607c18aeb7

image.png

3.创建一个jenkins目录 mkdir /home/jenkins_home;
4.启动一个jenkins容器 docker run -d –name jenkins_01 -p 8081:8080 -v /home/jenkins_01:/home/jenkins_01 jenkins/jenkins:lts ;

image.png

5.查看jenkins服务 docker ps | grep jenkins;

image.png

6.启动服务端 。localhost:8081;

image.png

7.进入容器内部docker exec -it jenkins_01 bash;
8.执行:cat /var/jenkins_home/secrets/initialAdminPassword,得到密码并粘贴过去

image.png


9.输入密码之后,重启docker镜像 docker restart {CONTAINER ID},安装完毕。

image.png

作者:王大合
链接:https://www.jianshu.com/p/12c9a9654f83
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Docker初级入门教程

在上一篇文章面向后端的Docker初级入门教程:DockerFile 命令详解 中,我们比较详细的讲解了docker镜像构建脚本DockerFile的使用和命令,DockerFile的出现让构建Docker镜像的过程更加直观和高效,但是,和我之前大多数文章中所提到的那句疑问一样。

难道这些就是全部吗?

当然不是

本篇文章是Docker初级入门教程的第五篇,在前四篇,基础篇 概念篇 实战篇 以及DockerFile那篇,我们从Docker是什么,到使用DockerFile构建自己的镜像,一步一步走来,我相信完整看完这些教程的人已经对Docker有了一个比较好的了解,并可以处理一部分现实中遇到的实际问题,但是仍然还有许多问题有着更好的解决方式,本篇文章呢,我将为大家介绍一个docker自动化部署神器,docker-compose,它可以使我们将传统的那些繁琐的docker操作指令做到自动化完成,并可以控制多个容器,实现多个容器的批量启动。

不说废话,直接看东西。

docker-compose解决了什么样的问题?

如果大家之前了解过微服务架构的话,对docker-compose的自动化部署绝对是相见恨晚,这里简单提一下,微服务架构就是将传统的单一服务拆分成多个单一的小服务,从而实现了应用的横向扩展,就拿一个大的电商平台为例,微服务就是将之前一个巨大的单体应用拆分成多个服务,比如仓库系统单独出来作为一个服务,订单系统单独作为一个应用提供服务,这样带来的好处是我们不需要像传统的方式那样升级整个服务器,而只需要根据特定业务的压力情况升级对应的服务器就好,比如双十一订单系统压力比较大,我单独把订单系统的服务器升级了就好,而不需要升级整套系统的硬件配置。

但是由于微服务各个服务间存在一定的依赖关系,比如SpringCloud里面,Eureka作为注册中心,就是要先启动的,要不然后面的服务启动的时候连接不上注册中心,注册不上去可还行,而微服务落地到Docker中,大概就是下面这么个启动过程,运维人员需要依次输入很多命令来确保各个服务按照正确的顺序启动:

docker run -d 服务A
docker run -d 服务B,必须在A之后启动
复制代码

要是只有两个服务还好说,如果十个八个咬咬牙也能接受,但是几十个,几百个服务呢?按照顺序启动的话,万一哪个没整好,换来的就是运维人员的一句我操。简直就是灾难,于是docker-compose应运而生了,docker-compose和DockerFile有着异曲同工之妙,只不过DockerFile是将镜像的构建过程给封装到了脚本里,而docker-compose则是可以将镜像的运行过程封装到了特定的脚本里,这就意味这我们可以把各个容器的启动顺序整理好,写到脚本里,运维工程师每次只需要运行这个脚本就行了,完全不用依次执行run 命令启动容器了。

为什么Docker-Compose被称作大杀器,是因为它真的解决了痛点,知识点呐,朋友们,要考的。

docker-compose 安装:

关于docker-compose安装这块,网上仍然有着非常多的教程,但是无一例外三个字,太麻烦,本次依然延续传统,只说最简单的那一种,首先确保电脑上安装了python3 和 docker

不用yum?

答:这次先不用,pip安装更好用

为啥不用python2 ? 爱python2用户表示强烈谴责

答:我用pip2装了一下,死活装不上,pip3 一下子就好了,所以我推荐pip3.

注:不会装python3 的朋友,算了,我也顺便写上去吧。另外,确保你的服务器已经装好了docker。

打开linux终端,输入以下命令:

##安装python3
yum install python3

#pip方式安装docker-compose,pip会自动寻找和你主机上docker版本相匹配的docker-compose版本
pip3 install docker-compose 
复制代码

查看是否安装成功,在终端输入:

docker-compose version
复制代码

如果显示出版本的话,则代表docker-compose在我们这台机器上已经算是按照成功了。对了,我的版本是1.24.1。

docker-compose 实战:

首先新建一个文件夹,不建也行,防止一会找不到自己把yml文件放哪了,对了,docker-compose的脚本格式是yaml文件格式,不了解的朋友可以下去补补,默认文件名是docker-compose.yml或者docker-compose.yaml

在新建的文件夹里新建一个docker-compose.yml文件,输入以下内容,这里我们仍然以tomcat为例:

mytomcat:
    image: tomcat
    ports:
       - "8086:8080"
复制代码

然后我们在当前目录(记得一定要是docker-compose.yml文件所在的目录哦,docker-compose默认是从当前目录搜索的) 输入:

docker-compose up ##根据yml文件启动容器
复制代码

然后,屏幕冒出来一大堆tomcat的日志输出,ctrl+c退出的话整个容器都退出了,这是因为默认的docker-compose up命令是前台启动的,容器内的日志输出都会在前台输出,类似于docker run -it

如果想要容器从后台启动,只需要在后面加一个 -d 就行了,如下:

docker-compose up -d
复制代码

如果启动成功,会显示

[root@iZbp1d7upppth01hp demo]# docker-compose up -d
Starting demo_mytomcat_1 ... done
复制代码

此时再执行docker ps,会发现我们的tomcat已经正常启动了,名字是demo_mytomcat_1 ,分别对应文件夹,容器名,以及编号,如果再启动一次,新的tomcat容器名字就会变成demo_mytomcat_2

docker-compose构建脚本详解:

既然容器已经运行成功了,那么接下来我们便深入了解一下docker-compose.yml 文件应该遵循的格式是如何的。

首先第一层:

  • mytomcat :我们声明构建的容器的名称,一个yaml文件可以定义多个容器。

然后是:

  • image :我们构建的镜像来源,这里是tomcat镜像,如果需要指定版本,可以写成tomcat:8 这种格式

    这个时候有人可能要问了,我如果想用我自己定义的镜像怎么办?同样是可以的,只需要写成如下这种格式即可:

    mytomcat:
        bulid: . #如果是 . docker-compose 便会在当前目录找DockerFile 文件,执行构建镜像然后启动,镜像名字是  当前目录_mytomcat
        ports:
           - "8086:8080"
    复制代码
  • ports: 相当于docker run 的 -p 参数,用来映射端口。列出端口的时候可以不带引号,但是像遇到56:56这种情况的时候,YAML会把它解析为60为基数的六十进制数字,所以强烈建议大家在写的时候加上引号。

就这么点?没了?不是,同样我们可以在yml脚本里面执行诸如设置环境变量,容器卷,链接,命令等操作。

  • environment:相当于docker run 命令的 -e 参数,用来设置环境变量。
  • volumes:相当于docker rum 命令的 -v 参数,用于配置数据卷,用法如下:
    mytomcat:
        image: tomcat
        ports:
           - "8086:8080"
        volumes: 
           - ./data:/data #把当前目录下的data文件夹挂载到容器内的data文件夹中
    复制代码
  • **links:**相当于docker run 命令中的 –link 参数,用来链接两个容器,links支持链接多个容器,用法如下:
    mytomcat:
        image: tomcat
        ports:
           - "8086:8080"
        links:
           -redis #链接到redis容器
           -mysql #链接到mysql容器,如果只需要链接一个容器,删掉一个就行了
        volumes: 
           - ./data:/data #把当前目录下的data文件夹挂载到容器内的data文件夹中
    复制代码
  • command: 使用 command 可以覆盖容器启动后默认执行的命令。
  • container_name: 如果不想使用默认生成的 <项目名称><服务名称><序号> 格式名称,可以使用container_name选项来自定义容器名称。

等,当然,docker-compose支持的命令肯定不止这几个,但是上面这几个命令无一例外是我们经常会用的,至于其他的比如日志什么的,我这里就不一一列举了,需要的时候去网上搜索就可以了。

前面有提到过,一个yml脚本是可以同时定义多个容器的,如果需要定义多个容器,直接另起一行写就行了,不过,一定要注意yaml文件本身的缩进格式

mytomcat01:
    image: tomcat
    ports:
       - "8086:8080"
       
mytomcat02:
    image: tomcat
    ports:
       - "8087:8080"
复制代码

当然,这个时候可能有人还有一个疑问,yml文件必须要是docker-compose.yml这个名字吗,我要是想用另外一个名字比如 xswl.yml 怎么办,当然是可以的,只需要加上 -f 选项 然后指定 yml文件的路径就可以了。

docker-compose -f xswl.yml up -d
复制代码

docker-compose命令:

到这里,我们的构建脚本常见的命令已经说的差不多了,当然,包括yml文件,这些都是针对docker 容器来进行操作的,而docker-compose这个软件如docker一样本身也提供了很多的命令供我们使用:

  • up: 启动所有在compose文件中定义的容器,并且把它们的日志信息汇集到一起,通常搭配 -d 使用
  • ps: 获取由Compose管理的容器的状态信息。
  • run: 启动一个容器,并允许一个一次性的命令,被链接的容器会同时启动。
  • bulid: 重新建造由DockerFile所构建的镜像,除非镜像不存在,否则up命令不会执行构建已经存在的镜像的命令,常常在需要更新镜像时使用build这个命令。
  • logs :汇集由Compose管理的容器产生的日志信息,并以彩色输出。
  • stop: 停止容器。
  • rm: 删除已经停止的容器,记得不要忘了加上 -v 来删除任何由Docker管理的数据卷。

如果说我突然不想用docker-compose这个软件了,可以执行

docker-compose stop #停止docker-compose
复制代码

如果说我又突然想用了,可以执行:

docker-compose start 或者 docker-compose up #重启相同的容器
复制代码

至于更加细致入微的骚操作,大家可以去docker官网参观学习,那么多命令,我实在是写不完(没时间写,而且有的命令我也没见过)

作者:韩数
链接:https://juejin.im/post/5dc220126fb9a04aa660dcfb
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在上一篇文章写给后端的Docker初级入门教程:实战篇最后我们有提到用DockerFile来构建和定制属于我们自己的镜像,因为时间和篇幅问题,上一篇文章对DockerFile只做了一个简单的介绍和使用,并没有对DockerFile具体的指令进行详细的介绍和解释,本篇,作为上一篇实战篇的额外补充篇,我们将从DockerFile基础的命令入手,一步一步的去构建一个属于我们自己的镜像出来。

DockerFile介绍:

Dockerfile是由一系列命令和参数构成的脚本,一个Dockerfile里面包含了构建整个image的完整命令。Docker通过docker build执行Dockerfile中的一系列命令自动构建image。

实例:

这里我们仍然选择我们上一篇使用的在centos基础上定制我们自己的镜像为本章的代码实例,代码如下:

FROM centos  //继承至centos
ENV mypath /tmp  //设置环境变量
WORKDIR $mypath //指定工作目录

RUN yum -y install vim //执行yum命令安装vim
RUN yum -y install net-tools //执行yum命令安装net-tools

EXPOSE 80 //对外默认暴露的端口是80
CMD /bin/bash //CMD 容器启动命令,在运行容器的时候会自动执行这行命令,比如当我们 docker run -it centos 的时候,就会直接进入bash
复制代码

之后再通过docker build 命令编译该DockerFile便可以得到一个属于自己的镜像了。

然后编译该镜像
docker build -f ./DockerFile -t mycentos:1.3.
-t 新镜像名字:版本
-f 文件 -d 文件夹
复制代码

运行该镜像会发现vim和net-tools在我们新的容器中已经可以正常使用了。

接下来呢,我们将从FROM命令开始逐行介绍,最终完成对DockerFile常用命令的了解和掌握。

常用命令:

FROM命令:

既然我们是在原有的centos镜像的基础上做定制,那么我们的新镜像也一定是需要以centos这个镜像为基础的,而FROM命令则代表了这个意思,在DockerFile中,基础镜像是必须指定的,FROM指令的作用就是指定基础镜像,因此一个DockerFile中,FROM是必备的指令,而且就像java,python的import关键字一样,在DockerFile中,FROM指令必须放在第一条指令的位置

当然,这个时候可能有朋友会问了,我要是不想在其他的镜像上定制镜像怎么办呢,没问题啊,Docker 提供了scratch 这个虚拟镜像,如果你选择 FROM scratch 的话,则意味着你不以任何镜像为基础,接下来所写的指令将作为镜像的第一层开始存在,当然,在某些情况下,比如linux下静态编译的程序,运行的时候不需要操作系统提供运行时的支持,这个时候FROM scratch 是没有问题的,反而会大幅降低我们的镜像体积。

ENV指令

功能:设置环境变量

同样的,DockerFile也提供了两种格式:

  • ENV key value
  • ENV key1=value1 key2=value2

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN, 还是运行时的应用,都可以直接使用这里定义的环境变量。

可以看到我们示例中使用ENV设置mypath变量之后,在下一行WORKDIR则使用到了mypath这个变量

ENV mypath /tmp  //设置环境变量
WORKDIR $mypath //指定工作目录
复制代码

WORKDIR 指令:

功能,指定工作目录

格式为:WORKDIR 工作目录路径,如果这个目录不存在的话,WORKDIR则会帮助我们创建这个目录。

设置过工作目录之后,当我们启动容器,会直接进入该工作目录

[root@8081304919c9 tmp]#
复制代码

RUN命令:

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令也是在定制镜像时是较为常用的指令之一。

RUN命令的格式一共有两种,分别是:

  • Shell 格式

    RUN 命令,就像直接在命令行中输入命令一样,比如RUN yum -y install vim就是使用的这种格式

  • exec 格式

    RUN[“可执行文件”,”参数1″,”参数2″],感觉就像调用函数一样

就像我们在上一篇文章中说过的那样,DockerFile中每一条指令都会建立一层,比如我们上面执行过下面这条命令

RUN yum -y install vim 
复制代码

执行结束之后,则调用commit提交这一层的修改,使之构成一个新的镜像,怎么样,是不是豁然开朗了呢。

并没有

那好吧

同样的,Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以 及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

提示:

如果使用apt方式安装的话,最后不要忘记清理掉额外产生的apt缓存文件,如果不清理的话会让我们的镜像显得非常臃肿。因为DockerFile生成一层新的镜像的时候,并不会删除上一层镜像所残留的文件。

EXPOSE指令:

功能:声明端口

格式: EXPOSE 端口1 端口2

EXPOSE 指令是声明运行时容器提供服务端口,这当然只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。这样声明主要是为了方便后期我们配置端口映射。

CMD指令:

之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

同样的,DockerFile也为我们提供了两种格式来使用CMD命令:

  • shell 格式:CMD 命令
  • exec 格式:CMD [“可执行文件”, “参数 1”, “参数 2″…]

示例中,我们使用的是第一种:

CMD /bin/bash
复制代码

这条指令带来的效果就是,当我们通过run -it 启动命令的时候,容器会自动执行/bin/bash,centos默认也是CMD /bin/bash,所以当我们运行centos镜像的时候,会自动进入bash环境里面。

当然,我们也可以通过运行时指定命令的方式来体换默认的命令,比如:

docker run -it centos cat /etc/os-release
复制代码

这样当我们运行镜像的时候,cat /etc/os-release就会替代默认的CMD /bin/bash 输出系统的版本信息了。

如果使用 shell 格式的话, 实际的命令会被包装为 sh -c 的参数的形式进行执行。

比如:

CMD echo $HOME
复制代码

在实际执行中,会将其变更为

CMD [ "sh", "-c", "echo $HOME" ]
复制代码

当然还有很多初学者特别容易犯的问题,就是去启动后台服务,比如:

CMD service nginx start
复制代码

这样子去用,会发现容器运行了一会就自动退出了。

所以,?????

我们之前不止一次的提醒过,容器不是虚拟机,容器就是进程,容器内的应用都应该以前台运行,而不是像虚拟机,物理机那样去运行后台服务,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

怎么理解呢?想想偶像剧,容器是女主角,主进程是男主角

你走了,我也不活了(撕心裂肺大哭),大概就是这么个意思。

正如我们前面所提出的,实际上CMD service nginx start 最终会被理解为:

CMD [ "sh", "-c", "service nginx start"]
复制代码

在这里,我们主进程实际就是sh,当我们service nginx start执行完毕之后,那么sh自然就会退出了,主进程退出,容器自然就会相应的停止。争取的做法是直接执行nginx可执行文件,并且声明以前台的形式运行:

CMD ["nginx", "-g", "daemon off;"]
复制代码

到这里,我们示例中所涉及到的命令已经讲完了,当然,这并不够,Docker中仍然有很多命令是我们使用比较频繁的,下面我们的部分作为补充,讲一下其他常用的DockerFile命令。

COPY 命令:

功能:复制文件

Docker依旧提供了两种格式供我们选择:

  • COPY [–chown=:] <源路径>… <目标路径>
  • COPY [–chown=:] [“<源路径 1>”,… “<目标路径>”]

到这里大家其实会发现,Docker提供的两种格式其实都是差不多的用法,一种类似于命令行,一种则类似于函数调用。

第一种例如(将package.json拷贝到/usr/src/app/目录下):

COPY package.json /usr/src/app/
复制代码

其次,目标路径 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径 ,工作目录可以用 WORKDIR 指令来指定,如果需要改变文件所属的用户或者用户组,可以加上–chown 选项。

需要注意的是,使用 COPY 指 令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这 个特性对于镜像定制很有用。

ADD命令:

ADD命令可以理解为COPY命令的高级版,格式和用法与COPY几乎一致,ADD在COPY的基础上增加了一些功能,比如源路径可以是一个URL链接,当你这么用的时候,Docker会尝试着先将该URL代表的文件下载下来,然后复制到目标目录上去,其他的则是在COPY的基础上增加了解压缩之类的操作,码字码的手疼,需要了解的朋友可以去官网查看相关的文档,这里我就不延申了。

VOLUME 定义匿名卷:

在上一篇中,我们有讲容器卷这个概念,为了防止运行时用户忘记 将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些 目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运 行,不会向容器存储层写入大量数据。

例如:

VOLUME /data
复制代码

运行时通过-v参数即可以覆盖默认的匿名卷设置。

USER 命令:

功能:指定当前用户

格式:USER 用户名:用户组

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户。

当然这个大前提是,你的User用户是事先存在好的。

完结撒花?

不知不觉间,Docker系列初级入门教程已经发到了第四篇,篇幅也到了一万多字,前三篇文章加起来在掘金上慢慢有了大概1500左右的阅读量,我知道这点对于很多掘金大佬来说只是微不足道的一点,但对于现阶段的我来说已经非常满足了,从来没有想到过有一天自己也可以通过分享去帮助到别人,正如我之前通过别人的技术博客学习那样。

这个系列完结了吗?我想初级篇应该是完结了,但是Nginx的初级入门教程,即将到来的Mysql,Netty等等并没有,由于目前尚未毕业,还没有接受过工作的毒打(滑稽),所以只能尽自己的能力去写一些基础的入门教程,所以完结了吗?并没有,技术之路永无止境,只要我们一直在坚持学习,我想,我们可以一直继续下去。

作者:韩数
链接:https://juejin.im/post/5dafa60ff265da5b62535bcf
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Nginx的超时timeout配置详解

本文介绍 Nginx 的 超时(timeout)配置。分享给大家,具体如下:

Nginx 处理的每个请求均有相应的超时设置。如果做好这些超时时间的限定,判定超时后资源被释放,用来处理其他的请求,以此提升 Nginx 的性能。

keepalive_timeout

HTTP 是一种无状态协议,客户端向服务器发送一个 TCP 请求,服务端响应完毕后断开连接。

如果客户端向服务器发送多个请求,每个请求都要建立各自独立的连接以传输数据。

HTTP 有一个 KeepAlive 模式,它告诉 webserver 在处理完一个请求后保持这个 TCP 连接的打开状态。若接收到来自客户端的其它请求,服务端会利用这个未被关闭的连接,而不需要再建立一个连接。

KeepAlive 在一段时间内保持打开状态,它们会在这段时间内占用资源。占用过多就会影响性能。

Nginx 使用 keepalive_timeout 来指定 KeepAlive 的超时时间(timeout)。指定每个 TCP 连接最多可以保持多长时间。Nginx 的默认值是 75 秒,有些浏览器最多只保持 60 秒,所以可以设定为 60 秒。若将它设置为 0,就禁止了 keepalive 连接。

# 配置段: http, server, location
keepalive_timeout 60s;

client_body_timeout

指定客户端与服务端建立连接后发送 request body 的超时时间。如果客户端在指定时间内没有发送任何内容,Nginx 返回 HTTP 408(Request Timed Out)。

# 配置段: http, server, location
client_body_timeout 20s;

client_header_timeout

客户端向服务端发送一个完整的 request header 的超时时间。如果客户端在指定时间内没有发送一个完整的 request header,Nginx 返回 HTTP 408(Request Timed Out)。

# 配置段: http, server, location
client_header_timeout 10s;

send_timeout

服务端向客户端传输数据的超时时间。

# 配置段 : http, server, location
send _ timeout 30 s;

客户度连接nginx超时, 建议5s内

接收客户端header超时, 默认60s, 如果60s内没有收到完整的http包头, 返回408

Syntax: client_header_timeout time;
Default:
client_header_timeout 60s;
Context: http, server
Defines a timeout for reading client request header. If a client does not transmit the entire header within this time,
the 408 (Request Time-out) error is returned to the client.

接收客户端body超时, 默认60s, 如果连续的60s内没有收到客户端的1个字节, 返回408

Syntax: client_body_timeout time;
Default:
client_body_timeout 60s;
Context: http, server, location
Defines a timeout for reading client request body. The timeout is set only for a period between two successive read operations, not for the transmission of the whole request body.
If a client does not transmit anything within this time,
the 408 (Request Time-out) error is returned to the client.

keepalive时间,默认75s,通常keepalive_timeout应该比client_body_timeout大

Syntax: keepalive_timeout timeout [header_timeout];
Default:
keepalive_timeout 75s;
Context: http, server, location
The first parameter sets a timeout during which a keep-alive client connection will stay open on the server side. The zero value disables keep-alive client connections.
The optional second parameter sets a value in the “Keep-Alive: timeout=time” response header field. Two parameters may differ.

The “Keep-Alive: timeout=time” header field is recognized by Mozilla and Konqueror. MSIE closes keep-alive connections by itself in about 60 seconds.

可以理解为TCP连接关闭时的SO_LINGER延时设置,默认5s

Syntax: lingering_timeout time;
Default:
lingering_timeout 5s;
Context: http, server, location
When lingering_close is in effect, this directive specifies the maximum waiting time for more client data to arrive. If data are not received during this time,
the connection is closed. Otherwise, the data are read and ignored, and nginx starts waiting for more data again.
The “wait-read-ignore” cycle is repeated, but no longer than specified by the lingering_time directive.

域名解析超时,默认30s

Syntax: resolver_timeout time;
Default:
resolver_timeout 30s;
Context: http, server, location
Sets a timeout for name resolution, for example:
resolver_timeout 5s;

发送数据至客户端超时, 默认60s, 如果连续的60s内客户端没有收到1个字节, 连接关闭

Syntax: send_timeout time;
Default:
send_timeout 60s;
Context: http, server, location
Sets a timeout for transmitting a response to the client. The timeout is set only between two successive write operations,
not for the transmission of the whole response. If the client does not receive anything within this time, the connection is closed.

nginx与upstream server的连接超时时间

Syntax: proxy_connect_timeout time;
Default:
proxy_connect_timeout 60s;
Context: http, server, location
Defines a timeout for establishing a connection with a proxied server. It should be noted that this timeout cannot usually exceed 75 seconds.

nginx接收upstream server数据超时, 默认60s, 如果连续的60s内没有收到1个字节, 连接关闭

Syntax: proxy_read_timeout time;
Default:
proxy_read_timeout 60s;
Context: http, server, location
Defines a timeout for reading a response from the proxied server. The timeout is set only between two successive read operations,
not for the transmission of the whole response. If the proxied server does not transmit anything within this time, the connection is closed.

nginx发送数据至upstream server超时, 默认60s, 如果连续的60s内没有发送1个字节, 连接关闭

Syntax: proxy_send_timeout time;
Default:
proxy_send_timeout 60s;
Context: http, server, location
Sets a timeout for transmitting a request to the proxied server. The timeout is set only between two successive write operations,
not for the transmission of the whole request. If the proxied server does not receive anything within this time, the connection is closed.

作者:架构师之路
链接:https://juejin.im/post/5b696a24e51d45191e0d3e56
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。