userver: WebSocket Client
Loading...
Searching...
No Matches
WebSocket Client

Before you start

Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server.

Step by step guide

Let's write a simple HTTP handler that:

  • receives url and message arguments
  • connects to a WebSocket server by the url as a WebSocket client
  • sends the message
  • receives the response and closes the WebSocket connection
  • returns the WebSocket response

WebSocket handler

class WebSocketClientHandler final : public server::handlers::HttpHandlerBase {
public:
static constexpr std::string_view kName = "handler-websocket-client";
WebSocketClientHandler(const components::ComponentConfig& config, const components::ComponentContext& context)
: HttpHandlerBase(config, context),
http_client_(context.FindComponent<components::HttpClient>().GetHttpClient())
{}
std::string HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&)
const override {
const auto& ws_server_url = request.GetArg("url");
// Perform WebSocket handshake
auto ws_response = http_client_.CreateRequest().url(ws_server_url).PerformWebSocketHandshake();
// Create WebSocket connection
auto conn = ws_response.MakeWebSocketConnection();
conn->SendText(request.GetArg("message"));
// Receive response
websocket::Message response;
conn->Recv(response);
// Close connection
conn->Close(websocket::CloseStatus::kNormal);
return response.data;
}
private:
clients::http::Client& http_client_;
};

To create a WebSocket connection:

  1. Use clients::http::Request::PerformWebSocketHandshake to perform the handshake
  2. Call clients::http::WebSocketResponse::MakeWebSocketConnection on the response to get a websocket::WebSocketConnection
  3. Use websocket::WebSocketConnection::SendText to send text messages, websocket::WebSocketConnection::SendBinary to send binary messages
  4. Use websocket::WebSocketConnection::Recv to receive messages
  5. Call websocket::WebSocketConnection::Close when done

Static config

Now we have to configure the service:

# yaml
components_manager:
task_processors: # Task processor is an executor for coroutine tasks
main-task-processor: # Make a task processor for CPU-bound coroutine tasks.
worker_threads: 4 # Process tasks in 4 threads.
fs-task-processor: # Make a separate task processor for filesystem bound tasks.
worker_threads: 4
default_task_processor: main-task-processor # Task processor in which components start.
components: # Configuring components that were registered via component_list
testsuite-support:
tests-control:
load-enabled: $testsuite-enabled
path: /tests/{action}
method: POST
task_processor: main-task-processor
server:
listener: # configuring the main listening socket...
port: 8080 # ...to listen on this port and...
task_processor: main-task-processor # ...process incoming requests on this task processor.
logging:
fs-task-processor: fs-task-processor
loggers:
default:
file_path: '@stderr'
level: debug
overflow_behavior: discard # Drop logs if the system is too busy to write them down.
dns-client:
fs-task-processor: fs-task-processor
http-client-core:
fs-task-processor: fs-task-processor
user-agent: userver-websocket-client-sample
handler-websocket-client:
path: /ws-client # Handle requests to /ws-client
method: GET
task_processor: main-task-processor

int main()

Finally, we add our component to the components::MinimalServerComponentList. Note that we use clients::http::ComponentList which registers both http-client and http-client-core components:

int main(int argc, char* argv[]) {
const auto component_list =
.Append<server::handlers::TestsControl>()
.AppendComponentList(clients::http::ComponentList())
.Append<samples::websocket_client::WebSocketClientHandler>();
return utils::DaemonMain(argc, argv, component_list);
}

Build and Run

To build the sample, execute the following build steps at the userver root directory:

mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-websocket_client

The sample could be started by running make start-userver-samples-websocket_client.

To start the service manually run ./samples/websocket_client/userver-samples-websocket_client -c </path/to/static_config.yaml>.

Now you can test the client by connecting it to a WebSocket echo server. First, start the WebSocket server sample:

make start-userver-samples-websocket_service

Then, in another terminal, send a request to the client:

bash
$ curl 'http://localhost:8080/ws-client?url=ws://localhost:8080/chat&message=Hello'
Hello

Functional testing

Functional tests for the service can be implemented using testsuite. The mockserver can emulate a WebSocket server by returning aiohttp.web.WebSocketResponse:

import aiohttp.web
async def test_websocket_client(service_client, mockserver):
"""Test WebSocket client connecting to echo server"""
@mockserver.aiohttp_handler('/chat')
async def _mock_websocket(request):
ws = aiohttp.web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws.send_str(msg.data)
return ws
response = await service_client.get(
'/ws-client',
params={
'url': mockserver.ws_url('/chat'),
'message': 'test message',
},
)
assert response.status == 200
assert response.text == 'test message'

Do not forget to add the plugin in conftest.py:

# /// [registration]
pytest_plugins = ['pytest_userver.plugins.core']
# /// [registration]

Full sources

See the full example: