zoukankan      html  css  js  c++  java
  • 基于Jmeter和Testlink的自动化测试框架研究与实施

    关于测试框架搭建的详细过程,会在另一篇文章中详细介绍:http://www.cnblogs.com/leeboke/p/6145977.html

    摘 要

    目前基于Jmeter的接口自动化测试框架,大多只实现脚本维护和自动调度,无法与Testlink进行互通,实现测试方案与自动化实施流程连接,本文基于Testlink、Jmeter、Jenkins实现:通过Testlink统一维护接口自动化测试用例,Jmeter编写和运行测试脚本,Jenkins实现统一调度,并返回执行结果和测试报告到Testlink。从而实现整个接口自动化测试框架,提高测试效率,降低后续维护成本。

    关键词: 自动化测试 Jmeter 接口 Testlink Jenkins

    1、引言

    为了实现项目快速开发-测试-交付,测试需要在项目开发过程介入,进行接口级别测试,并输出自动化测试脚本,便于后续集成测试使用自动化测试保证每次BUG修复,没有引发新的问题,故需要研究一种自动化测试框架,满足目前项目测试的需求

    2、目标

    需求梳理如下:

    1、     TestLink统一管理测试用例:因功能测试用例一直在Testlink上维护。

    2、     接口测试工具满足需求的同时,易上手掌握

    3、     测试脚本统一维护,如:SVN

    4、     脚本运行时,需要结合Testlink测试计划分配用例,将自动化运行结果返回到Testlink。

    5、     运行失败的接口,要自动提交到BUG系统

    6、      测试结束后,生成测试报告,统计自动化测试总体情况,并邮件发送给项目相关人员。

    7、      测试服务器的IP和端口变化后,在调度测试时统一指定测试IP和端口,避免测试脚本的重复修改

    8、      测试框架可扩展到其他项目。

    基于以上需求,梳理大致的目标图如下:

    期望框架达到的效果:

    若测试用例已经导入TestLink,且脚本已经设计完成并上传到SVN,则在web管理平台构建项目后,即可自动完成所有测试,并且项目可支持定时循环测试。

    测试结束后,可自动提交BUG到BUG系统,测试人员只需对最后的结果进行确认即可。

    图中中间部分:云计算管理平台接口自动化测试平台,则是本文应该要实现并达到的效果。

     

    3、整体框架

    以上是整体框架的思路介绍:

    • TestLink负责接口自动化用例维护
    • Jmeter则负责脚本编写和运行,统一通过SVN进行维护
    • 另外邮件收发服务器则根据内部情况灵活选择
    • 整个过程统一由Jenkins 进行调度管理;
    • BUG系统负责BUG管理,Jmeter脚本执行时,若失败,自动提交BUG到Build下。

    4、技术关键

    1、Testlink仅支持三种格式的结果返回:Junit、TestNG、TAP ,Jmeter生成报告无法直接通过Jenkins传递给Testlink,并正确识别。

    2、TAP格式文件与Testlink中对应用例关联是通过.tap文件关联的,即:一个用例就需要一个TAP文件,但是测试结束后需要输出整体测试结果,必须要求Jmeter测试结束后,输出一个测试结果文档,如何将测试结果转化为TAP文件,且实现与用例一一对应。

    5、实现思路

    本章给出第4章提到键技术点的解决,在说明前,需要以下前提准备:

    1、 规范化Jmeter脚本中的规范要求:

    1) HTTP Sample命名规范为:

    2) 如果1条用例对应有多个HTTP Sample,需要使用事务处理器,这时事务处理器名称必须符合1)的命名规范,事务控制器内的HTTP Sample可以任意命名,但建议按照:【用例名称:可修改内容】形式进行命名

    3)如果1条用例对应1个HTTP Sample,不强制使用事务处理器

    4) 每一个HTTP Sample最好都要有断言,判断是否执行成功,若没有断言,无法判断是否符合预期,则转换脚本默认为成功。

    5)事务控制器内:最好不要再套用事务控制器,若需要建议使用简单控制器或者一次性控制器等

    2、 TestLink上增加自定义字段,用于标记用例与脚本的对应关系,如:AutoTest,AutoTest字段内容则为:上述中的用例编号,用以将Testlink测试用例与脚本测试结果关联对应

    3、 TestLink上自动化的用例执行方式设置为:自动的

    4、上述括号和冒号为英文字符,且命名中不要出现空格,否则会出现异常

    5.1、脚本批量调用实现思路

    Jmeter作为测试工具,仅输出测试脚本,若要形成框架持续集成,需要进行批量调用,并且可以统一配置脚本的全局参数,如:接口服务器地址、端口、默认登录用户名和密码等;

    Ant是Apache软件基金会JAKARTA目录中的一个子项目,操作简单。Ant是由一个内置任务和可选任务组成的。Ant运行时需要一个XML文件(构建文件)。Ant通过调用target树,就可以执行各种task。每个task实现了特定接口对象。由于Ant构建文件时XML格式的文件,所以和容易维护和书写,而且结构很清晰。

    故直接使用Ant+Jmeter来实现接口测试脚本的批量调用,目前需要解决的问题就是,Jmeter脚本运行时,从Ant获取HTTP请求默认值中的服务器IP、端口信息,以便后续服务器地址变更后,不会影响接口测试脚本,减少维护的工作量。

    主要实现见下图:

    按照图中的流程配置,每次需要自动运行时,在Jenkins上配置TEST_URL的参数后,接口测试则使用该访问地址,进行测试,若后续服务器的IP修改后,只需要在Jenkins上配置即可快速完成测试环境的切换。

    5.2、测试结果回传思路

    以下则是XML2TAP.sh的设计思路,根据以下思路输出sh脚本

    5.2、具体实施过程

    具体的实施过程,如下述图所示:

    1. Jenkins每次执行时,首先从SVN指定目录,检查是否有用例更新,若有,则下载所有更新测试脚本到工程目录

    2. 通过API key与Testlink建立关联,并获取工程配置的测试项目对应测试计划下的自动化测试用例信息。

    3. 调用Ant Plugin插件,通过build.xml配置,执行所有的Jmeter脚本文件,并生成XML格式测试报告:1份。

    4、Jmeter脚本执行期间,若有失败的接口用例,自动提交BUG到BUG系统

    5. 对XML测试报告进行二次处理,调用XML2HTML.xsl样式表,生成HTML格式测试报告,用于邮件发送完整的测试报告和jenkins上发布测试结果。

    6. 对XML测试报告进行转换,调用XML2TAP.sh脚本,对XML中每一个Sample结果进行处理,生成Testlink可识别的TAP文件,以用例为单位,生成多个tap文件。

    7. 将tap文件与第二步中获取的自动化用例信息对应,返回测试结果及测试报告到Testlink。

    8. 将第五步生成的测试报告邮件发送给相关人员。

    参考文献

    [1] https://wiki.jenkins-ci.org/display/JENKINS/Integrating+TestLink++Jenkins++JMeter

    Jeninks官网

    [2] http://blog.csdn.net/wangmuming/article/details/22925127

    Jenkins配置

    附录

    XML2TAP.sh(推荐:多sample使用:事务处理器)

    #!/bin/bash
    LANG=zh_CN.UTF-8
    export LANG
    # Parse each result file from the tests, created by JMeter when run by ant.
    # Every file created by JMeter will be parsed consecutively by the following for loop.
    # 获取对应目录下的xml文件,并通过xpath定位属性包含lb的所有sample,lb的值即为jmeter中sample的名称
    for XML_resultfile in $(ls -1 results/*.xml); do
        echo "XML_resultfile: ${XML_resultfile}"
        BASE_filename=$(basename ${XML_resultfile} .xml)  
        TestSamples=( $(xpath  ${XML_resultfile} '//@lb' 2>/dev/null) ) 
        num_of_TestSamples=${#TestSamples[@]}
         echo "num_of_TestSamples: ${num_of_TestSamples}"
      # Reference to TAP13 file standard:
      # http://podwiki.hexten.net/TAP/TAP13.html?page=TAP13
      # 每一个testSample进行处理
      if [ ${num_of_TestSamples} -gt 0 ]; then
          for ((line=0; ${line} < ${num_of_TestSamples}; line++)); do    
            echo "------------------CASE ${line}--------------------------"    
            echo "TestCases fullname: ${TestSamples[${line}]}"
            #判断sample的命名格式,是否符合x-x-x(x)x,其中x表示任意字符
            #旧规则,此处去除:限制isFlag=$( echo ${TestSamples[${line}]} | grep '.*-.*-.*(.{1,}):.*')
            isFlag=$( echo ${TestSamples[${line}]} | grep '.*-.*-.*(.{1,}).*')
            if [ "${isFlag}" != "" ]; then
                #截取左括号之前的作为tap文件名称,与testlink自定义字段AutoTest中填写的内容一致
                filename=$( echo ${TestSamples[${line}]} | cut -d '"' -f 2 | cut -d '(' -f 1 )
                TAP_filename=${filename}.tap
                isExistTap=""
                #判断tap文件是已存在,若已存在,直接跳过,因为:有些用例测试需要多个sample配合完成,所以在第一个sample处理时,已经有结果,后面的可以直接跳过
                isExistTap=$(ls -R | grep "${TAP_filename}")
                if [ "${isExistTap}" == "" ]; then
                    #TestCase=$( echo ${TestSamples[${line}]} | cut -d '"' -f 2 | cut -d ':' -f 1 )
                    TestCase=$( echo ${TestSamples[${line}]} | cut -d '"' -f 2)
                    num_of_testcases=1        
                    #获取事务控制器的result message,以提取sample总数和失败总数
                    transaction_rm=($(xpath ${XML_resultfile} "//sample[contains(@rm,'Number of samples in transaction') and contains(@lb,'${filename}')]/@rm" 2>/dev/null))
                    # 此处对使用事务控制器的用例进行处理:若事务处理器,子采样器名称不需要按照规范编号,但是事务处理器必须符合                
                    echo "----type:transaction---num_of_transactionSamples: ${#transaction_rm[@]}"
                    echo "num_of_transactionSamples,正常值[0,15]: ${#transaction_rm[@]}"                
                    if [ ${#transaction_rm[@]} -gt 0 -a ${#transaction_rm[@]} -lt 20 ]; then
                        #获取事务下总Sanple个数,截取rm字段的结果
                        num_of_transactionSamples=$( echo ${transaction_rm[6]} | cut -d ',' -f 1 )
                        #echo "num_of_transactionSamples: ${num_of_transactionSamples}"
                        #获取事务下失败的总个数,截取rm字段的结果
                        num_of_transactionFailSamples=$( echo ${transaction_rm[12]} | cut -d '"' -f 1 )
                        #echo "num_of_FailTestSamples: ${num_of_transactionFailSamples}"                    
                        ((num_of_testcases=${num_of_transactionSamples}))
                        ((num_of_resultif_count=${num_of_transactionFailSamples}))                                    
                    #此处对发现有多个事务控制器同名的情况进行处理
                    elif [ ${#transaction_rm[@]} -gt 20 ]; then    
                        echo "发现至少两条用例编号相同:${filename}"    
                        continue
                    #此处对未使用事务控制器的sample或者httpSample进行处理
                    else
                        echo "----sample-------"
                        #在xml中定位到要处理的用例有多少个httpSample、sample、一般jdbc连接的结果为sample,故需要分别处理,二者相加,则为用例个数
                        TestCase_count1=( $(xpath ${XML_resultfile} "//httpSample[contains(@lb,'${filename}')]/@sc" 2>/dev/null) )
                        TestCase_count2=( $(xpath ${XML_resultfile} "//sample[contains(@lb,'${filename}')]/@sc" 2>/dev/null) )
                        ((num_of_testcases=${#TestCase_count1[@]}+${#TestCase_count2[@]}))
                        # 同获取用例个数,此处查看断言是否有失败的,以判断用例是否OK,同样需要httpSample和sample分别处理,若num_of_resultif_count=0表示执行结果OK                    
                        result_if1=( $(xpath ${XML_resultfile} "//httpSample[contains(@lb,'${filename}')]//assertionResult[failure='true']/failure" 2>/dev/null | tr -d '"' | tr ' ' '_' | sed 's/^_//') )
                        result_if2=( $(xpath ${XML_resultfile} "//sample[contains(@lb,'${filename}')]//assertionResult[failure='true']/failure" 2>/dev/null | tr -d '"' | tr ' ' '_' | sed 's/^_//') )
                        ((num_of_resultif_count=${#result_if1[@]}+${#result_if2[@]}))
                    fi
                    echo "TestCase:${TestCase}"
                    echo "filename:${TAP_filename}"
                    echo "TestCase_count:${num_of_testcases}"
                    echo ">>>${BASE_filename}.xml Covert to : ${TAP_filename}"                
                    #输出基础内容到tap文件                
                    label="${BASE_filename} - ${TestCase}"                            
                    echo "TAP version 13" > results/${TAP_filename}                
                    echo "1..${num_of_testcases}" >> results/${TAP_filename}                
                    #echo "result_if1:${#result_if1[@]} -- result_if2:${#result_if2[@]}"
                    echo ">TestResult:${num_of_resultif_count}"
                    #对于执行失败的用例,获取失败信息,并输出到结果文件
                    if [ ${num_of_resultif_count} -gt 0 ]; then
                        #获取失败的断言信息
                        msg_texts=( $(xpath ${XML_resultfile} "//httpSample[contains(@lb,'${filename}')]//assertionResult[failure='true']/failureMessage" 2>/dev/null | tr -d '"' | tr ' ' '_' | sed 's/^_//') )
                        result="not ok ${num_of_testcases}  ${label} - error[${num_of_resultif_count}]"
                        echo "${result}">> results/${TAP_filename}
                        echo "failureMessage:${msg_texts}">> results/${TAP_filename}                    
                    else            
                        result="ok ${num_of_testcases}  ${label} - error[${#result_if[@]}]"
                        echo "${result}">> results/${TAP_filename}
                        #echo "${result}"
                        #(( line=${line}+${num_of_testcases}-1 ))                                                
                    fi
                else
                    echo ">tap already converted"
                fi
            else
                echo "sample name  is not standard"
            fi
        done
        else
            echo "over" >> results/${TAP_filename}
        fi
    done
    View Code

    XML2TAP.sh(多sample:按sample名称一致处理)

     1 #!/bin/bash
     2 LANG=zh_CN.UTF-8
     3 export LANG
     4 # Parse each result file from the tests, created by JMeter when run by ant.
     5 # Every file created by JMeter will be parsed consecutively by the following for loop.
     6 # 获取对应目录下的xml文件,并通过xpath定位属性包含lb的所有sample,lb的值即为jmeter中sample的名称
     7 for XML_resultfile in $(ls -1 results/*.xml); do
     8     echo "XML_resultfile: ${XML_resultfile}"
     9     BASE_filename=$(basename ${XML_resultfile} .xml)  
    10     TestSamples=( $(xpath  ${XML_resultfile} '//@lb' 2>/dev/null) ) 
    11     num_of_TestSamples=${#TestSamples[@]}
    12      echo "num_of_TestSamples: ${num_of_TestSamples}"
    13   # Reference to TAP13 file standard:
    14   # http://podwiki.hexten.net/TAP/TAP13.html?page=TAP13
    15   # 每一个testSample进行处理
    16   if [ ${num_of_TestSamples} -gt 0 ]; then
    17       for ((line=0; ${line} < ${num_of_TestSamples}; line++)); do    
    18         echo "------------------CASE ${line}--------------------------"    
    19         echo "TestCases fullname: ${TestSamples[${line}]}"
    20         #判断sample的命名格式,是否符合x-x-x(x):x,其中x表示任意字符
    21         isFlag=$( echo ${TestSamples[${line}]} | grep '.*-.*-.*(.{1,}):.*')
    22         if [ "${isFlag}" != "" ]; then
    23             #截取左括号之前的作为tap文件名称,与testlink自定义字段AutoTest中填写的内容一致
    24             filename=$( echo ${TestSamples[${line}]} | cut -d '"' -f 2 | cut -d '(' -f 1 )
    25             TAP_filename=${filename}.tap
    26             isExistTap=""
    27             #判断tap文件是已存在,若已存在,直接跳过,因为:有些用例测试需要多个sample配合完成,所以在第一个sample处理时,已经有结果,后面的可以直接跳过
    28             isExistTap=$(ls -R | grep "${TAP_filename}")
    29             if [ "${isExistTap}" == "" ]; then
    30                 TestCase=$( echo ${TestSamples[${line}]} | cut -d '"' -f 2 | cut -d ':' -f 1 )
    31                 num_of_testcases=1
    32                 #在xml中定位到要处理的用例有多少个httpSample、sample、一般jdbc连接的结果为sample,故需要分别处理,二者相加,则为用例个数
    33                 TestCase_count1=( $(xpath ${XML_resultfile} "//httpSample[contains(@lb,'${filename}')]/@rc" 2>/dev/null) )
    34                 TestCase_count2=( $(xpath ${XML_resultfile} "//sample[contains(@lb,'${filename}')]/@rc" 2>/dev/null) )
    35                 ((num_of_testcases=${#TestCase_count1[@]}+${#TestCase_count2[@]}))
    36                 echo "TestCase:${TestCase}"
    37                 echo "filename:${TAP_filename}"
    38                 echo "TestCase_count:${num_of_testcases}"        
    39                 label="${BASE_filename} - ${TestCase}"    
    40                 #输出基础内容到tap文件                
    41                 echo "TAP version 13" > results/${TAP_filename}
    42                 echo ">>>${BASE_filename}.xml Covert to : ${TAP_filename}"
    43                 echo "1..${num_of_testcases}" >> results/${TAP_filename}
    44                 # 同获取用例个数,此处查看断言是否有失败的,以判断用例是否OK,同样需要httpSample和sample分别处理,若num_of_resultif_count=0表示执行结果OK
    45                 result_if1=( $(xpath ${XML_resultfile} "//httpSample[contains(@lb,'${filename}')]/assertionResult[failure='true']/failure" 2>/dev/null | tr -d '"' | tr ' ' '_' | sed 's/^_//') )
    46                 result_if2=( $(xpath ${XML_resultfile} "//sample[contains(@lb,'${filename}')]/assertionResult[failure='true']/failure" 2>/dev/null | tr -d '"' | tr ' ' '_' | sed 's/^_//') )
    47                 ((num_of_resultif_count=${#result_if1[@]}+${#result_if2[@]}))
    48                 echo ">TestResult:${num_of_resultif_count}"
    49                 if [ ${num_of_resultif_count} -gt 0 ]; then
    50                     #获取失败的断言信息
    51                     msg_texts=( $(xpath ${XML_resultfile} "//httpSample[contains(@lb,'${filename}')]/assertionResult[failure='true']/failureMessage" 2>/dev/null | tr -d '"' | tr ' ' '_' | sed 's/^_//') )
    52                     result="not ok ${num_of_testcases}  ${label} - error[${num_of_resultif_count}]"
    53                     echo "${result}">> results/${TAP_filename}
    54                     echo "failureMessage:${msg_texts}">> results/${TAP_filename}                    
    55                     else                
    56                         if [ ${num_of_testcases} -gt 0 ]; then
    57                             result="ok ${num_of_testcases}  ${label} - error[${#result_if[@]}]"
    58                             echo "${result}">> results/${TAP_filename}
    59                             #echo "${result}"
    60                             #(( line=${line}+${num_of_testcases}-1 ))
    61                         else
    62                             result="not ok 1 - ${num_of_testcases}  ${label} - error[XML Covert to TAP ERROR:Xpath query find 0 result]"
    63                             echo "${result}">> results/${TAP_filename}
    64                             #echo "${result}"
    65                             #(( line=${line}+${num_of_testcases} ))
    66                         fi                                    
    67                 fi
    68             else
    69                 echo ">tap already converted"
    70             fi
    71         else
    72             echo "sample name  is not standard"
    73         fi
    74     done
    75     else
    76         echo "over" >> results/${TAP_filename}
    77     fi
    78 done
    View Code

    build.xml

     1 <?xml version="1.0" encoding="UTF-8"?>
     2 <project name="ant-jmeter-test" default="run" basedir=".">
     3     <tstamp> <format property="time" pattern="yyyyMMddhhmm" />    </tstamp>
     4     <tstamp> <format property="report.datestamp" pattern="yyyy/MM/dd HH:mm" /></tstamp>
     5     <property environment="env"/>
     6     <property name="report.title" value="接口测试"/>   
     7     <property name="jmeter.home" value="${env.JMETER_HOME}" /><!-- 需要改成自己本地的 Jmeter 目录-->     
     8     <property name="jmeter.result.dir" value="${env.WORKSPACE}/results" /><!-- jmeter测试结果放置目录,这里是放在构建项目下的results目录-->
     9     <property name="jmeter.sum.dir" value="${env.WORKSPACE}/sum" /><!-- jmeter测试概要放置目录(主要用于邮件发送时,作正文内容),这里是放在构建项目下的sum目录-->
    10     <property name="testplan.dir" value="${env.WORKSPACE}" /><!-- 运行的jmx脚本目录-->
    11    
    12    
    13     <property name="ReportName" value="TestReport" /><!-- 生成的报告的前缀 -->
    14     <property name="jmeter.result.jtlName" value="${jmeter.result.dir}/${ReportName}.xml" /><!-- 生成的jtl报告名称 -->
    15     <property name="jmeter.result.htmlName" value="${jmeter.result.dir}/${ReportName}.html" /><!-- 生成的html报告名称 -->
    16     <property name="jmeter.sum.htmlName" value="${jmeter.sum.dir}/sum.html" />     <!-- 生成的sum报告的名称 -->    
    17 
    18     <target name="run">
    19         <echo message="start..."/>
    20         <antcall target="clean" />
    21         <antcall target="test" />
    22         <antcall target="report" />
    23     </target>
    24     <target name="clean">
    25         <delete dir="${env.WORKSPACE}/results/" />
    26         <mkdir dir="${env.WORKSPACE}/results" />
    27         <delete dir="${env.WORKSPACE}/sum/" />
    28         <mkdir dir="${env.WORKSPACE}/sum" />        
    29     </target>
    30     <target name="test">
    31         <taskdef name="jmeter" classname="org.programmerplanet.ant.taskdefs.jmeter.JMeterTask" />
    32         <jmeter jmeterhome="${jmeter.home}" resultlog="${jmeter.result.jtlName}">
    33             <!-- 声明要运行的脚本。"*.jmx"指包含此目录下的所有jmeter脚本 -->
    34             <!-- 配置构建参数TESTLINK_TESTCASE_TESTFILEPATH在Jenkins上    -->
    35             <!-- ant -DGUI=false -DTEST_ENVIRONMENT=2 -DTESTLINK_TESTCASE_TESTFILEPATH=test.jmx -->
    36             <testplans dir="${testplan.dir}" includes="${env.TESTLINK_TESTCASE_TESTFILEPATH}.jmx" />
    37             <property name="jmeter.save.saveservice.output_format" value="xml"/>
    38             <property name="jmeter.save.saveservice.assertion_results" value="all"/>
    39             <property name="jmeter.save.saveservice.bytes" value="true"/>             
    40         </jmeter>
    41     </target>
    42      <path id="xslt.classpath">
    43         <fileset dir="${jmeter.home}/lib" includes="xalan*.jar"/>
    44         <fileset dir="${jmeter.home}/lib" includes="serializer*.jar"/>
    45      </path>
    46 
    47       <target name="report">
    48        
    49         <xslt  classpathref="xslt.classpath"
    50                 force="true"
    51                 in="${jmeter.result.jtlName}" 
    52                 out="${jmeter.result.htmlName}" 
    53                 style="${jmeter.home}/extras/jmeter.results.shanhe.me.xsl"> <!-- jtl测试报告转化为html报告,下载地址:http://shanhe.me/download.php?file=jmeter.results.shanhe.me.xsl-->
    54         <param name="dateReport" expression="${report.datestamp}"/>
    55         </xslt>
    56         <xslt  classpathref="xslt.classpath"
    57                 force="true"
    58                 in="${jmeter.result.jtlName}" 
    59                 out="${jmeter.sum.htmlName}" 
    60                 style="${jmeter.home}/extras/jmeter-results-detail-report_21-lj.xsl"> <!-- sum.html报告,该xsl文件在jmeter-results-detail-report_21这个文件的body中仅保留summary部分的显示,用于jenkins构建后邮件发送的正文展示 -->
    61         <param name="dateReport" expression="${report.datestamp}"/>
    62         </xslt>
    63         <!-- 因为上面生成报告的时候,不会将相关的图片也一起拷贝至目标目录,所以,需要手动拷贝 -->
    64         <copy todir="${jmeter.result.dir}">
    65             <fileset dir="${jmeter.home}/extras">
    66                 <include name="collapse.png"/>
    67                 <include name="expand.png"/>
    68             </fileset>
    69         </copy>
    70     </target>
    71 </project>
    72  
    View Code
  • 相关阅读:
    二次开发注意
    LAMP集群项目五 nfs分发文件到服务器
    LAMP集群项目五 nfs存储的数据实时同步到backupserver
    LAMP集群项目五 项目备份
    LAMP集群项目五 部署NFS存储服务并设置WEB服务挂载
    LAMP集群项目四 安装apache、php及其插件
    iOS-单选cell的实现
    iOS-省市区选择的实现
    随机颜色的产生
    刷新轮的使用
  • 原文地址:https://www.cnblogs.com/leeboke/p/6145828.html
Copyright © 2011-2022 走看看