JMeter提供纯TCP协议级别Sampler,如果你不觉得麻烦可以通过它来完成所有基于TCP协议的性能测试,这样一个万金油式的Sampler由于很少进入使用者的视野,因此,在实际使用中存在着许多隐藏特性和误解,本篇将对TCP Sampler使用中的一些特点进行讲解。
为了更好的理解TCP Sampler的使用,我们将构建一个Mock TCP Server用于测试,并根据不同的TCP Sampler类型和设置进行调整,以达到直观易懂的目的,一个典型的TCP Server代码参考如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TCPServer {
private int port = 10009;
private ServerSocket serverSocket;
private ExecutorService executorService;
private final int POOL_SIZE = 20;
public TCPServer() throws IOException {
serverSocket = new ServerSocket(port);
executorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors() * POOL_SIZE);
}
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();
executorService.execute(new Handler(socket));
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new TCPServer().service();
}
}
class Handler implements Runnable {
private Socket socket = null;
public Handler(Socket socket) {
this.socket = socket;
}
private BufferedReader getReader(Socket socket) throws IOException {
InputStream in = socket.getInputStream();
return new BufferedReader(new InputStreamReader(in));
}
public void run() {
BufferedReader br = null;
PrintWriter out = null;
System.out.println("New connection accepted " + socket.getInetAddress()
+ ":" + socket.getPort());
try {
br = getReader(socket);
String recv = null;
recv = br.readLine();
System.out.println(recv);
out = new PrintWriter(socket.getOutputStream());
out.write("Hello Client\r\n");
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (out != null) {
out.close();
}
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
一个典型配置的TCP Sampler与下图所示:
TCP Sampler提供了3个Sampler的实现,分别是org.apache.jmeter.protocol.tcp.sampler.TCPClientImpl、org.apache.jmeter.protocol.tcp.sampler.BinaryTCPClientImpl和
org.apache.jmeter.protocol.tcp.sampler.LengthPrefixedBinaryTCPClientImpl。
其中TCPClientImpl实现了以文本编辑器中所编辑的纯文本为内容进行发送,BinaryTCPClientImpl则以文本编辑器中所编辑的16进制字符(hex)内容为基础转换为二进制的字节内容进行发送,LengthPrefixedBinaryTCPClientImpl则会在BinaryTCPClientImpl基础上默认以发送内容的长度以字节前缀进行填充。
我们可以通过配置jmeter.properties文件中tcp.handler属性来设置默认的TCPClient。
我们使用TCPClientImpl对Mock TCP Server进行测试,配置参考下图:
点击运行测试,你会发现测试发生了阻塞,原因是服务器使用了readLine获取客户端的发送数据,需要根据发送数据中的CRLF(\r或\n)判断一行的结束。而我们制作的发送内容并不包括CRLF标识内容,因此,服务器阻塞在了读数据,测试客户端得不到服务器响应,同样也阻塞在了读数据,正确的配置需要添加一个“回车”(不能是"\r"或"\n",因为TCPClientImpl会自动将其转换为对应的两个字符而不是CRLF标识)参考下图:
BinaryTCPClientImpl的配置只需要将“Hello Server”转换为hex就可以实现同样的测试内容,注意尾部增加0a(\n)作为CRLF标识:
48656c6c6f205365727665720a
LengthPrefixedBinaryTCPClientImpl会自动在前缀增加内容长度,我们需要对Mock TCP Server进行小的改造,将Handler进行以下调整,参考以下代码:
class Handler implements Runnable {
private Socket socket = null;
public Handler(Socket socket) {
this.socket = socket;
}
public static String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
public void run() {
InputStream in = null;
PrintWriter out = null;
System.out.println("New connection accepted " + socket.getInetAddress()
+ ":" + socket.getPort());
try {
byte[] buffer = new byte[1];
byte[] lengthBuffer = new byte[2];
StringBuilder sb = new StringBuilder();
in = socket.getInputStream();
int x = 0;
int y = 0;
while((x = in.read(buffer)) > -1){
if(y < 2){
lengthBuffer[y] = buffer[0];
} else {
sb.append(new String(buffer));
}
y++;
if(buffer[x - 1] == 0x0a){
break;
}
}
String recv = sb.toString();
System.out.println(Integer.valueOf(bytesToHexString(lengthBuffer), 16));
System.out.println(recv);
out = new PrintWriter(socket.getOutputStream());
out.write("Hello Client\r\n");
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
我们会发现以下输出结果:
默认的prefix占用2个字节来表示数据段的长度,可以通过binarylength.prefix.length属性进行配置。
很多时候我们可以需要自己定义所谓的EOL,但这个EOL指代的是什么呢?我们可以从JMeter的源代码找到结果:
(1)AbstractTCPClient中的代码片段:
protected boolean useEolByte = false;
/**
* {@inheritDoc}
*/
@Override
public byte getEolByte() {
return eolByte;
}
/**
* {@inheritDoc}
*/
@Override
public void setEolByte(int eolInt) {
if (eolInt >= Byte.MIN_VALUE && eolInt <= Byte.MAX_VALUE) {
this.eolByte = (byte) eolInt;
useEolByte = true;
} else {
useEolByte = false;
}
}
(2) TCPClientImpl(默认的TCPSampler)代码片段:
private static final int EOL_INT = JMeterUtils.getPropDefault("tcp.eolByte", 1000);
/**
* Reads data until the defined EOL byte is reached.
* If there is no EOL byte defined, then reads until
* the end of the stream is reached.
*/
@Override
public String read(InputStream is) throws ReadException{
ByteArrayOutputStream w = new ByteArrayOutputStream();
try {
byte[] buffer = new byte[4096];
int x = 0;
while ((x = is.read(buffer)) > -1) {
w.write(buffer, 0, x);
if (useEolByte && (buffer[x - 1] == eolByte)) {
break;
}
}
// do we need to close byte array (or flush it?)
if(log.isDebugEnabled()) {
log.debug("Read: " + w.size() + "\n" + w.toString());
}
return w.toString(CHARSET);
} catch (IOException e) {
throw new ReadException("Error reading from server, bytes read: " + w.size(), e, w.toString());
}
}
我们发现EOL原来是与读数据相关的,就是设定来自于服务器数据流的一个结束标识字节。没有设置EOL将会一直读到输入流结束为止。
这里值得注意的是,这是个十进制的值(千万不要写成hex),比如你可以查询ASCII表,来确认一个表示结束字符的十进制值,我们以$作为案例,改造一下Mock TCP Server,输出结尾为$,如下面代码:
out.write("Hello Client$");
out.flush();
out.write("Tail\r\n");
out.flush();
按之前的配置得到的响应如下:
增加EOL的配置如下:
运行后得到的响应如下:
我们可以通过配置一对状态闭合区域来识别来自服务器对于一些特殊状态的标识,比如HTTP的200、300、400、500状态,对于TCP Sampler默认是无法区分的,但可以通过配置如下属性,将[]作为闭合区域放在响应数据的最前端作为状态标识:
tcp.status.prefix=[
tcp.status.suffix=]
改造一下Mock TCP Server,如下面代码:
out.write("[400]Hello Client\r\n");
运行测试,结果如下:
这样我们就可以自定义状态标识了,并可以对Response message在properties文件中进行维护。
我们可以通过继承AbstractTCPClient自定义TCPClient,并指明classname进行使用,比如我们可以自定义一个发送数据和接受数据都已CRLF(\r\n)进行结尾的TCPClient,参考如下代码:
package org.xreztento.jmeter.tcp;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Arrays;
import org.apache.jmeter.protocol.tcp.sampler.AbstractTCPClient;
import org.apache.jmeter.protocol.tcp.sampler.ReadException;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.util.JOrphanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
public class CRLFTCPClientImpl extends AbstractTCPClient{
private static final Logger log = LoggerFactory.getLogger(CRLFTCPClientImpl.class);
private static final String CRLF = "0d0a";//\r\n
private static final String CHARSET = JMeterUtils.getPropDefault("tcp.charset", Charset.defaultCharset().name());
public CRLFTCPClientImpl(){
super();
setCharset(CHARSET);
String configuredCharset = JMeterUtils.getProperty("tcp.charset");
if(StringUtils.isEmpty(configuredCharset)) {
log.info("Using platform default charset:" + CHARSET);
} else {
log.info("Using charset:"+configuredCharset);
}
}
@Override
public String read(InputStream is) throws ReadException {
ByteArrayOutputStream w = new ByteArrayOutputStream();
try {
byte[] buffer = new byte[4096];
int x = 0;
while ((x = is.read(buffer)) > -1) {
w.write(buffer, 0, x);
int tail = CRLF.length() / 2;
byte[] eolBuffer = w.toByteArray();
if(JOrphanUtils.baToHexString(Arrays.copyOfRange(eolBuffer, eolBuffer.length - tail, eolBuffer.length))
.equals(JOrphanUtils.baToHexString(hexStringToByteArray(CRLF)))){
break;
}
}
return w.toString(CHARSET);
} catch (IOException e) {
throw new ReadException("Error reading from server, bytes read: " + w.size(), e, w.toString());
}
}
@Override
public void write(OutputStream os, InputStream is) throws IOException {
byte[] buff = new byte[512];
while(is.read(buff) > 0){
os.write(buff);
os.flush();
}
}
@Override
public void write(OutputStream os, String s) throws IOException {
byte[] buffer = s.getBytes(CHARSET);
byte[] eolBuffer = ArrayUtils.addAll(buffer, hexStringToByteArray(CRLF));
os.write(eolBuffer);
os.flush();
}
private static byte[] hexStringToByteArray(String hexEncodedBinary) {
if (hexEncodedBinary.length() % 2 == 0) {
char[] sc = hexEncodedBinary.toCharArray();
byte[] ba = new byte[sc.length / 2];
for (int i = 0; i < ba.length; i++) {
int nibble0 = Character.digit(sc[i * 2], 16);
int nibble1 = Character.digit(sc[i * 2 + 1], 16);
if (nibble0 == -1 || nibble1 == -1){
throw new IllegalArgumentException(
"Hex-encoded binary string contains an invalid hex digit in '"+sc[i * 2]+sc[i * 2 + 1]+"'");
}
ba[i] = (byte) ((nibble0 << 4) | (nibble1));
}
return ba;
} else {
throw new IllegalArgumentException(
"Hex-encoded binary string contains an uneven no. of digits");
}
}
}
使用该HTTPClient我们就可以免去前面手动载入“回车”建立结束标识的问题了。