This is an outline of a simple implementation to connect remote machines with a TCP or a UDP connection (UDP hasn't been set up yet)
The code works on all three systems (Linux, MacOS, and Windows). The library works, but there is still more to come.
- About the Program
- Compiling the tests
- Library Features
- Add links to the functions/classes descriptions.
- How to use Simple-Networking
- TO DO list
I wrote this program to gain experience with network programming. This project has lead to a lot of learning and I've come to see a whole new way to write code. I based a lot of what I did in this project of Lewis Van Winkle's wonderful book, Hands-On Network Programming with C. If you're looking for a comprehensive, engaging, and easy to follow book on networking, I can't recommend this book enough.
Not only is the book a great resource for network programming, Lewis Van Winkle himself is great at explaining complex ideas in a simple way that makes sense, is easy to follow, and is easy to replicate and advance. He is also willing to engage with his readers. When I was reading his book, I was struggling with wrapping my mind around the secure networking sections (Chapter 9/10), I sent him an email. He responded within a few days and was kind, supportive, helpful, and just overall wonderful. In summary, wondeful book and wonderful author! 😊
This program is basically me implementing everything I learned in that book. I am still working on some of it, but I've implemented the bigger aspects of networking so far (with a tcp_server and a tcp_client).
What I'm still planning on adding:
DNS-Querying.Emailingfunctions/objects.https/https structures(which allow for https communication).udp_serverandudp_clientforUDPcommunication.
The program itself has been written in C++23, though should work in C++17+.
The library can be used in most other networking programs as long as it's compiled in the proper manner.
The manner of compilation here can be applied to any other project this library is included in.
The structure of the program is as follows:
.
├── CMakeLists.txt
├── README.md
├── files // For testing purposes. Not required to run Simple-Networking
│ ├── cert.pem
│ └── key.pem
├── headers
│ ├── include
│ ├── included
│ ├── misc_functions
│ ├── networking
│ └── string_functions
├── libraries
│ ├── misc_functions.c++
│ ├── networking.c++
│ └── string_functions.c++
├── objects
│ └── test_server
└── tests
└── test_server.c++
Within this structure, the all the files in the headers directory are C/C++ header files.
(I just like the look of them without a file type extension), and they hold the prototypes for
functions and methods implemented in the libraries directory. The include file in the headers directory only serves as a header where all the basic macro definitions are defined that are used throughout the libraries/*c++ files. During compilation, the header files in the headers directory must be linked to their counterparts in the libraries directory.
In the CMakeLists.txt file, this line:
include_directories(${CMAKE_SOURCE_DIR}/headers)
tells the compiler to look at the files in the headers directory whenever an include statement
is called. Then the next code snippet in the CMakeLists.txt file, this one,
# Gather library sources
file(GLOB LIB_SOURCES
"${CMAKE_SOURCE_DIR}/libraries/*.c++"
"${CMAKE_SOURCE_DIR}/libraries/*.cpp"
"${CMAKE_SOURCE_DIR}/libraries/*.cc"
"${CMAKE_SOURCE_DIR}/libraries/*.cxx"
)
Is used to gather all the files that will be linked to the test executable. The test executable is created when the make command is executed and the executable object files are placed within the objects directory.
This library works well, but to run it, OpenSSL will need to be installed on the system. In this code package here, I created a testing program that will run the networking code. It demonstrates how the code is structured and how to use the coding library. To view these tests, take a look in the tests directory.
To compile this library as part of your own application, be sure to link all the files in the
headers & libraries directory. Also be sure to OpenSSL installed
and findable by your compiler during compilation time.
In this testing environment, I've installed OpenSSL and I've made sure it's findable by the compiler being used for compilation.
If you want to ensure that the compiler can find where you have OpenSSL installed, be sure to use use a good package manager for the installation of OpenSSL.
- On mac, Homebrew
- On linux (I used Ubuntu) - Installing OpenSSL on Ubuntu
- On Windows, I wasn't able to find a good reference (Ya, windows is not nice...) This is what worked for me instead:
- On Windows, I wasn't able to find a single good reference (Ya, windows is not nice...) So after some digging, I found a work around that worked for me:
-
- Download and install MSYS2
- Open MSYS2
- Upgrade the package installer:
- pacman -Syu
- Install OpenSSl :
- pacman -S mingw-w64-x86_64-openssl
- The packages should be installed now. Verify by checking if:
- Headers are in : /mingw64/include
- Libraries are in : /mingw64/lib
- DLLs are in : /mingw64/bin
- If you want to compile using powershell, you'll have to link the path to where the MSYS2 compiler is located and where the OpenSSL compiler is installed (In step 3, the MSYS2 compiler was installed along with openssl). In my CMakeLists.txt file, I have the commented out line that links the OpenSSL root directory location (this could be different on your machine). But I also linked the location where MSYS2 installed OPENSSL and the MSYS2 compiler (C:\mingw64\mingw64\bin) to the path variable in system environment.
-
Then in CMakeLists.txt, if you want the compiler to find the OpenSSL library, be sure to include these lines, no matter what system is being used:
find_package(OpenSSL REQUIRED)
target_link_libraries(your_target PRIVATE OpenSSL::SSL OpenSSL::Crypto)
The CMakeLists.txt file works well on unix systems and on windows systems too. Furthermore, the CMakeLists.txt file has been confirmed to work on MSYS2.
In order to run the cmake file, I've been running the commands (these commands are also in the cmake file at the top):
Unix : cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
make
Powershell:
cmake .. -G "MinGW Makefiles" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DOPENSSL_ROOT_DIR=/mingw64 // Not strictly necessary if directory was added to system path.
cmake --build build
MSYS2:
cmake .. -G "MinGW Makefiles" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DOPENSSL_ROOT_DIR=/mingw64 // This is still necessary even if OPENSSL is in the system variable path. It failed for me if I didn't have this linking.
cmake --build buid
- The secure connection has been tested and it works great so far.
To run the secure connection, be sure to be situated in the
build directory (if it doesn't exist, create it in the same directory
where the
CMakeLists.txtfile exists then enter into it) and run the commands above. Run the cmake command first, then once that's done, run themake command(makeon Unix, andcmake --build buildon Windows). After themake commandsuccessfully finishes it's execution, the executables will have been created and placed into theobjectsdirectory. Again this is only to create the tests for the networking library that are included in this codebase.
-
All the functions, objects, and features that this library hold are located within the
networkingnamespace. -
Data types:
- All the
primitivenetworking data types are still available (i.e ints for sockets on unix machines and SOCKETs for windows sockets), but their types have been abstracted away to allow for cross platform code. Here's a list of the networking types and their abstracted away working type names:
- All the
| type/func | Lin/Mac | Win | abstraction |
|---|---|---|---|
| adapter ptr | struct ifaddrs* |
PIP_ADAPTER_ADDRESSES |
adapter_type |
| adapter name | std::string(the_adapter->ifa_name) |
std::string(the_adapter->FriendlyName) |
get_adapter_name() |
| next adapter | the_adapter->ifa_next |
this_adapter->Next |
get_next_adapter() |
| addr from adapter | the_adapter |
this_adapter->FirstUnicastAddress |
get_address_from_adapter() |
| free adapters | freeifaddrs(the_adapters) |
std::free(the_adapters) |
free_adapters() |
| addr ptr | struct ifaddrs* |
PIP_ADAPTER_UNICAST_ADDRESS |
address_type |
| next addr | NULL |
this_address->Next |
get_next_address() |
| addr sockaddr | this_address->ifa_addr |
this_address->Address.lpSockaddr |
get_address_sockaddr() |
| addr sockaddr len | sizeof(*this_address->ifa_addr) |
this_address->Address.iSockaddrLength |
get_address_sockaddrlen() |
| addr family | this_address->ifa_addr->sa_family |
this_address->Address.lpSockaddr->sa_family |
get_address_family() |
| socket error | (errno) |
(WSAGetLastError()) |
socket_error() |
| error string | gai_strerror(error_number) |
gai_strerrorA(error_number) |
socket_error_string() |
| socket type | int |
SOCKET |
socket_type |
| socket family type | unsigned short (Linux),unsigned char (Mac) |
int |
socket_family_type |
| invalid socket | -1 |
INVALID_SOCKET |
invalid_socket |
| valid socket | (this_socket >= 0) |
(this_socket != invalid_socket) |
valid_socket() |
| close socket | close(the_socket) |
closesocket(the_socket) |
close_socket() |
-
Data types:
- And for dealing with the OpenSSL. This was not necessary and is still not necessary to use, but I find these renames easier to follow along
| data type/function | Linux/macOS | Windows | abstracted type/macro |
|---|---|---|---|
| secure socket type pointer (OpenSSL) | SSL* |
SSL* |
secure_socket_type |
| networking context pointer (OpenSSL) | SSL_CTX* |
SSL_CTX* |
context_type |
| certificate pointer (OpenSSL) | X509* |
X509* |
certificate_type |
| invalid secure socket | nullptr |
nullptr |
invalid_secure_socket |
| invalid context | nullptr |
nullptr |
invalid_context |
| invalid certificate | nullptr |
nullptr |
invalid_certificate |
| valid secure socket | (the_socket != invalid_secure_socket) |
(the_socket != invalid_secure_socket) |
valid_secure_socket() |
| valid context | (the_context != invalid_context) |
(the_context != invalid_context) |
valid_context() |
| valid certificate | (certificate != invalid_certificate) |
(certificate != invalid_certificate) |
valid_certificate() |
-
A networking exceptions specific to this networking library.
-
All networking exceptions are located within the namespace exceptions.
-
All exceptions are children of the
base_exceptiontype. So any of them can be caught as abase_exceptionreference:catch (networking::exceptions::base_exception& except) { // To see the exception type std::cerr << except.type() << std::endl; // To see the message generated for the exception std::cerr << except.message() << std::endl; // To see the file where the exception was thrown from std::cerr << except.file() << std::endl; // To se the line where the exception was thrown from std::cerr << except.error_line() << std::endl; // To see the error number std::cerr << except.error_number() << std::endl; // To print the error message associated with this exception being thrown. except.print(); }
-
-
A
network_address_familiesnamespace used for retrieving address families.-
There are two important aspects to the
network_address_familiesnamespace:- The Relevant adapter:
-
This is assigned at compilation time. It's the adapter that is referenced most on the machine. I assigned them based off my machine, but your machine could be different. Be sure to check the name of your adapter on your machine and change this to match that name for smooth utilization of the networking structures.
-
To retrieve rel_adapter, you can use the
machine_adapters()function to retrieve all the adapters on your current machie, then change this value to match what you want to be the adapter that is referenced most often on your machine. -
rel_adapteris the default parameter for the following method (So be sure to either pass in a value for this method, or change rel_adapter to something that will be promising):networking::network_structures::host::retrieve_hostname(std::unordered_set<std::string> {rel_adapter})
-
Platform rel_adapter value Windows ( crap_os)"Wi-Fi 3" macOS ( mac_os)"en0" Linux (other Unix) "enp0s8" - The family constants:
Constant Name Value Platform(s) unspec_address_family "Unspecified Address Family" All unrecognized_address_family "Unrecognized Address Family" All ip_version4_address_family "IP Version 4 Family" All ip_version6_address_family "IP Version 6 Family" All link_layer_address_family "Link-Layer Interface Address Family" macOS (Unix) netlink_address_family "Netlink Address Family" Linux (Unix, not macOS) packet_address_family "Packet Address Family" Linux (Unix, not macOS) netbios_address_family "NetBIOS Address Family" Windows irda_address_family "IrDa Address Family" Windows bluetooth_address_family "Bluetooth Address Family" Windows - The Relevant adapter:
-
The other aspect of the
network_address_familiesnamespace are the two functions:-
get_addresses_families()function which retrieves astd::set<std::string>of all the family constants that the Platform uses. -
resolve_address_to_string(const socket_type the_family)Converts thesocket_family_typeSee Library Features above into astd::stringin thefamily constantsabove.
-
-
-
Networking namespace functions:
-
This is a function that is really only useful for windows systems. But it works on all platforms. Windows systems need to have their networking libraries initialized. This function simply checks if the network was initialized within the context of using this networking library during the current runtime session.
-
Return type is a
bool. -
Namespace parent is
networking.
-
Again this is really only useful on windows systems. It initializes the networking library and returns
truewithin the context of the networking library namespace during the current runtime session,falseif it was not initialized, or an exception. -
Return type is a
bool. -
Namespace parent is
networking.
-
Again really only useful on windows systems. It uninitializes the networking library and returns
trueif the network was successfully uninitialized within the context of the networking library namespace during the current runtime session,falseif it was not or an exception is thrown. -
Return type is a
bool. -
Namespace parent is
networking.
-
Check if the OpenSSL library has been initialized within the context of the networking namespace during the current runtime session. Returns
trueif the library is initialized,falseif it's not. -
Return type is a
bool. -
Namespace parent is
networking.
-
This is to initialize the OpenSSL secure networking library within the context of the networking library namespace during the current runtime session. There are currently no checks for whether or not the network was initialized though, so this will pretty much always initialize the library then return
true. -
Return type is a
bool. -
Namespace parent is
networking.
-
Uninitialize the OpenSSL secure networking library within the context of the networking library namespace during the current runtime session, so this will pretty much always uninitilize the library then return
true. -
Return type is a
bool. -
Namespace parent is
networking.
resolve_hostname(const std::string hostname, const std::stirng port = default_port, const bool name = false)
resolve_hostname(const std::string hostname, const std::stirng port = default_port, const bool name = false)-
Parameters:
data type parameter name default value Notes const std::string hostname no default value, must be set when called. This is the hostname to be resolved into an IP address. This hostname's IP address(es) are what are returned. const std::string port default_port (macro - #define "8080") This is the port to use for the DNS query. Not usually necessary to change it, but it can be changed if necessary. const bool name false The name flag is used to specify whether or not to use the NI_NAMEREQDmacro in the call to retrieve the name information for the address. This is a flag because it can take a while to retrieve this data, for some reason, this is especially true on the windows system I've been using. -
Returns a
std::unordered_set<std::string>with all the IP addresses that were resolved for thehostnamepassed in. -
If the networking library fails to initilize, a
initialize_network_failureexception is thrown. -
Namespace parent is
networking.
- Parameters:
| data type | parameter name | default value | Notes |
|---|---|---|---|
| const bool | names | false | This is the same as resolve_hostname's names parameter. This specified whether or not to use the NI_NAMREQD with the getnameinfo function. It can take a while on windows machines, so it might not be worth using. |
-
Return type is a
std::unordered_map<std::string, std::unordered_map<std::string, std::set<std::string> > > -
Namespace parent is
networking.
-
This still hasn't been tested, but it's supposed to check if the socket is in a blocking state or not. If the socket is blocking, then
trueis returned, if it's not blocking or an error occured,falseis returned. -
So be sure to always check if the socket passed in is still valid after using this function.
-
Return type is
bool -
Namespace parent is
networking.
-
Set the socket passed in to blocking or non-blocking, depending on what
blockis.trueto set the socket to blocking,falseto set it to non-blocking. -
Return type is
bool -
Namespace parent is
networking.
-
Honestly, this is a bad function. It's almost always unreliable and breaks sockets. Gonna get rid of it soon.
-
Return type is
bool -
Namespace parent is
networking.
send_message(networking::network_structures::host_connection host, const data* the_message, const bytes byte_count, const int flags = 0, const std::chrono::duration<int> timeout = std::chrono::seconds(10))
send_message(networking::network_structures::host_connection host, const data* the_message, const bytes byte_count, const int flags = 0, const std::chrono::duration<int> timeout = std::chrono::seconds(10))-
This function sends a message to the host that is specified with the
hostparameters. -
Return type is
networking::network_structures::host_report -
Namespace parent is
networking.
*Note : These structures are implemened to be have their own comparison operations and they implement two different types of comparisions.
*Another Note : Another very useful book I used to learn about C++'s Standard Library and it's algorithms is Data Structuresa nd Algorithms in C++ Pocket Primer, by Lee Wittenberg. This book showed me basic data structures defined within C++'s Standard library, and it also went over how the data structures are implemented. For example it showed how a std::set is implemented using a Binary Search Tree, and how an std::unordered_set is implemented using a hash table. All the explanations in the book are short and easy to follow. This is good to learn about coding in C++, but also to learn about data structures & algorithms in general.
-
All the members of this struct are public.
-
The
client_idstructure is used primarily as a map key in thetcp_serverto keep track of all the connected clients. This structure consists of three different strings (All of which have to be set manually):hostnameportconnection_time
-
2 Constructors:
-
Default constructor:
client_id()
-
Parameter constructor:
client_id(const std::string hname, const std::string hport, const std::string ctime_)const std::string hname: The hostname of the client structure being pointed to.const std::string hport: The port this client structre being pointed to.const std::string ctime_: The time when the client connected to this machine.
-
-
Operator overrides:
-
Operator<() const: Allows for theclient_idobject to be used in anstd::set(),std::map(), and their unordered_counterparts too.- Parameters:
const client_id& other: The otherclient_idto be compared with. This comparision has been implemented to compare all three internal strings. This comparison also strongly compares thehostnameof eachclient_id, and weakly compares theportandconnection_time.
- Parameters:
-
Operator==() const: Used for comparisons between differentclient_idobjects.-
Parameters:
const client_id& other: The otherclient_idstructure to be compared with. This comparison uses thestd::string_functions::same_stringfunction because it allows for string comparisons in a case independent manner. This comparison also strongly compares thehostnameof eachclient_id, and weakly compares theportandconnection_time.
-
Returns a
bool.
-
-
Operator bool() const: This method is used to check if theclient_idis defined or not.-
To be defined, the
client_idneeds to have a hostname defined to it. Theportandconnection_timedon't need to be defined. -
Returns a
bool.
-
-
Operator=: Assigns anotherclient_idto the currentclient_id.-
Parameters:
const client_id& other: The otherclient_idto assign to the currentclient_id.
-
Returns a
client_idreference to the currentclient_idobject.
-
-
-
The
host_connectionis used as the base class for theclient_connection&server_connectionstructs. -
The
host_connectionstruct consists of three data types (Again all of which need to be set manually):host_informationof typeclient_id.connect_socketof typesocket_type.secure_connect_socketof typesecure_socket_type.
-
2 Constructors:
- Default constructor:
host_connection()
- Parameter constructor:
host_connection(client_id client_, const socket_type sock_, secure_socket_type sec_sock)client_: The identifier for thishost_connectionstructure.connect_socket: The socket that is used for creating connection to the remote machine.sec_sock: The secure connection socket that is used to establish secure connections.
- Default constructor:
-
Operator overrides:
-
Operator<() const: Used to compare ahost_connectionwith anotherhost_connection. This simply compares theclient_idobjects with eachhost_connection.-
Parameters:
const host_connection& other: The otherhost_connectionto compare the currenthost_connectionwith. -
Returns a
bool.
-
-
Operator==() const: This strongly compares thehost_connectionobjects'client_ids andconnect_sockets. It also weakly compares thehost_connection'ssecure_connect_sockets.-
Parameters:
const host_connection& other: The otherhost_connectionto compare thishost_connectionagainst. -
This is a
virtualmethod. -
Returns a
bool.
-
-
Operator bool() const: This operator checks that thehost_connection'shost_informationis true (utilizing theclient_id's operator bool()), and if theconnect_socketof thishost_connectionis valid or not. Thesecure_connect_socketis not checked in this operator.- Returns a
bool.
- Returns a
-
Operator=(): The assignment operator. Performs a deep copy of all data attributes ofother's data attributes, tothis's data attributes (providedthisis notother).- Returns a
host_connectionreference to the currenthost_connection.
- Returns a
-
-
A
host_reportis a struct that is the atomic type for host communication. It consists of the attributes:hostof typehost_connectionwhich holds the name of the host that either sent of received data.byte_countof typebyteswhich holds the total number of bytes that were sent or received (depending on the context in which thehost_reportis being returned).successof typeboolwhich states if the send or receive was successful.
-
3 Constructors:
-
Default constructors:
host_report
-
Parameter constructor:
host_report(const host_connection con_host, bytes total_bytes, const bool succeeded)- TODO: Add explanations
host_report(const std::string hname, const std::string hport, const std::string hctime, const socket_type conn_sock, secure_socket_type sec_sock)- TODO: Add explanations
-
-
Operator overrides:
-
Operator==() const: The comparison operator for thishost_report. It compares thishost_report'shost,byte_count, andsuccessdata fields, to theotherhost_report'shost,byte_count, andsuccessfields.-
Parameters:
otherof typeconst host_report&: The otherhost_reportto compare to the currenthost_reportobject.
-
Returns a
bool.
-
-
Operator bool() const: Theboolcheck for thishost_report. It returns value of thesuccessdata field.- Returns a
bool.
- Returns a
-
Operator<() const: The comparison operator that allows thishost_reportto be used instd::setandstd::mapand theirunorderedpartner types.-
Parameters:
otherof typeconst host_report&: The otherhost_reportto use for a comparison with the currenthost_report.
-
Returns a
bool.
-
-
Operator()=: This is the assignment operator to change the values of thishost_reportto the values ofother's field values.-
Parameters:
otherof typeconst host_report&: Thehost_reportwhose values will be assigned to thishost_report. Ifotheris the samehost_reportas thehost_reportcalling it, then nothing is changed.
-
Returns a reference to the current
host_report.
-
-
- A
complete_reportis a
Complete
- Make all the functions and objects thread safe when in execution. ✅
- Make the structures utilize operation overloading, allowing for simplified code. ✅
- Make sure the code works on all operating systems. ✅
- Implement the code so it works with both secure and non-secure connections. ✅
- Make the tcp_client capable of timing out when establishing a connection. ✅ (BUT STILL NEEDS TO BE TESTED)
- Make the tcp_server and tcp_client capable of sending any data type across the network connection. ✅ (BUT STILL NEEDS TO BE TESTED)
- Investigate if there's a way to check if the windows networking library has already been implemented, and if the secure networking library has already been initialized or not.
- Write documentation.✅ (STILL MORE TO COME)
- Add javadoc comments to all methods and functions (In progress- Still documenting the tcp_client object's methods)
- Write http_server and http_client classes that utilize the tcp_server and tcp_client classes for establishing connections.