mirror of
https://github.com/Steffo99/better-tee.git
synced 2024-11-26 00:34:18 +00:00
194 lines
8.3 KiB
C#
194 lines
8.3 KiB
C#
|
using System;
|
|||
|
using System.Collections.Concurrent;
|
|||
|
using System.Net;
|
|||
|
using System.Net.Sockets;
|
|||
|
using System.Threading;
|
|||
|
|
|||
|
namespace Telepathy
|
|||
|
{
|
|||
|
public class Client : Common
|
|||
|
{
|
|||
|
public TcpClient client;
|
|||
|
Thread receiveThread;
|
|||
|
Thread sendThread;
|
|||
|
|
|||
|
// TcpClient.Connected doesn't check if socket != null, which
|
|||
|
// results in NullReferenceExceptions if connection was closed.
|
|||
|
// -> let's check it manually instead
|
|||
|
public bool Connected => client != null &&
|
|||
|
client.Client != null &&
|
|||
|
client.Client.Connected;
|
|||
|
|
|||
|
// TcpClient has no 'connecting' state to check. We need to keep track
|
|||
|
// of it manually.
|
|||
|
// -> checking 'thread.IsAlive && !Connected' is not enough because the
|
|||
|
// thread is alive and connected is false for a short moment after
|
|||
|
// disconnecting, so this would cause race conditions.
|
|||
|
// -> we use a threadsafe bool wrapper so that ThreadFunction can remain
|
|||
|
// static (it needs a common lock)
|
|||
|
// => Connecting is true from first Connect() call in here, through the
|
|||
|
// thread start, until TcpClient.Connect() returns. Simple and clear.
|
|||
|
// => bools are atomic according to
|
|||
|
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables
|
|||
|
// made volatile so the compiler does not reorder access to it
|
|||
|
volatile bool _Connecting;
|
|||
|
public bool Connecting => _Connecting;
|
|||
|
|
|||
|
// send queue
|
|||
|
// => SafeQueue is twice as fast as ConcurrentQueue, see SafeQueue.cs!
|
|||
|
SafeQueue<byte[]> sendQueue = new SafeQueue<byte[]>();
|
|||
|
|
|||
|
// ManualResetEvent to wake up the send thread. better than Thread.Sleep
|
|||
|
// -> call Set() if everything was sent
|
|||
|
// -> call Reset() if there is something to send again
|
|||
|
// -> call WaitOne() to block until Reset was called
|
|||
|
ManualResetEvent sendPending = new ManualResetEvent(false);
|
|||
|
|
|||
|
// the thread function
|
|||
|
void ReceiveThreadFunction(string ip, int port)
|
|||
|
{
|
|||
|
// absolutely must wrap with try/catch, otherwise thread
|
|||
|
// exceptions are silent
|
|||
|
try
|
|||
|
{
|
|||
|
// connect (blocking)
|
|||
|
client.Connect(ip, port);
|
|||
|
_Connecting = false;
|
|||
|
|
|||
|
// set socket options after the socket was created in Connect()
|
|||
|
// (not after the constructor because we clear the socket there)
|
|||
|
client.NoDelay = NoDelay;
|
|||
|
client.SendTimeout = SendTimeout;
|
|||
|
|
|||
|
// start send thread only after connected
|
|||
|
sendThread = new Thread(() => { SendLoop(0, client, sendQueue, sendPending); });
|
|||
|
sendThread.IsBackground = true;
|
|||
|
sendThread.Start();
|
|||
|
|
|||
|
// run the receive loop
|
|||
|
ReceiveLoop(0, client, receiveQueue, MaxMessageSize);
|
|||
|
}
|
|||
|
catch (SocketException exception)
|
|||
|
{
|
|||
|
// this happens if (for example) the ip address is correct
|
|||
|
// but there is no server running on that ip/port
|
|||
|
Logger.Log("Client Recv: failed to connect to ip=" + ip + " port=" + port + " reason=" + exception);
|
|||
|
|
|||
|
// add 'Disconnected' event to message queue so that the caller
|
|||
|
// knows that the Connect failed. otherwise they will never know
|
|||
|
receiveQueue.Enqueue(new Message(0, EventType.Disconnected, null));
|
|||
|
}
|
|||
|
catch (Exception exception)
|
|||
|
{
|
|||
|
// something went wrong. probably important.
|
|||
|
Logger.LogError("Client Recv Exception: " + exception);
|
|||
|
}
|
|||
|
|
|||
|
// sendthread might be waiting on ManualResetEvent,
|
|||
|
// so let's make sure to end it if the connection
|
|||
|
// closed.
|
|||
|
// otherwise the send thread would only end if it's
|
|||
|
// actually sending data while the connection is
|
|||
|
// closed.
|
|||
|
sendThread?.Interrupt();
|
|||
|
|
|||
|
// Connect might have failed. thread might have been closed.
|
|||
|
// let's reset connecting state no matter what.
|
|||
|
_Connecting = false;
|
|||
|
|
|||
|
// if we got here then we are done. ReceiveLoop cleans up already,
|
|||
|
// but we may never get there if connect fails. so let's clean up
|
|||
|
// here too.
|
|||
|
client.Close();
|
|||
|
}
|
|||
|
|
|||
|
public void Connect(string ip, int port)
|
|||
|
{
|
|||
|
// not if already started
|
|||
|
if (Connecting || Connected) return;
|
|||
|
|
|||
|
// We are connecting from now until Connect succeeds or fails
|
|||
|
_Connecting = true;
|
|||
|
|
|||
|
// create a TcpClient with perfect IPv4, IPv6 and hostname resolving
|
|||
|
// support.
|
|||
|
//
|
|||
|
// * TcpClient(hostname, port): works but would connect (and block)
|
|||
|
// already
|
|||
|
// * TcpClient(AddressFamily.InterNetworkV6): takes Ipv4 and IPv6
|
|||
|
// addresses but only connects to IPv6 servers (e.g. Telepathy).
|
|||
|
// does NOT connect to IPv4 servers (e.g. Mirror Booster), even
|
|||
|
// with DualMode enabled.
|
|||
|
// * TcpClient(): creates IPv4 socket internally, which would force
|
|||
|
// Connect() to only use IPv4 sockets.
|
|||
|
//
|
|||
|
// => the trick is to clear the internal IPv4 socket so that Connect
|
|||
|
// resolves the hostname and creates either an IPv4 or an IPv6
|
|||
|
// socket as needed (see TcpClient source)
|
|||
|
client = new TcpClient(); // creates IPv4 socket
|
|||
|
client.Client = null; // clear internal IPv4 socket until Connect()
|
|||
|
|
|||
|
// clear old messages in queue, just to be sure that the caller
|
|||
|
// doesn't receive data from last time and gets out of sync.
|
|||
|
// -> calling this in Disconnect isn't smart because the caller may
|
|||
|
// still want to process all the latest messages afterwards
|
|||
|
receiveQueue = new ConcurrentQueue<Message>();
|
|||
|
sendQueue.Clear();
|
|||
|
|
|||
|
// client.Connect(ip, port) is blocking. let's call it in the thread
|
|||
|
// and return immediately.
|
|||
|
// -> this way the application doesn't hang for 30s if connect takes
|
|||
|
// too long, which is especially good in games
|
|||
|
// -> this way we don't async client.BeginConnect, which seems to
|
|||
|
// fail sometimes if we connect too many clients too fast
|
|||
|
receiveThread = new Thread(() => { ReceiveThreadFunction(ip, port); });
|
|||
|
receiveThread.IsBackground = true;
|
|||
|
receiveThread.Start();
|
|||
|
}
|
|||
|
|
|||
|
public void Disconnect()
|
|||
|
{
|
|||
|
// only if started
|
|||
|
if (Connecting || Connected)
|
|||
|
{
|
|||
|
// close client
|
|||
|
client.Close();
|
|||
|
|
|||
|
// wait until thread finished. this is the only way to guarantee
|
|||
|
// that we can call Connect() again immediately after Disconnect
|
|||
|
receiveThread?.Join();
|
|||
|
|
|||
|
// clear send queues. no need to hold on to them.
|
|||
|
// (unlike receiveQueue, which is still needed to process the
|
|||
|
// latest Disconnected message, etc.)
|
|||
|
sendQueue.Clear();
|
|||
|
|
|||
|
// let go of this one completely. the thread ended, no one uses
|
|||
|
// it anymore and this way Connected is false again immediately.
|
|||
|
client = null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public bool Send(byte[] data)
|
|||
|
{
|
|||
|
if (Connected)
|
|||
|
{
|
|||
|
// respect max message size to avoid allocation attacks.
|
|||
|
if (data.Length <= MaxMessageSize)
|
|||
|
{
|
|||
|
// add to send queue and return immediately.
|
|||
|
// calling Send here would be blocking (sometimes for long times
|
|||
|
// if other side lags or wire was disconnected)
|
|||
|
sendQueue.Enqueue(data);
|
|||
|
sendPending.Set(); // interrupt SendThread WaitOne()
|
|||
|
return true;
|
|||
|
}
|
|||
|
Logger.LogError("Client.Send: message too big: " + data.Length + ". Limit: " + MaxMessageSize);
|
|||
|
return false;
|
|||
|
}
|
|||
|
Logger.LogWarning("Client.Send: not connected!");
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|