Add first tcp server implementation
This commit is contained in:
commit
72121036a6
14
.clang-tidy
Normal file
14
.clang-tidy
Normal file
@ -0,0 +1,14 @@
|
||||
Checks: "-*,readability-identifier-naming"
|
||||
CheckOptions:
|
||||
readability-identifier-naming.NamespaceCase: lower_case
|
||||
readability-identifier-naming.EnumCase: CamelCase
|
||||
readability-identifier-naming.ClassCase: CamelCase
|
||||
readability-identifier-naming.StructCase: CamelCase
|
||||
|
||||
readability-identifier-naming.PrivateMemberCase: camelBack
|
||||
readability-identifier-naming.PrivateMemberPrefix: 'm_'
|
||||
|
||||
readability-identifier-naming.FunctionCase: lower_case
|
||||
readability-identifier-naming.VariableCase: camelBack
|
||||
readability-identifier-naming.GlobalVariablePrefix: 'g_'
|
||||
|
||||
4
.clangd
Normal file
4
.clangd
Normal file
@ -0,0 +1,4 @@
|
||||
CompileFlags:
|
||||
Remove: ['-fno-shrink-wrap', '-fstrict-volatile-bitfields', '-fno-tree-switch-conversion']
|
||||
Add: ['-std=c++23', '-D__cpp_concepts=202002L']
|
||||
|
||||
78
CMakeLists.txt
Normal file
78
CMakeLists.txt
Normal file
@ -0,0 +1,78 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# Global settings
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
project(playground)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# Dependencies
|
||||
#
|
||||
#
|
||||
|
||||
# spdlog
|
||||
|
||||
find_package(spdlog REQUIRED)
|
||||
|
||||
# Google Test
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
|
||||
)
|
||||
# For Windows: Prevent overriding the parent project's compiler/linker settings
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# Executabless
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
# add_executable(playground src/main.cpp src/tcp.cpp)
|
||||
# target_link_libraries(playground PRIVATE spdlog::spdlog)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# Tests
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
|
||||
enable_testing()
|
||||
|
||||
add_executable(
|
||||
tcp_client_test
|
||||
tests/tcp_client.cpp
|
||||
src/tcp.cpp
|
||||
)
|
||||
target_link_libraries(
|
||||
tcp_client_test
|
||||
spdlog::spdlog
|
||||
GTest::gtest_main
|
||||
)
|
||||
target_include_directories(
|
||||
tcp_client_test
|
||||
PRIVATE
|
||||
src
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(tcp_client_test)
|
||||
|
||||
15
scripts/tcp_client.py
Normal file
15
scripts/tcp_client.py
Normal file
@ -0,0 +1,15 @@
|
||||
import socket
|
||||
|
||||
|
||||
def main():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
sock.connect(("localhost", 1234))
|
||||
sock.sendall(b"asdf")
|
||||
data = sock.recv(1024)
|
||||
print(data.decode())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
80
scripts/tcp_server.py
Normal file
80
scripts/tcp_server.py
Normal file
@ -0,0 +1,80 @@
|
||||
import socket
|
||||
import sys
|
||||
import logging
|
||||
from time import sleep
|
||||
|
||||
|
||||
class TcpServer:
|
||||
def __init__(self, port, client_handler):
|
||||
self._port = port
|
||||
self._client_handler = client_handler
|
||||
|
||||
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._server_socket.setsockopt(
|
||||
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
server_address = ('0.0.0.0', port)
|
||||
self._server_socket.bind(server_address)
|
||||
self._server_socket.listen(1)
|
||||
|
||||
def handle_connection(self):
|
||||
try:
|
||||
connection, client_address = self._server_socket.accept()
|
||||
logging.info(f"Client connected: {client_address}")
|
||||
self._client_handler(connection)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit()
|
||||
|
||||
|
||||
def handle_client(connection):
|
||||
connection.settimeout(3.0)
|
||||
|
||||
while True:
|
||||
connection.sendall(
|
||||
b"""{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "SetLedPattern",
|
||||
"params": {
|
||||
"led": 19,
|
||||
"pattern": 3.1
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
try:
|
||||
res = connection.recv(1024)
|
||||
if not res:
|
||||
logging.info("Client disconnected")
|
||||
connection.close()
|
||||
break
|
||||
|
||||
logging.info(f"SetLedPattern response: {res.decode()}")
|
||||
except socket.timeout:
|
||||
logging.error("Connection timed out")
|
||||
break
|
||||
|
||||
sleep(1)
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
|
||||
datefmt='%H:%M:%S',
|
||||
level=logging.DEBUG,
|
||||
stream=sys.stdout)
|
||||
|
||||
port = 65432
|
||||
|
||||
server = TcpServer(port=port, client_handler=handle_client)
|
||||
logging.info(f"Started TCP server on port {port}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
server.handle_connection()
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"An error occurred while handling the client connection: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
101
src/main.cpp
Normal file
101
src/main.cpp
Normal file
@ -0,0 +1,101 @@
|
||||
#include "tcp.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
|
||||
int main() {
|
||||
spdlog::set_level(spdlog::level::debug);
|
||||
|
||||
tcp::NonBlockingClient client;
|
||||
|
||||
tcp::HostString host = {"192.168.0.12"};
|
||||
uint16_t port = 123;
|
||||
|
||||
spdlog::info("Connecting to {}:{}", host.data(), port);
|
||||
|
||||
if (!client.connect(host, port)) std::terminate();
|
||||
|
||||
tcp::ConnectionStatus status = tcp::ConnectionStatus::AttemptingConnection;
|
||||
while (status == tcp::ConnectionStatus::AttemptingConnection) {
|
||||
auto statusRes = client.get_last_connection_status();
|
||||
if (!statusRes) std::terminate();
|
||||
status = statusRes.value();
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case tcp::ConnectionStatus::Successful:
|
||||
spdlog::info("Connected successfully");
|
||||
break;
|
||||
case tcp::ConnectionStatus::Refused:
|
||||
spdlog::error("Connection refused");
|
||||
std::terminate();
|
||||
case tcp::ConnectionStatus::NoRouteToHost:
|
||||
spdlog::error("No route to host");
|
||||
std::terminate();
|
||||
case tcp::ConnectionStatus::TimedOut:
|
||||
spdlog::error("Connection timed out");
|
||||
std::terminate();
|
||||
case tcp::ConnectionStatus::HostUnreachable:
|
||||
spdlog::error("Host unreachable");
|
||||
std::terminate();
|
||||
case tcp::ConnectionStatus::OtherError:
|
||||
spdlog::error("Other error");
|
||||
std::terminate();
|
||||
case tcp::ConnectionStatus::AttemptingConnection:
|
||||
spdlog::error("Attempting connection");
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
char buffer[1024];
|
||||
|
||||
while (!client.data_available())
|
||||
;
|
||||
|
||||
auto recvRes = client.recv(std::span<char>(buffer, sizeof(buffer)));
|
||||
|
||||
if (!recvRes) {
|
||||
switch (recvRes.error()) {
|
||||
case tcp::Error::FailedToCreateSocket:
|
||||
spdlog::error("Failed to create socket");
|
||||
case tcp::Error::FailedToStartConnecting:
|
||||
spdlog::error("Failed to start connecting");
|
||||
case tcp::Error::FailedToReadSocketError:
|
||||
spdlog::error("Failed to read socket error");
|
||||
case tcp::Error::FailedToResolveHost:
|
||||
spdlog::error("Failed to resolve host");
|
||||
case tcp::Error::OtherOperationInProgress:
|
||||
spdlog::error("Other operation in progress");
|
||||
case tcp::Error::SocketNotConnected:
|
||||
spdlog::error("Socket not connected");
|
||||
case tcp::Error::BufferFull:
|
||||
spdlog::error("Buffer full");
|
||||
case tcp::Error::NoDataAvailable:
|
||||
spdlog::error("No data available");
|
||||
case tcp::Error::SendFailed:
|
||||
spdlog::error("Send failed");
|
||||
case tcp::Error::RecvFailed:
|
||||
spdlog::error("Recv failed");
|
||||
break;
|
||||
}
|
||||
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
spdlog::info("Received {} bytes", recvRes.value());
|
||||
if (recvRes.value() > 0) {
|
||||
spdlog::info("Received message: {}",
|
||||
std::string(buffer, recvRes.value()));
|
||||
}
|
||||
|
||||
EAGAIN;
|
||||
|
||||
const char* msg = "Hello, world!";
|
||||
auto sendRes = client.send(std::span<const char>(msg, strlen(msg)));
|
||||
if (!sendRes) {
|
||||
spdlog::error("Failed to send message");
|
||||
std::terminate();
|
||||
}
|
||||
spdlog::info("Sent {} bytes", sendRes.value());
|
||||
client.disconnect();
|
||||
|
||||
return 0;
|
||||
}
|
||||
259
src/tcp.cpp
Normal file
259
src/tcp.cpp
Normal file
@ -0,0 +1,259 @@
|
||||
#include "tcp.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <fcntl.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <poll.h>
|
||||
#include <span>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
std::expected<sockaddr_in, tcp::Error> get_server_address(const char* host,
|
||||
uint16_t port) {
|
||||
hostent* server = gethostbyname(host);
|
||||
if (server == nullptr) {
|
||||
spdlog::error("tcp: get_host_by_name() failed with h_errno={}",
|
||||
h_errno);
|
||||
return std::unexpected{tcp::Error::FailedToResolveHost};
|
||||
}
|
||||
|
||||
sockaddr_in address{};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(port);
|
||||
bcopy((char*)server->h_addr, (char*)&address.sin_addr.s_addr,
|
||||
server->h_length);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
namespace tcp {
|
||||
|
||||
|
||||
[[nodiscard]] std::expected<void, Error>
|
||||
NonBlockingClient::connect(HostString host, uint16_t port) {
|
||||
/// Resolve host
|
||||
|
||||
auto addrRes = get_server_address(host.data(), port);
|
||||
if (!addrRes) return std::unexpected{addrRes.error()};
|
||||
sockaddr_in serverAddress = addrRes.value();
|
||||
|
||||
/// Create socket
|
||||
|
||||
close_socket();
|
||||
if (auto res = create_socket(); !res) {
|
||||
return std::unexpected{res.error()};
|
||||
}
|
||||
|
||||
/// Connect
|
||||
|
||||
int connectRes = ::connect(m_socket, (struct sockaddr*)&serverAddress,
|
||||
sizeof(serverAddress));
|
||||
|
||||
if (connectRes < 0) {
|
||||
if (errno == EALREADY) {
|
||||
spdlog::error(
|
||||
"tcp::NonBlockingClient: connect() failed with errno={}",
|
||||
errno);
|
||||
return std::unexpected{Error::OtherOperationInProgress};
|
||||
} else if (errno != EINPROGRESS) {
|
||||
spdlog::error(
|
||||
"tcp::NonBlockingClient: connect() failed with errno={}",
|
||||
errno);
|
||||
return std::unexpected{Error::FailedToStartConnecting};
|
||||
}
|
||||
}
|
||||
|
||||
m_startedNewConAttemt = true;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<ConnectionStatus, Error>
|
||||
NonBlockingClient::get_last_connection_status() {
|
||||
if (!m_startedNewConAttemt) return m_lastKnownConStatus;
|
||||
|
||||
/// Check if connect operation has been completed
|
||||
|
||||
auto pendingOpRes = new_error_codes_available();
|
||||
if (!pendingOpRes) return std::unexpected{pendingOpRes.error()};
|
||||
if (pendingOpRes.value()) return ConnectionStatus::AttemptingConnection;
|
||||
|
||||
m_startedNewConAttemt = false;
|
||||
|
||||
/// Check for connection errors
|
||||
|
||||
int err = 0;
|
||||
socklen_t len = sizeof(err);
|
||||
if (getsockopt(m_socket, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
|
||||
spdlog::error(
|
||||
"tcp::NonBlockingClient: getsockopt() failed with errno={}", errno);
|
||||
return std::unexpected{Error::FailedToReadSocketError};
|
||||
}
|
||||
|
||||
if (err == 0) {
|
||||
m_lastKnownConStatus = ConnectionStatus::Successful;
|
||||
} else {
|
||||
switch (err) {
|
||||
case ECONNREFUSED:
|
||||
m_lastKnownConStatus = ConnectionStatus::Refused;
|
||||
break;
|
||||
case ENETUNREACH:
|
||||
m_lastKnownConStatus = ConnectionStatus::NoRouteToHost;
|
||||
break;
|
||||
case ETIMEDOUT:
|
||||
m_lastKnownConStatus = ConnectionStatus::TimedOut;
|
||||
break;
|
||||
case EHOSTUNREACH:
|
||||
m_lastKnownConStatus = ConnectionStatus::HostUnreachable;
|
||||
break;
|
||||
default:
|
||||
m_lastKnownConStatus = ConnectionStatus::OtherError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return m_lastKnownConStatus;
|
||||
}
|
||||
|
||||
void NonBlockingClient::disconnect() {
|
||||
close_socket();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<int, Error>
|
||||
NonBlockingClient::send(std::span<const char> data) {
|
||||
auto conRes = get_last_connection_status();
|
||||
if (!conRes) return std::unexpected{conRes.error()};
|
||||
if (conRes != ConnectionStatus::Successful) {
|
||||
spdlog::error("tcp::NonBlockingClient: Socket not connected");
|
||||
return std::unexpected{Error::SocketNotConnected};
|
||||
}
|
||||
|
||||
const int bytesWritten =
|
||||
::send(m_socket, (const char*)data.data(), data.size(), 0);
|
||||
|
||||
if (bytesWritten <= 0) {
|
||||
spdlog::error("tcp::NonBlockingClient: send() failed with errno={}",
|
||||
errno);
|
||||
|
||||
switch (errno) {
|
||||
case EWOULDBLOCK:
|
||||
return std::unexpected{Error::BufferFull};
|
||||
case EALREADY:
|
||||
return std::unexpected{Error::OtherOperationInProgress};
|
||||
default:
|
||||
return std::unexpected{Error::SendFailed};
|
||||
}
|
||||
}
|
||||
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
bool NonBlockingClient::data_available() {
|
||||
auto conRes = get_last_connection_status();
|
||||
if (!conRes) return false;
|
||||
if (conRes != ConnectionStatus::Successful) return false;
|
||||
|
||||
int ret = poll(&m_pfdIn, 1, 0);
|
||||
if (ret < 0) {
|
||||
spdlog::error("tcp::NonBlockingClient: poll() failed with errno={}",
|
||||
errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return (m_pfdIn.revents & POLLIN);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<int, Error>
|
||||
NonBlockingClient::recv(std::span<char> buffer) {
|
||||
const int bytesReceived =
|
||||
::recv(m_socket, (char*)buffer.data(), buffer.size(), 0);
|
||||
|
||||
if (bytesReceived < 0) {
|
||||
spdlog::error("tcp::NonBlockingClient: recv() failed with errno={}",
|
||||
errno);
|
||||
|
||||
switch (errno) {
|
||||
case EWOULDBLOCK:
|
||||
return std::unexpected{Error::NoDataAvailable};
|
||||
default:
|
||||
return std::unexpected{Error::RecvFailed};
|
||||
}
|
||||
}
|
||||
return {bytesReceived};
|
||||
}
|
||||
|
||||
void NonBlockingClient::close_socket() {
|
||||
if (shutdown(m_socket, 0) < 0) {
|
||||
spdlog::debug("tcp::NonBlockingClient: shutdown() failed with errno={}",
|
||||
errno);
|
||||
}
|
||||
|
||||
if (close(m_socket) < 0) {
|
||||
spdlog::debug("tcp::NonBlockingClient: close() failed with errno={}",
|
||||
errno);
|
||||
}
|
||||
|
||||
m_socket = -1;
|
||||
m_pfdOut.fd = -1;
|
||||
m_pfdIn.fd = -1;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, Error> NonBlockingClient::create_socket() {
|
||||
m_socket = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (m_socket < 1) {
|
||||
spdlog::error("tcp::NonBlockingClient: socket() failed with errno={}",
|
||||
errno);
|
||||
return std::unexpected{Error::FailedToCreateSocket};
|
||||
}
|
||||
|
||||
m_pfdOut.fd = m_socket;
|
||||
m_pfdIn.fd = m_socket;
|
||||
|
||||
auto currentFlags = fcntl(m_socket, F_GETFL, 0);
|
||||
if (currentFlags == -1) {
|
||||
spdlog::error("tcp::NonBlockingClient: fcntl() failed with errno={}",
|
||||
errno);
|
||||
close_socket();
|
||||
return std::unexpected{Error::FailedToCreateSocket};
|
||||
}
|
||||
|
||||
if (fcntl(m_socket, F_SETFL, (currentFlags) | O_NONBLOCK) == -1) {
|
||||
spdlog::error("tcp::NonBlockingClient: fcntl() failed with errno={}",
|
||||
errno);
|
||||
close_socket();
|
||||
return std::unexpected{Error::FailedToCreateSocket};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<bool, Error>
|
||||
NonBlockingClient::new_error_codes_available() {
|
||||
int ret = poll(&m_pfdOut, 1, 0);
|
||||
if (ret < 0) {
|
||||
spdlog::error("tcp::NonBlockingClient: poll() failed with errno={}",
|
||||
errno);
|
||||
return std::unexpected{Error::FailedToReadSocketError};
|
||||
}
|
||||
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
|
||||
} // namespace tcp
|
||||
94
src/tcp.hpp
Normal file
94
src/tcp.hpp
Normal file
@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <poll.h>
|
||||
#include <span>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
// C stdlib includes
|
||||
#include <arpa/inet.h>
|
||||
#include <expected>
|
||||
#include <fcntl.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
|
||||
namespace tcp {
|
||||
|
||||
|
||||
enum class Error {
|
||||
FailedToCreateSocket,
|
||||
FailedToStartConnecting,
|
||||
FailedToReadSocketError,
|
||||
FailedToResolveHost,
|
||||
OtherOperationInProgress,
|
||||
SocketNotConnected,
|
||||
BufferFull,
|
||||
NoDataAvailable,
|
||||
SendFailed,
|
||||
RecvFailed,
|
||||
};
|
||||
|
||||
|
||||
enum class ConnectionStatus {
|
||||
AttemptingConnection,
|
||||
Successful,
|
||||
Refused,
|
||||
NoRouteToHost,
|
||||
TimedOut,
|
||||
HostUnreachable,
|
||||
OtherError,
|
||||
};
|
||||
|
||||
|
||||
using HostString = std::array<char, 64>;
|
||||
|
||||
// struct ServerSettings {
|
||||
// uint16_t port = 0;
|
||||
// };
|
||||
//
|
||||
// class NonBlockingServer {
|
||||
// public:
|
||||
// NonBlockingServer(ServerSettings settings);
|
||||
// };
|
||||
|
||||
class NonBlockingClient {
|
||||
public:
|
||||
// clang-format off
|
||||
[[nodiscard]] std::expected<void, Error>
|
||||
connect(HostString host, uint16_t port);
|
||||
// clang-format off
|
||||
|
||||
void disconnect();
|
||||
|
||||
[[nodiscard]] std::expected<ConnectionStatus, Error>
|
||||
get_last_connection_status();
|
||||
|
||||
bool data_available();
|
||||
|
||||
[[nodiscard]] std::expected<int, Error> recv(std::span<char> buffer);
|
||||
[[nodiscard]] std::expected<int, Error> send(std::span<const char> data);
|
||||
|
||||
private:
|
||||
int m_socket = -1;
|
||||
pollfd m_pfdOut = {.fd = -1, .events = POLLOUT};
|
||||
pollfd m_pfdIn = {.fd = -1, .events = POLLIN};
|
||||
|
||||
bool m_startedNewConAttemt = false;
|
||||
ConnectionStatus m_lastKnownConStatus = ConnectionStatus::OtherError;
|
||||
|
||||
void close_socket();
|
||||
|
||||
[[nodiscard]] std::expected<void, Error> create_socket();
|
||||
[[nodiscard]] std::expected<bool, Error> new_error_codes_available();
|
||||
};
|
||||
|
||||
} // namespace tcp
|
||||
8
tests/tcp_client.cpp
Normal file
8
tests/tcp_client.cpp
Normal file
@ -0,0 +1,8 @@
|
||||
#include "tcp.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
TEST(TcpClient, Connect) {
|
||||
tcp::NonBlockingClient client;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user