Make sure that you can compile and run core tests as described at Configure, Build and Install and took a look at the TCP half-duplex server with static configs validation.
Let's write a TCP echo server. It should accept incoming connections, read the data from socket and send the received data back concurrently with read. The read/write operation continues as long as the socket is open.
We would also need production quality metrics and logs for the service.
Derive from components::TcpAcceptorBase and override the ProcessSocket
function to get the new sockets:
ProcessSocket
functions are invoked concurrently on the same instance of the class. Use synchronization primitives or do not modify shared data in ProcessSocket
.struct Stats
holds the statistics for the component and is defined as:
To automatically deliver the metrics they should be registered via utils::statistics::MetricTag and DumpMetric+ResetMetric functions should be defined:
Now the tag could be used in component constructor to get a reference to the struct Stats
:
Lets configure our component in the components
section:
tcp-echo:
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: 8181
We also need to configure the HTTP server and the handle that responds with statistics:
server:
listener:
port: 8182 # ...to listen on this port and...
task_processor: monitor-task-processor # ...process incoming requests on this task processor.
handler-server-monitor:
path: /service/monitor
method: GET
task_processor: monitor-task-processor
monitor-handler: false
The full-duplex communication means that the same engine::io::Socket is concurrently used for sending and receiving data. It is safe to concurrently read and write into socket. We would need two functions:
Those two functions could be implemented in the following way:
Now it's time to handle new sockets. In the ProcessSocket function consists of the following steps:
The tracing::Span and utils::Async work together to produce nice logs that allow you to trace particular file descriptor:
On scope exit (for example because of the exception or return) the destructors would work in the following order:
send_task
- it cancels the coroutine and waits for it finishFinally, add the component to the components::MinimalServerComponentList()
, and start the server with static configuration file passed from command line.
To build the sample, execute the following build steps at the userver root directory:
The sample could be started by running make start-userver-samples-tcp_full_duplex_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_full_duplex_service/userver-samples-tcp_full_duplex_service -c </path/to/static_config.yaml>
.
Now you can send a request to your server from another terminal:
$ nc localhost 8181
hello
hello
test test test
test test test
Functional tests for the service and its metrics could be implemented using the testsuite in the following way:
Note that in this case testsuite requires some help to detect that the service is ready to accept requests. To do that, override the service_non_http_health_checks:
See the full example at: