• oran code go through


    CM xAPP discussion

    荣涛 ASN.1编解码
     

    Ransim go through

     

    # connection process with E2T

    cmd/ransim/ransim.go -> main()

        pkg/manager/manager.go -> Run() -> Start() -> startE2Agents()

            pkg/e2agent/agents/agents.go -> Start()

                pkg/e2agent/agent.go -> Start()

                    pkg/controller/connection/controller.go -> NewController()

                        onos-lib-go/pkg/controller/watcher.go -> Start()

                    pkg/e2agent/connection/connection.go -> NewE2Connection()

                    pkg/e2agent/connection/connection.go -> Setup()

                    pkg/e2agent/connection/connection.go -> connectAndSetup()  # connect to the E2T controller

                    pkg/e2agent/connection/connection.go -> connect()  # connect to the E2T controller by STCP Connection

                    pkg/e2agent/connection/connection.go -> setup()  # negotiate E2 setup procedure

                        pkg/utils/e2ap/setup/setup.go -> NewSetupRequest()

                        pkg/utils/e2ap/setup/setup.go -> Build()

                            api/e2ap/v2/e2ap-pdu-contents/e2ap_pdu_contents.pb.go -> E2SetupRequest struct

    # RIC address 

    cmd/ransim/ransim.go -> main() -> modelName -> cfg

        pkg/manager/manager.go -> NewManager()

        pkg/manager/manager.go -> Run() -> Start() -> model.Load()

            pkg/model/load.go -> Load() -> LoadConfig()

        pkg/manager/manager.go -> Run() -> Start() -> startE2Agents()

            pkg/e2agent/agents/agents.go -> NewE2Agents()

                pkg/e2agent/agent.go -> NewE2Agent()

                    pkg/model/model.go -> GetController().Address

    #overview of ransim

    pkg/model/test.yaml show the overview define of ransim
     

    # STCP Connection

    onos-e2t/pkg/protocols/e2ap/client.go -> Connect()
        onos-lib-go/pkg/sctp/sctp.go -> DialSCTP()
    onos-e2t/pkg/protocols/e2ap/client_conn.go -> ClientConn interface
    onos-e2t/pkg/protocols/e2ap/client.go -> ClientInterface procedures.E2NodeProcedures
     

    # Sevcie ModelRegistry (HMO example)

    cmd/ransim/ransim.go -> main() -> modelName -> cfg 

        pkg/manager/manager.go -> NewManager() -> RegisterModelPlugin()

        pkg/modelplugins/registry.go -> RegisterModelPlugin()

        pkg/manager/manager.go -> Run() -> Start() -> model.Load()

            pkg/e2agent/agents/agents.go -> NewE2Agents()

                pkg/e2agent/agent.go -> NewE2Agent()

                    pkg/servicemodel/registry/registry.go -> NewServiceModelRegistry()

                    pkg/servicemodel/mho/mho.go -> NewServiceModel()

                        onos-e2-sm/servicemodels/e2sm_mho_go/servicemodel/servicemodel.go -> RanFuncDescriptionProtoToASN1()

                    pkg/servicemodel/registry/registry.go -> RegisterServiceModel()

                pkg/e2agent/agent.go -> Start()

                    pkg/e2agent/connection/connection.go -> NewE2Connection()

    # Sevcie Model execute  (RICSubscription)

    pkg/e2agent/connection/connection.go -> RICSubscription()

    # startNorthboundServer

    cmd/ransim/ransim.go -> main()

        pkg/manager/manager.go -> Run() -> Start() -> startNorthboundServer()  # Start gRPC server

     

    # E2ConnectionUpdate

     

    # processNodeEvents

     

     

     
     

    ASN.1

     

    1. https://github.com/nokia/asn1c
    2. https://www.cxyzjd.com/article/peng_yw/22437251
    3. https://webcache.googleusercontent.com/search?q=cache:WJRr26A-Af8J:https://www.iitter.com/other/43889.html+&cd=6&hl=zh-CN&ct=clnk&gl=us

    1. asn1c-s1ap

    这个软件提供了三个应用程序,asn1c,unber,enber,他们和应用程序之间的关系为:

    https://img-blog.csdnimg.cn/03ba7d85312a4715924c7dc9ece40625.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1JvbmdfVG9h,size_16,color_FFFFFF,t_70

    2. E2AP-C语言

    本章主要讲下面的内容:

    1. 如何使用 asn1c 工具将 ASN.1 编码编译成C语言?
    2. 如何开发 ASN.1 代码?【根据实际情况而定】
    3. 如何生成 ASN.1 二进制流?【不懂不会】

    2.1. 如何使用 asn1c 工具将 ASN.1 编码编译成C语言?

    这个步骤是繁琐的,为了尽可能清晰,我将写个脚本,简化操作流程,同时,我也将用 CMake 简化编译流程。

    2.1.1. 准备 ASN.1 文件

    这里我直接使用下面的两个文件,

    • e2ap-v01.00.00.asn:E2 Termination中提供;
    • e2ap-v01.01.asn1:我从 O-Ran 文档中提取出的 ASN.1;

    这两个文件内容较多,不贴出全部,只给出以小部分内容,以e2ap-v01.00.00.asn为例,文件中定义了一些枚举和结构体,以其中的InitiatingMessage为例:

    InitiatingMessage ::= SEQUENCE {​​​​​
    	procedureCode	E2AP-ELEMENTARY-PROCEDURE.&procedureCode		({​​​​​​​​​​​​E2AP-ELEMENTARY-PROCEDURES}​​​​​​​​​​​​​​​​​​​),
    	criticality		E2AP-ELEMENTARY-PROCEDURE.&criticality			({​​​​​​​​​​​​​​​​​​​E2AP-ELEMENTARY-PROCEDURES}​​​​​​​​​​​​​​​​​​​{​​​​​​​​​​​​​​​​​​​@procedureCode}​​​​​​​​​​​​​​​​​​​),
    	value			E2AP-ELEMENTARY-PROCEDURE.&InitiatingMessage	({​​​​​​​​​​​​​​​​​​​E2AP-ELEMENTARY-PROCEDURES}​​​​​​​​​​​​​​​​​​​{​​​​​​​​​​​​​​​​​​​@procedureCode}​​​​​​​​​​​​​​​​​​​)
    }​​​​​​​​​​​​​​​​​​​
    

    使用 asn1c 编译InitiatingMessage后将生成两个文件 InitiatingMessage.c和 InitiatingMessage.h,看一下结构体

    /* InitiatingMessage */
    typedef struct InitiatingMessage {​​​​​​​​​​​​​​​​​​​
    	ProcedureCode_t	 procedureCode;
    	Criticality_t	 criticality;
    	struct InitiatingMessage__value {​​​​​​​​​​​​​​​​​​​
    		InitiatingMessage__value_PR present;
    		union InitiatingMessage__value_u {​​​​​​​​​​​​​​​​​​​
    			RICsubscriptionRequest_t	 RICsubscriptionRequest;
    			RICsubscriptionDeleteRequest_t	 RICsubscriptionDeleteRequest;
    			RICserviceUpdate_t	 RICserviceUpdate;
    			RICcontrolRequest_t	 RICcontrolRequest;
    			E2setupRequest_t	 E2setupRequest;
    			ResetRequest_t	 ResetRequest;
    			RICindication_t	 RICindication;
    			RICserviceQuery_t	 RICserviceQuery;
    			ErrorIndication_t	 ErrorIndication;
    		}​​​​​​​​​​​​​​​​​​​ choice;
    		
    		/* Context for parsing across buffer boundaries */
    		asn_struct_ctx_t _asn_ctx;
    	}​​​​​​​​​​​​​​​​​​​ value;
    	
    	/* Context for parsing across buffer boundaries */
    	asn_struct_ctx_t _asn_ctx;
    }​​​​​​​​​​​​​​​​​​​ InitiatingMessage_t;
    

    再过复杂的问题此处不再讲解,因为我也不懂。

    2.1.2. 用ASN.1 文件生成C语言

    大家想关注的是如何将 ASN.1 代码编译成 C语言文件的,直接上代码吧:

    asn1c -fcompound-names \
    		-fincludes-quoted \
    		-fno-include-deps \
    		-findirect-choice \
    		-gen-PER -D. \
    		e2ap-v01.00.00.asn
    

    上面的参数,我也不适很懂,就这么用吧,需要注意的是,在执行上面的指令后,会生成很多的源文件,我们先来关注Makefile.am.asn1convert 和 Makefile.am.libasncodecMakefile.am.asn1convert和上面的指令的功能基本相同,这里不做解释,直接看下里面的内容:

    # [rongtao@localhost e2ap]$ more Makefile.am.asn1convert 
    include ./Makefile.am.libasncodec
    
    bin_PROGRAMS += asn1convert
    asn1convert_CFLAGS = $(ASN_MODULE_CFLAGS) -DASN_PDU_COLLECTION 
    asn1convert_CPPFLAGS = -I$(top_srcdir)/./
    asn1convert_LDADD = libasncodec.la
    asn1convert_SOURCES = \
    	./converter-example.c\
    	./pdu_collection.c
    regen: regenerate-from-asn1-source
    
    regenerate-from-asn1-source:
    	asn1c -fcompound-names -fincludes-quoted -fno-include-deps -findirect-choice -gen-PER -D. e2ap-v01.00.00.asn
    

    在上面的makefile文件中可以看到文件Makefile.am.libasncodec,这个文件中定义了ASN_MODULE_SRCSASN_MODULE_HDRSASN_MODULE_CFLAGS以及下面的变量:

    lib_LTLIBRARIES+=libasncodec.la
    libasncodec_la_SOURCES=$(ASN_MODULE_SRCS) $(ASN_MODULE_HDRS)
    libasncodec_la_CPPFLAGS=-I$(top_srcdir)/./
    libasncodec_la_CFLAGS=$(ASN_MODULE_CFLAGS)
    libasncodec_la_LDFLAGS=-lm
    

    后续,我们如果再生成C语言,即可使用下面的命令:

    make -f Makefile.am.asn1convert regen
    

    上面的指令也是我再写这个文档的时候才发现的。

    2.1.3. 生成的C语言源文件的编译

    编译是个难题,我们可以先看一下生成的测试例文件converter-example.c,该源文件中有个宏定义,该宏定义指定,当前文件要测试哪个数据结构,宏定义的使用如下:

    /* Convert "Type" defined by -DPDU into "asn_DEF_Type" */
    #ifdef PDU
    #define    ASN_DEF_PDU(t)    asn_DEF_ ## t
    #define    DEF_PDU_Type(t)    ASN_DEF_PDU(t)
    #define    PDU_Type    DEF_PDU_Type(PDU)
    extern asn_TYPE_descriptor_t PDU_Type;    /* ASN.1 type to be decoded */
    #define PDU_Type_Ptr (&PDU_Type)
    #else   /* !PDU */
    #define PDU_Type_Ptr    NULL
    #endif  /* PDU */
    

    我以RICcontrolRequest举例,该结构在e2ap-v01.00.00.asn中的定义为:

    RICcontrolRequest ::= SEQUENCE {​​​​​​​​​​​​​​​​​​​
    	protocolIEs					ProtocolIE-Container	{​​​​​​​​​​​​​​​​​​​{​​​​​​​​​​​​​​​​​​​RICcontrolRequest-IEs}​​​​​​​​​​​​​​​​​​​}​​​​​​​​​​​​​​​​​​​,
    	...
    }​​​​​​​​​​​​​​​​​​​
    

    在生成的头文件RICcontrolRequest.h中定义了这个数据结构

    /* RICcontrolRequest */
    typedef struct RICcontrolRequest {​​​​​​​​​​​​​​​​​​​
    	ProtocolIE_Container_1544P7_t	 protocolIEs;
    	/*
    	 * This type is extensible,
    	 * possible extensions are below.
    	 */
    	/* Context for parsing across buffer boundaries */
    	asn_struct_ctx_t _asn_ctx;
    }​​​​​​​​​​​​​​​​​​​ RICcontrolRequest_t;
    

    同时,该文件下方有一些声明

    /* Implementation */
    extern asn_TYPE_descriptor_t asn_DEF_RICcontrolRequest;
    extern asn_SEQUENCE_specifics_t asn_SPC_RICcontrolRequest_specs_1;
    extern asn_TYPE_member_t asn_MBR_RICcontrolRequest_1[1];
    

    其中asn_DEF_RICcontrolRequest即为PDU定义为RICcontrolRequest的宏定义ASN_DEF_PDU展开值,

    #define    ASN_DEF_PDU(t)    asn_DEF_ ## t
    #define    DEF_PDU_Type(t)    ASN_DEF_PDU(t)
    #define    PDU_Type    DEF_PDU_Type(PDU)
    

    所以,我在 Makefile /CMakeLists.txt 中添加宏定义 -DPDU=RICcontrolRequest,接着进行正常的编译即可,我使用的 CMakeLists.txt,文件内容如下:

    ###################################################
    # 编译使用 asn1c 编译 ASN.1 文件而生成的 C语言 程序
    #
    # 作者:荣涛 
    # 时间:2021年8月
    ###################################################
    
    CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
    
    PROJECT(e2ap)
    
    aux_source_directory(./ DIR_SRCS)
    
    include_directories(./)
    
    find_library(CONFIG config /usr/lib64)
    link_libraries(${​​​​​​​​​​​​​​​​​​​CONFIG}​​​​​​​​​​​​​​​​​​​)
    
    add_definitions( -MD -Wall -DPDU=RICcontrolRequest)
    
    ADD_EXECUTABLE(test ${​​​​​​​​​​​​​​​​​​​DIR_SRCS}​​​​​​​​​​​​​​​​​​​)
    

    接下来进行编译即可:

    [rongtao@localhost e2ap]$ cd build/
    [rongtao@localhost build]$ cmake ..
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /home/rongtao/test/ASN.1/asn1c/jt_sran/e2ap/build
    [rongtao@localhost build]$ make 
    [  0%] Building C object CMakeFiles/test.dir/ANY.c.o
    [  1%] Building C object CMakeFiles/test.dir/BIT_STRING.c.o
    [  2%] Building C object CMakeFiles/test.dir/BIT_STRING_oer.c.o
    [此处省略一万行。。。]
    [ 98%] Building C object CMakeFiles/test.dir/xer_encoder.c.o
    [ 99%] Building C object CMakeFiles/test.dir/xer_support.c.o
    [100%] Linking C executable test
    [100%] Built target test
    [rongtao@localhost build]$ 
    

    看一眼当前目录中,生成了可执行文件test,至此,我已经讲完了由 asn1c 生成的C语言文件的编译过程。置于如何进行测试,下一章再说。这里,我把上面的步骤谢了一个脚本,可以用,也可以不用。

    #!/bin/bash
    # 
    # 将 O-RAN E2AP ASN.1 转化为 C语言
    # 理论上,这个脚本并不限于 E2AP,其他 由 Nokia 发布的
    # O-RAN 文档 中的 ASN.1 均可由 此脚本进行C语言的生成,
    # 
    # 荣涛 rongtao@sylincom.com
    # 2021年8月
    # 
    
    # 默认的 ASN.1 文件
    file_asn1=""
    
    DEFAULT_GEN_DEMO="converter-example.c"
    
    function INFO() {​​​​​​​​​​​​​​​​​​​
    	echo -e "\033[1;34m $1 \033[0m"
    }​​​​​​​​​​​​​​​​​​​
    function ERROR() {​​​​​​​​​​​​​​​​​​​
    	echo -e "\033[1;31m $1 \033[0m"
    }​​​​​​​​​​​​​​​​​​​
    
    
    # 帮助信息
    function usage() {​​​​​​​​​​​​​​​​​​​
    	echo ""
    	echo Usage: ./genc.sh [ASN.1 file]
    	echo ""
    	echo "	[ASN.1 file] is ASN.1 file from some where that i dont know."
    	echo ""
    	echo "	You must install asn1c-s1ap, download in https://github.com/nokia/asn1c, version is v0.9.29"
    	echo "	asn1c's version must be v0.9.29"
    	echo ""
    }​​​​​​​​​​​​​​​​​​​
    
    # 检查软件是否安装,版本是否对应
    function check_asn1c() {​​​​​​​​​​​​​​​​​​​
    	which asn1c > /dev/null
    	if [ $? != 0 ]; then
    		ERROR "FATAL: asn1c not install"
    		exit 1
    	fi
    
    	# 必须使用 Nokia 的 https://github.com/nokia/asn1c ,也就是 asn1c-s1ap
    	if [ $(asn1c -v 2>&1 | grep ASN | awk '{​​​​​​​​​​​​​​​​​​​print $3}​​​​​​​​​​​​​​​​​​​') != "v0.9.29" ]; then
    		ERROR "FATAL: wrong asn1c version, must v0.9.29(https://github.com/nokia/asn1c)"
    		exit 1
    	fi
    
    }​​​​​​​​​​​​​​​​​​​
    
    # 检查 入参,以及 ASN.1 文件是否存在
    function check_asn1_file() {​​​​​​​​​​​​​​​​​​​
    	if [ $# -lt 1 ]; then
    		usage
    		exit 1
    	fi
    	file_asn1=$1
    	if [ ! -f $file_asn1 ]; then
    		ERROR "FATAL: file \"$file_asn1\" not exist."
    		exit 1
    	fi
    }​​​​​​​​​​​​​​​​​​​
    
    # 使用 asn1c 编译
    function compile_asn1_file() {​​​​​​​​​​​​​​​​​​​
    
    	# 编译成 C语言
    	asn1c -fcompound-names \
    			-fincludes-quoted \
    			-fno-include-deps \
    			-findirect-choice \
    			-gen-PER -D. \
    			$file_asn1
    }​​​​​​​​​​​​​​​​​​​
    
    # 修改 生成的 C语言测试文件
    function modify_test_demo() {​​​​​​​​​​​​​​​​​​​
    	if [ ! -f $DEFAULT_GEN_DEMO ]; then
    		WARNING "WARNING: file \"$DEFAULT_GEN_DEMO\" not exist."
    		return 1
    	fi
    	echo "#include <stdio.h>" > $DEFAULT_GEN_DEMO
    	echo "" >> $DEFAULT_GEN_DEMO
    	echo "int main(int argc, char *argv[])" >> $DEFAULT_GEN_DEMO
    	echo "{​​​​​​​​​​​​​​​​​​​" >> $DEFAULT_GEN_DEMO
    	echo "    printf(\"ASN.1 test running.\n\");" >> $DEFAULT_GEN_DEMO
    	echo "    return 0;" >> $DEFAULT_GEN_DEMO
    	echo "}​​​​​​​​​​​​​​​​​​​" >> $DEFAULT_GEN_DEMO
    }​​​​​​​​​​​​​​​​​​​
    
    
    # 检查软件是否安装,版本是否对应
    INFO "Check asn1c software"
    check_asn1c
    INFO "Check asn1c software, OK"
    
    # 检查 输入的文件
    INFO "Check ASN.1 file"
    check_asn1_file $*
    INFO "Check ASN.1 file, OK"
    
    # 使用 asn1c 编译
    INFO "Compile ASN.1 file"
    compile_asn1_file $file_asn1
    INFO "Compile ASN.1 file, DONE"
    
    
    # 修改 自动生成的 测试代码
    #INFO "Modify C file"
    #modify_test_demo
    #INFO "Modify C file, DONE"
    
    INFO ""
    INFO "Now, you can do some thing like:"
    INFO "$ mkdir build"
    INFO "$ cd build"
    INFO "$ cmake .."
    INFO "$ make"
    INFO "$ ./test"
    

    2.1.4. 分析生成的可执行文件

    1. 先看看能执行吗?
    [rongtao@localhost e2ap]$ ./test 
    ./test: No input files specified. Try '-h' for more information
    [rongtao@localhost e2ap]$ ./test -h
    Usage: ./test [options] <datafile> ...
    Where options are:
      -iber        Input is in BER (Basic Encoding Rules) or DER
      -ioer        Input is in OER (Octet Encoding Rules)
      -iper        Input is in Unaligned PER (Packed Encoding Rules) (DEFAULT)
      -iaper        Input is in Aligned PER (Packed Encoding Rules)
      -ixer        Input is in XER (XML Encoding Rules)
      -oder        Output as DER (Distinguished Encoding Rules)
      -ooer        Output as Canonical OER (Octet Encoding Rules)
      -oper        Output as Unaligned PER (Packed Encoding Rules)
      -oaper       Output as Aligned PER (Packed Encoding Rules)
      -oxer        Output as XER (XML Encoding Rules) (DEFAULT)
      -otext       Output as plain semi-structured text
      -onull       Verify (decode) input, but do not output
      -per-nopad   Assume PER PDUs are not padded (-iper)
      -1           Decode only the first PDU in file
      -b <size>    Set the i/o buffer size (default is 8192)
      -c           Check ASN.1 constraints after decoding
      -d           Enable debugging (-dd is even better)
      -n <num>     Process files <num> times
      -s <size>    Set the stack usage limit (default is 30000)
    
    1. 再看看有没有额外的依赖

    我最关心的是,生成的可执行文件有没有额外的依赖关系,虽然我没有在 CMakeLists.txt中加任何的动态库链接,但是为了验证,还是看看:

    [rongtao@localhost e2ap]$ ldd test 
    	linux-vdso.so.1 =>  (0x00007ffdcf9df000)
    	libconfig.so.9 => /usr/lib64/libconfig.so.9 (0x00007f2e2a8bf000)
    	libc.so.6 => /usr/lib64/libc.so.6 (0x00007f2e2a4f1000)
    	/lib64/ld-linux-x86-64.so.2 (0x00007f2e2aacb000)
    

    GOOD,没有任何其他的依赖,我很开心。

    1. 使用nm查看符号表
    [rongtao@localhost e2ap]$ nm test
    000000000044bd2b t add_bytes_to_buffer
    000000000040b714 t ANY__consume_bytes
    000000000040be42 T ANY_decode_aper
    000000000040b82b T ANY_decode_uper
    000000000040c0fa T ANY_encode_aper
    【此处省略一万行。。。】
    0000000000452af6 t xer__print2fp
    00000000004526e8 T xer_skip_unknown
    0000000000451d7d t xer__token_cb
    000000000045267a T xer_whitespace_span
    0000000000681cd0 b zeros.3886
    0000000000681cc0 b zeros.4008
    

    2.1.5. 运行生成的可执行文件

    上面已经给出了该测试文件的帮助信息,但是这里再给出一遍吧

    [rongtao@localhost e2ap]$ ./test -h
    Usage: ./test [options] <datafile> ...
    Where options are:
      -iber        Input is in BER (Basic Encoding Rules) or DER
      -ioer        Input is in OER (Octet Encoding Rules)
      -iper        Input is in Unaligned PER (Packed Encoding Rules) (DEFAULT)
      -iaper        Input is in Aligned PER (Packed Encoding Rules)
      -ixer        Input is in XER (XML Encoding Rules)
      -oder        Output as DER (Distinguished Encoding Rules)
      -ooer        Output as Canonical OER (Octet Encoding Rules)
      -oper        Output as Unaligned PER (Packed Encoding Rules)
      -oaper       Output as Aligned PER (Packed Encoding Rules)
      -oxer        Output as XER (XML Encoding Rules) (DEFAULT)
      -otext       Output as plain semi-structured text
      -onull       Verify (decode) input, but do not output
      -per-nopad   Assume PER PDUs are not padded (-iper)
      -1           Decode only the first PDU in file
      -b <size>    Set the i/o buffer size (default is 8192)
      -c           Check ASN.1 constraints after decoding
      -d           Enable debugging (-dd is even better)
      -n <num>     Process files <num> times
      -s <size>    Set the stack usage limit (default is 30000)
    

    这个可执行文件是比较复杂的,具体怎么使用,后续文档中再做解释,本文先到这。

    REF:  

    [4G&5G专题-60]:L3 RRC层 - 定义数据类型与数据结构的超级神器:ASN.1抽象语法标记 

    ASN.1 -- 使用asn1c完成ASN encode/decode 

    ASN.1 C++编译器使用入门  

    Ubuntu ASN1C实例分析  

  • 相关阅读:
    SpringBoot+Vue实现指定账号审批单据时前端进行语音播报
    Jquery中使用JsonP加载本地json文件解决跨域问题
    Android中WebView加载sdcard中的html显示
    Android中WebView加载sdcard中的html时提示:ERR_FILE_NOT_FOUND和ERR_ACCESS_DENIED
    【20220302】育己人
    保序回归问题学习笔记
    可持久化数据结构学习笔记
    WC2022 打不过去年记
    Solution Set 「LOCAL」冲刺省选 Round XI
    Solution Set 「LOCAL」冲刺省选 Round XII
  • 原文地址:https://www.cnblogs.com/shaohef/p/16175874.html
Copyright © 2020-2023  润新知