首页 > 学院 > 开发设计 > 正文

JAVA性能测试脚本分析

2019-11-17 03:55:17
字体:
来源:转载
供稿:网友
众所周知,java语言是一门非常适合网络开发的语言,用Java语言进行性能测试脚本开发将会大大提高测试开发效率。LoadRunner中提供了对Java虚拟用户的支持。Java虚拟用户依托于JDK,这大大扩充了LoadRunner的脚本开发功能。测试人员既可以用Java虚拟用户来测试基于Java的应用系统的性能,又可以使用Java语言的强大开发脚本功能来测试其他平台的系统性能。

本章将和读者一起探索LoadRunner的Java世界,重点讨论如何开发Java自定义的虚拟用户脚本。本章主要内容如下:

n  认识Java虚拟用户

n  Java脚本开发基础

n  Java算法测试案例

7.1  认识Java虚拟用户

7.1.1  Java虚拟用户协议

Java虚拟用户脚本主要有Java Vuser、Corba-Java、RMI-Java、EJB等类型。这些类型的虚拟用户脚本均可以用Java语言来手工编写。下面将对各类Java虚拟用户简单地进行介绍。

Java Vuser

Java Vuser是自定义的Java虚拟用户脚本,脚本中可以使用标准的Java语言。这种虚拟用户不可以录制,只能采用纯手工编写,其适用范围和C Vuser一样,非常广泛。

本章主要探讨Java Vuser的开发方法。

Corba-Java

Corba-Java类型的虚拟用户主要用来测试用Java编写的、使用Corba应用程序或小程序的性能,用户可以先运行VuGen录制的脚本,然后使用标准的Java库函数以及LoadRunner特有的Java方法来增强该脚本。

RMI-Java

RMI-Java 虚拟用户适用于测试RMI(远程方法调用)Java应用程序或小程序。选择RMI-Java 用户进行录制,VuGen可以创建对应的Java脚本。完成录制后,可以使用JDK或自定义类,通过标准的Java代码来增强或修改脚本,还可以通过特定于LoadRunner的Java方法增强该脚本。

EJB

EJB虚拟用户专门用于测试EnterPRise Java Beans对象。采用EJB协议,VuGen会自动创建脚本以测试EJB功能,无需录制或编程,这和标准的Java Vuser明显不同。

在使用EJB协议类型的虚拟用户生成脚本前,需要指定JNDI属性和关于应用程序服务器的其他信息。LoadRunner的EJB检测器首先扫描应用程序服务器并确定哪些EJB可用;接着选择要测试的EJB,LoadRunner将生成要测试每个EJB方法的脚本,并为每个方法创建事务,便于度量其性能并找出问题。

需要注意的是,创建EJB虚拟用户测试脚本必须在应用程序服务器主机上安装LoadRunner EJB检测器,而且检测器必须处于活动状态。EJB检测器是一个独立的代理程序,必须将其安装在要扫描查找EJB的每一台计算机上。安装EJB检测器前,计算机上还需要安装有效的JDK环境。

本书中将以Java Vuser为例来讲解Java虚拟用户的开发和使用方法。Corba-Java、RMI-Java、EJB、Jacada Vuser等类型的虚拟用户使用方法可以参考LoadRunner的联机帮助手册,但是其手工脚本的开发方法与Java Vuser是一样的。

在“新建虚拟用户”对话框中的“自定义”或“全部协议”类别下选择“Java Vuser”即可创建空的Java Vuser脚本,如图7-1所示。

在自动生成的脚本中,vuser_init、vuser_end部分没有任何内容,Actions部分生成的脚本如图7-2所示。对于Java类型的虚拟用户,可以编辑Actions类,而init、end部分则不可以进行编辑。在Actions类中,有三个方法init、action和end,通常在init方法中编写初始化代码、在action方法中编写业务流程、在end方法中编写诸如释放资源之类的代码。


图7-1  选择Java Vuser协议


图7-2  Java Vuser自动生成的Actions部分

Java Vuser脚本中可以放置任何标准Java代码,这也是Java虚拟用户的魅力所在。

7.1.2  Java虚拟用户适用范围

LoadRunner完全支持C语言,原则上可以用C语言实现任何想要录制的功能。但是C语言的特点决定了这样做是不现实的:一是性能测试脚本开发成本非常高;二是很多企业的测试人员开发基础不好。因此,性能测试开发多选用C++、C#、Java等面向对象语言——因为这类语言既有成熟的集成开发工具,又有庞大的类库来支撑,测试脚本开发速度会更快。下一章将重点介绍如何用.NET来开发性能测试脚本。

Java虚拟用户适用范围非常广,归纳起来主要有两大类:一类是不太适合录制的业务功能点的性能测试,例如网页上Http文件的下载过程、视频播放等;另一类是基于Java语言开发的应用系统的性能测试,这类应用更适合采用Java虚拟用户进行测试。

用Java Vuser实现无法录制的测试需求

这类测试需求往往关注于对服务器产生的压力,重点测试在一定压力下服务器的负载情况,为评估、部署、调优系统提供参考。在这类测试工作中,Java的作用仅是一门语言,用于辅助测试人员完成测试程序的开发。

在性能测试工作中,不能录制的测试点很多,主要有以下几类:

l  含有控件的Web应用

在Web应用中,很多包含一些插件的浏览器应用经常不能录制。这类应用往往涉及很多协议,因此录制起来不是特别方便。

对于这类应用,可以用Java语言调用其功能来完成用户行为的模拟。

l  一些和媒体播放、文件下载等相关的应用

媒体播放或文件下载等过程的录制往往不容易控制,如果是应用基于P2P的协议,则会更加复杂。因此,这类测试只能由测试人员开发虚拟用户脚本来完成。

用Java Vuser测试基于Java语言的应用系统性能

相比前面,基于Java语言开发的应用系统,在性能测试中采用Java虚拟用户更显得“门当户对”。这类虚拟用户主要应用在下面两类测试中:

l  核心算法或业务的执行效率

对银行、电信等大型的基于J2EE的架构应用系统中,开发阶段的性能测试是必不可少的。为了降低后期遇到性能问题的风险,往往在开发阶段进行一些核心业务的性能测试。这些核心业务或要求较高的执行效率,或者要求支持较多用户的并发。因此,可以利用Java Vuser来测试这些业务的执行效率。

l  Java应用中不能录制的业务模块

很多时候,一些J2EE架构的业务系统用LoadRunner录制起来不是特别方便,例如一些Java智能客户端程序。这时可以手工编写测试代码,完成测试任务。与非Java应用相比,Java应用中不能录制的业务功能更适合采用Java虚拟用户,因为可以直接对一些方法进行调用。

由于Java语言的强大功能,Java虚拟用户的应用远不止上面这些,读者可以慢慢挖掘。

7.1.3  脚本开发环境配置

Java虚拟用户需要Java开发与运行环境的支持,因此首先要安装JDK。JDK是Java开发工具包的简称(Java Development Kit),Sun公司将JDK1.2以后的版本通称为Java 2。JDK的另外一种叫法是J2SDK(Java 2 Software Development Kit),现在比较常见的J2SDK是1.4以上的版本。JDK可以从http://java.sun.com/网站上下载,这里不再赘述。

下面简单介绍Java虚拟用户开发与运行环境的配置。在本章中,JDK安装在“C:/j2sdk1.4.1”路径下,LoadRunner安装在“D:/Program Files/Mercury Interactive/Mercury LoadRunner”目录下,系统调试环境以Windows xp为例。

第一步:在桌面上右键点击“我的电脑”,进入到“系统属性”设置界面,切换到“高级”选项卡,如图7-3所示。

第二步:点击图7-3中的“环境变量”,进入到环境变量配置界面,如图7-4所示。

第三步:在图7-4的“系统变量”中,选中CLASSPATH,点击编辑,如果没有CLASSPATH,则点击“新建”,同样会进入图7-5所示的界面。

通常安装了LoadRunner等软件的系统会创建CLASSPATH变量。这里需要注意的是,一定要在CLASSPATH变量值的最前面输入当前路径“.”和JDK的类库路径,例如“.;C:/j2sdk1.4.1/lib; C:/j2sdk1.4.1/jre/lib;”,否则将可能导致Java虚拟用户的运行错误。

例如:在图7-4中,CLASSPAT变量值是“.;C:/j2sdk1.4.1/lib; C:/j2sdk1.4.1/jre/lib;D:/ Tomcat-5.0/common/lib; D:/Program Files/Mercury Interactive/Mercury LoadRunner/ classes;D:/ Program Files/ Mercury Interactive/Mercury LoadRunner/lib”。


图7-3  系统属性界面


图7-4  环境变量设置


图7-5  系统类路径设置

第四步:在图7-4的“系统变量”中,选中Path,点击编辑,会进入图7-6所示的界面。在变量值的最前面填上JDK开发工具包的bin目录路径。


图7-6  系统Path设置

这里仍然要在Path变量值的最前面输入当前路径“· ”和JDK的bin目录路径,如“.;C:/j2sdk1.4.1/bin;C:/j2sdk1.4.1/jre/bin;”,否则Java虚拟用户可能产生运行错误。

例如在图7-6中,Path变量值是:

“.;C:/j2sdk1.4.1/bin;C:/j2sdk1.4.1/jre/bin;D:/Program Files/Mercury Interactive/Mercury LoadRunner/bin;%SystemRoot%/system32;%SystemRoot%;%SystemRoot%/System32/Wbem; C:/Program Files/Intel/Wireless/Bin/”。

开发环境的配置会因操作系统和应用程序的安装路径不同而有所差异,根据实际情况进行相应的修改即可。

7.2  Java脚本开发基础

要想掌握Java虚拟用户的使用,测试脚本开发人员至少需要两方面的技能:一是具有Java语言方面的技能,并熟练使用至少一种Java集成开发工具,例如Eclipse;二是掌握LoadRunner的Java API,Java API是访问Vuser函数的基础,通过LoadRunner的Java API可以在脚本中很容易地创建事务与并发点、获取用户信息等功能。

不难看出,Java虚拟用户要求测试脚本开发人员具有良好的开发基础,否则Java Vuser很难发挥应有的作用。限于篇幅,本节在Java测试开发基础部分,仅对Java语言方面的知识做概要介绍,读者可以通过Java方面的书籍进行学习;在LoadRunner的Java API部分,将会介绍一些相对重要的方法。

7.2.1  Java虚拟用户开发基础

Java语言基础

Java语言博大精深,即使资深的Java开发工程师也未必敢自称精通,测试人员就更不容易成为“高手”了。但是如果仅仅为了满足测试开发,还是很容易快速入门的。表7-1列出了一些进行Java虚拟用户开发的知识点,读者可以自行学习。
测试人员在掌握了上面这些技能后,基本可以进行Java虚拟用户的开发了。同时,本着学无止境的原则,测试人员仍要以积极的态度来学习Java开发知识。

Java集成开发工具Eclipse

Eclipse是一个与NetBeans、Sun ONE Studio和Borland Jbuilder类似的,一种基于Java的整合型可扩展开发平台,也是目前最著名的开源项目之一。IBM一直在大力支持该项目的发展,并在2001年11月宣布投入4千万美元到该项目的研发,这也是Eclipse项目发展较快的原因之一。

Eclipse专注于为高度集成的工具开发提供一个全功能的、具有商业品质的工业平台。它主要由Eclipse项目、Eclipse工具项目和Eclipse技术项目组成,具体包括四个组成部分:Eclipse Platform、JDT、CDT和PDE。JDT支持Java开发、CDT支持C开发、PDE用来支持插件开发,Eclipse Platform则是一个开放的可扩展IDE,提供了一个通用的开发平台。

Eclipse SDK(软件开发包)是Eclipse Platform、JDT和PDE所生产的组件合并,它们可以从eclipse.org网站(http://www.eclipse.org/downloads)上下载。这些组件提供了一个具有丰富特性的开发环境,允许开发者有效地建造可以无缝集成到Eclipse Platform中的工具。Eclipse SDK由Eclipse项目生产的工具和来自其他开放源代码的第三方软件组合而成。

在接下来的内容里,将介绍用Eclipse创建与编译Java类文件的过程以及如何在Java虚拟用户中调用编译好的类文件。关于Eclipse更深入的内容请读者参考其相关书籍。

l  创建Java项目

启动Eclipse后,依次选择菜单的“文件”→“新建”→“项目”,进入图7-7所示的界面。


图7-7  新建项目

在图7-7中选择Java项目,单击“下一步”,进入图7-8所示的界面。


图7-8  配置项目

在图7-8的“项目名称”输入“LoadRunnerDev”,“位置”下选择“在外部位置创建项目”,目录下选择“D:/LoadRunner”。然后点击“配置缺省值(O)...”,进入图7-9所示的界面。


图7-9  配置缺省值

在图7-9的“源和输出文件夹”处选择“文件夹(F)”,“源文件夹名(S):”默认为“src”,无须改变,在“输出文件夹名(O):”处输入“classes”,完成后返回图7-8所示界面。在图7-8中点击“下一步”,进入图7-10所示的界面。


图7-10  配置结果界面

在图7-10中,单击“完成”,新建Java项目工作完成。在图7-11中可以看到新建的项目“LoadRunnerDev”。


图7-11  包资源管理器

l  建立Java文件

如图7-12所示,在LoadRunnerDer中选中“src”,点击鼠标右键,进入“新建”,点击“类”,进入图7-13所示的新建Java类文件设置界面。


图7-12  新建Java类文件

在图7-13中,输入包名称“com.lr.test”、类名称“HelloWord”,其他各项设置默认即可。点击“完成”,进入源代码编辑界面,如图7-14所示。


图7-13  文件基本配置信息


图7-14  文件编辑界面

在源代码编辑界面,输入测试语句“System.out.println("HelloWord!");”如图7-14所示。代码编辑完成后,按照图7-15编译与运行程序。如果编译执行成功,可以在Eclipse的控制台看到输出结果,如图7-16所示。


图7-15  执行Java程序


图7-16  查看编译结果

l  编译与运行Java程序

编译后的class文件可以在“D:/LoadRunner/classes/com/lr/test”下找到,如图7-16所示。需要注意的是,类文件的存放路径是根据建立项目的设置来决定的。

l  开发虚拟用户脚本

进入到“D:/LoadRunner/classes”目录下,把整个“com”文件夹复制到“C:/j2sdk1.4.1/lib”,这样LoadRunner创建的所有Java Vuer脚本均可以直接调用;如果放到虚拟用户脚本的当前路径下,则只有当前的虚拟用户脚本可以调用。多台主机进行并发测试时,应该把编译好的类文件放到对应的虚拟用户脚本目录下。根据图7-1和图7-2所示新建一个Java虚拟用户脚本,并对照图7-17,输入同样的测试脚本,尤其要在代码开始处输入包的导入语句“import com.lr.test.*;”。


图7-17  Java虚拟用户示例脚本

l  运行虚拟用户脚本

点击Virtual User Generator 工具栏上的  图标开始执行脚本。正确的执行结果如图7-18所示,可以看到执行日志中有“System.out: HelloWord!”。如果运行不正确,读者可以参照前面的过程进行分析,同时检查JDK的路径设置。


图7-18  正确执行的虚拟用户脚本

与 C Vuser脚本相比,Java Vuser 脚本是先编译再执行,而C Vuser脚本是解释性的。VuGen在JDK安装路径内查找javac编译器,并在脚本内编译Java代码。该阶段由VuGen窗口底部的“正在编译... ”状态消息来指示。如果在编译期间出错,则这些错误将在执行日志中列出。

测试脚本保存后,接下来可以放到Controller中来运行,读者可以自己进行实验。需要注意的是,如果进行多台计算机联机测试,则所有运行测试脚本的客户机必须安装JDK环境并正确设置路径,否则将会出现如图7-19所示的提示。


图7-19  JDK环境不正确的提示

要想正确使用Java虚拟用户,首先要保证测试环境配置正确。执行测试时,可以先用本节的示例程序来检查环境是否正确,然后再进行复杂功能的开发。

7.2.2  LoadRunner的Java API

LoadRunner为访问Vuser函数提供了特定的Java API,这些函数都是lrapi.lr类的静态方法。借助Java API可以大大增强Java虚拟用户脚本的可用性。本节将介绍常用的Java API的用法,更多的函数及其用法读者可以参考LoadRunner联机手册。

在Java虚拟用户中,Java API函数的用法与Vuser函数中的用法基本一致,只是写法稍稍不同,更符合Java语言的特点。

1. 事务函数(Transaction Functions)

(1)int lr.start_transaction( String transaction_name ):标记事务开始;

(2)int lr.end_transaction ( String transaction_name, int status ):标记事务结束。

2. 信息函数(Informational Functions)

(1)String lr.get_group_name( ):返回 Vuser 组的名称;

(2)String lr.get_host_name( ):返回执行 Vuser 脚本的负载生成器的名称;

(3)String lr.get_master_host_name ( ):返回运行Controller计算机的名称;

(4)int lr.get_scenario_id( ): 返回当前方案的ID;

(5)int lr.get_vuser_id( ) :返回当前 Vuser 的ID。

3. 运行时函数(Run-Time Functions)

(1)void lr.peek_events ( );:指示可以暂停Vuser 脚本的位置;

(2)int lr.rendezvous( String rendezvous_name ):在 Vuser 脚本中设置集合点;

(3)void lr.think_time( double time ):暂停脚本执行,模拟实际用户操作之间的思考时间。

4. 字符串函数(String Functions)

(1)String lr.eval_string ( String instring ):用当前值替换参数;

(2)int lr.eval_int ( String name ):用整型值替换参数;

(3)int lr.next_row ( String dat_file ):指示使用指定参数的下一行数据。

5. 消息函数(Message Functions)

(1)int lr.debug_message( int message_level, String message):向输出窗口发送测试过程的调试消息;

(2)int lr.error_message ( String message ):向Vuser日志文件和输出窗口发送错误消息以及位置的详细信息;

(3)int lr.log_message ( String message ):向 Vuser 日志文件发送消息;

(4)int lr.message ( String message ):向输出窗口发送消息;

(5)int lr.output_message ( String message ):向日志文件和输出窗口发送消息和位置信息;

(6)int lr.vuser_status_message ( String message ):向Controller窗口中的“Vuser状态”区域发送消息。

7.3  Java算法测试案例

本节将结合一个具体案例来讲解如何借助Java Vuser来测试Java程序的算法。在案例中,主要模拟了测试某银行的信用卡审批过程,这部分内容是开发阶段性能测试的一部分。在这个测试例子中,主要发现了在并发时的两个算法问题:提交任务处理结果发生异常时Socket没有正常关闭;申请任务方法giveOutWork()没有加同步控制关键字synchronized。

为了更好地演示测试效果,程序中忽略了实际程序中的一些细节,例如具体的任务申请以及处理过程。

测试内容简介

信用卡审批程序主要包括两个部分,即客户端程序与服务器端程序。客户端程序包含一个Client.java类文件,即仅包含一个类Client,主要封装客户端的“申请—处理—提交”操作。服务器端程序即WorkServer.java,包含WorkQueue、AcceptClientThread、WorkServer三类。类WorkQueue主要完成任务队列的构建与管理工作;类AcceptClientThread继承线程类Thead,以独立线程的方式来处理客户端申请任务并保存客户端对任务的处理结果;类WorkServer是服务器端的执行类,主要完成对WorkQueue、AcceptClientThread的调用。

下面具体介绍业务流程。客户端对一项任务的业务流程如下:

第一步:与服务器建立连接,向服务器发出处理任务申请,等待服务器返回任务;

第二步:从服务器得到任务后,开始进行处理;

第三步:处理完毕后,提交结果给服务器进行保存,然后等待服务器返回结果;

第四步:输出服务器的保存结果;

第五步:结束当前的任务处理。

客户端源程序清单:Clien.java

package com.loadrunner.test;

import java.io.*;

import java.net.*;

/**

* 客户端{申请任务、确认是否可以审批、处理、传递结果得到确认}

* @author ChenShaoying

*/

public class Client {

  Socket socket;

  int clientNumber;

  BufferedReader is;//读出服务器返回的输入流

  PrintWriter os;//反馈给服务器的输出流

  /**

   * 向服务器申请任务

   */

  Client(Socket s) {

    try {

      this.socket = s;

      this.is = new BufferedReader(new InputStreamReader(s

          .getInputStream()));

      this.os = new PrintWriter(s.getOutputStream());

      this.clientNumber = Integer.parseInt(is.readLine());

    } catch (Exception e) {

      System.err.println("Error:Can not init the network!");

    }

  }

  public int applyWork() {

    int workNumber=-1;

    try {

      this.os.println("Apply");//发出申请

      os.flush();

      workNumber = Integer.parseInt(this.is.readLine());//读出申请结果

      if (workNumber == -1) {

        System.out.println("Server has no Work to do");

        System.exit(1);//退出程序

      }

      

    } catch (Exception e) {

      System.err.println("Error:Can not apply the network!");

      }

    return workNumber;

  }

  /**

   * 处理任务:添加实际处理过程即可,本处略

   * @return deal with result

   * @author ChenShaoying

   */

  public int dealWithWork(int worknumber) {

    

    System.out.println("dealWithWork:"+worknumber);    

    return 1;

  }

  /**

   * 传递结果到服务器确认

   * @return ensure result

   * @author ChenShaoying

   */

  public boolean finishWork(int workNumber) {

    boolean finish=false;

    try {

      this.os.println("finish");

      os.flush();

      finish = Boolean.valueOf(this.is.readLine()).booleanValue();

      if (finish == false) {

        System.out.println("Error:Work finish can not be set!");

        System.exit(1);

        }

      

    } catch (Exception e) {

      System.err.println("Error:Can not start the network!");

      System.exit(1);

    }

    return finish;

  }

}

服务器端对一项任务的业务流程如下:

第一步:建立任务队列,等待审批人员进行申请;

第二步:服务器收到用户申请后,系统会先锁定记录;

第三步:修改当前记录状态,并把当前任务返回给客户端;

第四步:等待客户端审批人员返回处理结果;

第五步:收到客户端提交的处理结果后,保存处理结果。

服务器端源代码清单:WorkServer.java

package com.loadrunner.test;

import java.io.*;

import java.net.*;

/**

* 队列{原始N个任务,接受申请返回任务号,检查任务是否正在处理、接受审批任务确认}

* @author ChenShaoying

  */

class WorkQueue{

  private int []WorkFlag;//0-未申请;1-申请后正在处理;2-处理完成

  private int total;

  int nowNumber;

  

  //创建任务队列:total-队列长度;WorkFlag-用来监控队列中每个任务状态的数组;nowNumber-当前可以申请到的任务编号

  WorkQueue(int totalNumber)

  {

    this.total=totalNumber;

    this.WorkFlag=new int [this.total];

    for(int i=0;i<this.total;i++)

    {

      this.WorkFlag[i]=0;

    }

    this.nowNumber=1;

  }

  

  //接受客户端申请,把队列任务提供给当前申请的客户端

   int giveOutWork()

  {

    

    int k=this.nowNumber;

    this.WorkFlag[this.nowNumber]=1;

    

    try {

      Thread.sleep(1);//模拟服务器对任务的处理时间

    } catch (InterruptedException e) {

      e.printStackTrace();

    }

    this.nowNumber++;

    return k;

  }

  

  

  //如果当前任务的状态是正在处理,则修改其状态为完成并返回true,否则返回false。

  boolean finishWork(int worknumber)

  {

    int number=worknumber;

    if (this.WorkFlag[number]==1)

    {

      this.WorkFlag[number]=2;

      return true;

    }else{

    System.err.println("Work "+number+" Can not be finished");  

    }

    return false;

  }

}

/**

* 客户端连接对话线程{接受任务申请返回任务号、接受审批任务确认、接受任务处理结果、返回确认消息}

*

*/

class AcceptClientThread extends Thread

{

  private Socket socket=null;

  private int clientNumber;

  private WorkQueue workQueue;

  

  AcceptClientThread(Socket socket,WorkQueue q,int clientNumber)

  {

    this.socket=socket;

    this.workQueue=q;//初始化对任务队列的管理

    this.clientNumber=clientNumber;

  }

  int giveOutWork()//分配任务

  {

    try{

    sleep(100);//延迟100毫秒分派,用于模拟实际工作中分发前的准备工作

    }catch(Exception e)

    {

      System.err.println(e);

      System.exit(0);

    }    

    return workQueue.giveOutWork();

  }

  

  

  boolean finishWork(int worknumber) //结束工作

  {

    return workQueue.finishWork(worknumber);

  }

  public void run()

  {

    try{

      //创建输入输出流

BufferedReader is=new
BufferedReader(new InputStreamReader(socket.getInputStream()));

            PrintWriter os=new PrintWriter(socket.getOutputStream());

            os.println(this.clientNumber);

            os.flush();

            //1.接受任务申请返回任务号

            String step=is.readLine();

            while(step.equals("Apply")==false)

            {

              sleep((int)Math.random()*100);

              step=is.readLine();

            }

            int worknumber=this.giveOutWork();

            os.println(worknumber);//任务号返回给客户端

            os.flush();

           

            //2.任务处理完毕后,把处理结果返回给服务器

            step=is.readLine();

            while(step.equals("finish")==false)

            {

              sleep(100);

              step=is.readLine();

            }

            //3.返回确认消息,开始提交客户端的处理结果,

            //如果没有被处理过(状态为1),则可以提交客户端的结果

            boolean result=this.finishWork(worknumber);

            os.println(result);

            os.flush();

            if(result==true)

            {

              System.out.println("Work "+Integer.toString(worknumber)

                +"done by client "+Integer.toString(this.clientNumber)+
                   ".");

            }

            //关闭连接和输入输出流

            os.close();

            is.close();

        socket.close();

    }

    catch(Exception e)

    {

      System.err.println(e);

    }

  }

}

public class WorkServer {

  public static void main(String[] args) {

    // TODO Auto-generated method stub

    ServerSocket serverSocket = null;

    boolean listening = true;

    WorkQueue queue=new WorkQueue(200000);

    //创建一个端口监听

    try {

      serverSocket = new ServerSocket(8000);

      }

    catch (IOException e)

      {

      System.err.println("Could not listen on port: 8000.");

      System.exit(-1);

      }

    try

    {

      int clientnumber=0;

      while (listening)

      {

        Socket socket=new Socket();

        socket = serverSocket.accept();  //程序将在此等候客户端的连接

        clientnumber++;

        

        //客户申请后将启动一个独立线程来处理客户申请

        new AcceptClientThread(socket,queue,clientnumber).start();

      }

      serverSocket.close();

    }

    catch(Exception e)

    {

      System.err.println(e);

      //System.exit(-1);

    }

  }

}

测试源程序

测试思路很简单,主要是模拟多个客户端并发申请与处理任务,因此采用了手工Java虚拟用户。为了方便程序开发,测试程序Test.java先在Eclipse中开发完成。在Test.java类文件中,编写具体的测试执行类Test,用于调用Client.java中的方法。

下面是测试程序Test.java的程序清单:

测试程序清单:Test.java

package com.loadrunner.test;

import java.io.IOException;

import java.net.Socket;

import java.net.UnknownHostException;

public class Test {

  public  void ApplyProccess() throws IOException

  {

      Socket clientSocket = null;

        try {

          //建立服务器连接,创建输入输出流

            clientSocket = new Socket("127.0.0.1",8000);

            Client client=new Client(clientSocket);

            //1申请任务号

            int worknumber=client.applyWork();

            //2处理记录

            int result=client.dealWithWork(worknumber);

            //3发送处理结果到服务器确认

            boolean ensureResult=client.finishWork(worknumber);

            if(ensureResult!=true)

            {

              System.err.println("Error:Work check error!");

              System.exit(0);

            }

            else

            {

              System.out.println("Finish work No."

                 +Integer.toString(worknumber));

            }

        } catch (UnknownHostException e) {

            System.err.println("Don't know about host: 127.0.0.1.");

            System.exit(1);

        } catch (IOException e) {

            System.err.println("Couldn't get I/O for the connection to:
              127.0.0.1."+e);

            System.exit(1);

        }

        //关闭服务器连接

        clientSocket.close();

     }

}

虚拟用户脚本

上面三个程序在Elipse中编译完成后,将会按照类文件的包名称“com.loadrunner.test”生成对应的目录结构“com/loadrunner/test”,下面可以看到编译后的class文件。

启动VuGen,先创建空的虚拟用户脚本“SimpleJava”,然后把程序的编译结果放到虚拟用户脚本目录下,如图7-20所示。


图7-20  虚拟用户脚本结构

上面的工作完成后,接下来需要修改脚本,以调用Test类中的Test()方法。修改后的脚本如图7-21所示。

在Eclipse中运行WorkServer.java,启动WorkServer服务器后才可以调试脚本。在VuGen中运行脚本,如果在运行结果Log中看到“Finish work No.*”,则表示脚本运行正确,可以成功申请并处理任务。图7-22所示为成功申请并处理了1号任务。


图7-21  修改后的脚本


图7-22  成功处理任务后的运行结果

创建与执行场景

虚拟用户脚本通过调试后,接下来要放到Controller中创建场景。首先运行一个用户,以在Controller中验证脚本的正确性。把脚本迭代次数设置为200,部分运行结果如图7-23所示,说明脚本在Controller中运行正常。

把并发用户变为10个,运行场景,并发申请任务开始发生错误:图7-24是场景运行状态;图7-25是WorkServer运行结果。从服务器上的提示可以看出,Socket连接发生错误。如果没有正常关闭,则会有“ ”异常。


图7-23  单用户成功处理任务后的运行结果


图7-24  10个用户并发时的场景状态


图7-25  用户并发时的WorkServer状态

分析这个错误的具体原因很容易,Socket连接发生重置多是由于非正常关闭Socket所致。浏览一下Test.java可以看到程序中有很多System.exit()语句,这种语句会导致直接退出程序而没有执行最后的语句clientSocket.close()。当任务处理过程发生异常时,无疑会导致Socket连接没有正常关闭。解决的方法很简单,在System.exit()语句前加上clientSocket.close()即可。

修正Socket连接缺陷后,10个用户并发时的WorkServer运行信息如图7-26所示,可以看到服务器不能正常提交处理结果。


图7-26  成功处理任务后的运行结果

为了详细追踪问题,需要更改测试程序以及服务器程序。Java虚拟用户脚本需要输出一些信息到控制台,而WorkServer则需要输出不能提交保存结果的任务状态。

新的虚拟用户Actions部分的程序清单如下:

package com.loadrunner.test;

import lrapi.lr;

import java.io.IOException;

import java.net.Socket;

import java.net.UnknownHostException;

public class Actions

{

  public int init() {

    return 0;

  }//end of init

  public int action() {

  try {

     lr.rendezvous("申请任务");  

         this.ApplyProccess();

    }

    catch (java.io.IOException e) {

      e.printStackTrace();

    }

    return 0;

  }//end of action

  public int end() {

    return 0;

  }//end of end

public  void ApplyProccess() throws IOException

  {

      Socket clientSocket = null;

        try { //建立服务器连接,创建输入输出流

            clientSocket = new Socket("127.0.0.1",8000);

            Client client=new Client(clientSocket);

            //1申请任务号

            int worknumber=client.applyWork();            

            //2处理记录

            int result=client.dealWithWork(worknumber);

            //3发送处理结果到服务器确认

            boolean ensureResult=client.finishWork(worknumber);

            if(ensureResult!=true)

            {

            lr.error_message("Error:Work "+worknumber+"finish error!");

//System.err.println("Error:Work check error!");

               //clientSocket.close();

              // System.exit(1);            

            }

            else

            {

            System.out.println("Finish work No."+Integer.toString
               (worknumber));

            }

        } catch (UnknownHostException e) {

            System.err.println("Don't know about host: 127.0.0.1.");

           

        } catch (IOException e) {

            System.err.println("Couldn't get I/O for the connection to: 127.0.0.1."+e);

       }

        //关闭服务器连接

        clientSocket.close();

     }

}

程序中用“lr.error_message("Error:Work "+worknumber+"finish error!");”语句替换了“System.err.println("Error:Work check error!");”,目的是向Controller的控制台发出消息。

WorkServer类中则修改了finishWork(int worknumber)方法,把其中的“System.err. println("Work"+number+"Can not be finished");”替换成“System.err.println("Work "+number+" Can not be finished,"+"WorkFlag is "+WorkFlag[number]);”,以查找不能保存处理结果的当前状态任务。修改后的程序如下:


再次选择10个用户并发, Controller将会弹出一些错误提示,如图7-27所示。


图7-27  Controller运行时捕获的一些错误

WorkServer服务器弹出的消息如图7-28所示,可以看出不能提交处理结果的任务的状态标志为2,表示已经由其他用户处理完毕,因此提交发生错误。

通过客户端以及服务器的错误信息,基本可以断定任务分配存在重复现象——只有把同一任务分给多个客户端进行处理,才会发生不能提交保存结果的状况。这时自然会想到giveOutWork()方法可能存在问题。检查giveOutWork()方法,发现根本没有做并发同步控制!


图7-28  WorkServer运行结果日志

修正后的giveOutWork()方法如下所示,加了同步关键字synchronized。

synchronized int giveOutWork()

  {

    

    int k=this.nowNumber;

    this.WorkFlag[this.nowNumber]=1;

    

    try {

      Thread.sleep(1);//模拟服务器对任务的处理时间

    } catch (InterruptedException e) {

      e.printStackTrace();

    }

    this.nowNumber++;

    return k;

  }

再次运行并发场景,则可以看到任务处理过程完全正确,图7-29即为添加同步控制后的WorkServer运行日志。


图7-29  添加同步控制后的WorkServer运行日志

至此,已经完成了对算法测试以及缺陷修正工作。

本节案例中的程序缺陷看似很容易发现,但在实际项目中是在测试一段时间后才发现并发分配算法存在问题的。读者可以把giveOutWork()方法中模拟服务器对任务的处理时间即Thread.sleep(1)语句注释后再进行并发测试,这时几乎很难再现前面问题,尽管把同一任务分给多个用户进行处理的缺陷仍然存在。

调整后的giveOutWork()方法如下:

int giveOutWork()

  {

int k=this.nowNumber;

    this.WorkFlag[this.nowNumber]=1;

    /*  try {

      Thread.sleep(1);//模拟服务器对任务的处理时间

    } catch (InterruptedException e) {

      e.printStackTrace();

    }*/

    this.nowNumber++;

    return k;  }

通过本案例可以看出,很多算法需要认真全面的测试才可以挖出隐藏很深的缺陷



本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/kagrrahang/archive/2009/12/30/5104389.aspx


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/kagrrahang/archive/2009/12/30/5104389.aspx
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表