• ASN.1基础知识探讨


    ASN.1基础知识探讨

    转自: https://blog.csdn.net/shanzhizi/article/details/11727681

    感谢: wmfbravo

    2  基础知识

    &  注释:

    本章的内容主要翻译自《ASN.1 Communication between Heterogeneous Systems》。

    2.1  相关背景知识

    2.1.1  OSI参考模型

    Figure 2-1 OSI 七层参考模型

    虽然ASN.1OSI密切相关,但实际上它也应用在很多非OSI模型的情况下。

     

    II. 表示层 Presentation Layer

    两个系统在传输数据前需要协商共用的编码方式。表示层负责在两个应用系统间进行编码协商和实际信息编码。为了达到这个目的,表示层需使用以下概念:

    1)         抽象语法:定义了数据的常用结构(包括不同的数据类型),并且建立了和应用层对话所依赖的框架。

    2)         实际语法:本地的,并且定义本地系统的数据表示方法。

    3)         传输语法:定义两个系统间基于各自会话层的表示层间交换数据的表示方法。

    4)         编码规则:提供从本地实际语法到传输语法和其相反操作的方法。

    应用这些记法,表示层能够提供应用层这些服务:

    l           传输语法的协商(在会话开始前挑选一种传输语法的方法,改变传输语法的方法);

    l           确定传输语法的集合(抽象语法的多种表示方式);

    l           用实际语法(内部表示方式)的编码、解码规则来翻译为传输语法(外部表示方式)或者做相反操作;

    l           把一个协商后的传输语法和应用内部采用的抽象语法进行关联;

    l           访问会话层服务。

    Figure 2-2 表示层上下文协商

    Figure 2-2表示层上下文协商中,详细描述了这样一个过程:

    1)         应用A发送一个P-CONNECT.request原语给自己的表示层,并且声明操作这个传输的相关抽象语法名字为(AS1,AS2)。每个抽象语法的名字实际是一串成为对象标识符(Object Identifier,在ASN.1中是OBJECT IDENTIFIER类型的值)的数字,它能唯一的标识抽象语法:的确,我们应当注意到这是一个开放式的架构,能适应各种机器,各种抽象语法,各种传输语法。

    2)         表示层为每个抽象语法关联传输语法,并且为会话层编码表示协议数据值PPDV(Presentation Protocol Data Value),这些数据会被送往对方系统的表示层。PPDV中包含着可用的抽象语法。

    3)         表示层B收到这个PPDV数据后,向自己的应用层回送P-CONNECT.indication原语,指示应用A的抽象语法可用。

    4)         应用B以P-CONNECT.response原语应答,指示在这个传输中可用的抽象语法的名字(这里只指示AS2)。

    5)         会话层B接收到原语后,发送PPDV指明传输语法,这个语法应当是已经协商中的一个(如上图中是T2、T3中的T2)。

    6)         最后,表示层A收到PPDV后,检查对方推荐的传输语法,如果接受它则发送P-CONNECT.confirm原语给应用A。

    应当注意,表示层没有参与到应用间可用抽象语法的确定过程中。通常,会用多个抽象语法/传输语法的组合关系。一个抽象语法可以用多个传输语法来表示;一个传输语法也可以用来表示多个抽象语法。抽象语法/传输语法组合的协商结果被成为表示上下文(Presentation Context)。上下文用整数来标识,为了防止重新分配时出现覆盖,一般让一个实体采用偶数而另一个实体采用奇数。其它上下文可以在通讯过程中动态协商。

    在初始协商的最后,系统在它的配置中有了一套表示上下文,在任何时候,系统都能从中选出合适的上下文进行交换操作。

    来自应用层的数据会被根据相关的表示上下文进行编码。当只有一个上下文被指定时,他们将被直接编码(简单编码 Simple Encoding);否则应用数据包括嵌入数据(embedded data)都要在前面加上正确的上下文标识符(完整编码 Complete Encoding)。

    III. 应用层 Application Layer

    采用OSI模型时,我们称一个应用的一个通讯方面为一个应用实体;通讯实体用通讯协议和表示业务来共享信息。

    ASN.1中,各个应用的数据结构作为应用协议数据值APDV(Application Protocol Data Value)发送。每当它要传输数据时,应用实体都会在给出APDV的同时,告知表示层自己的ASN.1名字。通过参考ASN.1的定义,表示层可以得知数据单元的类型和长度,以及传输时应当采用的编码方法。在连接的另一端,表示层分析收到数据结构中的ASN.1标识符,就能得知第一个数据单元有多少比特,第二个有多少,等等。有了这些信息,表示层按照接收方内部格式对数据做必要的转换。

    自从ISO要求所有的应用层和表示层数据交换都要用ASN.1抽象语法描述后,ASN.1在OSI中只作为表示方法用。随着OSI模型的广泛应用,ASN.1主要用在高层中(部分因为在ASN.1出现前,许多低层已经存在了),但是这不是一种限制。如果ASN.1能广泛应用在低层中,就能充分影响编码过程,阻止数据在不同层上反复出现。

    2.1.2  边界对齐

    对于同样一条消息,在计算机内存中是以字节为单位存储的,在链路上则是以比特为单位传送的,对于其中每个信元(IE:Information Element),当从比特流映射到字节流的时候,就涉及到边界对齐的问题,如果一个信元的第一个比特也恰好是字节流中某字节的开始比特,我们称之为开始于边界对齐的,同样,如果信元的最后一个比特也恰好是字节流中某字节的最后一个比特,则可以称之为结束于边界对齐的。

    对于不是结束于边界对齐的情况,一般要进行补位。有两种方式:

    1)         对信元的结束立即进行补位,保证下一个信元是开始于边界对齐的;

    2)         从信元结束的位置开始新的信元,到消息结束,再进行补位操作。

    通常第一个种方式用在对消息大小要求不苛刻的编码过程中和通常的解码过程中;第二种方式用在对消息大小要求苛刻的编码过程中(如无线通讯系统的空中接口消息编码中)。

     

    当把边界对齐问题和大小端问题放在一起时,常常会造成理解上的困难。某些协议中为了明确表示位域跨字节情况下的排列,用MSB、LSB标识最高、最低比特位置所在。


    2.1.3  大小端(Bid Endian vs.  Little Endian)

    来源于Swift的小说Gulliver’s Travels中一个故事:两个国家连年征战,起因在于吃煮鸡蛋时到底是从比较尖的一端开始,还是从比较圆的一端开始。

    不幸的是类似事情在计算机中也存在如下情况:

    l           大端方式-Motorola的PPC系列,IP协议中(MSDN中说的网络序)

    l           小端方式-VAX计算机,Intel的x86系列(MSDN中说的主机序)

    字段内部的比特高低次序相同(左高右低),而字段之间的高低次序相反。

     

    32位系统中我们分以下这些情况来一一说明:

    1)         Byte类型(8bits)

    在只有一个字节的情况下,大端方式和小端方式没有分别。如:0x34

     

    bit7

    bit6

    bit5

    bit4

    bit3

    bit2

    bit1

    bit0

     

    Bid Endian

    0

    0

    1

    1

    0

    1

    0

    0

    0x34

     Little Endian

    0

    0

    1

    1

    0

    1

    0

    0

    0x34

    2)         Short类型(16bits)

    大小端方式之间有差别。如:0x1234

     

    bit7

    bit6

    bit5

    bit4

    bit3

    bit2

    bit1

    bit0

     

    Bid Endian (0)

    0

    0

    0

    1

    0

    0

    1

    0

    0x12

    Bid Endian (1)

    0

    0

    1

    1

    0

    1

    0

    0

    0x34

    Little Endian (0)

    0

    0

    1

    1

    0

    1

    0

    0

    0x34

    Little Endian (1)

    0

    0

    0

    1

    0

    0

    1

    0

    0x12

    3)         Long类型(32bits)

    大小端方式之间有差别。如:0x12345678

     

    bit7

    bit6

    bit5

    bit4

    bit3

    bit2

    bit1

    bit0

     

    Bid Endian (0)

    0

    0

    0

    1

    0

    0

    1

    0

    0x12

    Bid Endian (1)

    0

    0

    1

    1

    0

    1

    0

    0

    0x34

    Bid Endian (2)

    0

    1

    0

    1

    0

    1

    1

    0

    0x56

    Bid Endian (3)

    0

    1

    1

    1

    1

    0

    0

    0

    0x78

    Little Endian (0)

    0

    1

    1

    1

    1

    0

    0

    0

    0x78

    Little Endian (1)

    0

    1

    0

    1

    0

    1

    1

    0

    0x56

    Little Endian (2)

    0

    0

    1

    1

    0

    1

    0

    0

    0x34

    Little Endian (3)

    0

    0

    0

    1

    0

    0

    1

    0

    0x12

     

    4)         位域的情况:

    大小端方式之间有明显的差别。

    在一个字节内,如3-4-1结构下的{4, 15, 0}

     

    bit7

    bit6

    bit5

    bit4

    bit3

    bit2

    bit1

    bit0

     

    Bid Endian

    1

    0

    0

    1

    1

    1

    1

    0

    0x9E

    Little Endian

    0

    1

    1

    1

    1

    1

    0

    0

    0x7C

     

    跨字节,边界不对齐,如5-4-7结构下的{2, 15, 0}

     

    bit7

    bit6

    bit5

    bit4

    bit3

    bit2

    bit1

    bit0

     

    Bid Endian (0)

    0

    0

    0

    1

    0

    1

    1

    1

    0x17

    Bid Endian (1)

    1

    0

    0

    0

    0

    0

    0

    0

    0x80

    Little Endian (0)

    0

    0

    0

    0

    0

    0

    0

    1

    0x01

    Little Endian (1)

    1

    1

    1

    0

    0

    0

    1

    0

    0xE2

     

    2.2  基本语法规则

    到目前为止,ASN.1记法仍然主要是BNF(Backus-Naur Form)形式的。

     

    1)         在ASN.1中,符号的定义没有先后次序:只要能够找到该符号的定义即可,而不必关心在使用它之前是否被定义过。如:

    employeeNumber EmployeeNumber ::= 12345

    EmployeeNumber ::= [APPLICATION 2] INTEGER

    2)         所有的标识符、参考、关键字都要以一个字母开头,后接字母(大、小写都可以)、数字或者连字符“-”。不能出现下划线“_”。不能以连字符“-”结尾,不能出现两个连字符(注释格式)。

    合法的:

    INTEGER

    v1515

    No-final-dash

    MY-CLASS

    不合法的:

    Final-dashdouble--

    dash

    under score

    1515

    3M

    3)         关键字一般都是全部大写的,除了一些字符串类型(如PrintableString,UTF8String,等。因为这些都是由原类型OCTET STRING衍生出来的)。

    4)         在标识符中,只有类型和模块名字是以大写字母开头的,其它标识符都是以小写字母开头的。

    5)         字符串有三种形式:

    l           用引号引用的字符串

    “This is a string”

    l           单引号引用的二进制串后加大写字母B

    ‘01101’B

    l           单引号引用的十六进制串后加大写字母H

    ‘0123456789ABCDEF’H

    6)         带小数点的小数形式不能在ASN.1中直接使用,在ASN.1中实数实际定义为三个整数:尾数、基数和指数。

    7)         注释以两个连字符“--”开始,结束于行的结尾或者该行中另一个双连字符。

    8)         如同大多数计算机语言,ASN.1不对空格、制表符、换行符和注释做翻译。但是在定义符号(或者分配符号Assignment)“::=”中不能有分隔符,否则不能正确处理。

    2.3  类型与类型定义

    2.3.1   ASN.1中的类型

    ASN.1中的一个重要概念就是类型。类型是一个非空的值的集合,可以被编码后传输。相比与高级语言中复杂的数据结构,ASN.1中的类型主要是为了数据的传输(比如为SEQUENCE和SET定义了OPTIONAL条目)。而BIT STRING和EMBEDDED PDV则是为了通讯专门设计的。

    ASN.1中的基本类型如Table 2-1中所示,更为复杂的类型可以由基本类型通过如Table 2-2中的组合类型构造得到。

     

    Table 2-1 内建数据类型

    类型

    含义

    NULL

    只包含一个值NULL,用于传送一个报告或者作为CHOICE类型中某些值

    INTEGER

    全部整数(包括正数和负数)

    REAL

    实数,表示浮点数

    ENUMERATED

    标识符的枚举(实例状态机的状态)

    BIT STRING

    比特串

    OCTET STRING

    字节串

    OBJECT IDENTIFIER,

    RELATIVE-OID

    一个实体的标识符,它在一个全世界范围树状结构中注册

    EXTERNAL, EMBEDDED PDV

    表示层上下文交换类型

    …String

    各种字符串,有NumericString、PrintableString、VisibleStirng、ISO64String、IA5String、TeletexStirng、T61String、VideotexString、GraphicString、GeneralString、UniversalString、BMPString和UTF8String

    CHARACTER STRING

    允许为字符串协商一个明确的字符表

    UTCTime, GeneralizedTime

    日期

     

    Table 2-2 组合类型

    类型

    含义

    CHOICE

    在类型中选择(类似C中的联合)

    SEQUENCE

    由不同类型的值组成一个有序的结构

    SET

    由不同类型的值组成一个无序的结构

    SEQUENCE OF

    由相同类型的值组成一个有序的结构

    SET OF

    由相同类型的值组成一个无序的结构

    2.3.2  类型定义

    <新类型的名字> ::= <类型描述>

     

    其中:

    <新类型的名字>是一个以大写字母开头的标识符;

    <类型描述>是基于内建类型或在其它地方定义的类型。

    如:

             Married ::= BOOLEAN

             Age ::= INTEGER

             Picture ::= BIT STRING

             Form ::= SEQUENCE

    {

                           name                           PrintableString,

                           age                              Age,

                           married                        Married,

                           marriage-certificate Picture OPTIONAL

    }

     

             Payment-method ::= CHOICE

    {

                           check                          Check-number,

                           credit-card                    SEQUENCE

    {

                                  number                 Card-number,

                                  expiry-date            Date

    }

    }

    注意:在SEQUENCE和SET等定义中,最后一个成员结尾没有逗号“,”。

     

    为了接收方能正确解码,发送方为每个值的类型附加一个数,称为tag,在描述中以“[]”标识。缺省情况下,编码器会使用universal的tag。很多时候,缺省情况不能消除所有的模糊性,有必要明确指出各成员的tag。

    如:

             Coordinates ::= SET

             {

                           x                                 [1] INTEGER,

                           y                                 [2] INTEGER,

                           z                                 [3] INTEGER OPTIONAL

             }

     

     

             Afters ::= CHOICE

             {

                           cheese                         [0] PrintableString,

                           dessert                        [1] PrintableString

             }

     

    注意:ASN.1允许递归式的类型分配(Assignment),但我们应当保证其中包含至少一个非递归的值,因为编码规则无法处理无限的值。当然,绝大多数结构类型的成员都终结于简单类型。

     

    时不时地,为了准确描述一个类型,我们需要对值的集合进行一定的限制。这用到子类型约束,在类型之后用圆括号进行标识。

    如:

             Lottery-number ::= INTERGER(1..49)

             Lottery-draw ::= SEQUENCE SIZE(6) OF Lottery-number

             Upper-case-words ::= IA5String (FROM(“A”..”Z”))

             Phone-number ::= NumericString (FROM(“0”..”9”))(SIZE(10))

             Coordinates-in-plan ::= Coordinates (WITH COMPONENTS {…, z ABSENT})

    有约束的类型当然还是类型,可以用在任何一个可以使用类型的地方。

     

    最后,因为版本升级,在新的描述中,出现新的成员被加入到SEQUENCE、SET或者CHOICE或者在上述类型基础上添加约束而衍生的子类型时,两个连接的机器(特别是在开放网络中)不一定使用的是相同版本描述而生成的编解码器。为了防止一方因收到过多或者过少数据而出现错误,ASN.1中用符合“…”来标记可能以后是其它类型的地方。这样即使是旧的编解码器就不会因为描述扩充而导致编解码错误。

    如:

             Type ::= SEQUENCE

             {

                           component1                 INTERGER,

                           component2                 BOOLEAN,    -- version 1

                           …

             }

     

    以后新的版本中,描述可能为:

             Type ::= SEQUENCE

             {

                           component1                 INTERGER,

                           component2                 BOOLEAN,

                           …,

                           [[component3               REAL]],          -- version 2

                           …

             }

    注意:新加入的类型成员要嵌套在“[[]]”中。

     

    2.4  值定义

    <新的值的名字> <该值的类型> ::= <值描述>

     

    其中:

    <新的值的名字>是以小写字母开头的标识符;

    <该值的类型>可以是一个类型的名字,也可以是类型描述;

    值描述>是基于整数、字符串、标识符的组合。

     

    如:

    counter Lottery-number ::= 45

    sextuple Lottery-draw ::= { 7, 12, 23, 31, 33, 41 }

    pair Coordinates ::= { x 5, y -3 }

    son-choice Afters ::= dessert:"profiterolles"

    date Date ::= "20000824"

    ASN.1的应用中,绝大多数的值都是动态获得的,但是指定某些值特别是在约束型子类型中,提高描述的可读性。

     

    如:

    upper-bound INTEGER ::= 12

    Interval ::= INTEGER (0..upper-bound)

    default-value Interval ::= 0

    Pair ::= SEQUENCE

    {

    first                      [0] Interval DEFAULT default-value,

    second         [1] Interval DEFAULT default-value

    }

    2.5  信息对象类和信息对象

    <信息对象类> ::= CLASS <类描述>

    WITH SYNTAX <信息描述>

     

    有时我们需要表达比注释更为正式的一些信息,例如我们要说明这样一个事实:一个结构类型的成员依赖于和这个结构类型相关的值。

     

    如:

    OPERATION ::= CLASS

    {

    &number INTEGER UNIQUE,

    &Argument-type,

    &Return-result-type

    }

    WITH SYNTAX

    {

    OPERATION NUMBER &number

    TAKES AN ARGUMENT OF TYPE &Argument-type

    AND RETURNS A VALUE OF TYPE &Return-result-type

    }

    ASN.1中我们没有指出操作的具体内容,但是我们给出了一个机器要求另一个机器执行这个操作所需要交换的数据结构。

    l           域&number以小写字母开头,后面跟了类型INTEGER。这个整数值用作信息的标识符,因为后面加了UNIQUE关键字。

    l           域&Argument-type和&Return-result-type以大写字母开头,后面什么都没有跟。他们是一些ASN.1类型的引用(这可能会因为类型没有定义而不能编码)。

    l           WITH SYNTAX块定义了一个用户友好的语法,说明了这个类的成员。

     

    上述信息对象类的一个对象定义如下:

    plan-projection OPERATION ::=

    {

    OPERATION NUMBER     12

    TAKES AN ARGUMENT OF TYPE Coordinates

    AND RETURNS A VALUE OF TYPE Coordinates-in-plan

    }

    因为两个通讯中的机器都使用相同的描述,因此相同的信息对象就能在这两个机器间共享。

     

    一个给定类的所有对象,可以在描述中收集到一个对象集合中,如下:

    SupportedOperations OPERATION ::=

    {

    plan-projection | translation | symmetry, ...

    }

    注意:和对象不同,对象集合以大写字母开头。字符“…”表明,新的对象可以有通讯中的机器动态加入。

     

    信息对象永远不会被编码,传递他们各项信息的唯一方法是在一个类型中引用他们。我们用上面这个信息对象集合来约束一个结构类型:

    Execute-operation ::= SEQUENCE

    {

    code OPERATION.&number({SupportedOperations}),

    argument OPERATION.&Argument-type

    ({SupportedOperations}{@code})

    }

     

    Receive-result ::= SEQUENCE

    {

    code OPERATION.&number({SupportedOperations}),

    result OPERATION.&Return-result-type

    ({SupportedOperations}{@code})

    }

    这段晦涩的描述,声明了这样一种情况:

    l           Execute-operation操作它的编号在code成员中,当其argument发送给对方是,一定要满足域&Argument-type的类型。链接(或者表约束)“@code”说明这个参数是依赖于操作的。

    l           对方返回的Receive-result也有类似约束。

    2.6  模块定义

    <模块名字>  DEFINITIONS <缺省Tag> ::=

    BEGIN

    EXPORTS <导出描述>

    IMPORTS <导入描述>

    <模块体描述>

    END

     

    一般协议由一个或者多个模块组成,模块用来收集数据结构定义。

    模块名字必须以大写字母开头。模块可以一种“全局指针”(Universal Pointer)的方式来引用,称为对象标识符(Object Identifier),用花括号标识在名字之后。

     

    如:

    Module2 { iso member-body(2) f(250) type-org(1) ft(16)

    asn1-book(9) chapter5(0) module2(1) }

    DEFINITIONS AUTOMATIC TAGS ::=

    BEGIN

    EXPORTS Type2;

    IMPORTS Type1, value FROM Module1 {iso member-body(2)

    f(250) type-org(1) ft(16) asn1-book(9)

    chapter5(0) module1(0)};

    Type2 ::= SEQUENCE OF Choice

    Choice ::= CHOICE

    {

    a           INTEGER (0..value),

    b           Type1

    }

    END

     

    l           AUTOMATIC TAGS或者缺省,则说明不关注模块的tag交给编译器自己自动处理;如果需指明具体的tag值,则需要用“[]”来标识。此外tag的模式还可以是EXPLICIT TAGS和IMPLICIT TAGS。还可以使用EXTENSIBILITY IMPLIED来使模块中所有SEQUENCE、SET、CHOICE和ENUMERATED类型可扩展。

    l           条目IMPORTS和EXPORTS(不是必须的)定义模块的接口。其中IMPORTS声明在其它模块定义但在本模块会用到的类型或者值;EXPORT声明在本模块之外可以访问的类型或者值。

    注意IMPORTS的语法为:

    IMPORTS <名字>, value FROM < 其它模块的Object Identifier >;

    EXPORTS的语法为:

    EXPORTS <名字>;

    l           使用IMPORTS时,没有类似IMPORTS ALL的语句能将一个模块中所有定义都导入,因此需要协议描述者逐条写出每个需要导入的符号。对于参数化的分配,需要使用花括号,如:

    ModuleName DEFINITIONS ::=

    BEGIN

    IMPORTS T{} FROM Module1;

    U ::= T{INTEGER}

    END

    l           在BEGIN之后,IMPORTS之前的语句,则不使用导入的符号。

     

    2.7  模块与分配(Assignment)

    2.7.1  分配Assignment

    有六种不同形式的数据与分配相关:类型、值、值的集合、信息对象类、信息对象和信息对象集合。

    I. 定义一个新类型

    如前所述,定义新类型,如:

                    TypeReference ::= CHOICE

                    {

                                  integer                  INTEGER,

                                  boolean                 BOOLEAN

                    }

    在协议最高层定义的类型中,不被其它类型引用的那种类型,我们称为PDU(Protocol Data Unit),PDU是应用件交换的数据。

    II. 定义一个值(绝对值)

    如前所述,这种值不被编码,通常是用作DEFAULT,上下界或者信息对象中的。如:

                    value-reference TypeReference ::= integer:12

    如果两个类型在语法上是完全一样的,则这两种类型的值可以相互赋值。如:

                    Pair ::= SEQUENCE

                    {

                                  x                          INTEGER,

                                  y                          INTEGER

                    }

     

                    Couple ::= SEQUENCE

                    {

                                  x                          INTEGER,

                                  y                          INTEGER

                    }

     

                    pair Pair ::= {x 5, y 13}

                    couple Couple ::= pair

                   

     

    Lighter-state ::= ENUMERATED

    {

    on(0),

    off(1),

    out-of-order(2)

    }

     

    Kettle-state ::= ENUMERATED

    {

    on(0),

    off(1),

    out-of-order(2)

    }

     

    lighter Lighter-state ::= on

    kettle Kettle-state ::= lighter

    在最后一个例子中,这种类型和值之间的对应,是由称为“ASN.1语义模型”()来负责的。

    III. 定义值集合

    在语义上一个值集合相当于一个添加约束后的类型。 如:

                    PrimeNumbers INTEGER ::= {2 | 3 | 5 | 7 | 11 | 13}

     

    总结类型定义、值定义和值集合定义,我们可以得出这样的结论:

    Table 2-3 分配语义定义

    第一个词首字母

    第二个子首字母

    ::=

    分配内容

    大写

    /

    ::=

    类型或者信息对象类

    小写

    大写

    ::=

    值或者信息对象

    大写

    大写

    ::=

    值集合或者信息对象集合

     

    2.7.2  本地和外部引用

    除本地应用外,ASN.1还支持外部应用,但不推荐使用。如:

    MyType ::= SET OF OtherModule.Type

    最好的方式依然是使用IMPORTS。

  • 相关阅读:
    高阶函数
    Vue-cli 3.0 搭建,以及vuex、axios、使用
    Git --- 基本操作以及Git 特殊命令,公司常用命令
    Git 剖析,以及Git相关操作
    git ssh key 生成
    React.Fragment 组件没有必要的多层嵌套,外层不需要过多嵌套
    spring cloud连载第三篇之Spring Cloud Netflix
    spring cloud连载第二篇之Spring Cloud Config
    AbstractQueuedSynchronizer
    Timer定时器
  • 原文地址:https://www.cnblogs.com/cqx6388/p/14098242.html
Copyright © 2020-2023  润新知