1
0
Fork 0

cNetwork: Added UDP API.

This commit is contained in:
Mattes D 2015-02-20 14:28:05 +01:00
parent 70d54054e3
commit 9c5162041e
14 changed files with 1588 additions and 48 deletions

View File

@ -31,11 +31,12 @@ local Server = cNetwork:Listen(1024, ListenCallbacks);
specific plugin-provided function is called. The callbacks are stored in tables which are passed
to the API functions, each table contains multiple callbacks for the various situations.</p>
<p>
There are three different callback variants used: LinkCallbacks, LookupCallbacks and
ListenCallbacks. Each is used in the situation appropriate by its name - LinkCallbacks are used
There are four different callback variants used: LinkCallbacks, LookupCallbacks, ListenCallbacks
and UDPCallbacks. Each is used in the situation appropriate by its name - LinkCallbacks are used
for handling the traffic on a single network link (plus additionally creation of such link when
connecting as a client), LookupCallbacks are used when doing DNS and reverse-DNS lookups, and
ListenCallbacks are used for handling incoming connections as a server.</p>
connecting as a client), LookupCallbacks are used when doing DNS and reverse-DNS lookups,
ListenCallbacks are used for handling incoming connections as a server and UDPCallbacks are used
for incoming UDP datagrams.</p>
<p>
LinkCallbacks have the following structure:<br/>
<pre class="prettyprint lang-lua">
@ -111,7 +112,7 @@ local ListenCallbacks =
OnError = function (a_ErrorCode, a_ErrorMsg)
-- The specified error has occured while trying to listen
-- No other callback will be called for this lookup from now on
-- No other callback will be called for this server handle from now on
-- This callback is called before the cNetwork:Listen() call returns
-- All returned values are ignored
end,
@ -124,6 +125,25 @@ local ListenCallbacks =
end,
}
</pre></p>
<p>
UDPCallbacks have the following structure:<br/>
<pre class="prettyprint lang-lua">
local UDPCallbacks =
{
OnError = function (a_ErrorCode, a_ErrorMsg)
-- The specified error has occured when trying to listen for incoming UDP datagrams
-- No other callback will be called for this endpoint from now on
-- This callback is called before the cNetwork:CreateUDPEndpoint() call returns
-- All returned values are ignored
end,
OnReceivedData = function ({{cUDPEndpoint|a_UDPEndpoint}}, a_Data, a_RemotePeer, a_RemotePort)
-- A datagram has been received on the {{cUDPEndpoint|endpoint}} from the specified remote peer
-- a_Data contains the raw received data, as a string
-- All returned values are ignored
end,
}
</pre>
]],
},
@ -268,12 +288,31 @@ g_Server = nil
Functions =
{
Connect = { Params = "Host, Port, LinkCallbacks", Return = "bool", Notes = "(STATIC) Begins establishing a (client) TCP connection to the specified host. Uses the LinkCallbacks table to report progress, success, errors and incoming data. Returns false if it fails immediately (bad port value, bad hostname format), true otherwise. Host can be either an IP address or a hostname." },
CreateUDPEndpoint = { Params = "Port, UDPCallbacks", Return = "{{cUDPEndpoint|UDPEndpoint}}", Notes = "(STATIC) Creates a UDP endpoint that listens for incoming datagrams on the specified port, and can be used to send or broadcast datagrams. Uses the UDPCallbacks to report incoming datagrams or errors. If the endpoint cannot be created, the OnError callback is called with the error details and the returned endpoint will report IsOpen() == false. The plugin needs to store the returned endpoint object for as long as it needs the UDP port open; if the endpoint is garbage-collected by Lua, the socket will be closed and no more incoming data will be reported." },
HostnameToIP = { Params = "Host, LookupCallbacks", Return = "bool", Notes = "(STATIC) Begins a DNS lookup to find the IP address(es) for the specified host. Uses the LookupCallbacks table to report progress, success or errors. Returns false if it fails immediately (bad hostname format), true if the lookup started successfully. Host can be either a hostname or an IP address." },
IPToHostname = { Params = "Address, LookupCallbacks", Return = "bool", Notes = "(STATIC) Begins a reverse-DNS lookup to find out the hostname for the specified IP address. Uses the LookupCallbacks table to report progress, success or errors. Returns false if it fails immediately (bad address format), true if the lookup started successfully." },
Listen = { Params = "Port, ListenCallbacks", Return = "{{cServerHandle|ServerHandle}}", Notes = "(STATIC) Starts listening on the specified port. Uses the ListenCallbacks to report incoming connections or errors. Returns a {{cServerHandle}} object representing the server. If the listen operation failed, the OnError callback is called with the error details and the returned server handle will report IsListening() == false. The plugin needs to store the server handle object for as long as it needs the server running, if the server handle is garbage-collected by Lua, the listening socket will be closed and all current connections dropped." },
},
}, -- cNetwork
cServerHandle =
{
Desc =
[[
This class provides an interface for TCP sockets listening for a connection. In order to listen, the
plugin needs to use the {{cNetwork}}:Listen() function to create the listening socket.</p>
<p>
Note that when Lua garbage-collects this class, the listening socket is closed. Therefore the plugin
should keep it referenced in a global variable for as long as it wants the server running.
]],
Functions =
{
Close = { Params = "", Return = "", Notes = "Closes the listening socket. No more connections will be accepted, and all current connections will be closed." },
IsListening = { Params = "", Return = "bool", Notes = "Returns true if the socket is listening." },
},
}, -- cServerHandle
cTCPLink =
{
Desc =
@ -307,24 +346,24 @@ g_Server = nil
},
}, -- cTCPLink
cServerHandle =
cUDPEndpoint =
{
Desc =
[[
This class provides an interface for TCP sockets listening for a connection. In order to listen, the
plugin needs to use the {{cNetwork}}:Listen() function to create the listening socket.</p>
Represents a UDP socket that is listening for incoming datagrams on a UDP port and can send or broadcast datagrams to other peers on the network. Plugins can create an instance of the endpoint by calling {{cNetwork}}:CreateUDPEndpoint(). The endpoints are callback-based - when a datagram is read from the network, the OnRececeivedData() callback is called with details about the datagram. See the additional information in {{cNetwork}} documentation for details.</p>
<p>
Note that when Lua garbage-collects this class, the listening socket is closed. Therefore the plugin
should keep it referenced in a global variable for as long as it wants the server running.
Note that when Lua garbage-collects this class, the listening socket is closed. Therefore the plugin should keep this object referenced in a global variable for as long as it wants the endpoint open.
]],
Functions =
{
Close = { Params = "", Return = "", Notes = "Closes the listening socket. No more connections will be accepted, and all current connections will be closed." },
IsListening = { Params = "", Return = "bool", Notes = "Returns true if the socket is listening." },
Close = { Params = "", Return = "", Notes = "Closes the UDP endpoint. No more datagrams will be reported through the callbacks, the UDP port will be closed." },
EnableBroadcasts = { Params = "", Return = "", Notes = "Some OSes need this call before they allow UDP broadcasts on an endpoint." },
GetPort = { Params = "", Return = "number", Notes = "Returns the local port number of the UDP endpoint listening for incoming datagrams. Especially useful if the UDP endpoint was created with auto-assign port (0)." },
IsOpen = { Params = "", Return = "bool", Notes = "Returns true if the UDP endpoint is listening for incoming datagrams." },
Send = { Params = "RawData, RemoteHost, RemotePort", Return = "bool", Notes = "Sends the specified raw data (string) to the specified remote host. The RemoteHost can be either a hostname or an IP address; if it is a hostname, the endpoint will queue a DNS lookup first, if it is an IP address, the send operation is executed immediately. Returns true if there was no immediate error, false on any failure. Note that the return value needn't represent whether the packet was actually sent, only if it was successfully queued." },
},
}, -- cServerHandle
}, -- cUDPEndpoint
}

View File

@ -84,6 +84,48 @@ g_PluginInfo =
},
}, -- lookup
udp =
{
Subcommands =
{
close =
{
Handler = HandleConsoleNetUdpClose,
ParameterCombinations =
{
{
Params = "[Port]",
Help = "Closes the UDP endpoint on the specified port [1024].",
}
},
}, -- close
listen =
{
Handler = HandleConsoleNetUdpListen,
ParameterCombinations =
{
{
Params = "[Port]",
Help = "Listens on the specified UDP port [1024], dumping the incoming datagrams into log",
},
},
}, -- listen
send =
{
Handler = HandleConsoleNetUdpSend,
ParameterCombinations =
{
{
Params = "[Host] [Port] [Message]",
Help = "Sends the message [\"hello\"] through UDP to the specified host [localhost] and port [1024]",
},
},
} -- send
}, -- Subcommands ("net udp")
}, -- udp
wasc =
{
HelpString = "Requests the webadmin homepage using https",

View File

@ -11,6 +11,10 @@
-- g_Servers[PortNum] = cServerHandle
local g_Servers = {}
--- Map of all UDP endpoints currently open
-- g_UDPEndpoints[PortNum] = cUDPEndpoint
local g_UDPEndpoints = {}
--- List of fortune messages for the fortune server
-- A random message is chosen for each incoming connection
-- The contents are loaded from the splashes.txt file on plugin startup
@ -268,7 +272,7 @@ function HandleConsoleNetClose(a_Split)
-- Get the port to close:
local Port = tonumber(a_Split[3] or 1024)
if not(Port) then
return true, "Bad port number: \"" .. Port .. "\"."
return true, "Bad port number: \"" .. a_Split[3] .. "\"."
end
-- Close the server, if there is one:
@ -354,6 +358,93 @@ end
function HandleConsoleNetUdpClose(a_Split)
-- Get the port to close:
local Port = tonumber(a_Split[4] or 1024)
if not(Port) then
return true, "Bad port number: \"" .. a_Split[4] .. "\"."
end
-- Close the server, if there is one:
if not(g_UDPEndpoints[Port]) then
return true, "There is no UDP endpoint currently listening on port " .. Port .. "."
end
g_UDPEndpoints[Port]:Close()
g_UDPEndpoints[Port] = nil
return true, "UDP Port " .. Port .. " closed."
end
function HandleConsoleNetUdpListen(a_Split)
-- Get the params:
local Port = tonumber(a_Split[4] or 1024)
if not(Port) then
return true, "Invalid port: \"" .. a_Split[4] .. "\"."
end
local Callbacks =
{
OnReceivedData = function (a_Endpoint, a_Data, a_RemotePeer, a_RemotePort)
LOG("Incoming UDP datagram from " .. a_RemotePeer .. " port " .. a_RemotePort .. ":\r\n" .. a_Data)
end,
OnError = function (a_Endpoint, a_ErrorCode, a_ErrorMsg)
LOG("Error in UDP endpoint: " .. a_ErrorCode .. " (" .. a_ErrorMsg .. ")")
end,
}
g_UDPEndpoints[Port] = cNetwork:CreateUDPEndpoint(Port, Callbacks)
return true, "UDP listener on port " .. Port .. " started."
end
function HandleConsoleNetUdpSend(a_Split)
-- Get the params:
local Host = a_Split[4] or "localhost"
local Port = tonumber(a_Split[5] or 1024)
if not(Port) then
return true, "Invalid port: \"" .. a_Split[5] .. "\"."
end
local Message
if (a_Split[6]) then
Message = table.concat(a_Split, " ", 6)
else
Message = "hello"
end
-- Define minimum callbacks for the UDP endpoint:
local Callbacks =
{
OnError = function (a_Endpoint, a_ErrorCode, a_ErrorMsg)
LOG("Error in UDP datagram sending: " .. a_ErrorCode .. " (" .. a_ErrorMsg .. ")")
end,
OnReceivedData = function ()
-- ignore
end,
}
-- Send the data:
local Endpoint = cNetwork:CreateUDPEndpoint(0, Callbacks)
Endpoint:EnableBroadcasts()
if not(Endpoint:Send(Message, Host, Port)) then
Endpoint:Close()
return true, "Sending UDP datagram failed"
end
Endpoint:Close()
return true, "UDP datagram sent"
end
function HandleConsoleNetWasc(a_Split)
local Callbacks =
{

View File

@ -12,6 +12,7 @@ SET (SRCS
LuaServerHandle.cpp
LuaState.cpp
LuaTCPLink.cpp
LuaUDPEndpoint.cpp
LuaWindow.cpp
ManualBindings.cpp
ManualBindings_Network.cpp
@ -31,6 +32,7 @@ SET (HDRS
LuaServerHandle.h
LuaState.h
LuaTCPLink.h
LuaUDPEndpoint.h
LuaWindow.h
ManualBindings.h
Plugin.h

View File

@ -692,6 +692,18 @@ void cLuaState::Push(cLuaTCPLink * a_TCPLink)
void cLuaState::Push(cLuaUDPEndpoint * a_UDPEndpoint)
{
ASSERT(IsValid());
tolua_pushusertype(m_LuaState, a_UDPEndpoint, "cUDPEndpoint");
m_NumCurrentFunctionArgs += 1;
}
void cLuaState::Push(cMonster * a_Monster)
{
ASSERT(IsValid());

View File

@ -61,6 +61,7 @@ class cBlockEntity;
class cBoundingBox;
class cLuaTCPLink;
class cLuaServerHandle;
class cLuaUDPEndpoint;
typedef cBoundingBox * pBoundingBox;
typedef cWorld * pWorld;
@ -212,6 +213,7 @@ public:
void Push(cItems * a_Items);
void Push(cLuaServerHandle * a_ServerHandle);
void Push(cLuaTCPLink * a_TCPLink);
void Push(cLuaUDPEndpoint * a_UDPEndpoint);
void Push(cMonster * a_Monster);
void Push(cPickup * a_Pickup);
void Push(cPlayer * a_Player);

View File

@ -0,0 +1,221 @@
// LuaUDPEndpoint.cpp
// Implements the cLuaUDPEndpoint class representing a Lua wrapper for the cUDPEndpoint class and the callbacks it needs
#include "Globals.h"
#include "LuaUDPEndpoint.h"
cLuaUDPEndpoint::cLuaUDPEndpoint(cPluginLua & a_Plugin, int a_CallbacksTableStackPos):
m_Plugin(a_Plugin),
m_Callbacks(a_Plugin.GetLuaState(), a_CallbacksTableStackPos)
{
// Warn if the callbacks aren't valid:
if (!m_Callbacks.IsValid())
{
LOGWARNING("cLuaUDPEndpoint in plugin %s: callbacks could not be retrieved", m_Plugin.GetName().c_str());
cPluginLua::cOperation Op(m_Plugin);
Op().LogStackTrace();
}
}
cLuaUDPEndpoint::~cLuaUDPEndpoint()
{
// If the endpoint is still open, close it:
cUDPEndpointPtr Endpoint = m_Endpoint;
if (Endpoint != nullptr)
{
Endpoint->Close();
}
Terminated();
}
bool cLuaUDPEndpoint::Open(UInt16 a_Port, cLuaUDPEndpointPtr a_Self)
{
ASSERT(m_Self == nullptr); // Must not be opened yet
ASSERT(m_Endpoint == nullptr);
m_Self = a_Self;
m_Endpoint = cNetwork::CreateUDPEndpoint(a_Port, *this);
return m_Endpoint->IsOpen();
}
bool cLuaUDPEndpoint::Send(const AString & a_Data, const AString & a_RemotePeer, UInt16 a_RemotePort)
{
// Safely grab a copy of the endpoint:
cUDPEndpointPtr Endpoint = m_Endpoint;
if (Endpoint == nullptr)
{
return false;
}
// Send the data:
return Endpoint->Send(a_Data, a_RemotePeer, a_RemotePort);
}
UInt16 cLuaUDPEndpoint::GetPort(void) const
{
// Safely grab a copy of the endpoint:
cUDPEndpointPtr Endpoint = m_Endpoint;
if (Endpoint == nullptr)
{
return 0;
}
// Get the port:
return Endpoint->GetPort();
}
bool cLuaUDPEndpoint::IsOpen(void) const
{
// Safely grab a copy of the endpoint:
cUDPEndpointPtr Endpoint = m_Endpoint;
if (Endpoint == nullptr)
{
// No endpoint means that we're not open
return false;
}
// Get the state:
return Endpoint->IsOpen();
}
void cLuaUDPEndpoint::Close(void)
{
// If the endpoint is still open, close it:
cUDPEndpointPtr Endpoint = m_Endpoint;
if (Endpoint != nullptr)
{
Endpoint->Close();
m_Endpoint.reset();
}
Terminated();
}
void cLuaUDPEndpoint::EnableBroadcasts(void)
{
// Safely grab a copy of the endpoint:
cUDPEndpointPtr Endpoint = m_Endpoint;
if (Endpoint != nullptr)
{
Endpoint->EnableBroadcasts();
}
}
void cLuaUDPEndpoint::Release(void)
{
// Close the endpoint, if not already closed:
Close();
// Allow self to deallocate:
m_Self.reset();
}
void cLuaUDPEndpoint::Terminated(void)
{
// Disable the callbacks:
if (m_Callbacks.IsValid())
{
m_Callbacks.UnRef();
}
// If the endpoint is still open, close it:
{
cUDPEndpointPtr Endpoint = m_Endpoint;
if (Endpoint != nullptr)
{
Endpoint->Close();
m_Endpoint.reset();
}
}
}
void cLuaUDPEndpoint::OnReceivedData(const char * a_Data, size_t a_NumBytes, const AString & a_RemotePeer, UInt16 a_RemotePort)
{
// Check if we're still valid:
if (!m_Callbacks.IsValid())
{
return;
}
// Call the callback:
cPluginLua::cOperation Op(m_Plugin);
if (!Op().Call(cLuaState::cTableRef(m_Callbacks, "OnReceivedData"), this, AString(a_Data, a_NumBytes), a_RemotePeer, a_RemotePort))
{
LOGINFO("cUDPEndpoint OnReceivedData callback failed in plugin %s.", m_Plugin.GetName().c_str());
}
}
void cLuaUDPEndpoint::OnError(int a_ErrorCode, const AString & a_ErrorMsg)
{
// Check if we're still valid:
if (!m_Callbacks.IsValid())
{
return;
}
// Call the callback:
cPluginLua::cOperation Op(m_Plugin);
if (!Op().Call(cLuaState::cTableRef(m_Callbacks, "OnError"), a_ErrorCode, a_ErrorMsg))
{
LOGINFO("cUDPEndpoint OnError() callback failed in plugin %s; the endpoint error is %d (%s).",
m_Plugin.GetName().c_str(), a_ErrorCode, a_ErrorMsg.c_str()
);
}
Terminated();
}

View File

@ -0,0 +1,86 @@
// LuaUDPEndpoint.h
// Declares the cLuaUDPEndpoint class representing a Lua wrapper for the cUDPEndpoint class and the callbacks it needs
#pragma once
#include "../OSSupport/Network.h"
#include "PluginLua.h"
// fwd:
class cLuaUDPEndpoint;
typedef SharedPtr<cLuaUDPEndpoint> cLuaUDPEndpointPtr;
class cLuaUDPEndpoint:
public cUDPEndpoint::cCallbacks
{
public:
/** Creates a new instance of the endpoint, attached to the specified plugin and wrapping the callbacks that are in a table at the specified stack pos. */
cLuaUDPEndpoint(cPluginLua & a_Plugin, int a_CallbacksTableStackPos);
~cLuaUDPEndpoint();
/** Opens the endpoint so that it starts listening for incoming data on the specified port.
a_Self is the shared pointer to self that the object keeps to keep itself alive for as long as it needs (for Lua). */
bool Open(UInt16 a_Port, cLuaUDPEndpointPtr a_Self);
/** Sends the data contained in the string to the specified remote peer.
Returns true if successful, false on immediate failure (queueing the data failed etc.) */
bool Send(const AString & a_Data, const AString & a_RemotePeer, UInt16 a_RemotePort);
/** Returns the port on which endpoint is listening for incoming data. */
UInt16 GetPort(void) const;
/** Returns true if the endpoint is open for incoming data. */
bool IsOpen(void) const;
/** Closes the UDP listener. */
void Close(void);
/** Enables outgoing broadcasts to be made using this endpoint. */
void EnableBroadcasts(void);
/** Called when Lua garbage-collects the object.
Releases the internal SharedPtr to self, so that the instance may be deallocated. */
void Release(void);
protected:
/** The plugin for which the link is created. */
cPluginLua & m_Plugin;
/** The Lua table that holds the callbacks to be invoked. */
cLuaState::cRef m_Callbacks;
/** SharedPtr to self, so that the object can keep itself alive for as long as it needs (for Lua). */
cLuaUDPEndpointPtr m_Self;
/** The underlying network endpoint.
May be nullptr. */
cUDPEndpointPtr m_Endpoint;
/** Common code called when the endpoint is considered as terminated.
Releases m_Endpoint and m_Callbacks, each when applicable. */
void Terminated(void);
// cUDPEndpoint::cCallbacks overrides:
virtual void OnError(int a_ErrorCode, const AString & a_ErrorMsg) override;
virtual void OnReceivedData(const char * a_Data, size_t a_Size, const AString & a_RemotePeer, UInt16 a_RemotePort) override;
};

View File

@ -11,6 +11,7 @@
#include "LuaTCPLink.h"
#include "LuaNameLookup.h"
#include "LuaServerHandle.h"
#include "LuaUDPEndpoint.h"
@ -73,6 +74,61 @@ static int tolua_cNetwork_Connect(lua_State * L)
/** Binds cNetwork::CreateUDPEndpoint */
static int tolua_cNetwork_CreateUDPEndpoint(lua_State * L)
{
// Function signature:
// cNetwork:CreateUDPEndpoint(Port, Callbacks) -> cUDPEndpoint
cLuaState S(L);
if (
!S.CheckParamUserTable(1, "cNetwork") ||
!S.CheckParamNumber(2) ||
!S.CheckParamTable(3) ||
!S.CheckParamEnd(4)
)
{
return 0;
}
// Get the plugin instance:
cPluginLua * Plugin = GetLuaPlugin(L);
if (Plugin == nullptr)
{
// An error message has been already printed in GetLuaPlugin()
S.Push(false);
return 1;
}
// Read the params:
int Port;
S.GetStackValues(2, Port);
// Check validity:
if ((Port < 0) || (Port > 65535))
{
LOGWARNING("cNetwork:CreateUDPEndpoint() called with invalid port (%d), failing the request.", Port);
S.Push(false);
return 1;
}
// Create the LuaUDPEndpoint glue class:
auto Endpoint = std::make_shared<cLuaUDPEndpoint>(*Plugin, 3);
Endpoint->Open(Port, Endpoint);
// Register the endpoint to be garbage-collected by Lua:
tolua_pushusertype(L, Endpoint.get(), "cUDPEndpoint");
tolua_register_gc(L, lua_gettop(L));
// Return the endpoint object:
S.Push(Endpoint.get());
return 1;
}
/** Binds cNetwork::HostnameToIP */
static int tolua_cNetwork_HostnameToIP(lua_State * L)
{
@ -159,7 +215,7 @@ static int tolua_cNetwork_IPToHostname(lua_State * L)
static int tolua_cNetwork_Listen(lua_State * L)
{
// Function signature:
// cNetwork:Listen(Port, Callbacks) -> bool
// cNetwork:Listen(Port, Callbacks) -> cServerHandle
cLuaState S(L);
if (
@ -211,6 +267,90 @@ static int tolua_cNetwork_Listen(lua_State * L)
////////////////////////////////////////////////////////////////////////////////
// cServerHandle bindings (routed through cLuaServerHandle):
/** Called when Lua destroys the object instance.
Close the server and let it deallocate on its own (it's in a SharedPtr). */
static int tolua_collect_cServerHandle(lua_State * L)
{
cLuaServerHandle * Srv = static_cast<cLuaServerHandle *>(tolua_tousertype(L, 1, nullptr));
Srv->Release();
return 0;
}
/** Binds cLuaServerHandle::Close */
static int tolua_cServerHandle_Close(lua_State * L)
{
// Function signature:
// ServerInstance:Close()
cLuaState S(L);
if (
!S.CheckParamUserType(1, "cServerHandle") ||
!S.CheckParamEnd(2)
)
{
return 0;
}
// Get the server handle:
cLuaServerHandle * Srv;
if (lua_isnil(L, 1))
{
LOGWARNING("cServerHandle:Close(): invalid server handle object. Stack trace:");
S.LogStackTrace();
return 0;
}
Srv = *static_cast<cLuaServerHandle **>(lua_touserdata(L, 1));
// Close it:
Srv->Close();
return 0;
}
/** Binds cLuaServerHandle::IsListening */
static int tolua_cServerHandle_IsListening(lua_State * L)
{
// Function signature:
// ServerInstance:IsListening() -> bool
cLuaState S(L);
if (
!S.CheckParamUserType(1, "cServerHandle") ||
!S.CheckParamEnd(2)
)
{
return 0;
}
// Get the server handle:
cLuaServerHandle * Srv;
if (lua_isnil(L, 1))
{
LOGWARNING("cServerHandle:IsListening(): invalid server handle object. Stack trace:");
S.LogStackTrace();
return 0;
}
Srv = *static_cast<cLuaServerHandle **>(lua_touserdata(L, 1));
// Close it:
S.Push(Srv->IsListening());
return 1;
}
////////////////////////////////////////////////////////////////////////////////
// cTCPLink bindings (routed through cLuaTCPLink):
@ -549,14 +689,15 @@ static int tolua_cTCPLink_StartTLSServer(lua_State * L)
////////////////////////////////////////////////////////////////////////////////
// cServerHandle bindings (routed through cLuaServerHandle):
// cUDPEndpoint bindings (routed through cLuaUDPEndpoint):
/** Called when Lua destroys the object instance.
Close the server and let it deallocate on its own (it's in a SharedPtr). */
static int tolua_collect_cServerHandle(lua_State * L)
Close the endpoint and let it deallocate on its own (it's in a SharedPtr). */
static int tolua_collect_cUDPEndpoint(lua_State * L)
{
cLuaServerHandle * Srv = static_cast<cLuaServerHandle *>(tolua_tousertype(L, 1, nullptr));
Srv->Release();
LOGD("Lua: Collecting cUDPEndpoint");
cLuaUDPEndpoint * Endpoint = static_cast<cLuaUDPEndpoint *>(tolua_tousertype(L, 1, nullptr));
Endpoint->Release();
return 0;
}
@ -564,33 +705,32 @@ static int tolua_collect_cServerHandle(lua_State * L)
/** Binds cLuaServerHandle::Close */
static int tolua_cServerHandle_Close(lua_State * L)
/** Binds cLuaUDPEndpoint::Close */
static int tolua_cUDPEndpoint_Close(lua_State * L)
{
// Function signature:
// ServerInstance:Close()
// EndpointInstance:Close()
cLuaState S(L);
if (
!S.CheckParamUserType(1, "cServerHandle") ||
!S.CheckParamUserType(1, "cUDPEndpoint") ||
!S.CheckParamEnd(2)
)
{
return 0;
}
// Get the server handle:
cLuaServerHandle * Srv;
// Get the endpoint:
if (lua_isnil(L, 1))
{
LOGWARNING("cServerHandle:Close(): invalid server handle object. Stack trace:");
LOGWARNING("cUDPEndpoint:Close(): invalid endpoint object. Stack trace:");
S.LogStackTrace();
return 0;
}
Srv = *static_cast<cLuaServerHandle **>(lua_touserdata(L, 1));
auto Endpoint = *static_cast<cLuaUDPEndpoint **>(lua_touserdata(L, 1));
// Close it:
Srv->Close();
Endpoint->Close();
return 0;
}
@ -598,33 +738,147 @@ static int tolua_cServerHandle_Close(lua_State * L)
/** Binds cLuaServerHandle::IsListening */
static int tolua_cServerHandle_IsListening(lua_State * L)
/** Binds cLuaUDPEndpoint::EnableBroadcasts */
static int tolua_cUDPEndpoint_EnableBroadcasts(lua_State * L)
{
// Function signature:
// ServerInstance:IsListening() -> bool
// EndpointInstance:EnableBroadcasts()
cLuaState S(L);
if (
!S.CheckParamUserType(1, "cServerHandle") ||
!S.CheckParamUserType(1, "cUDPEndpoint") ||
!S.CheckParamEnd(2)
)
{
return 0;
}
// Get the server handle:
cLuaServerHandle * Srv;
// Get the endpoint:
if (lua_isnil(L, 1))
{
LOGWARNING("cServerHandle:IsListening(): invalid server handle object. Stack trace:");
LOGWARNING("cUDPEndpoint:EnableBroadcasts(): invalid endpoint object. Stack trace:");
S.LogStackTrace();
return 0;
}
Srv = *static_cast<cLuaServerHandle **>(lua_touserdata(L, 1));
auto Endpoint = *static_cast<cLuaUDPEndpoint **>(lua_touserdata(L, 1));
// Enable the broadcasts:
Endpoint->EnableBroadcasts();
return 0;
}
/** Binds cLuaUDPEndpoint::GetPort */
static int tolua_cUDPEndpoint_GetPort(lua_State * L)
{
// Function signature:
// Endpoint:GetPort() -> number
cLuaState S(L);
if (
!S.CheckParamUserType(1, "cUDPEndpoint") ||
!S.CheckParamEnd(2)
)
{
return 0;
}
// Get the endpoint:
if (lua_isnil(L, 1))
{
LOGWARNING("cUDPEndpoint:GetPort(): invalid endpoint object. Stack trace:");
S.LogStackTrace();
return 0;
}
auto Endpoint = *static_cast<cLuaUDPEndpoint **>(lua_touserdata(L, 1));
// Get the Port:
S.Push(Endpoint->GetPort());
return 1;
}
/** Binds cLuaUDPEndpoint::IsOpen */
static int tolua_cUDPEndpoint_IsOpen(lua_State * L)
{
// Function signature:
// Endpoint:IsOpen() -> bool
cLuaState S(L);
if (
!S.CheckParamUserType(1, "cUDPEndpoint") ||
!S.CheckParamEnd(2)
)
{
return 0;
}
// Get the endpoint:
if (lua_isnil(L, 1))
{
LOGWARNING("cUDPEndpoint:IsListening(): invalid server handle object. Stack trace:");
S.LogStackTrace();
return 0;
}
auto Endpoint = *static_cast<cLuaUDPEndpoint **>(lua_touserdata(L, 1));
// Close it:
S.Push(Srv->IsListening());
S.Push(Endpoint->IsOpen());
return 1;
}
/** Binds cLuaUDPEndpoint::Send */
static int tolua_cUDPEndpoint_Send(lua_State * L)
{
// Function signature:
// LinkInstance:Send(DataString)
cLuaState S(L);
if (
!S.CheckParamUserType(1, "cUDPEndpoint") ||
!S.CheckParamString(2, 3) ||
!S.CheckParamNumber(4) ||
!S.CheckParamEnd(5)
)
{
return 0;
}
// Get the link:
if (lua_isnil(L, 1))
{
LOGWARNING("cUDPEndpoint:Send(): invalid endpoint object. Stack trace:");
S.LogStackTrace();
return 0;
}
auto Endpoint = *static_cast<cLuaUDPEndpoint **>(lua_touserdata(L, 1));
// Get the data to send:
AString Data, RemotePeer;
int RemotePort;
S.GetStackValues(2, Data, RemotePeer, RemotePort);
// Check the port:
if ((RemotePort < 0) || (RemotePort > USHRT_MAX))
{
LOGWARNING("cUDPEndpoint:Send() called with invalid port (%d), failing.", RemotePort);
S.LogStackTrace();
S.Push(false);
return 1;
}
// Send the data:
S.Push(Endpoint->Send(Data, RemotePeer, static_cast<UInt16>(RemotePort)));
return 1;
}
@ -644,13 +898,21 @@ void ManualBindings::BindNetwork(lua_State * tolua_S)
tolua_cclass(tolua_S, "cTCPLink", "cTCPLink", "", nullptr);
tolua_usertype(tolua_S, "cServerHandle");
tolua_cclass(tolua_S, "cServerHandle", "cServerHandle", "", tolua_collect_cServerHandle);
tolua_usertype(tolua_S, "cUDPEndpoint");
tolua_cclass(tolua_S, "cUDPEndpoint", "cUDPEndpoint", "", tolua_collect_cUDPEndpoint);
// Fill in the functions (alpha-sorted):
tolua_beginmodule(tolua_S, "cNetwork");
tolua_function(tolua_S, "Connect", tolua_cNetwork_Connect);
tolua_function(tolua_S, "HostnameToIP", tolua_cNetwork_HostnameToIP);
tolua_function(tolua_S, "IPToHostname", tolua_cNetwork_IPToHostname);
tolua_function(tolua_S, "Listen", tolua_cNetwork_Listen);
tolua_function(tolua_S, "Connect", tolua_cNetwork_Connect);
tolua_function(tolua_S, "CreateUDPEndpoint", tolua_cNetwork_CreateUDPEndpoint);
tolua_function(tolua_S, "HostnameToIP", tolua_cNetwork_HostnameToIP);
tolua_function(tolua_S, "IPToHostname", tolua_cNetwork_IPToHostname);
tolua_function(tolua_S, "Listen", tolua_cNetwork_Listen);
tolua_endmodule(tolua_S);
tolua_beginmodule(tolua_S, "cServerHandle");
tolua_function(tolua_S, "Close", tolua_cServerHandle_Close);
tolua_function(tolua_S, "IsListening", tolua_cServerHandle_IsListening);
tolua_endmodule(tolua_S);
tolua_beginmodule(tolua_S, "cTCPLink");
@ -665,10 +927,14 @@ void ManualBindings::BindNetwork(lua_State * tolua_S)
tolua_function(tolua_S, "StartTLSServer", tolua_cTCPLink_StartTLSServer);
tolua_endmodule(tolua_S);
tolua_beginmodule(tolua_S, "cServerHandle");
tolua_function(tolua_S, "Close", tolua_cServerHandle_Close);
tolua_function(tolua_S, "IsListening", tolua_cServerHandle_IsListening);
tolua_beginmodule(tolua_S, "cUDPEndpoint");
tolua_function(tolua_S, "Close", tolua_cUDPEndpoint_Close);
tolua_function(tolua_S, "EnableBroadcasts", tolua_cUDPEndpoint_EnableBroadcasts);
tolua_function(tolua_S, "GetPort", tolua_cUDPEndpoint_GetPort);
tolua_function(tolua_S, "IsOpen", tolua_cUDPEndpoint_IsOpen);
tolua_function(tolua_S, "Send", tolua_cUDPEndpoint_Send);
tolua_endmodule(tolua_S);
}

View File

@ -18,6 +18,7 @@ SET (SRCS
ServerHandleImpl.cpp
StackTrace.cpp
TCPLinkImpl.cpp
UDPEndpointImpl.cpp
)
SET (HDRS
@ -36,6 +37,7 @@ SET (HDRS
ServerHandleImpl.h
StackTrace.h
TCPLinkImpl.h
UDPEndpointImpl.h
)
if(NOT MSVC)

View File

@ -69,12 +69,24 @@ void cHostnameLookup::Callback(int a_ErrCode, evutil_addrinfo * a_Addr, void * a
case AF_INET: // IPv4
{
sockaddr_in * sin = reinterpret_cast<sockaddr_in *>(a_Addr->ai_addr);
if (!Self->m_Callbacks->OnNameResolvedV4(Self->m_Hostname, sin))
{
// Callback indicated that the IP shouldn't be serialized to a string, just continue with the next address:
HasResolved = true;
continue;
}
evutil_inet_ntop(AF_INET, &(sin->sin_addr), IP, sizeof(IP));
break;
}
case AF_INET6: // IPv6
{
sockaddr_in6 * sin = reinterpret_cast<sockaddr_in6 *>(a_Addr->ai_addr);
if (!Self->m_Callbacks->OnNameResolvedV6(Self->m_Hostname, sin))
{
// Callback indicated that the IP shouldn't be serialized to a string, just continue with the next address:
HasResolved = true;
continue;
}
evutil_inet_ntop(AF_INET6, &(sin->sin6_addr), IP, sizeof(IP));
break;
}

View File

@ -130,6 +130,64 @@ public:
/** Interface that provides methods available on UDP communication endpoints. */
class cUDPEndpoint
{
public:
/** Interface for the callbacks for events that can happen on the endpoint. */
class cCallbacks
{
public:
// Force a virtual destructor in all descendants:
virtual ~cCallbacks() {}
/** Called when an error occurs on the endpoint. */
virtual void OnError(int a_ErrorCode, const AString & a_ErrorMsg) = 0;
/** Called when there is an incoming datagram from a remote host. */
virtual void OnReceivedData(const char * a_Data, size_t a_Size, const AString & a_RemoteHost, UInt16 a_RemotePort) = 0;
};
// Force a virtual destructor for all descendants:
virtual ~cUDPEndpoint() {}
/** Closes the underlying socket.
Note that there still might be callbacks in-flight after this method returns. */
virtual void Close(void) = 0;
/** Returns true if the endpoint is open. */
virtual bool IsOpen(void) const = 0;
/** Returns the local port to which the underlying socket is bound. */
virtual UInt16 GetPort(void) const = 0;
/** Sends the specified payload in a single UDP datagram to the specified host+port combination.
Note that in order to send to a broadcast address, you need to call EnableBroadcasts() first. */
virtual bool Send(const AString & a_Payload, const AString & a_Host, UInt16 a_Port) = 0;
/** Marks the socket as capable of sending broadcast, using whatever OS API is needed.
Without this call, sending to a broadcast address using Send() may fail. */
virtual void EnableBroadcasts(void) = 0;
protected:
/** The callbacks used for various events on the endpoint. */
cCallbacks & m_Callbacks;
/** Creates a new instance of an endpoint, with the specified callbacks. */
cUDPEndpoint(cCallbacks & a_Callbacks):
m_Callbacks(a_Callbacks)
{
}
};
typedef SharedPtr<cUDPEndpoint> cUDPEndpointPtr;
class cNetwork
{
public:
@ -183,9 +241,22 @@ public:
/** Called when the hostname is successfully resolved into an IP address.
May be called multiple times if a name resolves to multiple addresses.
a_IP may be either an IPv4 or an IPv6 address with their proper formatting. */
a_IP may be either an IPv4 or an IPv6 address with their proper formatting.
Each call to OnNameResolved() is preceded by a call to either OnNameResolvedV4() or OnNameResolvedV6(). */
virtual void OnNameResolved(const AString & a_Name, const AString & a_IP) = 0;
/** Called when the hostname is successfully resolved into an IPv4 address.
May be called multiple times if a name resolves to multiple addresses.
Each call to OnNameResolvedV4 is followed by OnNameResolved with the IP address serialized to a string.
If this callback returns false, the OnNameResolved() call is skipped for this address. */
virtual bool OnNameResolvedV4(const AString & a_Name, const sockaddr_in * a_IP) { return true; }
/** Called when the hostname is successfully resolved into an IPv6 address.
May be called multiple times if a name resolves to multiple addresses.
Each call to OnNameResolvedV4 is followed by OnNameResolved with the IP address serialized to a string.
If this callback returns false, the OnNameResolved() call is skipped for this address. */
virtual bool OnNameResolvedV6(const AString & a_Name, const sockaddr_in6 * a_IP) { return true; }
/** Called when an error is encountered while resolving.
If an error is reported, the OnFinished() callback is not called. */
virtual void OnError(int a_ErrorCode, const AString & a_ErrorMsg) = 0;
@ -242,6 +313,11 @@ public:
const AString & a_IP,
cResolveNameCallbacksPtr a_Callbacks
);
/** Opens up an UDP endpoint for sending and receiving UDP datagrams on the specified port.
If a_Port is 0, the OS is free to assign any port number it likes to the endpoint.
Returns the endpoint object that can be interacted with. */
static cUDPEndpointPtr CreateUDPEndpoint(UInt16 a_Port, cUDPEndpoint::cCallbacks & a_Callbacks);
};

View File

@ -0,0 +1,608 @@
// UDPEndpointImpl.cpp
// Implements the cUDPEndpointImpl class representing an implementation of an endpoint in UDP communication
#include "Globals.h"
#include "UDPEndpointImpl.h"
#include "NetworkSingleton.h"
////////////////////////////////////////////////////////////////////////////////
// Globals:
static bool IsValidSocket(evutil_socket_t a_Socket)
{
#ifdef _WIN32
return (a_Socket != INVALID_SOCKET);
#else // _WIN32
return (a_Socket >= 0);
#endif // else _WIN32
}
/** Converts a_SrcAddr in IPv4 format to a_DstAddr in IPv6 format (using IPv4-mapped IPv6). */
static void ConvertIPv4ToMappedIPv6(sockaddr_in & a_SrcAddr, sockaddr_in6 & a_DstAddr)
{
memset(&a_DstAddr, 0, sizeof(a_DstAddr));
a_DstAddr.sin6_family = AF_INET6;
a_DstAddr.sin6_addr.s6_addr[10] = 0xff;
a_DstAddr.sin6_addr.s6_addr[11] = 0xff;
a_DstAddr.sin6_addr.s6_addr[12] = static_cast<Byte>((a_SrcAddr.sin_addr.s_addr >> 0) & 0xff);
a_DstAddr.sin6_addr.s6_addr[13] = static_cast<Byte>((a_SrcAddr.sin_addr.s_addr >> 8) & 0xff);
a_DstAddr.sin6_addr.s6_addr[14] = static_cast<Byte>((a_SrcAddr.sin_addr.s_addr >> 16) & 0xff);
a_DstAddr.sin6_addr.s6_addr[15] = static_cast<Byte>((a_SrcAddr.sin_addr.s_addr >> 24) & 0xff);
a_DstAddr.sin6_port = a_SrcAddr.sin_port;
}
////////////////////////////////////////////////////////////////////////////////
// cUDPSendAfterLookup:
/** A hostname-to-IP resolver callback that sends the data stored within to the resolved IP address.
This is used for sending UDP datagrams to hostnames, so that the cUDPEndpoint::Send() doesn't block.
Instead an instance of this callback is queued for resolving and the data is sent once the IP is resolved. */
class cUDPSendAfterLookup:
public cNetwork::cResolveNameCallbacks
{
public:
cUDPSendAfterLookup(const AString & a_Data, UInt16 a_Port, evutil_socket_t a_MainSock, evutil_socket_t a_SecondSock, bool a_IsMainSockIPv6):
m_Data(a_Data),
m_Port(a_Port),
m_MainSock(a_MainSock),
m_SecondSock(a_SecondSock),
m_IsMainSockIPv6(a_IsMainSockIPv6),
m_HasIPv4(false),
m_HasIPv6(false)
{
}
protected:
/** The data to send after the hostname is resolved. */
AString m_Data;
/** The port to which to send the data. */
UInt16 m_Port;
/** The primary socket to use for sending. */
evutil_socket_t m_MainSock;
/** The secondary socket to use for sending, if needed by the OS. */
evutil_socket_t m_SecondSock;
/** True if m_MainSock is an IPv6 socket. */
bool m_IsMainSockIPv6;
/** The IPv4 address resolved, if any. */
sockaddr_in m_AddrIPv4;
/** Set to true if the name resolved to an IPv4 address. */
bool m_HasIPv4;
/** The IPv6 address resolved, if any. */
sockaddr_in6 m_AddrIPv6;
/** Set to true if the name resolved to an IPv6 address. */
bool m_HasIPv6;
// cNetwork::cResolveNameCallbacks overrides:
virtual void OnNameResolved(const AString & a_Name, const AString & a_PI) override
{
// Not needed
}
virtual bool OnNameResolvedV4(const AString & a_Name, const sockaddr_in * a_IP) override
{
if (!m_HasIPv4)
{
m_AddrIPv4 = *a_IP;
m_AddrIPv4.sin_port = htons(m_Port);
m_HasIPv4 = true;
}
// Don't want OnNameResolved() callback
return false;
}
virtual bool OnNameResolvedV6(const AString & a_Name, const sockaddr_in6 * a_IP) override
{
if (!m_HasIPv6)
{
m_AddrIPv6 = *a_IP;
m_AddrIPv6.sin6_port = htons(m_Port);
m_HasIPv6 = true;
}
// Don't want OnNameResolved() callback
return false;
}
virtual void OnFinished(void) override
{
// Send the actual data, through the correct socket and using the correct resolved address:
if (m_IsMainSockIPv6)
{
if (m_HasIPv6)
{
sendto(m_MainSock, m_Data.data(), static_cast<socklen_t>(m_Data.size()), 0, reinterpret_cast<const sockaddr *>(&m_AddrIPv6), static_cast<socklen_t>(sizeof(m_AddrIPv6)));
}
else if (m_HasIPv4)
{
// If the secondary socket is valid, it is an IPv4 socket, so use that:
if (m_SecondSock != -1)
{
sendto(m_SecondSock, m_Data.data(), static_cast<socklen_t>(m_Data.size()), 0, reinterpret_cast<const sockaddr *>(&m_AddrIPv4), static_cast<socklen_t>(sizeof(m_AddrIPv4)));
}
else
{
// Need an address conversion from IPv4 to IPv6-mapped-IPv4:
ConvertIPv4ToMappedIPv6(m_AddrIPv4, m_AddrIPv6);
sendto(m_MainSock, m_Data.data(), static_cast<socklen_t>(m_Data.size()), 0, reinterpret_cast<const sockaddr *>(&m_AddrIPv6), static_cast<socklen_t>(sizeof(m_AddrIPv6)));
}
}
else
{
LOGD("UDP endpoint queued sendto: Name not resolved");
return;
}
}
else // m_IsMainSockIPv6
{
// Main socket is IPv4 only, only allow IPv4 dst address:
if (!m_HasIPv4)
{
LOGD("UDP endpoint queued sendto: Name not resolved to IPv4 for an IPv4-only socket");
return;
}
sendto(m_MainSock, m_Data.data(), static_cast<socklen_t>(m_Data.size()), 0, reinterpret_cast<const sockaddr *>(&m_AddrIPv4), static_cast<socklen_t>(sizeof(m_AddrIPv4)));
}
}
virtual void OnError(int a_ErrorCode, const AString & a_ErrorMsg) override
{
// Nothing needed
}
};
////////////////////////////////////////////////////////////////////////////////
// cUDPEndpointImpl:
cUDPEndpointImpl::cUDPEndpointImpl(UInt16 a_Port, cUDPEndpoint::cCallbacks & a_Callbacks):
super(a_Callbacks),
m_Port(0),
m_MainSock(-1),
m_IsMainSockIPv6(true),
m_SecondarySock(-1),
m_MainEvent(nullptr),
m_SecondaryEvent(nullptr)
{
Open(a_Port);
}
void cUDPEndpointImpl::Close(void)
{
if (m_Port == 0)
{
// Already closed
return;
}
// Close the LibEvent handles:
if (m_MainEvent != nullptr)
{
event_free(m_MainEvent);
m_MainEvent = nullptr;
}
if (m_SecondaryEvent != nullptr)
{
event_free(m_SecondaryEvent);
m_SecondaryEvent = nullptr;
}
// Close the OS sockets:
evutil_closesocket(m_MainSock);
m_MainSock = -1;
evutil_closesocket(m_SecondarySock);
m_SecondarySock = -1;
// Mark as closed:
m_Port = 0;
}
bool cUDPEndpointImpl::IsOpen(void) const
{
return (m_Port != 0);
}
UInt16 cUDPEndpointImpl::GetPort(void) const
{
return m_Port;
}
bool cUDPEndpointImpl::Send(const AString & a_Payload, const AString & a_Host, UInt16 a_Port)
{
// If a_Host is an IP address, send the data directly:
sockaddr_storage sa;
int salen = static_cast<int>(sizeof(sa));
memset(&sa, 0, sizeof(sa));
if (evutil_parse_sockaddr_port(a_Host.c_str(), reinterpret_cast<sockaddr *>(&sa), &salen) != 0)
{
// a_Host is a hostname, we need to do a lookup first:
auto queue = std::make_shared<cUDPSendAfterLookup>(a_Payload, a_Port, m_MainSock, m_SecondarySock, m_IsMainSockIPv6);
return cNetwork::HostnameToIP(a_Host, queue);
}
// a_Host is an IP address and has been parsed into "sa"
// Insert the correct port and send data:
int NumSent;
switch (sa.ss_family)
{
case AF_INET:
{
reinterpret_cast<sockaddr_in *>(&sa)->sin_port = htons(a_Port);
if (m_IsMainSockIPv6)
{
if (IsValidSocket(m_SecondarySock))
{
// The secondary socket, which is always IPv4, is present:
NumSent = static_cast<int>(sendto(m_SecondarySock, a_Payload.data(), static_cast<socklen_t>(a_Payload.size()), 0, reinterpret_cast<const sockaddr *>(&sa), static_cast<socklen_t>(salen)));
}
else
{
// Need to convert IPv4 to IPv6 address before sending:
sockaddr_in6 IPv6;
ConvertIPv4ToMappedIPv6(*reinterpret_cast<sockaddr_in *>(&sa), IPv6);
NumSent = static_cast<int>(sendto(m_MainSock, a_Payload.data(), static_cast<socklen_t>(a_Payload.size()), 0, reinterpret_cast<const sockaddr *>(&IPv6), static_cast<socklen_t>(sizeof(IPv6))));
}
}
else
{
NumSent = static_cast<int>(sendto(m_MainSock, a_Payload.data(), static_cast<socklen_t>(a_Payload.size()), 0, reinterpret_cast<const sockaddr *>(&sa), static_cast<socklen_t>(salen)));
}
break;
}
case AF_INET6:
{
reinterpret_cast<sockaddr_in6 *>(&sa)->sin6_port = htons(a_Port);
NumSent = static_cast<int>(sendto(m_MainSock, a_Payload.data(), static_cast<socklen_t>(a_Payload.size()), 0, reinterpret_cast<const sockaddr *>(&sa), static_cast<socklen_t>(salen)));
break;
}
default:
{
LOGD("UDP sendto: Invalid address family for address \"%s\".", a_Host.c_str());
return false;
}
}
return (NumSent > 0);
}
void cUDPEndpointImpl::EnableBroadcasts(void)
{
ASSERT(IsOpen());
// Enable broadcasts on the main socket:
// Some OSes use ints, others use chars, so we try both
int broadcastInt = 1;
char broadcastChar = 1;
// (Note that Windows uses const char * for option values, while Linux uses const void *)
if (setsockopt(m_MainSock, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<const char *>(&broadcastInt), sizeof(broadcastInt)) == -1)
{
if (setsockopt(m_MainSock, SOL_SOCKET, SO_BROADCAST, &broadcastChar, sizeof(broadcastChar)) == -1)
{
int err = EVUTIL_SOCKET_ERROR();
LOGWARNING("Cannot enable broadcasts on port %d: %d (%s)", m_Port, err, evutil_socket_error_to_string(err));
return;
}
// Enable broadcasts on the secondary socket, if opened (use char, it worked for primary):
if (IsValidSocket(m_SecondarySock))
{
if (setsockopt(m_SecondarySock, SOL_SOCKET, SO_BROADCAST, &broadcastChar, sizeof(broadcastChar)) == -1)
{
int err = EVUTIL_SOCKET_ERROR();
LOGWARNING("Cannot enable broadcasts on port %d (secondary): %d (%s)", m_Port, err, evutil_socket_error_to_string(err));
}
}
return;
}
// Enable broadcasts on the secondary socket, if opened (use int, it worked for primary):
if (IsValidSocket(m_SecondarySock))
{
if (setsockopt(m_SecondarySock, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<const char *>(&broadcastInt), sizeof(broadcastInt)) == -1)
{
int err = EVUTIL_SOCKET_ERROR();
LOGWARNING("Cannot enable broadcasts on port %d (secondary): %d (%s)", m_Port, err, evutil_socket_error_to_string(err));
}
}
}
void cUDPEndpointImpl::Open(UInt16 a_Port)
{
ASSERT(m_Port == 0); // Must not be already open
// Make sure the cNetwork internals are innitialized:
cNetworkSingleton::Get();
// Set up the main socket:
// It should listen on IPv6 with IPv4 fallback, when available; IPv4 when IPv6 is not available.
bool NeedsTwoSockets = false;
m_IsMainSockIPv6 = true;
m_MainSock = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
int err;
if (!IsValidSocket(m_MainSock))
{
// Failed to create IPv6 socket, create an IPv4 one instead:
m_IsMainSockIPv6 = false;
err = EVUTIL_SOCKET_ERROR();
LOGD("Failed to create IPv6 MainSock: %d (%s)", err, evutil_socket_error_to_string(err));
m_MainSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (!IsValidSocket(m_MainSock))
{
err = EVUTIL_SOCKET_ERROR();
m_Callbacks.OnError(err, Printf("Cannot create UDP socket for port %d: %s", a_Port, evutil_socket_error_to_string(err)));
return;
}
// Allow the port to be reused right after the socket closes:
if (evutil_make_listen_socket_reuseable(m_MainSock) != 0)
{
err = EVUTIL_SOCKET_ERROR();
LOG("UDP Port %d cannot be made reusable: %d (%s). Restarting the server might not work.",
a_Port, err, evutil_socket_error_to_string(err)
);
}
// Bind to all interfaces:
sockaddr_in name;
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = ntohs(a_Port);
if (bind(m_MainSock, reinterpret_cast<const sockaddr *>(&name), sizeof(name)) != 0)
{
err = EVUTIL_SOCKET_ERROR();
m_Callbacks.OnError(err, Printf("Cannot bind UDP port %d: %s", a_Port, evutil_socket_error_to_string(err)));
evutil_closesocket(m_MainSock);
return;
}
}
else
{
// IPv6 socket created, switch it into "dualstack" mode:
UInt32 Zero = 0;
#ifdef _WIN32
// WinXP doesn't support this feature, so if the setting fails, create another socket later on:
int res = setsockopt(m_MainSock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char *>(&Zero), sizeof(Zero));
err = EVUTIL_SOCKET_ERROR();
NeedsTwoSockets = ((res == SOCKET_ERROR) && (err == WSAENOPROTOOPT));
#else
setsockopt(m_MainSock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char *>(&Zero), sizeof(Zero));
#endif
// Allow the port to be reused right after the socket closes:
if (evutil_make_listen_socket_reuseable(m_MainSock) != 0)
{
err = EVUTIL_SOCKET_ERROR();
LOG("UDP Port %d cannot be made reusable: %d (%s). Restarting the server might not work.",
a_Port, err, evutil_socket_error_to_string(err)
);
}
// Bind to all interfaces:
sockaddr_in6 name;
memset(&name, 0, sizeof(name));
name.sin6_family = AF_INET6;
name.sin6_port = ntohs(a_Port);
if (bind(m_MainSock, reinterpret_cast<const sockaddr *>(&name), sizeof(name)) != 0)
{
err = EVUTIL_SOCKET_ERROR();
m_Callbacks.OnError(err, Printf("Cannot bind to UDP port %d: %s", a_Port, evutil_socket_error_to_string(err)));
evutil_closesocket(m_MainSock);
return;
}
}
if (evutil_make_socket_nonblocking(m_MainSock) != 0)
{
err = EVUTIL_SOCKET_ERROR();
m_Callbacks.OnError(err, Printf("Cannot make socket on UDP port %d nonblocking: %s", a_Port, evutil_socket_error_to_string(err)));
evutil_closesocket(m_MainSock);
return;
}
m_MainEvent = event_new(cNetworkSingleton::Get().GetEventBase(), m_MainSock, EV_READ | EV_PERSIST, RawCallback, this);
event_add(m_MainEvent, nullptr);
// Read the actual port number on which the socket is listening:
{
sockaddr_storage name;
socklen_t namelen = static_cast<socklen_t>(sizeof(name));
getsockname(m_MainSock, reinterpret_cast<sockaddr *>(&name), &namelen);
switch (name.ss_family)
{
case AF_INET:
{
sockaddr_in * sin = reinterpret_cast<sockaddr_in *>(&name);
m_Port = ntohs(sin->sin_port);
break;
}
case AF_INET6:
{
sockaddr_in6 * sin6 = reinterpret_cast<sockaddr_in6 *>(&name);
m_Port = ntohs(sin6->sin6_port);
break;
}
}
}
// If we don't need to create another socket, bail out now:
if (!NeedsTwoSockets)
{
return;
}
// If a secondary socket is required (WinXP dual-stack), create it here:
LOGD("Creating a second UDP socket for IPv4");
m_SecondarySock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (!IsValidSocket(m_SecondarySock))
{
// Don't report as an error, the primary socket is working
err = EVUTIL_SOCKET_ERROR();
LOGD("Socket creation failed for secondary UDP socket for port %d: %d, %s", m_Port, err, evutil_socket_error_to_string(err));
return;
}
// Allow the port to be reused right after the socket closes:
if (evutil_make_listen_socket_reuseable(m_SecondarySock) != 0)
{
// Don't report as an error, the primary socket is working
err = EVUTIL_SOCKET_ERROR();
LOGD("UDP Port %d cannot be made reusable (second socket): %d (%s). Restarting the server might not work.",
a_Port, err, evutil_socket_error_to_string(err)
);
evutil_closesocket(m_SecondarySock);
m_SecondarySock = -1;
return;
}
// Make the secondary socket nonblocking:
if (evutil_make_socket_nonblocking(m_SecondarySock) != 0)
{
// Don't report as an error, the primary socket is working
err = EVUTIL_SOCKET_ERROR();
LOGD("evutil_make_socket_nonblocking() failed for secondary UDP socket: %d, %s", err, evutil_socket_error_to_string(err));
evutil_closesocket(m_SecondarySock);
m_SecondarySock = -1;
return;
}
// Bind to all IPv4 interfaces:
sockaddr_in name;
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = ntohs(m_Port);
if (bind(m_SecondarySock, reinterpret_cast<const sockaddr *>(&name), sizeof(name)) != 0)
{
// Don't report as an error, the primary socket is working
err = EVUTIL_SOCKET_ERROR();
LOGD("Cannot bind secondary socket to UDP port %d: %d (%s)", m_Port, err, evutil_socket_error_to_string(err));
evutil_closesocket(m_SecondarySock);
m_SecondarySock = -1;
return;
}
m_SecondaryEvent = event_new(cNetworkSingleton::Get().GetEventBase(), m_SecondarySock, EV_READ | EV_PERSIST, RawCallback, this);
event_add(m_SecondaryEvent, nullptr);
}
void cUDPEndpointImpl::RawCallback(evutil_socket_t a_Socket, short a_What, void * a_Self)
{
cUDPEndpointImpl * Self = reinterpret_cast<cUDPEndpointImpl *>(a_Self);
Self->Callback(a_Socket, a_What);
}
void cUDPEndpointImpl::Callback(evutil_socket_t a_Socket, short a_What)
{
if ((a_What & EV_READ) != 0)
{
// Receive datagram from the socket:
char buf[64 KiB];
int buflen = static_cast<int>(sizeof(buf));
sockaddr_storage sa;
socklen_t salen = static_cast<socklen_t>(sizeof(sa));
int len = recvfrom(a_Socket, buf, buflen, 0, reinterpret_cast<sockaddr *>(&sa), &salen);
if (len >= 0)
{
// Convert the remote IP address to a string:
char RemoteHost[128];
UInt16 RemotePort;
switch (sa.ss_family)
{
case AF_INET:
{
auto sin = reinterpret_cast<sockaddr_in *>(&sa);
evutil_inet_ntop(sa.ss_family, &sin->sin_addr, RemoteHost, sizeof(RemoteHost));
RemotePort = ntohs(sin->sin_port);
break;
}
case AF_INET6:
{
auto sin = reinterpret_cast<sockaddr_in6 *>(&sa);
evutil_inet_ntop(sa.ss_family, &sin->sin6_addr, RemoteHost, sizeof(RemoteHost));
RemotePort = ntohs(sin->sin6_port);
break;
}
default:
{
return;
}
}
// Call the callback:
m_Callbacks.OnReceivedData(buf, len, RemoteHost, RemotePort);
}
}
}
////////////////////////////////////////////////////////////////////////////////
// cNetwork API:
cUDPEndpointPtr cNetwork::CreateUDPEndpoint(UInt16 a_Port, cUDPEndpoint::cCallbacks & a_Callbacks)
{
return std::make_shared<cUDPEndpointImpl>(a_Port, a_Callbacks);
}

View File

@ -0,0 +1,81 @@
// UDPEndpointImpl.h
// Declares the cUDPEndpointImpl class representing an implementation of an endpoint in UDP communication
#pragma once
#include "Network.h"
#include <event2/event.h>
// fwd:
class cUDPEndpointImpl;
typedef SharedPtr<cUDPEndpointImpl> cUDPEndpointImplPtr;
class cUDPEndpointImpl:
public cUDPEndpoint
{
typedef cUDPEndpoint super;
public:
/** Creates a new instance of the endpoint, with the specified callbacks.
Tries to open on the specified port; if it fails, the endpoint is left in the "closed" state.
If a_Port is 0, the OS is free to assign any port number it likes to the endpoint. */
cUDPEndpointImpl(UInt16 a_Port, cUDPEndpoint::cCallbacks & a_Callbacks);
// cUDPEndpoint overrides:
virtual void Close(void) override;
virtual bool IsOpen(void) const override;
virtual UInt16 GetPort(void) const override;
virtual bool Send(const AString & a_Payload, const AString & a_Host, UInt16 a_Port) override;
virtual void EnableBroadcasts(void) override;
protected:
/** The local port on which the endpoint is open.
If this is zero, it means the endpoint is closed - either opening has failed, or it has been closed explicitly. */
UInt16 m_Port;
/** The primary underlying OS socket. */
evutil_socket_t m_MainSock;
/** True if m_MainSock is in the IPv6 namespace (needs IPv6 addresses for sending). */
bool m_IsMainSockIPv6;
/** The secondary OS socket (if primary doesn't support dualstack). */
evutil_socket_t m_SecondarySock;
/** The LibEvent handle for the primary socket. */
event * m_MainEvent;
/** The LibEvent handle for the secondary socket. */
event * m_SecondaryEvent;
/** Creates and opens the socket on the specified port.
If a_Port is 0, the OS is free to assign any port number it likes to the endpoint.
If the opening fails, the OnError() callback is called and the endpoint is left "closed" (IsOpen() returns false). */
void Open(UInt16 a_Port);
/** The callback that LibEvent calls when an event occurs on one of the sockets.
Calls Callback() on a_Self. */
static void RawCallback(evutil_socket_t a_Socket, short a_What, void * a_Self);
/** The callback that is called when an event occurs on one of the sockets. */
void Callback(evutil_socket_t a_Socket, short a_What);
};