userver: Writing your WebSocket service
Loading...
Searching...
No Matches
Writing your WebSocket service

Before you start

Make sure that you can compile and run core tests as described at Configure, Build and Install.

Step by step guide

Typical WebSocket server application in userver consists of the following parts:

  • WebSocket handler component - main logic of your application
  • Static config - startup config that does not change for the whole lifetime of an application
  • int main() - startup code

Let's write a simple chat server that echoes incoming messages as outgoing messages for any websocket connected to /chat URL.

WebSocket handler component

WebSocket handlers must derive from server::websocket::WebsocketHandlerBase and have a name, that is obtainable at compile time via kName variable and is obtainable at runtime via HandlerName().

namespace samples::websocket {
class WebsocketsHandler final : public server::websocket::WebsocketHandlerBase {
public:
// `kName` is used as the component name in static config
static constexpr std::string_view kName = "websocket-handler";
// Component is valid after construction and is able to accept requests
using WebsocketHandlerBase::WebsocketHandlerBase;
chat.Recv(message); // throws on closed/dropped connection
if (message.close_status) break; // explicit close if any
chat.Send(std::move(message)); // throws on closed/dropped connection
}
if (message.close_status) chat.Close(*message.close_status);
}
};
} // namespace samples::websocket

Static config

Now we have to configure the service by providing task_processors and default_task_processor options for the components::ManagerControllerComponent and configuring each component in components section:

# 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
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.
websocket-handler: # Finally! Websocket handler.
path: /chat # Registering handlers '/*' find files.
method: GET # Handle only GET requests.
task_processor: main-task-processor # Run it on CPU bound task processor
max-remote-payload: 100000
fragment-size: 100000

Note that all the components and handlers have their static options additionally described in docs.

int main()

Finally, we add our component to the components::MinimalServerComponentList(), and start the server with static configuration file passed from command line.

int main(int argc, char* argv[]) {
const auto component_list =
components::MinimalServerComponentList().Append<samples::websocket::WebsocketsHandler>();
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_service

The sample could be started by running make start-userver-samples-websocket_service. The command would invoke testsuite start target that sets proper paths in the configuration files and starts the service.

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

Note
Without file path to static_config.yaml userver-samples-websocket_service will look for a file with name config_dev.yaml
CMake doesn't copy static_config.yaml file from samples directory into build directory.

Now you can send messages to your server from another terminal:

bash
$ wscat --connect ws://localhost:8080/chat
Connected (press CTRL+C to quit)
> hello
< hello

Functional testing

Functional tests for the service could be implemented using the websocket_client fixture from pytest_userver.plugins.core in the following way:

async def test_echo(websocket_client):
async with websocket_client.get('chat') as chat:
await chat.send('hello')
response = await chat.recv()
assert response == 'hello'

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

pytest_plugins = ['pytest_userver.plugins.core']

Full sources

See the full example at: