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:
task_processor: main-task-processor # Run socket accepts on CPU bound task processor
sockets_task_processor: main-task-processor # Run ProcessSocket() for each new socket on CPU bound task processor
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 socket
import pytest
async def test_basic(service_client, loop, tcp_service_port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', tcp_service_port))
await loop.sock_sendall(sock, b'hi')
hello = await loop.sock_recv(sock, 5)
assert hello == b'hello'
await loop.sock_sendall(sock, b'whats up?')
with pytest.raises(ConnectionResetError):
await loop.sock_recv(sock, 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']
@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: