Skip to content

Commit

Permalink
Add TCP server and client classes (#34)
Browse files Browse the repository at this point in the history
* Add TCP server and client classes

* Update tests

* Remove connect from TCP sockets
  • Loading branch information
domire8 authored May 30, 2023
1 parent c2600a1 commit 97ec448
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 1 deletion.
4 changes: 3 additions & 1 deletion source/communication_interfaces/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ add_library(${PROJECT_NAME} SHARED
${PROJECT_SOURCE_DIR}/src/sockets/ZMQPublisher.cpp
${PROJECT_SOURCE_DIR}/src/sockets/ZMQSubscriber.cpp
${PROJECT_SOURCE_DIR}/src/sockets/ZMQPublisherSubscriber.cpp
)
${PROJECT_SOURCE_DIR}/src/sockets/TCPSocket.cpp
${PROJECT_SOURCE_DIR}/src/sockets/TCPClient.cpp
${PROJECT_SOURCE_DIR}/src/sockets/TCPServer.cpp)
target_include_directories(${PROJECT_NAME} PUBLIC include)
target_link_libraries(${PROJECT_NAME} PUBLIC cppzmq)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#pragma once

#include "communication_interfaces/sockets/TCPSocket.hpp"

namespace communication_interfaces::sockets {

/**
* @struct TCPClientConfiguration
* @brief Configuration parameters for a TCP client
*/
struct TCPClientConfiguration {
std::string ip_address;
int port;
int buffer_size;
};

class TCPClient : public TCPSocket {
public:
/**
* @brief Constructor taking the configuration struct
* @param The configuration struct
*/
explicit TCPClient(TCPClientConfiguration configuration);

/**
* @copydoc ISocket::open()
* @details Connect the client socket to the server
*/
void open() override;

private:
TCPClientConfiguration config_; ///< Socket configuration struct
};
} // namespace communication_interfaces::sockets
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once

#include "communication_interfaces/sockets/TCPSocket.hpp"

namespace communication_interfaces::sockets {

/**
* @struct TCPServerConfiguration
* @brief Configuration parameters for a TCP server
*/
struct TCPServerConfiguration {
int port;
int buffer_size;
bool enable_reuse;
};

class TCPServer : public TCPSocket {
public:
/**
* @brief Constructor taking the configuration struct
* @param The configuration struct
*/
explicit TCPServer(TCPServerConfiguration configuration);

/**
* @brief Close the sockets by calling TCPServer::close()
*/
~TCPServer() override;

/**
* @copydoc ISocket::open()
* @details Wait for connection requests from clients and accept new connections. This method blocks until a
* connection is established
*/
void open() override;

/**
* @brief Close the sockets
*/
void close() override;

private:
TCPServerConfiguration config_; ///< Socket configuration struct
int server_fd_; ///< File descriptor of the connected server socket
};
} // namespace communication_interfaces::sockets
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once

#include <arpa/inet.h>

#include "communication_interfaces/sockets/ISocket.hpp"

namespace communication_interfaces::sockets {

/**
* @brief Abstract class to define a generic TCP socket
* @details TCP is a connection-based communication protocol. Hence, TCP sockets need to implement an additional
* interface method `connect()`.
*/
class TCPSocket : public ISocket {
public:
/**
* @brief Close the socket by calling TCPSocket::close()
*/
~TCPSocket() override;

/**
* @copydoc ISocket::receive_bytes(std::string&)
*/
bool receive_bytes(std::string& buffer) override;

/**
* @copydoc ISocket::receive_bytes(std::string&)
*/
bool send_bytes(const std::string& buffer) override;

/**
* @brief Close the socket
*/
void close() override;

protected:
explicit TCPSocket(int buffer_size);

sockaddr_in server_address_; ///< Address of the TCP server
int socket_fd_; ///< File descriptor of the socket
int buffer_size_; ///< Buffer size
};
} // namespace communication_interfaces::sockets
32 changes: 32 additions & 0 deletions source/communication_interfaces/src/sockets/TCPClient.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include "communication_interfaces/sockets/TCPClient.hpp"

#include <cstring>

#include "communication_interfaces/exceptions/SocketConfigurationException.hpp"

namespace communication_interfaces::sockets {

TCPClient::TCPClient(TCPClientConfiguration configuration) : TCPSocket(configuration.buffer_size), config_(configuration) {}

void TCPClient::open() {
try {
bzero((char*) &this->server_address_, sizeof(this->server_address_));
this->server_address_.sin_family = AF_INET;
this->server_address_.sin_port = htons(this->config_.port);
if (inet_pton(AF_INET, this->config_.ip_address.c_str(), &this->server_address_.sin_addr) <= 0) {
throw std::invalid_argument("IP Address not supported");
}
} catch (const std::exception& ex) {
throw exceptions::SocketConfigurationException("Socket configuration failed: " + std::string(ex.what()));
}

this->socket_fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (this->socket_fd_ < 0) {
throw exceptions::SocketConfigurationException("Opening socket failed");
}

if (::connect(this->socket_fd_, (sockaddr*) &this->server_address_, sizeof(this->server_address_)) != 0) {
throw exceptions::SocketConfigurationException("Connecting client failed");
}
}
} // namespace communication_interfaces::sockets
58 changes: 58 additions & 0 deletions source/communication_interfaces/src/sockets/TCPServer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include "communication_interfaces/sockets/TCPServer.hpp"

#include <cstring>
#include <unistd.h>

#include "communication_interfaces/exceptions/SocketConfigurationException.hpp"

namespace communication_interfaces::sockets {

TCPServer::TCPServer(TCPServerConfiguration configuration) :
TCPSocket(configuration.buffer_size), config_(configuration), server_fd_() {
}

TCPServer::~TCPServer() {
TCPServer::close();
}

void TCPServer::open() {
try {
bzero((char*) &this->server_address_, sizeof(this->server_address_));
this->server_address_.sin_family = AF_INET;
this->server_address_.sin_addr.s_addr = htonl(INADDR_ANY);
this->server_address_.sin_port = htons(this->config_.port);
} catch (const std::exception& ex) {
throw exceptions::SocketConfigurationException("Socket configuration failed: " + std::string(ex.what()));
}

// open stream oriented socket with internet address
this->server_fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (this->server_fd_ < 0) {
throw exceptions::SocketConfigurationException("Opening socket failed");
}
if (this->config_.enable_reuse) {
const int opt_reuse = 1;
if (setsockopt(this->server_fd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt_reuse, sizeof(opt_reuse)) != 0) {
throw exceptions::SocketConfigurationException("Setting socket option (enable reuse) failed");
}
}
if (bind(this->server_fd_, (sockaddr*) &(this->server_address_), sizeof(this->server_address_)) != 0) {
throw exceptions::SocketConfigurationException("Binding socket failed");
}
// listen for only 1 request at a time
listen(this->server_fd_, 1);
// receive a request from client using accept
sockaddr_in new_socket_address{};
socklen_t new_addr_len = sizeof(new_socket_address);
// accept, create a new socket descriptor to handle the new connection with client
this->socket_fd_ = accept(this->server_fd_, (sockaddr*) &new_socket_address, &new_addr_len);
if (this->socket_fd_ < 0) {
throw exceptions::SocketConfigurationException("Connecting server failed");
}
}

void TCPServer::close() {
::close(this->server_fd_);
TCPSocket::close();
}
} // namespace communication_interfaces::sockets
38 changes: 38 additions & 0 deletions source/communication_interfaces/src/sockets/TCPSocket.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "communication_interfaces/sockets/TCPSocket.hpp"

#include <unistd.h>
#include <vector>

#include "communication_interfaces/exceptions/SocketConfigurationException.hpp"

namespace communication_interfaces::sockets {

TCPSocket::TCPSocket(int buffer_size) : server_address_(), socket_fd_(), buffer_size_(buffer_size) {
if (buffer_size <= 0) {
throw exceptions::SocketConfigurationException("Configuration parameter 'buffer_size' has to be greater than 0.");
}
}

TCPSocket::~TCPSocket() {
TCPSocket::close();
}

bool TCPSocket::receive_bytes(std::string& buffer) {
std::vector<char> local_buffer(this->buffer_size_);
auto receive_length = recv(this->socket_fd_, local_buffer.data(), this->buffer_size_, 0);
if (receive_length < 0) {
return false;
}
buffer.assign(local_buffer.data(), local_buffer.size());
return true;
}

bool TCPSocket::send_bytes(const std::string& buffer) {
int send_length = send(this->socket_fd_, buffer.data(), buffer.size(), 0);
return send_length == static_cast<int>(buffer.size());
}

void TCPSocket::close() {
::close(this->socket_fd_);
}
} // namespace communication_interfaces::sockets
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#include <gtest/gtest.h>
#include <thread>

#include "communication_interfaces/sockets/TCPClient.hpp"
#include "communication_interfaces/sockets/TCPServer.hpp"

using namespace communication_interfaces;

class TestTCPSockets : public ::testing::Test {
public:
TestTCPSockets() {
server_ = std::make_shared<sockets::TCPServer>(sockets::TCPServerConfiguration{6000, 50, true});
client_ = std::make_shared<sockets::TCPClient>(sockets::TCPClientConfiguration{"127.0.0.1", 6000, 50});
}

std::thread start_server() {
return std::thread([this] { this->serve(); });
}

void serve() const {
server_->open();
std::string recv_message;
EXPECT_TRUE(server_->receive_bytes(recv_message));
EXPECT_STREQ(recv_message.c_str(), client_message_.c_str());
EXPECT_TRUE(server_->send_bytes(server_message_));
}

std::thread start_client() {
return std::thread([this] { this->client(); });
}

void client() const {
client_->open();
EXPECT_TRUE(client_->send_bytes(client_message_));
std::string recv_message;
EXPECT_TRUE(client_->receive_bytes(recv_message));
EXPECT_STREQ(recv_message.c_str(), server_message_.c_str());
}

std::string client_message_ = "Hello server";
std::string server_message_ = "Hello client";

std::shared_ptr<sockets::TCPClient> client_;
std::shared_ptr<sockets::TCPServer> server_;
};

TEST_F(TestTCPSockets, TestCommunication) {
std::thread server = start_server();
usleep(100000);
std::thread client = start_client();

client.join();
server.join();
}

0 comments on commit 97ec448

Please sign in to comment.