initial commit
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
using Pulsar.Common.Extensions;
|
||||
using Pulsar.Common.Messages;
|
||||
using Pulsar.Common.Messages.Other;
|
||||
using Pulsar.Common.Networking;
|
||||
using Pulsar.Server.Forms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Pulsar.Server.Networking
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the state of the server changes.
|
||||
/// </summary>
|
||||
public event ServerStateEventHandler ServerState;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a method that will handle a change in the server's state.
|
||||
/// </summary>
|
||||
/// <param name="s">The server which changed its state.</param>
|
||||
/// <param name="listening">The new listening state of the server.</param>
|
||||
/// <param name="port">The port the server is listening on, if listening is True.</param>
|
||||
public delegate void ServerStateEventHandler(Server s, bool listening, ushort port);
|
||||
|
||||
/// <summary>
|
||||
/// Fires an event that informs subscribers that the server has changed it's state.
|
||||
/// </summary>
|
||||
/// <param name="listening">The new listening state of the server.</param>
|
||||
private void OnServerState(bool listening)
|
||||
{
|
||||
if (Listening == listening) return;
|
||||
|
||||
Listening = listening;
|
||||
|
||||
var handler = ServerState;
|
||||
handler?.Invoke(this, listening, Port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the state of a client changes.
|
||||
/// </summary>
|
||||
public event ClientStateEventHandler ClientState;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a method that will handle a change in a client's state.
|
||||
/// </summary>
|
||||
/// <param name="s">The server, the client is connected to.</param>
|
||||
/// <param name="c">The client which changed its state.</param>
|
||||
/// <param name="connected">The new connection state of the client.</param>
|
||||
public delegate void ClientStateEventHandler(Server s, Client c, bool connected);
|
||||
|
||||
/// <summary>
|
||||
/// Fires an event that informs subscribers that a client has changed its state.
|
||||
/// </summary>
|
||||
/// <param name="c">The client which changed its state.</param>
|
||||
/// <param name="connected">The new connection state of the client.</param>
|
||||
private void OnClientState(Client c, bool connected)
|
||||
{
|
||||
if (!connected)
|
||||
RemoveClient(c);
|
||||
|
||||
var handler = ClientState;
|
||||
handler?.Invoke(this, c, connected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a message is received by a client.
|
||||
/// </summary>
|
||||
public event ClientReadEventHandler ClientRead;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a method that will handle a message received from a client.
|
||||
/// </summary>
|
||||
/// <param name="s">The server, the client is connected to.</param>
|
||||
/// <param name="c">The client that has received the message.</param>
|
||||
/// <param name="message">The message that received by the client.</param>
|
||||
public delegate void ClientReadEventHandler(Server s, Client c, IMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Fires an event that informs subscribers that a message has been
|
||||
/// received from the client.
|
||||
/// </summary>
|
||||
/// <param name="c">The client that has received the message.</param>
|
||||
/// <param name="message">The message that received by the client.</param>
|
||||
/// <param name="messageLength">The length of the message.</param>
|
||||
private void OnClientRead(Client c, IMessage message, int messageLength)
|
||||
{
|
||||
BytesReceived += messageLength;
|
||||
var handler = ClientRead;
|
||||
handler?.Invoke(this, c, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a message is sent by a client.
|
||||
/// </summary>
|
||||
public event ClientWriteEventHandler ClientWrite;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the method that will handle the sent message by a client.
|
||||
/// </summary>
|
||||
/// <param name="s">The server, the client is connected to.</param>
|
||||
/// <param name="c">The client that has sent the message.</param>
|
||||
/// <param name="message">The message that has been sent by the client.</param>
|
||||
public delegate void ClientWriteEventHandler(Server s, Client c, IMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Fires an event that informs subscribers that the client has sent a message.
|
||||
/// </summary>
|
||||
/// <param name="c">The client that has sent the message.</param>
|
||||
/// <param name="message">The message that has been sent by the client.</param>
|
||||
/// <param name="messageLength">The length of the message.</param>
|
||||
private void OnClientWrite(Client c, IMessage message, int messageLength)
|
||||
{
|
||||
BytesSent += messageLength;
|
||||
var handler = ClientWrite;
|
||||
handler?.Invoke(this, c, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The port on which the server is listening.
|
||||
/// For multi-port scenarios, this is the last port that was started.
|
||||
/// </summary>
|
||||
public ushort Port { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of received bytes.
|
||||
/// </summary>
|
||||
public long BytesReceived { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of sent bytes.
|
||||
/// </summary>
|
||||
public long BytesSent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The keep-alive time in ms.
|
||||
/// </summary>
|
||||
private const uint KeepAliveTime = 25000; // 25 s
|
||||
|
||||
/// <summary>
|
||||
/// The keep-alive interval in ms.
|
||||
/// </summary>
|
||||
private const uint KeepAliveInterval = 25000; // 25 s
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The listening state of the server. True if listening, else False.
|
||||
/// </summary>
|
||||
public bool Listening { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the clients currently connected to the server.
|
||||
/// </summary>
|
||||
protected Client[] Clients
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_clientsLock)
|
||||
{
|
||||
return _clients.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of clients currently connected to the server without array allocation.
|
||||
/// </summary>
|
||||
public int ClientCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_clientsLock)
|
||||
{
|
||||
return _clients.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle(s) of the Server Socket(s).
|
||||
/// </summary>
|
||||
private readonly List<Socket> _handles = new List<Socket>();
|
||||
private readonly object _handlesLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Accept event args, one per listening socket.
|
||||
/// </summary>
|
||||
private readonly Dictionary<Socket, SocketAsyncEventArgs> _acceptArgs = new Dictionary<Socket, SocketAsyncEventArgs>();
|
||||
|
||||
/// <summary>
|
||||
/// The server certificate.
|
||||
/// </summary>
|
||||
protected readonly X509Certificate2 ServerCertificate;
|
||||
|
||||
/// <summary>
|
||||
/// List of the clients connected to the server.
|
||||
/// </summary>
|
||||
private readonly List<Client> _clients = new List<Client>();
|
||||
|
||||
/// <summary>
|
||||
/// The UPnP service used to create port mappings per port.
|
||||
/// </summary>
|
||||
private readonly Dictionary<ushort, UPnPService> _upnpByPort = new Dictionary<ushort, UPnPService>();
|
||||
|
||||
/// <summary>
|
||||
/// Lock object for the list of clients.
|
||||
/// </summary>
|
||||
private readonly object _clientsLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the server is currently processing Disconnect method.
|
||||
/// </summary>
|
||||
protected bool ProcessingDisconnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor of the server.
|
||||
/// </summary>
|
||||
/// <param name="serverCertificate">The server certificate.</param>
|
||||
protected Server(X509Certificate2 serverCertificate)
|
||||
{
|
||||
ServerCertificate = serverCertificate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the status strip icon for the server listening state.
|
||||
/// </summary>
|
||||
/// <param name="isListening">True if server is listening, false otherwise.</param>
|
||||
private void UpdateServerStatusIcon(bool isListening)
|
||||
{
|
||||
var mainForm = GetMainFormSafe();
|
||||
if (mainForm == null) return;
|
||||
|
||||
var iconResource = isListening
|
||||
? Properties.Resources.bullet_green
|
||||
: Properties.Resources.bullet_red;
|
||||
|
||||
try
|
||||
{
|
||||
if (mainForm.InvokeRequired)
|
||||
{
|
||||
mainForm.BeginInvoke(new Action(() => SetStatusStripIcon(mainForm, iconResource)));
|
||||
}
|
||||
else
|
||||
{
|
||||
SetStatusStripIcon(mainForm, iconResource);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely gets the main form instance if it exists and is valid.
|
||||
/// </summary>
|
||||
/// <returns>The main form instance or null if not available.</returns>
|
||||
private static FrmMain GetMainFormSafe()
|
||||
{
|
||||
var mainForm = Application.OpenForms.OfType<FrmMain>().FirstOrDefault();
|
||||
return (mainForm != null && !mainForm.IsDisposed && !mainForm.Disposing) ? mainForm : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the status strip icon if the control is valid.
|
||||
/// </summary>
|
||||
/// <param name="mainForm">The main form instance.</param>
|
||||
/// <param name="icon">The icon to set.</param>
|
||||
private static void SetStatusStripIcon(FrmMain mainForm, System.Drawing.Image icon)
|
||||
{
|
||||
if (mainForm.statusStrip?.IsDisposed == false &&
|
||||
mainForm.statusStrip.Items.ContainsKey("listenToolStripStatusLabel"))
|
||||
{
|
||||
mainForm.statusStrip.Items["listenToolStripStatusLabel"].Image = icon;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins listening for clients on a single port.
|
||||
/// </summary>
|
||||
public void Listen(ushort port, bool ipv6, bool enableUPnP)
|
||||
{
|
||||
ListenMany(new[] { port }, ipv6, enableUPnP);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins listening for clients on multiple ports.
|
||||
/// </summary>
|
||||
/// <param name="ports">Ports to listen on.</param>
|
||||
/// <param name="ipv6">If set to true, use a dual-stack socket to allow IPv4/6 connections. Otherwise use IPv4-only socket.</param>
|
||||
/// <param name="enableUPnP">Enables the automatic UPnP port forwarding for each port.</param>
|
||||
public void ListenMany(IEnumerable<ushort> ports, bool ipv6, bool enableUPnP)
|
||||
{
|
||||
var startNow = !Listening;
|
||||
|
||||
foreach (var port in ports.Distinct())
|
||||
{
|
||||
lock (_handlesLock)
|
||||
{
|
||||
if (_handles.Any(h => (h.LocalEndPoint as IPEndPoint)?.Port == port))
|
||||
continue;
|
||||
}
|
||||
Socket handle;
|
||||
if (Socket.OSSupportsIPv6 && ipv6)
|
||||
{
|
||||
handle = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
|
||||
handle.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0);
|
||||
handle.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
|
||||
}
|
||||
else
|
||||
{
|
||||
handle = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
handle.Bind(new IPEndPoint(IPAddress.Any, port));
|
||||
}
|
||||
|
||||
handle.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
handle.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
|
||||
handle.Listen(1000);
|
||||
lock (_handlesLock)
|
||||
{
|
||||
_handles.Add(handle);
|
||||
}
|
||||
|
||||
if (enableUPnP)
|
||||
{
|
||||
var upnp = new UPnPService();
|
||||
_upnpByPort[port] = upnp;
|
||||
upnp.CreatePortMapAsync(port);
|
||||
}
|
||||
|
||||
var item = new SocketAsyncEventArgs();
|
||||
item.Completed += AcceptClient;
|
||||
_acceptArgs[handle] = item;
|
||||
|
||||
Port = port; // keep last started port for compatibility
|
||||
|
||||
if (!handle.AcceptAsync(item))
|
||||
AcceptClient(handle, item);
|
||||
|
||||
var mainForm = GetMainFormSafe();
|
||||
if (mainForm != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (mainForm.InvokeRequired)
|
||||
{
|
||||
mainForm.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
mainForm.EventLog($"Started listening for connections on port: {port}", "info");
|
||||
UpdateServerStatusIcon(true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
mainForm.EventLog($"Started listening for connections on port: {port}", "info");
|
||||
UpdateServerStatusIcon(true);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startNow && _handles.Count > 0)
|
||||
{
|
||||
OnServerState(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts and begins authenticating an incoming client.
|
||||
/// </summary>
|
||||
/// <param name="s">The listening socket.</param>
|
||||
/// <param name="e">Asynchronous socket event.</param>
|
||||
private void AcceptClient(object s, SocketAsyncEventArgs e)
|
||||
{
|
||||
var listenSocket = s as Socket;
|
||||
if (listenSocket == null)
|
||||
{
|
||||
// Try to recover the listen socket from our map
|
||||
listenSocket = _acceptArgs.Keys.FirstOrDefault();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
switch (e.SocketError)
|
||||
{
|
||||
case SocketError.Success:
|
||||
try
|
||||
{
|
||||
Socket clientSocket = e.AcceptSocket;
|
||||
clientSocket.SetKeepAliveEx(KeepAliveInterval, KeepAliveTime);
|
||||
clientSocket.NoDelay = true;
|
||||
|
||||
var networkStream = new NetworkStream(clientSocket, true);
|
||||
var client = new Client(networkStream, (IPEndPoint)clientSocket.RemoteEndPoint, ServerCertificate);
|
||||
AddClient(client);
|
||||
OnClientState(client, true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
e.AcceptSocket?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SocketError.ConnectionReset:
|
||||
break;
|
||||
default:
|
||||
throw new SocketException((int)e.SocketError);
|
||||
}
|
||||
|
||||
e.AcceptSocket = null; // enable reuse
|
||||
} while (listenSocket != null && !listenSocket.AcceptAsync(e));
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a connected client to the list of clients,
|
||||
/// subscribes to the client's events.
|
||||
/// </summary>
|
||||
/// <param name="client">The client to add.</param>
|
||||
private void AddClient(Client client)
|
||||
{
|
||||
lock (_clientsLock)
|
||||
{
|
||||
client.ClientState += OnClientState;
|
||||
client.ClientRead += OnClientRead;
|
||||
|
||||
_clients.Add(client);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a disconnected client from the list of clients,
|
||||
/// unsubscribes from the client's events.
|
||||
/// </summary>
|
||||
/// <param name="client">The client to remove.</param>
|
||||
private void RemoveClient(Client client)
|
||||
{
|
||||
if (ProcessingDisconnect) return;
|
||||
|
||||
lock (_clientsLock)
|
||||
{
|
||||
client.ClientState -= OnClientState;
|
||||
client.ClientRead -= OnClientRead;
|
||||
|
||||
_clients.Remove(client);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect the server from all of the clients and discontinue
|
||||
/// listening (placing the server in an "off" state).
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
if (ProcessingDisconnect) return;
|
||||
ProcessingDisconnect = true;
|
||||
|
||||
List<Socket> toClose;
|
||||
lock (_handlesLock)
|
||||
{
|
||||
toClose = _handles.ToList();
|
||||
_handles.Clear();
|
||||
}
|
||||
foreach (var handle in toClose)
|
||||
{
|
||||
try { handle.Close(); } catch { }
|
||||
}
|
||||
|
||||
foreach (var kvp in _acceptArgs.ToList())
|
||||
{
|
||||
try { kvp.Value.Dispose(); } catch { }
|
||||
}
|
||||
_acceptArgs.Clear();
|
||||
|
||||
foreach (var upnpKvp in _upnpByPort.ToList())
|
||||
{
|
||||
try { upnpKvp.Value.DeletePortMapAsync(upnpKvp.Key); } catch { }
|
||||
}
|
||||
_upnpByPort.Clear();
|
||||
|
||||
lock (_clientsLock)
|
||||
{
|
||||
var clientsToDisconnect = _clients.ToList();
|
||||
_clients.Clear();
|
||||
|
||||
foreach (var client in clientsToDisconnect)
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Disconnect();
|
||||
client.ClientState -= OnClientState;
|
||||
client.ClientRead -= OnClientRead;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProcessingDisconnect = false;
|
||||
OnServerState(false);
|
||||
UpdateServerStatusIcon(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ports the server is currently listening on.
|
||||
/// </summary>
|
||||
public ushort[] GetListeningPorts()
|
||||
{
|
||||
lock (_handlesLock)
|
||||
{
|
||||
return _handles.Select(h => (ushort)((IPEndPoint)h.LocalEndPoint).Port).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user