using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Generic;
using NetcodeIO.NET.Utils;
using NetcodeIO.NET.Utils.IO;
using NetcodeIO.NET.Internal;
namespace NetcodeIO.NET
{
///
/// State of a client object
///
public enum ClientState
{
///
/// The connect token has expired
///
ConnectTokenExpired = -6,
///
/// The connect token is invalid
///
InvalidConnectToken = -5,
///
/// Connection timed out while connected to a server
///
ConnectionTimedOut = -4,
///
/// Connection timed out while sending challenge response
///
ChallengeResponseTimedOut = -3,
///
/// Connection timed out while sending connection request
///
ConnectionRequestTimedOut = -2,
///
/// The connection request was denied by the server
///
ConnectionDenied = -1,
///
/// Client is not currently connected
///
Disconnected = 0,
///
/// Client is currently sending a connection request
///
SendingConnectionRequest = 1,
///
/// Client is currently sending a connection response to a server
///
SendingChallengeResponse = 2,
///
/// The client is connected to a server
///
Connected = 3,
}
///
/// Event handler for when a client's state changes
///
public delegate void ClientStateChangedHandler(ClientState state);
///
/// Event handler for when payloads are received from the server
///
public delegate void ClientMessageReceivedHandler(byte[] payload, int payloadSize);
///
/// Class for connecting to and communicating with Netcode.IO servers
///
public sealed class Client
{
#region Public Fields/Properties
///
/// Gets or sets the internal tickrate of the client in ticks per second. Value must be between 1 and 1000
///
public int Tickrate
{
get { return tickrate; }
set
{
if (value < 1 || value > 1000) throw new ArgumentOutOfRangeException();
tickrate = value;
}
}
///
/// Gets the current state of the client
///
public ClientState State
{
get { return state; }
}
///
/// Gets the client index as assigned by a server, or -1 if not connected to a server
///
public int ClientIndex
{
get
{
if (state == ClientState.Connected)
return (int)clientIndex;
else
return -1;
}
}
///
/// Gets the maximum client slots on the server, or -1 if not connected to a server
///
public int MaxSlots
{
get
{
if (state == ClientState.Connected)
return (int)maxSlots;
else
return -1;
}
}
///
/// Gets the port the client socket is bound to, or -1 if not bound
///
public int Port
{
get
{
if (socket == null)
return -1;
return socket.BoundPort;
}
}
///
/// Event triggered when client state changes
///
public event ClientStateChangedHandler OnStateChanged;
///
/// Event triggered when a payload is received from the server
///
public event ClientMessageReceivedHandler OnMessageReceived;
#endregion
#region Private fields
private int tickrate = 60;
private ISocketContext socket;
private bool isRunning = false;
private ClientState state;
private ClientState pendingDisconnectState;
private double lastResponseTime;
internal double time;
internal double dt;
private uint clientIndex;
private uint maxSlots;
private byte[] clientToServerKey;
private byte[] serverToClientKey;
private ulong nextPacketSequence = 0;
private NetcodePublicConnectToken connectToken;
private NetcodeConnectionChallengeResponsePacket challengeResponse;
private Queue connectServers = new Queue();
private EndPoint currentServerEndpoint;
private NetcodeReplayProtection replayProtection;
private Func socketFactory;
#endregion
public Client()
{
state = ClientState.Disconnected;
pendingDisconnectState = ClientState.Disconnected;
replayProtection = new NetcodeReplayProtection();
replayProtection.Reset();
socketFactory = (endpoint) =>
{
var socket = new UDPSocketContext(endpoint.AddressFamily);
var socketEndpoint = new IPEndPoint(endpoint.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any, 0);
socket.Bind(socketEndpoint);
return socket;
};
}
internal Client(Func socketFactory)
{
state = ClientState.Disconnected;
pendingDisconnectState = ClientState.Disconnected;
replayProtection = new NetcodeReplayProtection();
replayProtection.Reset();
this.socketFactory = socketFactory;
}
#region Public Methods
///
/// Disconnect the client from the server, if connected
///
public void Disconnect()
{
disconnect(ClientState.Disconnected);
}
///
/// Connect to a server using the connect token
///
public void Connect(byte[] connectToken)
{
Connect(connectToken, true);
}
internal void Connect(byte[] connectToken, bool autoTick)
{
if (state != ClientState.Disconnected)
throw new InvalidOperationException();
keepAliveTimer = 0.0;
connectServers.Clear();
replayProtection.Reset();
if (connectToken.Length != Defines.NETCODE_CONNECT_TOKEN_PUBLIC_BYTES)
{
changeState(ClientState.InvalidConnectToken);
return;
}
NetcodePublicConnectToken tokenData = new NetcodePublicConnectToken();
using (var reader = ByteArrayReaderWriter.Get(connectToken))
{
if (!tokenData.Read(reader))
{
changeState(ClientState.InvalidConnectToken);
return;
}
}
if (tokenData.CreateTimestamp >= tokenData.ExpireTimestamp)
{
changeState(ClientState.InvalidConnectToken);
return;
}
clientToServerKey = tokenData.ClientToServerKey;
serverToClientKey = tokenData.ServerToClientKey;
foreach (var server in tokenData.ConnectServers)
connectServers.Enqueue(server.Endpoint);
this.connectToken = tokenData;
this.state = ClientState.SendingConnectionRequest;
// bind socket, spin up threads, and start trying to connect
isRunning = true;
currentServerEndpoint = connectServers.Dequeue();
createSocket(currentServerEndpoint);
if (autoTick)
{
this.time = DateTime.Now.GetTotalSeconds();
ThreadPool.QueueUserWorkItem(clientTick);
}
}
///
/// Send a payload to the server
///
public void Send(byte[] payload, int payloadSize)
{
if (state != ClientState.Connected)
throw new InvalidOperationException();
serializePacket(new NetcodePacketHeader() { PacketType = NetcodePacketType.ConnectionPayload }, (writer) =>
{
writer.WriteBuffer(payload, payloadSize);
}, clientToServerKey);
}
#endregion
#region Core
private void createSocket(EndPoint endpoint)
{
this.socket = socketFactory(endpoint);
}
private void stopSocket()
{
if (socket != null)
{
socket.Close();
socket = null;
}
}
private void disconnect(ClientState disconnectState)
{
if (state == ClientState.Connected)
sendDisconnect();
isRunning = false;
pendingDisconnectState = ClientState.Disconnected;
changeState(disconnectState);
stopSocket();
connectServers.Clear();
currentServerEndpoint = null;
nextPacketSequence = 0;
serverToClientKey = null;
}
internal void Tick(double time)
{
if (this.socket == null) return;
this.socket.Pump();
this.dt = time - this.time;
this.time = time;
// process buffered packets
Datagram datagram;
while (socket != null && socket.Read(out datagram))
{
processDatagram(datagram);
datagram.Release();
}
// process current state
switch (state)
{
case ClientState.SendingConnectionRequest:
sendingConnectionRequest();
break;
case ClientState.SendingChallengeResponse:
sendingChallengeResponse();
break;
case ClientState.Connected:
connected();
break;
}
}
private double timer = 0.0;
private void clientTick(Object stateInfo)
{
while (isRunning)
{
Tick(DateTime.Now.GetTotalSeconds());
// sleep until next tick
double tickLength = 1.0 / tickrate;
Thread.Sleep((int)(tickLength * 1000));
}
}
private bool checkTimer(int customTickrate)
{
if (timer <= 0.0)
{
timer = 1.0 / customTickrate;
return true;
}
return false;
}
private void consumeTimer()
{
timer -= dt;
}
private void processDatagram(Datagram datagram)
{
if (!MiscUtils.AddressEqual(datagram.sender, currentServerEndpoint))
return;
using (var reader = ByteArrayReaderWriter.Get(datagram.payload))
{
NetcodePacketHeader packetHeader = new NetcodePacketHeader();
packetHeader.Read(reader);
int length = datagram.payloadSize - (int)reader.ReadPosition;
switch (packetHeader.PacketType)
{
case NetcodePacketType.ConnectionChallenge:
processChallengePacket(packetHeader, length, reader);
break;
case NetcodePacketType.ConnectionDenied:
processConnectionDenied(packetHeader, length, reader);
break;
case NetcodePacketType.ConnectionKeepAlive:
processConnectionKeepAlive(packetHeader, length, reader);
break;
case NetcodePacketType.ConnectionPayload:
processConnectionPayload(packetHeader, length, reader);
break;
case NetcodePacketType.ConnectionDisconnect:
processConnectionDisconnect(packetHeader, length, reader);
break;
}
}
}
#endregion
#region states
private double keepAliveTimer = 0.0;
private void connected()
{
keepAliveTimer += dt;
if (keepAliveTimer >= 0.1)
{
keepAliveTimer = 0.0;
sendKeepAlive();
}
if ((time - lastResponseTime) >= Defines.NETCODE_TIMEOUT_SECONDS)
{
disconnect(ClientState.ConnectionTimedOut);
}
}
private double connectionTimer = 0.0;
private void sendingConnectionRequest()
{
// check and make sure connect token hasn't expired while we've been trying to connect
if ((ulong)Math.Truncate(time) >= connectToken.ExpireTimestamp)
{
disconnect(ClientState.ConnectTokenExpired);
return;
}
connectionTimer += dt;
if (checkTimer(10))
{
// send a connection request 10 times per second
sendConnectionRequest(currentServerEndpoint);
}
// if we don't get a response within timeout, move on.
if ((int)connectionTimer >= connectToken.TimeoutSeconds && connectToken.TimeoutSeconds >= 0)
{
pendingDisconnectState = ClientState.ConnectionRequestTimedOut;
connectionMoveNextEndpoint();
}
consumeTimer();
}
private void sendingChallengeResponse()
{
connectionTimer += dt;
if (checkTimer(10))
{
// send a connection response 10 times per second
sendConnectionResponse(currentServerEndpoint);
}
// if we don't get a response within timeout, move on.
if ((int)connectionTimer >= connectToken.TimeoutSeconds && connectToken.TimeoutSeconds >= 0)
{
pendingDisconnectState = ClientState.ChallengeResponseTimedOut;
connectionMoveNextEndpoint();
}
consumeTimer();
}
#endregion
#region Process messages
private void processConnectionDisconnect(NetcodePacketHeader header, int length, ByteArrayReaderWriter stream)
{
if (checkReplay(header)) return;
if (this.state != ClientState.Connected) return;
var decryptKey = serverToClientKey;
var disconnectPacket = new NetcodeDisconnectPacket() { Header = header };
if (!disconnectPacket.Read(stream, length, decryptKey, connectToken.ProtocolID))
{
return;
}
Disconnect();
}
private void processConnectionPayload(NetcodePacketHeader header, int length, ByteArrayReaderWriter stream)
{
if (checkReplay(header)) return;
if (this.state != ClientState.Connected) return;
var decryptKey = serverToClientKey;
var payloadPacket = new NetcodePayloadPacket() { Header = header };
if (!payloadPacket.Read(stream, length, decryptKey, connectToken.ProtocolID))
{
return;
}
lastResponseTime = time;
if (OnMessageReceived != null)
OnMessageReceived(payloadPacket.Payload, payloadPacket.Length);
payloadPacket.Release();
}
private void processConnectionKeepAlive(NetcodePacketHeader header, int length, ByteArrayReaderWriter stream)
{
if (checkReplay(header)) return;
var decryptKey = serverToClientKey;
var keepAlive = new NetcodeKeepAlivePacket() { Header = header };
if (!keepAlive.Read(stream, length, decryptKey, connectToken.ProtocolID))
{
return;
}
if (this.state == ClientState.Connected || this.state == ClientState.SendingChallengeResponse)
lastResponseTime = time;
if (this.state == ClientState.SendingChallengeResponse)
{
this.clientIndex = keepAlive.ClientIndex;
this.maxSlots = keepAlive.MaxSlots;
changeState(ClientState.Connected);
}
}
private void processConnectionDenied(NetcodePacketHeader header, int length, ByteArrayReaderWriter stream)
{
var decryptKey = serverToClientKey;
var denyPacket = new NetcodeDenyConnectionPacket() { Header = header };
if (!denyPacket.Read(stream, length, decryptKey, connectToken.ProtocolID))
{
return;
}
pendingDisconnectState = ClientState.ConnectionDenied;
// move onto the next server
timer = 0.0;
connectionTimer = 0.0;
connectionMoveNextEndpoint();
}
private void processChallengePacket(NetcodePacketHeader header, int length, ByteArrayReaderWriter stream)
{
var decryptKey = serverToClientKey;
var challengePacket = new NetcodeConnectionChallengeResponsePacket() { Header = header };
if (!challengePacket.Read(stream, length, decryptKey, connectToken.ProtocolID))
{
return;
}
if (state == ClientState.SendingConnectionRequest)
{
this.challengeResponse = challengePacket;
this.challengeResponse.Header.PacketType = NetcodePacketType.ChallengeResponse;
// move onto sending challenge response
timer = 0.0;
connectionTimer = 0.0;
changeState(ClientState.SendingChallengeResponse);
}
}
#endregion
#region Send methods
private void changeState(ClientState newState)
{
if (newState != state)
{
state = newState;
if (OnStateChanged != null)
OnStateChanged(state);
}
}
private void connectionMoveNextEndpoint()
{
timer = 0.0;
connectionTimer = 0.0;
if (connectServers.Count > 0)
{
currentServerEndpoint = connectServers.Dequeue();
stopSocket();
createSocket(currentServerEndpoint);
changeState(ClientState.SendingConnectionRequest);
}
else
{
disconnect(pendingDisconnectState);
}
}
private void sendKeepAlive()
{
serializePacket(new NetcodePacketHeader() { PacketType = NetcodePacketType.ConnectionKeepAlive }, (writer) =>
{
writer.Write(clientIndex);
writer.Write(maxSlots);
}, clientToServerKey);
}
private void sendDisconnect()
{
for (int i = 0; i < Defines.NUM_DISCONNECT_PACKETS; i++)
{
serializePacket(new NetcodePacketHeader() { PacketType = NetcodePacketType.ConnectionDisconnect }, (writer) =>
{
}, clientToServerKey);
}
}
private void sendConnectionResponse(EndPoint server)
{
serializePacket(challengeResponse.Header, (writer) =>
{
challengeResponse.Write(writer);
}, clientToServerKey);
}
private void sendConnectionRequest(EndPoint server)
{
byte[] packetBuffer = BufferPool.GetBuffer(1 + 13 + 8 + 8 + 8 + Defines.NETCODE_CONNECT_TOKEN_PRIVATE_BYTES);
using (var stream = ByteArrayReaderWriter.Get(packetBuffer))
{
stream.Write((byte)0);
stream.WriteASCII(Defines.NETCODE_VERSION_INFO_STR);
stream.Write(connectToken.ProtocolID);
stream.Write(connectToken.ExpireTimestamp);
stream.Write(connectToken.ConnectTokenSequence);
stream.Write(connectToken.PrivateConnectTokenBytes);
}
socket.SendTo(packetBuffer, server);
BufferPool.ReturnBuffer(packetBuffer);
}
#endregion
#region Util methods
private void sendPacket(NetcodePacketHeader packetHeader, byte[] packetData, int packetDataLen, byte[] key)
{
// assign a sequence number to this packet
packetHeader.SequenceNumber = this.nextPacketSequence++;
// encrypt packet data
byte[] encryptedPacketBuffer = BufferPool.GetBuffer(2048);
int encryptedBytes = PacketIO.EncryptPacketData(packetHeader, connectToken.ProtocolID, packetData, packetDataLen, key, encryptedPacketBuffer);
int packetLen = 0;
// write packet to byte array
var packetBuffer = BufferPool.GetBuffer(2048);
using (var packetWriter = ByteArrayReaderWriter.Get(packetBuffer))
{
packetHeader.Write(packetWriter);
packetWriter.WriteBuffer(encryptedPacketBuffer, encryptedBytes);
packetLen = (int)packetWriter.WritePosition;
}
// send packet
try {
socket.SendTo(packetBuffer, packetLen, currentServerEndpoint);
}
catch { }
BufferPool.ReturnBuffer(packetBuffer);
BufferPool.ReturnBuffer(encryptedPacketBuffer);
}
private void serializePacket(NetcodePacketHeader packetHeader, Action write, byte[] key)
{
byte[] tempPacket = BufferPool.GetBuffer(2048);
int writeLen = 0;
using (var writer = ByteArrayReaderWriter.Get(tempPacket))
{
write(writer);
writeLen = (int)writer.WritePosition;
}
sendPacket(packetHeader, tempPacket, writeLen, key);
BufferPool.ReturnBuffer(tempPacket);
}
private bool checkReplay(NetcodePacketHeader packetHeader)
{
return replayProtection.AlreadyReceived(packetHeader.SequenceNumber);
}
#endregion
}
}