WebSocket
Learn the possible WebSocket operations with Gatling: connect, close, send.
WebSocket support was initially contributed by Andrew Duffy.
WebSocket support is an extension to the HTTP DSL, whose entry point is the ws
method.
WebSocket protocol is very different from the HTTP one as the communication is 2 ways: both client-to-server and server-to-client, so the model is different from the HTTP request/response pair.
As a consequence, the main HTTP branch and a WebSocket branch can exist in a Gatling scenario in a dissociated way, in parallel. When doing so, each flow branch has its own state, so a user might have to reconcile them, for example when capturing data from a WebSocket check and wanting this data to be available to the HTTP branch.
wsName
If you want to deal with several WebSockets per virtual users, you have to give them a name and pass this name on each ws operation:
ws("WS Operation").wsName("myCustomName");
ws("WS Operation").wsName("myCustomName")
ws("WS Operation").wsName("myCustomName")
If you set an explicit name for the WebSocket, you’ll have to make it explicit for every other WebSocket actions you’ll define later in the scenario.
Of course, this step is not required if you deal with one single WebSocket per virtual user.
connect
The first thing is to connect a WebSocket:
exec(ws("Connect WS").connect("/room/chat?username=gatling"));
exec(ws("Connect WS").connect("/room/chat?username=gatling"))
exec(ws("Connect WS").connect("/room/chat?username=gatling"))
You can specify a subprotocol:
exec(ws("Connect WS").connect("/room/chat?username=gatling").subprotocol("custom"));
exec(ws("Connect WS").connect("/room/chat?username=gatling").subprotocol("custom"))
exec(ws("Connect WS").connect("/room/chat?username=gatling").subprotocol("custom"))
You can define a chain of actions to be performed after (re-)connecting with onConnected
:
exec(ws("Connect WS").connect("/room/chat?username=gatling")
.onConnected(
exec(ws("Perform auth")
.sendText("Some auth token"))
.pause(1)
));
exec(ws("Connect WS").connect("/room/chat?username=gatling")
.onConnected(
exec(ws("Perform auth")
.sendText("Some auth token"))
.pause(1)
))
exec(ws("Connect WS").connect("/room/chat?username=gatling")
.onConnected(
exec(ws("Perform auth")
.sendText("Some auth token"))
.pause(1)
))
close
Once you’re done with a WebSocket, you can close it:
// close with a 1000 status
exec(ws("Close WS").close());
// close with arbitrary status and reason
exec(ws("Close WS").close(1007, "Invalid payload data"));
// close with a 1000 status
exec(ws("Close WS").close())
// close with arbitrary status and reason
exec(ws("Close WS").close(1007, "Invalid payload data"))
// close with a 1000 status
exec(ws("Close WS").close)
// close with arbitrary status and reason
exec(ws("Close WS").close(1007, "Invalid payload data"))
Send a message
You may send text or binary messages:
sendText(text: Expression[String])
sendBytes(bytes: Expression[Array[Byte]])
For example:
// send text with a Gatling EL string
exec(ws("Message")
.sendText("{\"text\": \"Hello, I'm #{id} and this is message #{i}!\"}"));
// send text with a function
exec(ws("Message")
.sendText(session -> "{\"text\": \"Hello, I'm " + session.getString("id") + " and this is message " + session.getString("i") + "!\"}"));
// send text with ElFileBody
exec(ws("Message")
.sendText(ElFileBody("filePath")));
// send text with ElFileBody
exec(ws("Message")
.sendText(PebbleStringBody("somePebbleTemplate")));
// send text with ElFileBody
exec(ws("Message")
.sendText(PebbleFileBody("filePath")));
// send bytes with a Gatling EL string referencing a byte array in the Session
exec(ws("Message")
.sendBytes("#{bytes}"));
// send bytes with a function
exec(ws("Message")
.sendBytes(session -> new byte[] { 0, 5, 3, 1 }));
// send bytes with RawFileBody
exec(ws("Message")
.sendBytes(RawFileBody("filePath")));
// send bytes with RawFileBody
exec(ws("Message")
.sendBytes(ByteArrayBody("#{bytes}")));
// send text with a Gatling EL string
exec(ws("Message")
.sendText("""{"text": "Hello, I'm #{id} and this is message #{i}!"}"""))
// send text with a function
exec(ws("Message")
.sendText { session -> """{"text": "Hello, I'm ${session.getString("id")} and this is message ${session.getString("i")}!"}""" })
// send text with ElFileBody
exec(ws("Message")
.sendText(ElFileBody("filePath")))
// send text with ElFileBody
exec(ws("Message")
.sendText(PebbleStringBody("somePebbleTemplate")))
// send text with ElFileBody
exec(ws("Message")
.sendText(PebbleFileBody("filePath")))
// send bytes with a Gatling EL string referencing a byte array in the Session
exec(ws("Message")
.sendBytes("#{bytes}"))
// send bytes with a function
exec(ws("Message")
.sendBytes { session -> byteArrayOf(0, 5, 3, 1) })
// send bytes with RawFileBody
exec(ws("Message")
.sendBytes(RawFileBody("filePath")))
// send bytes with RawFileBody
exec(ws("Message")
.sendBytes(ByteArrayBody("#{bytes}")))
// send text with a Gatling EL string
exec(ws("Message")
.sendText("""{"text": "Hello, I'm #{id} and this is message #{i}!"}"""))
// send text with a function
exec(ws("Message")
.sendText(session => s"""{"text": "Hello, I'm ${session("id").as[String]} and this is message ${session("i").as[String]}!"}"""))
// send text with ElFileBody
exec(ws("Message")
.sendText(ElFileBody("filePath")))
// send text with ElFileBody
exec(ws("Message")
.sendText(PebbleStringBody("somePebbleTemplate")))
// send text with ElFileBody
exec(ws("Message")
.sendText(PebbleFileBody("filePath")))
// send bytes with a Gatling EL string referencing a byte array in the Session
exec(ws("Message")
.sendBytes("#{bytes}"))
// send bytes with a function
exec(ws("Message")
.sendBytes(session => Array[Byte](0, 5, 3, 1)))
// send bytes with RawFileBody
exec(ws("Message")
.sendBytes(RawFileBody("filePath")))
// send bytes with RawFileBody
exec(ws("Message")
.sendBytes(ByteArrayBody("#{bytes}")))
Note that:
ElFileBody
,PebbleStringBody
andPebbleFileBody
can be used withsendText
RawFileBody
andByteArrayBody
can be used withsendBytes
.
See HTTP request body for more information.
Checks
Gatling currently only supports blocking checks that will wait until receiving expected message or timing out.
Set a check
You can set a check right after connecting:
exec(ws("Connect").connect("/foo").await(30).on(wsCheck));
exec(ws("Connect").connect("/foo").await(30).on(wsCheck))
exec(ws("Connect").connect("/foo").await(30)(wsCheck))
Or you can set a check right after sending a message to the server:
exec(ws("Send").sendText("hello").await(30).on(wsCheck));
exec(ws("Send").sendText("hello").await(30).on(wsCheck))
exec(ws("Send").sendText("hello").await(30)(wsCheck))
You can set multiple checks sequentially. Each one will expect one single frame.
You can configure multiple checks in a single sequence:
// expecting 2 messages
// 1st message will be validated against wsCheck1
// 2nd message will be validated against wsCheck2
// whole sequence must complete withing 30 seconds
exec(ws("Send").sendText("hello")
.await(30).on(wsCheck1, wsCheck2));
// expecting 2 messages
// 1st message will be validated against wsCheck1
// 2nd message will be validated against wsCheck2
// whole sequence must complete withing 30 seconds
exec(ws("Send").sendText("hello")
.await(30).on(wsCheck1, wsCheck2))
// expecting 2 messages
// 1st message will be validated against wsCheck1
// 2nd message will be validated against wsCheck2
// whole sequence must complete withing 30 seconds
exec(ws("Send").sendText("hello")
.await(30)(wsCheck1, wsCheck2))
You can also configure multiple check sequences with different timeouts:
// expecting 2 messages
// 1st message will be validated against wsCheck1
// 2nd message will be validated against wsCheck2
// both sequences must complete withing 15 seconds
// 2nd sequence will start after 1st one completes
exec(ws("Send").sendText("hello")
.await(15).on(wsCheck1)
.await(15).on(wsCheck2)
);
// expecting 2 messages
// 1st message will be validated against wsCheck1
// 2nd message will be validated against wsCheck2
// both sequences must complete withing 15 seconds
// 2nd sequence will start after 1st one completes
exec(ws("Send").sendText("hello")
.await(15).on(wsCheck1)
.await(15).on(wsCheck2)
)
// expecting 2 messages
// 1st message will be validated against wsCheck1
// 2nd message will be validated against wsCheck2
// both sequences must complete withing 15 seconds
// 2nd sequence will start after 1st one completes
exec(ws("Send").sendText("hello")
.await(15)(wsCheck1)
.await(15)(wsCheck2)
)
Create a check
You can create checks for text and binary frames with checkTextMessage
and checkBinaryMessage
.
You can use almost all the same check criteria as for HTTP requests.
// with a static name
ws.checkTextMessage("checkName")
.check(regex("hello (.*)").saveAs("name"));
// with a Gatling EL string name
ws.checkTextMessage("#{checkName}")
.check(regex("hello (.*)").saveAs("name"));
// with a function name
ws.checkTextMessage(session -> "checkName")
.check(regex("hello (.*)").saveAs("name"));
// checking a binary frame
ws.checkBinaryMessage("checkName")
.check(
bodyBytes().is("hello".getBytes(StandardCharsets.UTF_8)),
bodyLength().is(3)
);
// with a static name
ws.checkTextMessage("checkName")
.check(regex("hello (.*)").saveAs("name"))
// with a Gatling EL string name
ws.checkTextMessage("#{checkName}")
.check(regex("hello (.*)").saveAs("name"))
// with a function name
ws.checkTextMessage{ session -> "checkName" }
.check(regex("hello (.*)").saveAs("name"))
// checking a binary frame
ws.checkBinaryMessage("checkName")
.check(
bodyBytes().shouldBe("hello".toByteArray(StandardCharsets.UTF_8)),
bodyLength().shouldBe(3)
)
// with a static name
ws.checkTextMessage("checkName")
.check(regex("hello (.*)").saveAs("name"))
// with a Gatling EL string name
ws.checkTextMessage("#{checkName}")
.check(regex("hello (.*)").saveAs("name"))
// with a function name
ws.checkTextMessage(session => "checkName")
.check(regex("hello (.*)").saveAs("name"))
// checking a binary frame
ws.checkBinaryMessage("checkName")
.check(
bodyBytes.is("hello".getBytes(StandardCharsets.UTF_8)),
bodyLength.is(3)
)
You can have multiple criteria for a given message:
ws.checkTextMessage("checkName")
.check(
jsonPath("$.code").ofInt().is(1).saveAs("code"),
jsonPath("$.message").is("OK")
);
ws.checkTextMessage("checkName")
.check(
jsonPath("$.code").ofInt().shouldBe(1).saveAs("code"),
jsonPath("$.message").shouldBe("OK")
)
ws.checkTextMessage("checkName")
.check(
jsonPath("$.code").ofType[Int].is(1).saveAs("code"),
jsonPath("$.message").is("OK")
)
checks can be marked as silent
.
Silent checks won’t be reported whatever their outcome.
ws.checkTextMessage("checkName")
.check(regex("hello (.*)").saveAs("name"))
.silent();
ws.checkTextMessage("checkName")
.check(regex("hello (.*)").saveAs("name"))
.silent()
ws.checkTextMessage("checkName")
.check(regex("hello (.*)").saveAs("name"))
.silent
Matching messages
You can define matching
criteria to filter messages you want to check.
Matching criterion is a standard check, except it doesn’t take saveAs
.
Non-matching messages will be ignored.
ws.checkTextMessage("checkName")
.matching(jsonPath("$.uuid").is("#{correlation}"))
.check(jsonPath("$.code").ofInt().is(1));
ws.checkTextMessage("checkName")
.matching(jsonPath("$.uuid").shouldBe("#{correlation}"))
.check(jsonPath("$.code").ofInt().shouldBe(1))
ws.checkTextMessage("checkName")
.matching(jsonPath("$.uuid").is("#{correlation}"))
.check(jsonPath("$.code").ofType[Int].is(1))
Processing unmatched messages
You can use processUnmatchedMessages
to process inbound messages that haven’t been matched with a check and have been buffered.
By default, unmatched inbound messages are not buffered, you must enable this feature by setting the size of the buffer on the protocol with .wsUnmatchedInboundMessageQueueSize(maxSize)
.
The buffer is reset when:
- sending an outbound message
- calling
processUnmatchedMessages
so we don’t present the same message twice
You can then pass your processing logic as a function.
The list of messages passed to this function is sorted in timestamp ascending (meaning older messages first).
It contains instances of types io.gatling.http.action.ws.WsInboundMessage.Text
and io.gatling.http.action.ws.WsInboundMessage.Binary
.
exec(
// store the unmatched messages in the Session
ws.processUnmatchedMessages((messages, session) -> session.set("messages", messages))
);
exec(
// collect the last text message and store it in the Session
ws.processUnmatchedMessages(
(messages, session) -> {
Collections.reverse(messages);
String lastTextMessage =
messages.stream()
.filter(m -> m instanceof io.gatling.http.action.ws.WsInboundMessage.Text)
.map(m -> ((io.gatling.http.action.ws.WsInboundMessage.Text) m).message())
.findFirst()
.orElse(null);
if (lastTextMessage != null) {
return session.set("lastTextMessage", lastTextMessage);
} else {
return session;
}
})
);
exec(
// store the unmatched messages in the Session
ws.processUnmatchedMessages { messages, session -> session.set("messages", messages) }
)
exec(
// collect the last text message and store it in the Session
ws.processUnmatchedMessages { messages, session ->
messages.reverse()
val lastTextMessage =
messages.stream()
.filter { m -> m is io.gatling.http.action.ws.WsInboundMessage.Text }
.map { m -> (m as io.gatling.http.action.ws.WsInboundMessage.Text).message() }
.findFirst()
.orElse(null)
if (lastTextMessage != null) {
session.set("lastTextMessage", lastTextMessage)
} else {
session
}
}
)
exec(
// store the unmatched messages in the Session
ws.processUnmatchedMessages((messages, session) => session.set("messages", messages))
)
exec(
// collect the last text message and store it in the Session
ws.processUnmatchedMessages { (messages, session) =>
val lastTextMessage =
messages
.reverseIterator
.collectFirst { case io.gatling.http.action.ws.WsInboundMessage.Text(_, text) => text }
lastTextMessage.fold(session)(m => session.set("lastTextMessage", m))
}
)
Configuration
WebSocket support introduces new HttpProtocol parameters:
http
// similar to standard `baseUrl` for HTTP,
// serves as root that will be prepended to all relative WebSocket urls
.wsBaseUrl("url")
// similar to standard `baseUrls` for HTTP,
// serves as round-robin roots that will be prepended
// to all relative WebSocket urls
.wsBaseUrls("url1", "url2")
// automatically reconnect a WebSocket that would have been
// closed by someone else than the client.
.wsReconnect()
// set a limit on the number of times a WebSocket will be
// automatically reconnected
.wsMaxReconnects(5)
// configure auto reply for specific WebSocket text messages.
// Example: `wsAutoReplyTextFrame({ case "ping" => "pong" })`
// will automatically reply with message `"pong"`
// when message `"ping"` is received.
// Those messages won't be visible in any reports or statistics.
.wsAutoReplyTextFrame( text ->
text.equals("ping") ? "pong" : null
)
// enable partial support for Engine.IO v4.
// Gatling will automatically respond
// to server ping messages (`2`) with pong (`3`).
// Cannot be used together with `wsAutoReplyTextFrame`.
.wsAutoReplySocketIo4()
// enable unmatched WebSocket inbound messages buffering,
// with a max buffer size of 5
.wsUnmatchedInboundMessageBufferSize(5);
http
// similar to standard `baseUrl` for HTTP,
// serves as root that will be prepended to all relative WebSocket urls
.wsBaseUrl("url")
// similar to standard `baseUrls` for HTTP,
// serves as round-robin roots that will be prepended
// to all relative WebSocket urls
.wsBaseUrls("url1", "url2")
// automatically reconnect a WebSocket that would have been
// closed by someone else than the client.
.wsReconnect()
// set a limit on the number of times a WebSocket will be
// automatically reconnected
.wsMaxReconnects(5)
// configure auto reply for specific WebSocket text messages.
// Example: `wsAutoReplyTextFrame({ case "ping" => "pong" })`
// will automatically reply with message `"pong"`
// when message `"ping"` is received.
// Those messages won't be visible in any reports or statistics.
.wsAutoReplyTextFrame {
text -> if (text == "ping") "pong" else null
}
// enable partial support for Engine.IO v4.
// Gatling will automatically respond
// to server ping messages (`2`) with pong (`3`).
// Cannot be used together with `wsAutoReplyTextFrame`.
.wsAutoReplySocketIo4()
// enable unmatched WebSocket inbound messages buffering,
// with a max buffer size of 5
.wsUnmatchedInboundMessageBufferSize(5)
http
// similar to standard `baseUrl` for HTTP,
// serves as root that will be prepended to all relative WebSocket urls
.wsBaseUrl("url")
// similar to standard `baseUrls` for HTTP,
// serves as round-robin roots that will be prepended
// to all relative WebSocket urls
.wsBaseUrls("url1", "url2")
// automatically reconnect a WebSocket that would have been
// closed by someone else than the client.
.wsReconnect
// set a limit on the number of times a WebSocket will be
// automatically reconnected
.wsMaxReconnects(5)
// configure auto reply for specific WebSocket text messages.
// Example: `wsAutoReplyTextFrame({ case "ping" => "pong" })`
// will automatically reply with message `"pong"`
// when message `"ping"` is received.
// Those messages won't be visible in any reports or statistics.
.wsAutoReplyTextFrame {
case "ping" => "pong"
}
// enable partial support for Engine.IO v4.
// Gatling will automatically respond
// to server ping messages (`2`) with pong (`3`).
// Cannot be used together with `wsAutoReplyTextFrame`.
.wsAutoReplySocketIo4
// enable unmatched WebSocket inbound messages buffering,
// with a max buffer size of 5
.wsUnmatchedInboundMessageBufferSize(5)
Debugging
You can inspect WebSocket traffic if you add the following logger to your logback configuration:
<logger name="io.gatling.http.action.ws.fsm" level="DEBUG" />
Example
Here’s an example that runs against Play 2.2’s chatroom sample (beware that this sample is missing from Play 2.3 and above):
HttpProtocolBuilder httpProtocol = http
.baseUrl("http://localhost:9000")
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.doNotTrackHeader("1")
.acceptLanguageHeader("en-US,en;q=0.5")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Gatling2")
.wsBaseUrl("ws://localhost:9000");
ScenarioBuilder scn = scenario("WebSocket")
.exec(
http("Home").get("/"),
pause(1),
exec(session -> session.set("id", "Gatling" + session.userId())),
http("Login").get("/room?username=#{id}"),
pause(1),
ws("Connect WS").connect("/room/chat?username=#{id}"),
pause(1),
repeat(2, "i").on(
ws("Say Hello WS")
.sendText("{\"text\": \"Hello, I'm #{id} and this is message #{i}!\"}")
.await(30).on(
ws.checkTextMessage("checkName").check(regex(".*I'm still alive.*"))
)
),
pause(1),
ws("Close WS").close()
);
val httpProtocol = http
.baseUrl("http://localhost:9000")
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.doNotTrackHeader("1")
.acceptLanguageHeader("en-US,en;q=0.5")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Gatling2")
.wsBaseUrl("ws://localhost:9000")
val scn = scenario("WebSocket")
.exec(
http("Home").get("/"),
pause(1),
exec { session -> session.set("id", "Gatling" + session.userId()) },
http("Login").get("/room?username=#{id}"),
pause(1),
ws("Connect WS").connect("/room/chat?username=#{id}"),
pause(1),
repeat(2, "i").on(
ws("Say Hello WS")
.sendText("""{"text": "Hello, I'm #{id} and this is message #{i}!"}""")
.await(30).on(
ws.checkTextMessage("checkName").check(regex(".*I'm still alive.*"))
)
),
pause(1),
ws("Close WS").close()
)
val httpProtocol = http
.baseUrl("http://localhost:9000")
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.doNotTrackHeader("1")
.acceptLanguageHeader("en-US,en;q=0.5")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Gatling2")
.wsBaseUrl("ws://localhost:9000")
val scn = scenario("WebSocket")
.exec(
http("Home").get("/"),
pause(1),
exec(session => session.set("id", "Steph" + session.userId)),
http("Login").get("/room?username=#{id}"),
pause(1),
ws("Connect WS").connect("/room/chat?username=#{id}"),
pause(1),
repeat(2, "i")(
ws("Say Hello WS")
.sendText("""{"text": "Hello, I'm #{id} and this is message #{i}!"}""")
.await(30)(
ws.checkTextMessage("checkName").check(regex(".*I'm still alive.*"))
)
),
pause(1),
ws("Close WS").close
)