Removed socket mutexes. Added more tests. Improved examples.

Socket mutexes are no longer really required, and so have been removed.

Added more tests for network encoding functions, and the URL parser.

The URL parser now returns a path preceeded with a '/' instead of cutting it out. Added get_uri() to URL, for getting the whole URI, so users don't have to concat it themselves from the more specialised functions.

Fixed default socket connect timeout checking for the wrong value.

Fixed request_type_strings not containing all of the possible request types.

Fixed README using old socket close syntax.

Cleaned up the examples a bit.
This commit is contained in:
Fred Nicolson 2018-02-01 11:56:34 +00:00
parent ad847f0052
commit ff25d11089
16 changed files with 266 additions and 204 deletions

View File

@ -131,7 +131,7 @@ while(true)
} }
//Close connection //Close connection
client.close(); client.close_socket();
} }
``` ```
After binding to the port, we infinitely try and receive a new request, construct a response with the body of 'Hello, World!' and send it back to the client before closing the socket. fr::HttpRequest, and fr::HttpResponse both inherit fr::Sendable, which allows them to be sent and received through sockets just like fr::Packets. After binding to the port, we infinitely try and receive a new request, construct a response with the body of 'Hello, World!' and send it back to the client before closing the socket. fr::HttpRequest, and fr::HttpResponse both inherit fr::Sendable, which allows them to be sent and received through sockets just like fr::Packets.

View File

@ -1,8 +1,8 @@
add_executable(simple_client tcpsocket_client.cpp) add_executable(simple_http_client SimpleHttpClient.cpp)
target_link_libraries(simple_client frnetlib) target_link_libraries(simple_http_client frnetlib)
add_executable(simple_server tcpsocket_server.cpp) add_executable(simple_http_server SimpleHttpServer.cpp)
target_link_libraries(simple_server frnetlib) target_link_libraries(simple_http_server frnetlib)
install(TARGETS simple_client simple_server install(TARGETS simple_http_client simple_http_server
DESTINATION "bin") DESTINATION "bin")

View File

@ -0,0 +1,54 @@
//
// Created by fred.nicolson on 01/02/18.
//
#include <iostream>
#include <frnetlib/TcpSocket.h>
#include <frnetlib/HttpRequest.h>
#include <frnetlib/URL.h>
#include <frnetlib/HttpResponse.h>
int main()
{
//Get an address to query from stdin
std::string url;
std::cout << "Enter URL: ";
std::cin >> url;
//Parse it into something easy to use
fr::URL parsed_url(url);
if(parsed_url.get_port().empty())
{
std::cerr << "No schema or port specified. Unable to connect." << std::endl;
return EXIT_FAILURE;
}
//Try to connect to the parsed address
fr::TcpSocket socket;
if(socket.connect(parsed_url.get_host(), parsed_url.get_port(), {}) != fr::Socket::Success)
{
std::cerr << "Failed to connect to the specified URL" << std::endl;
return EXIT_FAILURE;
}
//Construct a request, requesting the user provided URI
fr::HttpRequest request;
request.set_uri(parsed_url.get_uri());
if(socket.send(request) != fr::Socket::Success)
{
std::cerr << "Failed to send HTTP request" << std::endl;
return EXIT_FAILURE;
}
//Now wait for a response
fr::HttpResponse response;
if(socket.receive(response) != fr::Socket::Success)
{
std::cerr << "Failed to receive HTTP response" << std::endl;
return EXIT_FAILURE;
}
//Print out the response body
std::cout << response.get_body() << std::endl;
}

View File

@ -0,0 +1,52 @@
//
// Created by fred.nicolson on 01/02/18.
//
#include <frnetlib/HttpRequest.h>
#include <frnetlib/HttpResponse.h>
#include <frnetlib/TcpListener.h>
#include <iostream>
int main()
{
fr::Socket::Status err;
fr::TcpSocket client;
fr::TcpListener listener;
//Bind to a port
if((err = listener.listen("8081")) != fr::Socket::Success)
{
std::cerr << "Failed to bind to port: " << err << std::endl;
return EXIT_FAILURE;
}
while(true)
{
//Accept a new connection
if((err = listener.accept(client)) != fr::Socket::Success)
{
std::cerr << "Failed to accept new connection: " << err << std::endl;
continue;
}
//Receive client HTTP request
fr::HttpRequest request;
if((err = client.receive(request)) != fr::Socket::Success)
{
std::cerr << "Failed to receive request from client: " << err << std::endl;
}
//Construct a response
fr::HttpResponse response;
response.set_body("<h1>Hello, World!</h1>");
//Send it
if((err = client.send(response)) != fr::Socket::Success)
{
std::cerr << "Failed to send response to client: " << err << std::endl;
}
//Close connection
client.close_socket();
}
}

View File

@ -1,90 +0,0 @@
#include <iostream>
#include <frnetlib/Packet.h>
#include <frnetlib/TcpSocket.h>
#include <frnetlib/TcpListener.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT "8081"
int send_a_packet(fr::TcpSocket &socket)
{
std::cout << "Going to send something..." << std::endl;
//Send the request
fr::Packet packet;
packet << "Hello there, I am " << (float)1.2 << " years old";
if(socket.send(packet) != fr::Socket::Success)
{
//Failed to send packet
std::cout << "Seems got something wrong when sending" << std::endl;
return -1;
}
//Receive a response
std::cout << "Waiting for a response..." << std::endl;
if(socket.receive(packet) != fr::Socket::Success)
{
std::cout << "Failed to receive server response!" << std::endl;
return -2;
}
//Extract the response, you can surround this in a try/catch block to catch errors
std::string str1, str2;
float age;
packet >> str1 >> age >> str2;
std::cout << "Server sent: " << str1 << ", " << age << ", " << str2 << "\n\n\n" << std::endl;
return 0;
}
int main()
{
//Try and connect to the server, create a new TCP object for the job and then connect
fr::TcpSocket socket; //Use an fr::SSLSocket if SSL
if(socket.connect(SERVER_IP, SERVER_PORT, std::chrono::seconds(20)) != fr::Socket::Success)
{
//Failed to connect
std::cout << "Failed to connect to: " << SERVER_IP << ":" << SERVER_PORT << std::endl;
return -1;
}
//For storing user input and send_a_packet response
std::string op_str;
int rtn = 0;
//Keep going until either we, or the server closes the connection
while(true)
{
//Ask the user what to do
std::cout << "Choose what you want to do, `c` for `continue`, `q` for `quit`:" << std::endl;
std::cin >> op_str;
if(op_str.length() > 1)
{
std::cout << "Seems that you inputted more than one character, please retry." << std::endl;
continue;
}
switch(op_str[0])
{
case 'c':
std::cout << "continue" << std::endl;
rtn = send_a_packet(socket);
break;
case 'q':
break;
default:
std::cout << "Invalid input!" << std::endl;
}
//Exit/error check
if(op_str[0] == 'q')
break;
if(rtn != 0)
break;
}
std::cout << "All done, bye!" << std::endl;
}

View File

@ -1,78 +0,0 @@
#include <iostream>
#include <frnetlib/Packet.h>
#include <frnetlib/TcpSocket.h>
#include <frnetlib/TcpListener.h>
#define PORT "8081"
int main()
{
//Create a new TCP socket to maintain connections, and a new Tcp Listener to accept connections
fr::TcpSocket client; //fr::SSLSocket for SSL
fr::TcpListener listener; //fr::SSLListener for HTTPS
//Bind to a port
if(listener.listen(PORT) != fr::Socket::Success)
{
std::cout << "Failed to bind to port: " << PORT << std::endl;
return -1;
}
std::cout << "Listener is listening on port: " << PORT << "..." << std::endl;
//Start accepting connections
while(true)
{
std::cout << "Waiting for a new connection ..." << std::endl;
//Accept a new connection
if(listener.accept(client) != fr::Socket::Success)
{
std::cout << "Failed to accept client, shutdown" << std::endl;
break;
}
while(true) //Infinite loop to keep the connection active
{
try
{
//Receive an fr::Packet from the client
fr::Packet packet;
if(client.receive(packet) != fr::Socket::Success)
{
std::cout << "Failed to receive request" << std::endl;
client.close_socket();
break;
}
//Extract the data from the packet
std::string str1, str2;
float age;
packet >> str1 >> age >> str2;
//Print out what we got
std::cout << "Client sent:" << str1 << ", " << age << ", " << str2 << std::endl;
//Send back the same packet
if(client.send(packet) != fr::Socket::Success)
{
throw std::string("Failed to send packet to client");
}
}
catch(const std::exception &e)
{
//Print out what happened
std::cout << "Error: " << e.what() << std::endl;
//Close connection
client.close_socket();
break;
}
}
}
return 0;
}

View File

@ -23,7 +23,7 @@ namespace fr
Put = 3, Put = 3,
Delete = 4, Delete = 4,
Patch = 5, Patch = 5,
RequestTypeCount = 3, RequestTypeCount = 6, //Keep me at the end and updated
}; };
enum RequestStatus enum RequestStatus
{ {

View File

@ -47,20 +47,10 @@ namespace fr
virtual ~Socket() noexcept = default; virtual ~Socket() noexcept = default;
Socket(Socket &&o) noexcept Socket(Socket &&o) noexcept
{ {
outbound_mutex.lock();
inbound_mutex.lock();
o.inbound_mutex.lock();
o.outbound_mutex.lock();
remote_address = std::move(o.remote_address); remote_address = std::move(o.remote_address);
is_blocking = o.is_blocking; is_blocking = o.is_blocking;
ai_family = o.ai_family; ai_family = o.ai_family;
max_receive_size = o.max_receive_size; max_receive_size = o.max_receive_size;
outbound_mutex.unlock();
inbound_mutex.unlock();
o.inbound_mutex.unlock();
o.outbound_mutex.unlock();
} }
/*! /*!
@ -195,7 +185,7 @@ namespace fr
/*! /*!
* Sets the maximum receivable size that may be received by the socket. This does * Sets the maximum receivable size that may be received by the socket. This does
* not apply to receive_raw(), but only things like fr::Packet, or HTTP responses. * not apply to receive_raw(), but only things like fr::Packet.
* *
* If a client attempts to send a packet larger than sz bytes, then * If a client attempts to send a packet larger than sz bytes, then
* the client will be disconnected and an fr::Socket::MaxPacketSizeExceeded * the client will be disconnected and an fr::Socket::MaxPacketSizeExceeded
@ -214,6 +204,7 @@ namespace fr
* Gets the max packet size. See set_max_packet_size * Gets the max packet size. See set_max_packet_size
* for more information. * for more information.
* *
*
* @return The max packet size * @return The max packet size
*/ */
inline uint32_t get_max_receive_size() inline uint32_t get_max_receive_size()
@ -240,8 +231,6 @@ namespace fr
std::string remote_address; std::string remote_address;
bool is_blocking; bool is_blocking;
std::mutex outbound_mutex;
std::mutex inbound_mutex;
int ai_family; int ai_family;
uint32_t max_receive_size; uint32_t max_receive_size;
}; };

View File

@ -32,7 +32,10 @@ namespace fr
explicit URL(const std::string &url); explicit URL(const std::string &url);
/*! /*!
* Parses a given URL, extracting its various components * Parses a given URL, extracting its various components. This will always
* try to make something out of the provided URL. It will also try its best
* to guess any potentially missing information. For example, if the URL is
* https://example.com, then the 'port' will be guessed as 443.
* *
* @param url The URL to parse * @param url The URL to parse
*/ */
@ -86,6 +89,25 @@ namespace fr
return fragment; return fragment;
} }
/*!
* Returns the combination of other URL elements into a single URI string.
*
* The URL is everything after the IP/Address & Port.
*
* @return The URL's URI
*/
inline std::string get_uri() const
{
std::string result;
if(!get_path().empty())
result += get_path();
if(!get_query().empty())
result += "?" + get_query();
if(!get_fragment().empty())
result += "#" + get_fragment();
return result;
}
/*! /*!
* Converts a string to a scheme enum. * Converts a string to a scheme enum.
* *

View File

@ -9,14 +9,19 @@
namespace fr namespace fr
{ {
const static std::string request_type_strings[Http::RequestType::RequestTypeCount] = {"UNKNOWN", "GET", "POST"}; const static std::string request_type_strings[Http::RequestType::RequestTypeCount] = {"UNKNOWN",
"GET",
"POST",
"PUT",
"DELETE",
"PATCH"};
Http::Http() Http::Http()
: request_type(Unknown), : request_type(Unknown),
uri("/"), uri("/"),
status(Ok) status(Ok)
{ {
static_assert(Http::RequestType::RequestTypeCount == 6, "Please update request_type_strings");
} }
Http::RequestType Http::get_type() const Http::RequestType Http::get_type() const

View File

@ -44,11 +44,9 @@ namespace fr
//Ensure that body doesn't exceed maximum length //Ensure that body doesn't exceed maximum length
if(body.size() > MAX_HTTP_BODY_SIZE) if(body.size() > MAX_HTTP_BODY_SIZE)
{
return fr::Socket::HttpBodyTooBig; return fr::Socket::HttpBodyTooBig;
}
//Cut off any data if it exceeds content length //Cut off any data if it exceeds content length, todo: potentially an issue, could cut the next request off
if(body.size() > content_length) if(body.size() > content_length)
body.resize(content_length); body.resize(content_length);
else if(body.size() < content_length) else if(body.size() < content_length)

View File

@ -41,7 +41,6 @@ namespace fr
if(!connected()) if(!connected())
return Socket::Disconnected; return Socket::Disconnected;
std::lock_guard<std::mutex> guard(inbound_mutex);
return obj.receive(this); return obj.receive(this);
} }

View File

@ -23,7 +23,6 @@ namespace fr
Socket::Status TcpSocket::send_raw(const char *data, size_t size) Socket::Status TcpSocket::send_raw(const char *data, size_t size)
{ {
std::lock_guard<std::mutex> guard(outbound_mutex);
size_t sent = 0; size_t sent = 0;
while(sent < size) while(sent < size)
{ {
@ -128,7 +127,7 @@ namespace fr
//Wait for the socket to do something/expire //Wait for the socket to do something/expire
timeval tv = {}; timeval tv = {};
tv.tv_sec = timeout.count() == -1 ? DEFAULT_SOCKET_TIMEOUT : timeout.count(); tv.tv_sec = timeout.count() == 0 ? DEFAULT_SOCKET_TIMEOUT : timeout.count();
tv.tv_usec = 0; tv.tv_usec = 0;
fd_set set = {}; fd_set set = {};
FD_ZERO(&set); FD_ZERO(&set);

View File

@ -38,12 +38,12 @@ namespace fr
{ {
auto scheme_pos = scheme_string_map.find(to_lower(url.substr(0, pos))); auto scheme_pos = scheme_string_map.find(to_lower(url.substr(0, pos)));
scheme = (scheme_pos == scheme_string_map.end()) ? URL::Unknown : scheme_pos->second; scheme = (scheme_pos == scheme_string_map.end()) ? URL::Unknown : scheme_pos->second;
parse_offset = pos + 3; parse_offset = pos + 3; // skip the ://
} }
//Check to see if there's a port //Check to see if there's a port
pos = url.find(':', parse_offset); pos = url.find(':', parse_offset);
if(pos != std::string::npos) if(pos != std::string::npos) //A port is provided
{ {
//Store host //Store host
host = url.substr(parse_offset, pos - parse_offset); host = url.substr(parse_offset, pos - parse_offset);
@ -55,7 +55,7 @@ namespace fr
port = url.substr(pos + 1, port_end - pos - 1); port = url.substr(pos + 1, port_end - pos - 1);
parse_offset = port_end + 1; parse_offset = port_end + 1;
} }
else else //There's no port
{ {
//Store host //Store host
pos = url.find('/', parse_offset); pos = url.find('/', parse_offset);
@ -96,7 +96,7 @@ namespace fr
pos = url.find('?', parse_offset); pos = url.find('?', parse_offset);
if(pos != std::string::npos) if(pos != std::string::npos)
{ {
path = url.substr(parse_offset, pos - parse_offset); path.append("/").append(url.substr(parse_offset, pos - parse_offset));
parse_offset = pos + 1; parse_offset = pos + 1;
} }
else else
@ -104,7 +104,7 @@ namespace fr
pos = url.find('#', parse_offset); pos = url.find('#', parse_offset);
pos = (pos != std::string::npos) ? pos : url.find('?', parse_offset); pos = (pos != std::string::npos) ? pos : url.find('?', parse_offset);
pos = (pos != std::string::npos) ? pos : url.size(); pos = (pos != std::string::npos) ? pos : url.size();
path = url.substr(parse_offset, pos - parse_offset); path.append("/").append(url.substr(parse_offset, pos - parse_offset));
parse_offset = pos + 1; parse_offset = pos + 1;
} }

View File

@ -0,0 +1,88 @@
//
// Created by fred.nicolson on 01/02/18.
//
#include <gtest/gtest.h>
#include "frnetlib/NetworkEncoding.h"
constexpr bool is_little_endian()
{
unsigned short x=0x0001;
auto p = reinterpret_cast<unsigned char*>(&x);
return *p != 0;
}
TEST(NetworkEncodingTest, test_htonf)
{
float input = std::numeric_limits<float>::max();
float result = htonf(input);
if(is_little_endian())
{
float manual;
std::reverse_copy((char*)&input, (char*)&input + sizeof(input), (uint8_t*)&manual);
ASSERT_EQ(memcmp(&result, &manual, sizeof(manual)), 0);
}
else
{
ASSERT_EQ(result, input);
}
}
TEST(NetworkEncodingTest, test_ntohf)
{
float input = std::numeric_limits<float>::max();
float encoded = htonf(input);
float decoded = ntohf(encoded);
ASSERT_EQ(input, decoded);
}
TEST(NetworkEncodingTest, test_htond)
{
double input = std::numeric_limits<double>::max();
double result = htond(input);
if(is_little_endian())
{
double manual;
std::reverse_copy((char*)&input, (char*)&input + sizeof(input), (uint8_t*)&manual);
ASSERT_EQ(memcmp(&result, &manual, sizeof(manual)), 0);
}
else
{
ASSERT_EQ(result, input);
}
}
TEST(NetworkEncodingTest, test_ntohd)
{
double input = std::numeric_limits<double>::max();
double encoded = htond(input);
double decoded = ntohd(encoded);
ASSERT_EQ(input, decoded);
}
TEST(NetworkEncodingTest, test_htonll)
{
uint64_t input = std::numeric_limits<uint64_t>::max();
uint64_t result = htonll(input);
if(is_little_endian())
{
uint64_t manual;
std::reverse_copy((char*)&input, (char*)&input + sizeof(input), (uint8_t*)&manual);
ASSERT_EQ(manual, result);
}
else
{
ASSERT_EQ(result, input);
}
}
TEST(NetworkEncodingTest, test_ntohll)
{
uint64_t input = std::numeric_limits<uint64_t>::max();
uint64_t encoded = htonll(input);
uint64_t decoded = ntohll(encoded);
ASSERT_EQ(input, decoded);
}

View File

@ -10,7 +10,7 @@ TEST(URLTest, full_parse)
fr::URL url("http://example.com:80/path/path?query=10&bob=20#frag"); fr::URL url("http://example.com:80/path/path?query=10&bob=20#frag");
ASSERT_EQ(url.get_host(), "example.com"); ASSERT_EQ(url.get_host(), "example.com");
ASSERT_EQ(url.get_scheme(), fr::URL::HTTP); ASSERT_EQ(url.get_scheme(), fr::URL::HTTP);
ASSERT_EQ(url.get_path(), "path/path"); ASSERT_EQ(url.get_path(), "/path/path");
ASSERT_EQ(url.get_query(), "query=10&bob=20"); ASSERT_EQ(url.get_query(), "query=10&bob=20");
ASSERT_EQ(url.get_fragment(), "frag"); ASSERT_EQ(url.get_fragment(), "frag");
} }
@ -38,6 +38,30 @@ TEST(URLTest, fragment_test)
TEST(URLTest, path_test) TEST(URLTest, path_test)
{ {
fr::URL url("example.com/path/hey#frag"); fr::URL url("example.com/path/hey#frag");
ASSERT_EQ(url.get_path(), "path/hey"); ASSERT_EQ(url.get_path(), "/path/hey");
ASSERT_EQ(url.get_fragment(), "frag"); ASSERT_EQ(url.get_fragment(), "frag");
}
TEST(URLTest, uri_test)
{
fr::URL url("http://example.com:80/path/path?query=10#frag");
ASSERT_EQ(url.get_uri(), "/path/path?query=10#frag");
}
TEST(URLTest, uri_test2)
{
fr::URL url("http://example.com:80/path/path?query=10");
ASSERT_EQ(url.get_uri(), "/path/path?query=10");
}
TEST(URLTest, uri_test3)
{
fr::URL url("http://example.com:80/path/path#frag");
ASSERT_EQ(url.get_uri(), "/path/path#frag");
}
TEST(URLTest, uri_test4)
{
fr::URL url("http://example.com:80/?bob=10#frag");
ASSERT_EQ(url.get_uri(), "/?bob=10#frag");
} }