Before you start
Make sure that you can compile and run core tests as described at Configure, Build and Install.
Step by step guide
Let's write a simple TCP server that accepts incoming connections, and as long as the client sends "hi" responds with greeting from configuration file.
TCP server
Derive from components::TcpAcceptorBase and override the ProcessSocket
function to get the new sockets:
namespace samples::tcp {
public:
static constexpr std::string_view kName = "tcp-hello";
: TcpAcceptorBase(config, context), greeting_(config[
"greeting"].As<
std::string>(
"hi")) {}
private:
const std::string greeting_;
};
}
- Warning
ProcessSocket
functions are invoked concurrently on the same instance of the class. Use synchronization primitives or do not modify shared data in ProcessSocket
.
Static config
Our new "tcp-hello" component should support the options of the components::TcpAcceptorBase and the "greeting" option. To achieve that we would need the following implementation of the GetStaticConfigSchema
function:
return yaml_config::MergeSchemas<TcpAcceptorBase>(R"(
type: object
description: |
Component for accepting incoming TCP connections and responding with some
greeting as long as the client sends 'hi'.
additionalProperties: false
properties:
greeting:
type: string
description: greeting to send to client
defaultDescription: hi
)");
}
Now lets configure our component in the components
section:
# yaml
tcp-hello:
port: 8180
greeting: hello
ProcessSocket
It's time to deal with new sockets. The code is quite straightforward:
std::string data;
data.resize(2);
const auto read_bytes = sock.ReadAll(data.data(), 2, {});
if (read_bytes != 2 || data != "hi") {
sock.Close();
return;
}
const auto sent_bytes = sock.SendAll(greeting_.data(), greeting_.size(), {});
if (sent_bytes != greeting_.size()) {
return;
}
}
}
int main()
Finally, add the component to the components::MinimalComponentList()
, and start the server with static configuration file passed from command line.
int main(int argc, const char* const argv[]) {
}
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-tcp_service
The sample could be started by running make start-userver-samples-tcp_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/tcp_service/userver-samples-tcp_service -c </path/to/static_config.yaml>
.
Now you can send a request to your server from another terminal:
bash
$ nc localhost 8180
hi
hello
Functional testing
Functional tests for the service could be implemented using the testsuite in the following way:
import pytest
async def test_basic(service_client, asyncio_socket, tcp_service_port):
sock = asyncio_socket.tcp()
await sock.connect(('localhost', tcp_service_port))
await sock.sendall(b'hi')
hello = await sock.recv(5)
assert hello == b'hello'
await sock.sendall(b'whats up?')
with pytest.raises(ConnectionResetError):
await sock.recv(1)
Note that in this case testsuite requires some help to detect that the service is ready to accept requests. To do that, override the pytest_userver.plugins.service.service_non_http_health_checks :
import pytest
pytest_plugins = ['pytest_userver.plugins.core']
USERVER_CONFIG_HOOKS = ['userver_config_tcp_port']
@pytest.fixture(scope='session')
def userver_config_tcp_port(choose_free_port):
def patch_config(config, _config_vars) -> None:
components = config['components_manager']['components']
tcp_hello = components['tcp-hello']
tcp_hello['port'] = choose_free_port(tcp_hello['port'])
return patch_config
@pytest.fixture(name='tcp_service_port', scope='session')
def _tcp_service_port(service_config) -> int:
components = service_config['components_manager']['components']
tcp_hello = components.get('tcp-hello')
assert tcp_hello, 'No "tcp-hello" component found'
return int(tcp_hello['port'])
@pytest.fixture(scope='session')
def service_non_http_health_checks(
service_config,
tcp_service_port,
) -> net.HealthChecks:
checks = net.get_health_checks_info(service_config)
checks.tcp.append(net.HostPort(host='localhost', port=tcp_service_port))
return checks
Full sources
See the full example at: