My class design is as follows:
- The Main Application contains the objects: Orbital Aero Library, Network, and Visual Model Library (among others, but I'm just talking networking for now).
- The Network contains the OANet object (in the future, other network protocol objects).
- The OANet object contains the TCPServer and TCPClient objects and references to the Orbital Aero Library (so it can add/remove bodies) and the Visual Model Library (so it can associate visual models to the newly created entities)..
- The actual networking code is in the TCPServer and TCPClient objects. Function callbacks relay packets back up to OANet so incoming entities can be created and updated.
It's working really well. Currently each entity is updating its position on the network every frame. I don't have any kind of dead reckoning / threshold enabled (yet) which would allow me to cut down the packets but still keep the simulation in sync. That will be a future post.
Here's a screenshot of the TCP network in action:
The left panel is the TCP server creating only the nearby Earth (using a Moon visual model). The center panel is a TCP client creating one of the distant Moons. The right panel is another TCP client creating a second Moon. But all of the bodies are interacting / influencing each other in the simulation.
I'm only supporting one packet type now, an Entity State which shares where the body is in the world. Just that packet is enough for gravity influences to work great. Collisions won't reliably work between the separate simulations yet. I need to create a Collision style packet such that if one simulation detects a collision that it gets updated the same across all the simulations.
Orbital Aero App
I'll be covering just the the new network-specific code below, but it will be easier to follow that way. The TCPServer and TCPClient code can likely be a straight copy-paste into your own application, and then you'll just need to create your own variant of the OANet/Network code. Honestly for most software there is no need to separate the logic in OANet and Network. I plan on supporting multiple network protocols so it made since for me to split them.
Entity.h
Now that the software can support both internally and externally generated entities, I need to keep track of who originated / is managing those entities. I've added a new source variable to my Entity class and timeOfLastUpdate_seconds. The timeOfLastUpdate_seconds is used to timeout entities that have gone stale on the network (likely network dropped out / remote software shut down).
#pragma once
class BodyModel;
class VisualModel;
class Entity
{
public:
Entity(void);
Entity(BodyModel *newBody);
~Entity(void);
BodyModel *bodyObject;
VisualModel *visualModelObject;
bool isVisible;
enum class SourceEnum
{
Undefined = 0,
Internal = 1,
OANet = 2,
};
SourceEnum source;
double timeOfLastUpdate_seconds;
private:
};
If the body is internally created, I do:
newEntity->source = Entity::SourceEnum::Internal;
If the body is externally created from my OANet, I do:
incomingEntity->source = Entity::SourceEnum::OANet;
OrbitalAeroApp.cpp
I'm including the new Network.h header inside the main app file.
#include ".\Network\Network.h"
.
.
int main(int argc, char* argv[])
{
For testing purposes, I ask the user at program start if this program is standalone, a server, or a client.
For testing purposes, I ask the user at program start if this program is standalone, a server, or a client.
bool isStandAlone = false;
bool isOANetServer = false;
bool isOANetClient = false;
cout << "Network Type:" << endl;
cout << " 0 Stand Alone" << endl;
cout << " 1 TCP Server" << endl;
cout << " 2 TCP Client" << endl;
int networkInput;
cin >> networkInput;
switch (networkInput)
{
case 0: // Stand Alone
isStandAlone = true;
break;
case 1: // TCP Server
isOANetServer = true;
break;
case 2: // TCP Client
isOANetClient = true;
break;
}
The server and all clients that connect need to have a unique identification for their own entities so they don't conflict with each other. To do this, I'm asking the user to enter a unique Site ID; all bodies created by that instance of the software will increment off that Site ID to prevent duplicate IDs. Ultimately I'll have the server assign an identification to clients as they connect, but this is quick and easy to start with.
cout << "Site ID:" << endl;
int siteID;
cin >> siteID;
Each time a body is created, the Orbital Aero model pulls a new UInt64 value from a counter to uniquely identify the new body. Then the counter increments by 1 to be ready for the next new body. By initializing the counter to some large initial number based on the user's siteID, I can keep conflicts from happening (in all but very extreme situations). Ultimately I'll have a lower and upper bound for the indices so different networked simulations never step on each other.
printf("Initializing Orbital Aero...\n");
OrbitalAeroModel orbitalAero;
orbitalAero.setIndexCounter(siteID * 100000);
The network is connected based on the options entered earlier. Be sure to have the server running before starting the clients.
printf("Connecting Network...\n");
Network network(&orbitalAero, &visualModelLibrary);
if (isOANetServer)
{
network.startOANetServer(4000);
}
if (isOANetClient)
{
network.connectToOANetServer("127.0.0.1", 4000);
}
printf("- %lf\n", Timer::instance()->getElapsedTimeAndResetTimer_seconds(timerReference_seconds));
For testing I only create the Earth if it's a server, and I only create the moon if it's a client.
printf("Adding bodies...\n");
if (isStandAlone || isOANetServer)
{
addBodyEarth(orbitalAero, &visualModelLibrary.planet);
}
if (isStandAlone || isOANetClient)
{
addBodyMoon(orbitalAero, &visualModelLibrary.planet);
}
The main program loop (described in detail earlier)...
bool isRunning = true;
while (isRunning)
{
orbitalAero.update(0.2);
.
Inside the main loop after all the bodies have updated their positions, I update internally generated entities over the network so other simulations can have the latest positions. I also timeout externally generated entities that haven't been updated in a while (likely the network connection has failed.)
This update frequency (every frame) is very spammy on the network. The long term fix for this is called dead reckoning coupled with threshold breaking. Basically I should send the position of the entity along with its velocities, accelerations, etc. Receiving simulations can take the position I gave it along with the velocities and accelerations and continue flying out that entity without updated network packets. The local simulation keeps track of the true position of the entity and where other simulations think the entity is (based on the velocity/acceleration dead reckoning). When the truth and projected positions exceed a threshold, then the local simulation broadcasts a new packet to resync the entity across the network. Future code to write...
// Loop through all bodies and update them on the network.
map <uint64_t, BodyModel*>::iterator iteratorBody2 = orbitalAero.tableBodies.begin();
while (iteratorBody2 != orbitalAero.tableBodies.end())
{
BodyModel *loopBody = iteratorBody2->second;
// Increment iterator immediately, as the loopBody can be removed here which
// would invalidate the iterator if it still pointed to a removed body.
iteratorBody2++;
if (loopBody->isActive)
{
Entity *loopEntity = (Entity*)loopBody->hostData;
switch (loopEntity->source)
{
case Entity::SourceEnum::Internal:
network.updateEntityToNetwork(loopEntity);
break;
case Entity::SourceEnum::OANet:
if (Timer::instance()->getElapsedTime_seconds(loopEntity->timeOfLastUpdate_seconds) > 1.0)
{
// Entity hasn't updated in over a second, time out.
orbitalAero.removeBody(loopBody->index);
}
break;
}
} // (loopBody.isActive)
} // (iteratorBody2 != tableBodies.end())
Sleep(20);
}
Network
The Network class contains currently contains just my proprietary OANet. It includes functions so the application can start/stop the OANet server, connect/disconnect to the OANet server, or update an entity on the network.
Network.h
#include ".\OANet\OANet.h"
class Entity;
class OrbitalAeroModel;
class VisualModelLibrary;
class Network
{
private:
// oaNet networking object.
OANet oaNet;
public:
// Network Constructor
Network(OrbitalAeroModel* newOrbitalAero, VisualModelLibrary* newVisualModelLibrary);
// Network Destructor
~Network();
// Start the OANet server.
bool startOANetServer(int port);
// Stop the OANet server.
bool stopOANetServer();
// Connect to an OANet server.
bool connectToOANetServer(char ipAddress[16], int port);
// Disconnect from an OANet server.
bool disconnectFromOANetServer();
// Update an entity to active networks.
bool disconnectFromOANetServer();
// Update an entity to active networks.
bool updateEntityToNetwork(Entity* entity);
};
Network.cpp
#include "..\Entity.h"
#include ".\OANet\Packets\OANetEntityState.h"
#include "Network.h"
// Network Constructor
Network::Network(OrbitalAeroModel* newOrbitalAero, VisualModelLibrary* newVisualModelLibrary) :
oaNet(newOrbitalAero, newVisualModelLibrary)
{
return;
}
// Network Destructor
Network::~Network()
{
return;
}
// Start the OANet server.
bool Network::startOANetServer(int port)
{
return oaNet.startServer(port);
}
// Stop the OANet server.
bool Network::stopOANetServer()
{
return oaNet.stopServer();
}
// Connect to an OANet server.
bool Network::stopOANetServer()
{
return oaNet.stopServer();
}
// Connect to an OANet server.
bool Network::connectToOANetServer(char ipAddress[16], int port)
{
return oaNet.connectToServer(ipAddress, port);
}
// Disconnect from an OANet server.
bool Network::disconnectFromOANetServer()
{
return oaNet.disconnectFromServer();
}
// Update an entity to active networks.
bool Network::disconnectFromOANetServer()
{
return oaNet.disconnectFromServer();
}
// Update an entity to active networks.
bool Network::updateEntityToNetwork(Entity* entity)
{
if (oaNet.isServerActive() || oaNet.isConnectedToServer())
{
oaNet.sendEntityState(entity);
}
return true;
}
OANet
OANet is my proprietary network protocol for sharing entity information between simulations. It is based on a TCP Server <--> Client connection. A single Orbital Aero app acts as the server and any number of clients can connect to it.
OANet.h
#include "..\TCP\TCPServer.h"
#include "..\TCP\TCPClient.h"
class Entity;
class OrbitalAeroModel;
class VisualModelLibrary;
class OANet
{
private:
// TCP Server and Client objects that contain the actual network code.
TCPServer tcpServer;
TCPClient tcpClient;
// Reference to host orbital aero so the network can manipulate network created entities.
OrbitalAeroModel* orbitalAero;
// Reference to the visual library so visual models can be associated with incoming entities.
VisualModelLibrary* visualModelLibrary;
public:
// OANet Constructor
OANet(OrbitalAeroModel* newOrbitalAero, VisualModelLibrary* newVisualModelLibrary);
// OANet Destructor
~OANet();
// Static function used with callbacks for when a packet is received from the network.
static void callBackProcessIncomingPacket(void* hostNetwork, TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes);
// Function called when a packet is received from the network.
void processIncomingPacket(TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes);
// Send an entity over the network.
bool sendEntityState(Entity* entity);
// Start a TCP server at a specified port.
bool startServer(int tcpPort);
// Stop the TCP server.
bool stopServer();
// Return if we're actively hosting a server.
bool isServerActive();
// Connect as a client to a server at a specified IP address and port.
bool connectToServer(char ipAddress[16], int port);
// Disconnect from a server.
bool disconnectFromServer();
// Return if we're a client connected to a server.
bool disconnectFromServer();
// Return if we're a client connected to a server.
bool isConnectedToServer();
};
OANet.cpp
#include "OANet.h"
#include ".\Packets\OANetEntityState.h"
#include "..\..\Entity.h"
#include "..\..\VisualModelLibrary.h"
#include "..\..\..\OrbitalAeroModel\BodyModel.h"
#include "..\..\..\OrbitalAeroModel\OrbitalAeroModel.h"
#include "..\..\..\OrbitalAeroModel\Euler.h"
#include "..\..\..\OrbitalAeroModel\Vector.h"
#include "..\..\..\OrbitalAeroModel\Quaternion.h"
#include "..\..\..\OrbitalAeroModel\ControlSystemModel.h"
#include "..\..\..\OrbitalAeroModel\PartModel.h"
#include "..\..\..\OrbitalAeroModel\BodyModel.h"
#include "..\..\Timer.h"
// OANet Constructor
OANet::OANet(OrbitalAeroModel* newOrbitalAero, VisualModelLibrary* newVisualModelLibrary) :
// Initialize tcpServer and tcpClient variables.
// Pass in functions callbacks for when packets are received from the network.
tcpServer(this, OANet::callBackProcessIncomingPacket),
tcpClient(this, OANet::callBackProcessIncomingPacket)
{
// And hold references to the orbital aero and visual model library in this OANet object.
this->orbitalAero = newOrbitalAero;
this->visualModelLibrary = newVisualModelLibrary;
return;
}
// OANet Destructor
OANet::~OANet()
{
return;
}
// Static function used with callbacks for when a packet is received from the network.
void OANet::callBackProcessIncomingPacket(void* hostNetwork, TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes)
{
// Packet received from the network!
// Interpret the hostNetwork pointer as the OA Network, then call its processIncomingPacket function.
OANet* oaNet = (OANet*)hostNetwork;
oaNet->processIncomingPacket(sourceConnection, packetData, packetLength_bytes);
return;
}
// Function called when a packet is received from the network.
void OANet::processIncomingPacket(TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes)
{
// Packet received from the network.
// Parse the packet header.
OANetHeader* packetHeader = (OANetHeader*)packetData;
// Depending on the packet type, process as appropriate.
switch (packetHeader->packetType)
{
case OANetHeader::PacketTypes::EntityState:
// Entity State Packet
if (this->isServerActive())
{
// We are a TCP Server, relay incoming EntityState packets to all clients.
tcpServer.sendPacketToClients(sourceConnection, packetData, packetLength_bytes);
}
// Process the packet locally.
// Interpret the incoming data as an entity state structure.
OANetEntityState* entityStatePacket = (OANetEntityState*)packetData;
// Get the BodyModel for the incoming packet.
BodyModel* incomingBody;
Entity* incomingEntity;
if (orbitalAero->hasBody(entityStatePacket->index))
{
// Existing body found, get its reference from the orbital aero.
incomingBody = orbitalAero->getBody(entityStatePacket->index);
incomingEntity = (Entity*)incomingBody->hostData;
}
else
{
// Existing body not found, create a new body in the orbital aero.
orbitalAero->createBody(entityStatePacket->index);
incomingBody = orbitalAero->getBody(entityStatePacket->index);
// Establish an entity for the body and associate a visual model.
incomingEntity = new Entity(incomingBody);
incomingBody->hostData = incomingEntity;
incomingEntity->visualModelObject = &visualModelLibrary->planet;
// Set the source for the entity as OANet.
incomingEntity->source = Entity::SourceEnum::OANet;
}
incomingEntity->timeOfLastUpdate_seconds = Timer::instance()->getSystemTime_seconds();
// Update position & orientation from the packet.
incomingBody->location_meters = entityStatePacket->location_meters;
incomingBody->linearVelocity_metersPerSecond = entityStatePacket->linearVelocity_metersPerSecond;
incomingBody->linearAcceleration_metersPerSecond2 = entityStatePacket->linearAcceleration_metersPerSecond2;
incomingBody->orientation_quaternions = entityStatePacket->orientation_quaternions;
// Update physical properties from the packet.
incomingBody->massBase_kilograms = entityStatePacket->massBase_kilograms;
incomingBody->radius_meters = entityStatePacket->radius_meters;
break;
}
return;
}
// Start a TCP server at a specified port.
bool OANet::startServer(int tcpPort)
{
return tcpServer.startServer(tcpPort);
}
// Stop the TCP server.
bool OANet::stopServer()
{
return tcpServer.stopServer();
}
// Return if we're actively hosting a server.
bool OANet::isServerActive()
{
return tcpServer.isActive();
}
// Return if we're a client connected to a server.
bool OANet::isConnectedToServer()
{
return tcpClient.isConnected();
}
// Connect as a client to a server at a specified IP address and port.
bool OANet::connectToServer(char ipAddress[16], int port)
{
return tcpClient.tcpConnect(ipAddress, port);
}
// Disconnect from a server.
bool OANet::disconnectFromServer()
{
return tcpClient.tcpDisconnect();
}
// Send an entity to the connected networks.
bool OANet::sendEntityState(Entity* entity)
{
// Create an entity state packet from the entity object.
OANetEntityState outgoingEntityStatePacket(entity);
if (tcpServer.isActive())
{
// I am acting as a server, send the entity to all the clients.
tcpServer.sendPacketToClients(nullptr, (char*)&outgoingEntityStatePacket, sizeof(outgoingEntityStatePacket));
}
else if (tcpClient.isConnected())
{
// I am a client connected to a server, send only to the server.
tcpClient.sendPacketToTcp((char*)&outgoingEntityStatePacket, sizeof(outgoingEntityStatePacket));
}
return true;
}
OANet Packets
The OANet will eventually support many packets. To be able to parse different packet types, I'm creating a common OANetHeader that all packets will contain. The first item of that header is a single byte that contains the PacketType. The next item is the timeStamp the packet was sent.
OANetHeader.h
#pragma once
#include "stdint.h"
// Header for all OANet Network Messages
struct OANetHeader
{
// Enumerations of the Packet Types that are sent over OANet
enum class PacketTypes : uint8_t
{
EntityState = 1
};
// The packet type of the message.
PacketTypes packetType;
// The time stamp the packet was sent.
double timeStamp_seconds;
};
OANetEntityState.h
The first message I'm supporting is an Entity State. It includes the OANetHeader first (as all my network structures will) followed by the entity's unique index, location, velocity, acceleration, orientation, and other important properties.
#pragma once
#include "stdint.h"
#include "OANetHeader.h"
#include "..\..\..\..\OrbitalAeroModel\Vector.h"
#include "..\..\..\..\OrbitalAeroModel\Quaternion.h"
class Entity;
struct OANetEntityState
{
OANetHeader header;
uint64_t index; // Unique identification of this body.
Vector location_meters;
Vector linearVelocity_metersPerSecond;
Vector linearAcceleration_metersPerSecond2;
Quaternion orientation_quaternions;
double massBase_kilograms;
double radius_meters;
OANetEntityState(Entity* entity);
};
OANetEntityState.cpp
The constructor for the OANetEntityState takes in an Entity pointer and will populate the packet as necessary.
#include <vector>
#include "..\..\..\Entity.h"
#include "..\..\..\..\OrbitalAeroModel\Euler.h"
#include "..\..\..\..\OrbitalAeroModel\Vector.h"
#include "..\..\..\..\OrbitalAeroModel\Quaternion.h"
#include "..\..\..\..\OrbitalAeroModel\ControlSystemModel.h"
#include "..\..\..\..\OrbitalAeroModel\PartModel.h"
#include "..\..\..\..\OrbitalAeroModel\BodyModel.h"
#include "OANetHeader.h"
#include "OANetEntityState.h"
OANetEntityState::OANetEntityState(Entity* entity)
{
this->header.packetType = OANetHeader::PacketTypes::EntityState;
this->header.timeStamp_seconds = 0.0;
this->index = entity->bodyObject->index;
this->location_meters = entity->bodyObject->location_meters;
this->linearVelocity_metersPerSecond = entity->bodyObject->linearVelocity_metersPerSecond;;
this->linearAcceleration_metersPerSecond2 = entity->bodyObject->linearAcceleration_metersPerSecond2;
this->orientation_quaternions = entity->bodyObject->orientation_quaternions;
this->massBase_kilograms = entity->bodyObject->massBase_kilograms;
this->radius_meters = entity->bodyObject->radius_meters;
return;
}
TCP
Now into the meat of the networking code. I'm actually not going to go into too many details here as it's fairly well commented and covered in a previous blog post. The basic idea is I have a TCPServer class and a TCPClient class.
Creating a server in the TCPServer class causes it to spawn a new thread that listens for connections. When it establishes a new connection, it spools off another thread to handle communication with that new connection. The previous thread listening for connections continues in case someone else wants to connect. If another clients comes in, then another new thread is creating to listen for packets from that second client.
TCPServer.h
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <WinSock2.h>
#include <ws2tcpip.h>
#include <mutex>
#include <vector>
class TCPServer
{
public:
// Constant denoting the maximum packet size that can be handled.
static const int maximumPacketLength_bytes = 8192;
// Class that stores information related to each of the established connections.
class ConnectionModel
{
public:
SOCKET clientSocket;
// Add other variables as desired associated with this client connection.
// Constructor defining the client socket.
ConnectionModel(SOCKET newClientSocket)
{
this->clientSocket = newClientSocket;
}
};
// TCP Server Constructor
TCPServer(void* newHostNetwork, void(*newCallbackProcessIncomingPacket)(void* hostNetwork, TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes));
// TCP Server Destructor
~TCPServer();
// Start a TCP server at the given port.
bool startServer(int tcpPort);
// Stop the TCP server.
bool stopServer();
// Send a packet to all clients except to the client that originated the packet (sourceConnection).
bool sendPacketToClients(ConnectionModel* sourceConnection, char packetData[maximumPacketLength_bytes], int packetLength_bytes);
// Return if the server is active and listening for connections.
bool isActive();
private:
// Socket that establishes new connections.
SOCKET listenSocket;
// Flag if the server is active.
bool _isActive;
// Reference to the host network that instantiated the TCP client object.
// Packets will be relayed to the host network.
void* hostNetwork;
// Multithreading lock to protect changes to connections list while iterating through it.
std::mutex clientConnectionsMutex;
// List that holds all client connections.
std::vector<ConnectionModel*> clientConnections;
// Disconnect all clients. (This does not prevent new clients from connecting later.)
bool disconnectAllClients();
// Add a new client to the maintained connections.
ConnectionModel* addClient(SOCKET clientSocket);
// Remove a client from the maintained connections.
bool removeClient(ConnectionModel* clientConnection);
// Thread function listening for new clients.
void tcpClientListener();
// Thread function for listening for packets from clients.
void tcpPacketListener(ConnectionModel* clientConnection);
// Variable of the function callback in the host network to call when a packet is received.
void(*callbackProcessIncomingPacket)(void *hostNetwork, ConnectionModel* sourceConnection, char packetData[maximumPacketLength_bytes], int packetLength_bytes);
};
TCPServer.cpp
#include <process.h>
#include <stdlib.h>
#include <stdio.h>
#include <thread>
#include <iostream>
#pragma comment (lib, "Ws2_32.lib")
#include "TCPServer.h"
//#include <windows.h>
// TCP Server Constructor
TCPServer::TCPServer(void* newHostNetwork, void(*newCallbackProcessIncomingPacket)(void* hostNetwork, TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes))
{
this->hostNetwork = newHostNetwork;
this->callbackProcessIncomingPacket = newCallbackProcessIncomingPacket;
this->_isActive = false;
// Initialize Winsock.
WSADATA wsd;
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
std::cout << "WSAStartup failed." << std::endl;
}
return;
}
// TCP Server Destructor
TCPServer::~TCPServer()
{
WSACleanup();
return;
}
// Start a TCP server at the given port.
bool TCPServer::startServer(int tcpPort)
{
// Bound checking for given TCP port.
if (tcpPort <= 0 || tcpPort >= 65535)
{
std::cout << "Invalid port: " << tcpPort << std::endl;
return false;
}
std::cout << "Listening on port: " << tcpPort << std::endl;
// Setup address info hints.
struct addrinfo addressInfoHints;
memset(&addressInfoHints, 0, sizeof(addressInfoHints));
addressInfoHints.ai_family = AF_INET;
addressInfoHints.ai_socktype = SOCK_STREAM;
addressInfoHints.ai_protocol = IPPROTO_TCP;
addressInfoHints.ai_flags = AI_PASSIVE;
// Get address TCP port string.
char portString[20];
sprintf(portString, "%d", tcpPort);
// Resolve the server address and port.
struct addrinfo* addressInfoResult = NULL;
int getaddrinfoReturn = getaddrinfo(NULL, portString, &addressInfoHints, &addressInfoResult);
if (getaddrinfoReturn != 0)
{
std::cout << "getaddrinfo failed." << std::endl;
return false;
}
// Create a TCP socket to listen for clients.
listenSocket = socket(addressInfoResult->ai_family, addressInfoResult->ai_socktype, addressInfoResult->ai_protocol);
if (listenSocket == INVALID_SOCKET)
{
std::cout << "listen socket failed." << std::endl;
freeaddrinfo(addressInfoResult);
return false;
}
// Bind the TCP socket to the address.
int bindReturn = bind(listenSocket, addressInfoResult->ai_addr, (int)addressInfoResult->ai_addrlen);
if (bindReturn == SOCKET_ERROR)
{
std::cout << "bind socket failed." << std::endl;
freeaddrinfo(addressInfoResult);
closesocket(listenSocket);
return false;
}
freeaddrinfo(addressInfoResult);
// Initialize / clear the connections list.
clientConnections.clear();
// Start the TCP Client Listener thread to listen for connections.
std::thread{ &TCPServer::tcpClientListener, this }.detach();
this->_isActive = true;
return true;
}
// Stop the TCP server.
bool TCPServer::stopServer()
{
this->_isActive = false;
// Close the listen socket to stop receiving new connections.
closesocket(listenSocket);
// Disconnect all clients.
disconnectAllClients();
return true;
}
// Thread function listening for new clients.
void TCPServer::tcpClientListener()
{
// Listen for new connections until the listenSocket is closed.
while (1)
{
// listen will wait until a connection is requested on the socket. If the socket is closed
// while waiting, then listen will break from its wait and throw a socket error.
int listenResult = listen(this->listenSocket, SOMAXCONN);
if (listenResult != SOCKET_ERROR)
{
// Accept a client socket
SOCKET clientSocket = accept(this->listenSocket, NULL, NULL);
if (clientSocket != INVALID_SOCKET)
{
// Create a connection to the client and begin a listener thread.
ConnectionModel* clientConnection = addClient(clientSocket);
std::thread{ &TCPServer::tcpPacketListener, this, clientConnection }.detach();
}
else
{
std::cout << "TCP accept failed." << std::endl;
break;
}
}
else
{
std::cout << "TCP listen failed." << std::endl;
break;
}
}
return;
}
// Thread function for listening for packets from clients.
void TCPServer::tcpPacketListener(ConnectionModel* clientConnection)
{
char receiveBuffer[maximumPacketLength_bytes];
// Receive until the client connection is closed.
while (1)
{
// recv will wait until a packet is received on the socket. If the socket is closed
// while waiting, then recv will break from its wait and throw a socket error.
int packetLength_bytes = recv(clientConnection->clientSocket, receiveBuffer, maximumPacketLength_bytes, 0);
if (packetLength_bytes > 0)
{
// Relay the packet back to the host for processing.
callbackProcessIncomingPacket(this->hostNetwork, clientConnection, receiveBuffer, packetLength_bytes);
}
else if (packetLength_bytes == 0)
{
std::cout << "TCP connection closed." << std::endl;
break;
}
else
{
std::cout << "TCP socket receive error." << std::endl;
break;
}
};
// Client connection was closed, remove it from our connection list.
removeClient(clientConnection);
return;
}
// Add a new client to the maintained connections.
TCPServer::ConnectionModel* TCPServer::addClient(SOCKET clientSocket)
{
// Create a new connection for the socket.
ConnectionModel* clientConnection = new ConnectionModel(clientSocket);
clientConnectionsMutex.lock();
// Add the client to the connections list.
clientConnections.push_back(clientConnection);
std::cout << "Client added!" << std::endl;
std::cout << "Active Connections: " << clientConnections.size() << std::endl;
clientConnectionsMutex.unlock();
return clientConnection;
}
// Remove a client from the maintained connections.
bool TCPServer::removeClient(ConnectionModel* clientConnection)
{
clientConnectionsMutex.lock();
// Shutdown and close the socket.
shutdown(clientConnection->clientSocket, SD_BOTH);
closesocket(clientConnection->clientSocket);
// Remove the client from our connection list.
auto removedConnection = std::find(clientConnections.begin(), clientConnections.end(), clientConnection);
if (removedConnection != clientConnections.end())
{
clientConnections.erase(removedConnection);
}
// Free the connection memory.
delete clientConnection;
std::cout << "Client removed!" << std::endl;
std::cout << "Active Connections: " << clientConnections.size() << std::endl;
clientConnectionsMutex.unlock();
return true;
}
// Disconnect all clients. (This does not prevent new clients from connecting later.)
bool TCPServer::disconnectAllClients()
{
clientConnectionsMutex.lock();
// Loop through the connections and close each of their connections.
std::vector<ConnectionModel*>::iterator clientConnectionIterator;
for (clientConnectionIterator = clientConnections.begin(); clientConnectionIterator != clientConnections.end(); clientConnectionIterator++)
{
ConnectionModel* loopClientConnection = *clientConnectionIterator;
// Closing the socket will cause the listener threads for those sockets to close.
shutdown(loopClientConnection->clientSocket, SD_BOTH);
closesocket(loopClientConnection->clientSocket);
}
clientConnectionsMutex.unlock();
return true;
}
// Send a packet to all clients except to the client that originated the packet (sourceConnection).
bool TCPServer::sendPacketToClients(ConnectionModel* sourceConnection, char packetData[maximumPacketLength_bytes], int packetLength_bytes)
{
clientConnectionsMutex.lock();
// Loop through the connections and send the packet to each.
std::vector<ConnectionModel*>::iterator clientConnectionIterator;
for (clientConnectionIterator = clientConnections.begin(); clientConnectionIterator != clientConnections.end(); clientConnectionIterator++)
{
ConnectionModel* loopClientConnection = *clientConnectionIterator;
// Skip sending the packet if it's to the source.
if ((sourceConnection != nullptr) && (loopClientConnection->clientSocket == sourceConnection->clientSocket))
{
continue;
}
// Send the packet on the socket!
int bytesSent = send(loopClientConnection->clientSocket, packetData, packetLength_bytes, 0);
if (bytesSent == SOCKET_ERROR)
{
std::cout << "Send failed." << std::endl;
}
}
clientConnectionsMutex.unlock();
return true;
}
// Return if the server is active and listening for connections.
bool TCPServer::isActive()
{
return this->_isActive;
}
TCPClient.h
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <WinSock2.h>
#include <ws2tcpip.h>
#include "TCPServer.h"
class TCPClient
{
private:
// Flag if the client has an active connection to a server.
bool _isConnected;
// Thread function listening for TCP packets from the server.
void tcpListener();
// Socket of the TCP connection to the server.
SOCKET tcpSocket;
// Variable of the function callback in the host network to call when a packet is received.
void(*callbackProcessIncomingPacket)(void *hostNetwork, TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes);
// Reference to the host network that instantiated the TCP client object.
// Packets will be relayed to the host network.
void* hostNetwork;
public:
// Constant denoting the maximum packet size that can be handled.
static const int maximumPacketLength_bytes = 8192;
// Constructor for the TCP Client.
TCPClient(void* newHostNetwork, void(*newCallbackProcessIncomingPacket)(void* hostNetwork, TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes));
// Destructor for the TCP Client.
~TCPClient();
// Connect to the TCP server at the specified server address and port.
bool tcpConnect(char serverAddress[16], int tcpPort);
// Close the TCP connection with the server.
bool tcpDisconnect();
// Send a packet to the TCP server.
bool sendPacketToTcp(char packetData[maximumPacketLength_bytes], int packetLength_bytes);
// Return if currently connected to a server.
bool isConnected();
};
TCPClient.cpp
#include <process.h>
#include <stdio.h>
#include <map>
#include <string>
#include <iostream>
#include <sstream>
#include <time.h>
#include <thread>
#pragma comment (lib, "Ws2_32.lib")
#include "TCPClient.h"
// Constructor for the TCP Client.
TCPClient::TCPClient(void* newHostNetwork, void(*newCallbackProcessIncomingPacket)(void* hostNetwork, TCPServer::ConnectionModel* sourceConnection, char packetData[TCPServer::maximumPacketLength_bytes], int packetLength_bytes))
{
this->hostNetwork = newHostNetwork;
this->callbackProcessIncomingPacket = newCallbackProcessIncomingPacket;
_isConnected = false;
// Initialize Winsock.
WSADATA wsd;
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
std::cout << "WSAStartup failed." << std::endl;
}
return;
}
// Destructor for the TCP Client.
TCPClient::~TCPClient()
{
// Disconnect the TCP socket.
tcpDisconnect();
WSACleanup();
return;
}
// Connect to the TCP server at the specified server address and port.
bool TCPClient::tcpConnect(char serverAddress[16], int tcpPort)
{
// Setup address info hints.
struct addrinfo addressInfoHints;
memset(&addressInfoHints, 0, sizeof(addressInfoHints));
addressInfoHints.ai_family = AF_UNSPEC;
addressInfoHints.ai_socktype = SOCK_STREAM;
addressInfoHints.ai_protocol = IPPROTO_TCP;
// Resolve the server address.
char portString[20];
sprintf(portString, "%d", tcpPort);
struct addrinfo* addressInfoResult = NULL;
int getaddrinfoReturn = getaddrinfo(serverAddress, portString, &addressInfoHints, &addressInfoResult);
if (getaddrinfoReturn != 0)
{
std::cout << "getaddrinfo failed." << std::endl;
return false;
}
// Loop through potential server addresses to attempt connection.
for (struct addrinfo *ptr = addressInfoResult; ptr != NULL; ptr = ptr->ai_next)
{
// Create the TCP socket.
tcpSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (tcpSocket == INVALID_SOCKET)
{
std::cout << "TCP socket failed." << std::endl;
return false;
}
// Connect to the server.
int connectReturn = connect(tcpSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (connectReturn == SOCKET_ERROR)
{
closesocket(tcpSocket);
tcpSocket = INVALID_SOCKET;
continue;
}
break;
}
freeaddrinfo(addressInfoResult);
if (tcpSocket == INVALID_SOCKET)
{
std::cout << "Unable to connect to server!" << std::endl;
return false;
}
_isConnected = true;
// Start the TCP Listener thread.
std::thread{ &TCPClient::tcpListener, this }.detach();
return true;
}
// Close the TCP connection with the server.
bool TCPClient::tcpDisconnect()
{
// Shutdown and close the socket.
shutdown(tcpSocket, SD_BOTH);
closesocket(tcpSocket);
return true;
}
// Thread function listening for TCP packets from the server.
void TCPClient::tcpListener()
{
char receiveBuffer[maximumPacketLength_bytes];
// Receive until the connection is closed.
while (1)
{
// recv will wait until a packet is received on the socket. If the socket is closed
// while waiting, then recv will break from its wait and throw a socket error.
int packetLength_bytes = recv(tcpSocket, receiveBuffer, maximumPacketLength_bytes, 0);
if (packetLength_bytes > 0)
{
// Relay the packet back to the host for processing.
callbackProcessIncomingPacket(this->hostNetwork, nullptr, receiveBuffer, packetLength_bytes);
}
else if (packetLength_bytes == 0)
{
std::cout << "TCP connection closed." << std::endl;
break;
}
else
{
std::cout << "TCP socket receive error." << std::endl;
break;
} // receiveSize_bytes
}
_isConnected = false;
return;
}
// Return if currently connected to a server.
bool TCPClient::isConnected()
{
return _isConnected;
}
// Send a packet to the TCP server.
bool TCPClient::sendPacketToTcp(char packetData[maximumPacketLength_bytes], int packetLength_bytes)
{
// Send the packet.
int sendResult = send(tcpSocket, packetData, packetLength_bytes, 0);
if (sendResult == packetLength_bytes)
{
// Bytes successfully sent.
return true;
}
else if (sendResult < 0)
{
//std::cout << "TCP send failed at socket level." << std::endl;
return false;
}
else
{
//std::cout << "TCP failed to send entire message." << std::endl;
return false;
}
}
Copyright (c) 2017 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.