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");
auto ws_response = http_client_.CreateRequest().url(ws_server_url).PerformWebSocketHandshake();
auto conn = ws_response.MakeWebSocketConnection();
conn->SendText(request.
GetArg(
"message"));
websocket::Message response;
conn->Recv(response);
conn->Close(websocket::CloseStatus::kNormal);
}
private:
clients::http::Client& http_client_;
};
To create a WebSocket connection:
- Use clients::http::Request::PerformWebSocketHandshake to perform the handshake
- Call clients::http::WebSocketResponse::MakeWebSocketConnection on the response to get a websocket::WebSocketConnection
- Use websocket::WebSocketConnection::SendText to send text messages, websocket::WebSocketConnection::SendBinary to send binary messages
- Use websocket::WebSocketConnection::Recv to receive messages
- 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>()
.Append<samples::websocket_client::WebSocketClientHandler>();
}
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: