Get Telepathy from nuget instead

This commit is contained in:
Paul Pacheco 2018-08-25 11:34:07 -05:00 committed by vis2k
parent 507b781250
commit fdccbe621d
14 changed files with 11 additions and 1127 deletions

View File

@ -56,6 +56,9 @@
<Reference Include="UnityEditor" Condition=" '$(Configuration)' == 'Release-Editor' or '$(Configuration)' == 'Debug-Editor'">
<HintPath>..\lib\UnityEditor.dll</HintPath>
</Reference>
<Reference Include="Telepathy">
<HintPath>..\packages\Telepathy.1.0.114\lib\net35\Telepathy.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="ClientScene.cs" />
@ -86,16 +89,6 @@
<Compile Include="Transport\LLAPITransport.cs" />
<Compile Include="Transport\TelepathyTransport.cs" />
<Compile Include="Transport\TelepathyWebsocketsMultiplexTransport.cs" />
<Compile Include="Transport\Telepathy\Client.cs" />
<Compile Include="Transport\Telepathy\Common.cs" />
<Compile Include="Transport\Telepathy\EventType.cs" />
<Compile Include="Transport\Telepathy\Logger.cs" />
<Compile Include="Transport\Telepathy\Message.cs" />
<Compile Include="Transport\Telepathy\NetworkStreamExtensions.cs" />
<Compile Include="Transport\Telepathy\SafeCounter.cs" />
<Compile Include="Transport\Telepathy\SafeDictionary.cs" />
<Compile Include="Transport\Telepathy\SafeQueue.cs" />
<Compile Include="Transport\Telepathy\Server.cs" />
<Compile Include="Transport\Transport.cs" />
<Compile Include="UNetwork.cs" />
<Compile Include="NetworkReader.cs" />
@ -104,21 +97,20 @@
<Compile Include="NetworkWriter.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Transport\Telepathy\LICENSE" />
<Content Include="Transport\Telepathy\README.md" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="AfterBuild" DependsOnTargets="AfterBuildEditor;AfterBuildStandalone"/>
<Target Name="AfterBuild" DependsOnTargets="AfterBuildEditor;AfterBuildStandalone" />
<Target Name="AfterBuildEditor" Condition=" '$(Configuration)' == 'Release-Editor' or '$(Configuration)' == 'Debug-Editor'" >
<Target Name="AfterBuildEditor" Condition=" '$(Configuration)' == 'Release-Editor' or '$(Configuration)' == 'Debug-Editor'">
<MakeDir Directories="$(ProjectDir)..\Output\Plugins" />
<Copy SourceFiles="$(TargetDir)$(TargetName).dll" DestinationFiles="$(ProjectDir)..\Output\Plugins\$(TargetName).dll" />
<Copy Condition="Exists('$(TargetDir)$(TargetName).dll.mdb')" SourceFiles="$(TargetDir)$(TargetName).dll.mdb" DestinationFiles="$(ProjectDir)..\Output\Plugins\$(TargetName).dll.mdb" />
<Copy Condition="Exists('$(TargetDir)$(TargetName).pdb')" SourceFiles="$(TargetDir)$(TargetName).pdb" DestinationFiles="$(ProjectDir)..\Output\Plugins\$(TargetName).pdb" />
<Copy SourceFiles="Mirror-Editor.Runtime.dll.meta" DestinationFiles="$(ProjectDir)..\Output\Plugins\Mirror.Runtime.dll.meta" />
</Target>
<Target Name="AfterBuildStandalone" Condition=" '$(Configuration)' == 'Release' or '$(Configuration)' == 'Debug'" >
<Target Name="AfterBuildStandalone" Condition=" '$(Configuration)' == 'Release' or '$(Configuration)' == 'Debug'">
<MakeDir Directories="$(ProjectDir)..\Output\Plugins\Standalone" />
<Copy SourceFiles="$(TargetDir)$(TargetName).dll" DestinationFiles="$(ProjectDir)..\Output\Plugins\Standalone\$(TargetName).dll" />
<Copy Condition="Exists('$(TargetDir)$(TargetName).dll.mdb')" SourceFiles="$(TargetDir)$(TargetName).dll.mdb" DestinationFiles="$(ProjectDir)..\Output\Plugins\Standalone\$(TargetName).dll.mdb" />

View File

@ -1,126 +0,0 @@
using System;
using System.Net.Sockets;
using System.Threading;
namespace Telepathy
{
public class Client : Common
{
TcpClient client;
public bool Connected
{
get
{
// TcpClient.Connected doesn't check if socket != null, which
// results in NullReferenceExceptions if connection was closed.
// -> let's check it manually instead
return client != null &&
client.Client != null &&
client.Client.Connected;
}
}
public bool Connecting
{
// client was created by Connect() call but not fully connected yet?
get { return client != null && !Connected; }
}
// the thread function
// (static to reduce state for maximum reliability)
static void ThreadFunction(TcpClient client, string ip, int port, SafeQueue<Message> messageQueue)
{
// absolutely must wrap with try/catch, otherwise thread
// exceptions are silent
try
{
// connect (blocking)
// (NoDelay disables nagle algorithm. lowers CPU% and latency)
client.NoDelay = true;
client.Connect(ip, port);
// run the receive loop
ReceiveLoop(0, client, messageQueue);
}
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: 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
messageQueue.Enqueue(new Message(0, EventType.Disconnected, null));
}
catch (Exception exception)
{
// something went wrong. probably important.
Logger.LogError("Client Exception: " + exception);
}
// 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;
// TcpClient can only be used once. need to create a new one each
// time.
client = new TcpClient();
// 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
messageQueue.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
Thread thread = new Thread(() => { ThreadFunction(client, ip, port, messageQueue); });
thread.IsBackground = true;
thread.Start();
}
public void Disconnect()
{
// only if started
if (Connecting || Connected)
{
// close client, ThreadFunc will end and clean up
client.Close();
// clear client reference so that we can call Connect again
// immediately after calling Disconnect.
// -> this client's thread will end in the background in a few
// milliseconds, we don't need to worry about it anymore
// -> setting it null here won't set it null in ThreadFunction,
// because it's static and we pass a reference. so there
// won't be any NullReferenceExceptions. the thread will just
// end gracefully.
client = null;
Logger.Log("Client: disconnected");
}
}
public bool Send(byte[] data)
{
if (Connected)
{
return SendMessage(client.GetStream(), data);
}
Logger.LogWarning("Client.Send: not connected!");
return false;
}
}
}

View File

@ -1,195 +0,0 @@
// common code used by server and client
using System;
using System.Net.Sockets;
namespace Telepathy
{
public abstract class Common
{
// common code /////////////////////////////////////////////////////////
// incoming message queue of <connectionId, message>
// (not a HashSet because one connection can have multiple new messages)
protected SafeQueue<Message> messageQueue = new SafeQueue<Message>();
// warning if message queue gets too big
// if the average message is about 20 bytes then:
// - 1k messages are 20KB
// - 10k messages are 200KB
// - 100k messages are 1.95MB
// 2MB are not that much, but it is a bad sign if the caller process
// can't call GetNextMessage faster than the incoming messages.
public static int messageQueueSizeWarning = 100000;
// removes and returns the oldest message from the message queue.
// (might want to call this until it doesn't return anything anymore)
// -> Connected, Data, Disconnected events are all added here
// -> bool return makes while (GetMessage(out Message)) easier!
// -> no 'is client connected' check because we still want to read the
// Disconnected message after a disconnect
public bool GetNextMessage(out Message message)
{
return messageQueue.TryDequeue(out message);
}
// static helper functions /////////////////////////////////////////////
// fast ushort to byte[] conversion and vice versa
// -> test with 100k conversions:
// BitConverter.GetBytes(ushort): 144ms
// bit shifting: 11ms
// -> 10x speed improvement makes this optimization actually worth it
// -> this way we don't need to allocate BinaryWriter/Reader either
static byte[] UShortToBytes(ushort value)
{
return new byte[]
{
(byte)value,
(byte)(value >> 8)
};
}
static ushort BytesToUShort(byte[] bytes)
{
return (ushort)((bytes[1] << 8) + bytes[0]);
}
// send message (via stream) with the <size,content> message structure
protected static bool SendMessage(NetworkStream stream, byte[] content)
{
// can we still write to this socket (not disconnected?)
if (!stream.CanWrite)
{
Logger.LogWarning("Send: stream not writeable: " + stream);
return false;
}
// check size
if (content.Length > ushort.MaxValue)
{
Logger.LogError("Send: message too big(" + content.Length + ") max=" + ushort.MaxValue);
return false;
}
// stream.Write throws exceptions if client sends with high
// frequency and the server stops
try
{
// construct header (size)
byte[] header = UShortToBytes((ushort)content.Length);
// write header+content at once via payload array. writing
// header,payload separately would cause 2 TCP packets to be
// sent if nagle's algorithm is disabled(2x TCP header overhead)
byte[] payload = new byte[header.Length + content.Length];
Array.Copy(header, payload, header.Length);
Array.Copy(content, 0, payload, header.Length, content.Length);
stream.Write(payload, 0, payload.Length);
// flush to make sure it is being sent immediately
stream.Flush();
return true;
}
catch (Exception exception)
{
// log as regular message because servers do shut down sometimes
Logger.Log("Send: stream.Write exception: " + exception);
return false;
}
}
// read message (via stream) with the <size,content> message structure
protected static bool ReadMessageBlocking(NetworkStream stream, out byte[] content)
{
content = null;
// read exactly 2 bytes for header (blocking)
byte[] header = new byte[2];
if (!stream.ReadExactly(header, 2))
return false;
ushort size = BytesToUShort(header);
// read exactly 'size' bytes for content (blocking)
content = new byte[size];
if (!stream.ReadExactly(content, size))
return false;
return true;
}
// thread receive function is the same for client and server's clients
// (static to reduce state for maximum reliability)
protected static void ReceiveLoop(int connectionId, TcpClient client, SafeQueue<Message> messageQueue)
{
// get NetworkStream from client
NetworkStream stream = client.GetStream();
// keep track of last message queue warning
DateTime messageQueueLastWarning = DateTime.Now;
// absolutely must wrap with try/catch, otherwise thread exceptions
// are silent
try
{
// add connected event to queue with ip address as data in case
// it's needed
messageQueue.Enqueue(new Message(connectionId, EventType.Connected, null));
// let's talk about reading data.
// -> normally we would read as much as possible and then
// extract as many <size,content>,<size,content> messages
// as we received this time. this is really complicated
// and expensive to do though
// -> instead we use a trick:
// Read(2) -> size
// Read(size) -> content
// repeat
// Read is blocking, but it doesn't matter since the
// best thing to do until the full message arrives,
// is to wait.
// => this is the most elegant AND fast solution.
// + no resizing
// + no extra allocations, just one for the content
// + no crazy extraction logic
while (true)
{
// read the next message (blocking) or stop if stream closed
byte[] content;
if (!ReadMessageBlocking(stream, out content))
break;
// queue it
messageQueue.Enqueue(new Message(connectionId, EventType.Data, content));
// and show a warning if the queue gets too big
// -> we don't want to show a warning every single time,
// because then a lot of processing power gets wasted on
// logging, which will make the queue pile up even more.
// -> instead we show it every 10s, so that the system can
// use most it's processing power to hopefully process it.
if (messageQueue.Count > messageQueueSizeWarning)
{
TimeSpan elapsed = DateTime.Now - messageQueueLastWarning;
if (elapsed.TotalSeconds > 10)
{
Logger.LogWarning("ReceiveLoop: messageQueue is getting big(" + messageQueue.Count + "), try calling GetNextMessage more often. You can call it more than once per frame!");
messageQueueLastWarning = DateTime.Now;
}
}
}
}
catch (Exception exception)
{
// something went wrong. the thread was interrupted or the
// connection closed or we closed our own connection or ...
// -> either way we should stop gracefully
Logger.Log("ReceiveLoop: finished receive function for connectionId=" + connectionId + " reason: " + exception);
}
// if we got here then either the client while loop ended, or an
// exception happened. disconnect
messageQueue.Enqueue(new Message(connectionId, EventType.Disconnected, null));
// clean up no matter what
stream.Close();
client.Close();
}
}
}

View File

@ -1,9 +0,0 @@
namespace Telepathy
{
public enum EventType
{
Connected,
Data,
Disconnected
}
}

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2018, vis2k
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,32 +0,0 @@
// A simple logger class that uses Console.WriteLine by default.
// Can also do Logger.LogMethod = Debug.Log for Unity etc.
// (this way we don't have to depend on UnityEngine.DLL and don't need a
// different version for every UnityEngine version here)
using System;
namespace Telepathy
{
public static class Logger
{
// log regular
public static Action<string> LogMethod = Console.WriteLine;
public static void Log(string msg)
{
LogMethod(msg);
}
// log warning
public static Action<string> LogWarningMethod = Console.WriteLine;
public static void LogWarning(string msg)
{
LogWarningMethod(msg);
}
// log error
public static Action<string> LogErrorMethod = Console.Error.WriteLine;
public static void LogError(string msg)
{
LogErrorMethod(msg);
}
}
}

View File

@ -1,17 +0,0 @@
// incoming message queue of <connectionId, message>
// (not a HashSet because one connection can have multiple new messages)
namespace Telepathy
{
public struct Message
{
public int connectionId;
public EventType eventType;
public byte[] data;
public Message(int connectionId, EventType eventType, byte[] data)
{
this.connectionId = connectionId;
this.eventType = eventType;
this.data = data;
}
}
}

View File

@ -1,55 +0,0 @@
using System.IO;
using System.Net.Sockets;
public static class NetworkStreamExtensions
{
// .Read returns '0' if remote closed the connection but throws an
// IOException if we voluntarily closed our own connection.
//
// lets's add a ReadSafely method that returns '0' in both cases so we don't
// have to worry about exceptions, since a disconnect is a disconnect...
public static int ReadSafely(this NetworkStream stream, byte[] buffer, int offset, int size)
{
try
{
return stream.Read(buffer, offset, size);
}
catch (IOException)
{
return 0;
}
}
// helper function to read EXACTLY 'n' bytes
// -> default .Read reads up to 'n' bytes. this function reads exactly 'n'
// bytes
// -> this is blocking until 'n' bytes were received
// -> immediately returns false in case of disconnects
public static bool ReadExactly(this NetworkStream stream, byte[] buffer, int amount)
{
// there might not be enough bytes in the TCP buffer for .Read to read
// the whole amount at once, so we need to keep trying until we have all
// the bytes (blocking)
//
// note: this just is a faster version of reading one after another:
// for (int i = 0; i < amount; ++i)
// if (stream.Read(buffer, i, 1) == 0)
// return false;
// return true;
int bytesRead = 0;
while (bytesRead < amount)
{
// read up to 'remaining' bytes with the 'safe' read extension
int remaining = amount - bytesRead;
int result = stream.ReadSafely(buffer, bytesRead, remaining);
// .Read returns 0 if disconnected
if (result == 0)
return false;
// otherwise add to bytes read
bytesRead += result;
}
return true;
}
}

View File

@ -1,297 +0,0 @@
![Telepathy Logo](https://i.imgur.com/eUk2rmT.png)
[![Build status](https://img.shields.io/appveyor/ci/vis2k73562/telepathy.svg)](https://ci.appveyor.com/project/vis2k73562/telepathy/)
[![AppVeyor tests branch](https://img.shields.io/appveyor/tests/vis2k73562/telepathy.svg)](https://ci.appveyor.com/project/vis2k73562/telepathy/branch/master/tests)
[![Discord](https://img.shields.io/discord/343440455738064897.svg)](https://discordapp.com/invite/N9QVxbM)
[![Codecov](https://codecov.io/gh/vis2k/telepathy/graph/badge.svg)](https://codecov.io/gh/vis2k/telepathy)
Simple, message based, MMO Scale TCP networking in C#. And no magic.
Telepathy was designed with the [KISS Principle](https://en.wikipedia.org/wiki/KISS_principle) in mind.<br/>
Telepathy is fast and extremely reliable, designed for [MMO](https://www.assetstore.unity3d.com/#!/content/51212) scale Networking.<br/>
Telepathy uses framing, so anything sent will be received the same way.<br/>
Telepathy is raw C# and can be used in Unity3D too.<br/>
# What makes Telepathy special?
Telepathy was originally designed for [uMMORPG](https://www.assetstore.unity3d.com/#!/content/51212) after 3 years in UDP hell.
We needed a library that is:
* Stable & Bug free: Telepathy uses only 400 lines of code. There is no magic.
* High performance: Telepathy can handle thousands of connections and packages.
* Concurrent: Telepathy uses one thread per connection. It can make heavy use of multi core processors.
* Simple: Telepathy takes care of everything. All you need to do is call Connect/GetNextMessage/Disconnect.
* Message based: if we send 10 and then 2 bytes, then the other end receives 10 and then 2 bytes, never 12 at once.
MMORPGs are insanely difficult to make and we created Telepathy so that we would never have to worry about low level Networking again.
# What about...
* Async Sockets: didn't perform better in our benchmarks.
* ConcurrentQueue: .NET 3.5 compatibility is important for Unity. Wasn't faster than our SafeQueue anyway.
* UDP vs. TCP: Minecraft and World of Warcraft are two of the biggest multiplayer games of all time and they both use TCP networking. There is a reason for that.
# Using the Telepathy Server
```C#
// create and start the server
Telepathy.Server server = new Telepathy.Server();
server.Start(1337);
// grab all new messages. do this in your Update loop.
Telepathy.Message msg;
while (server.GetNextMessage(out msg))
{
switch (msg.eventType)
{
case Telepathy.EventType.Connect:
Console.WriteLine(msg.connectionId + " Connected");
break;
case Telepathy.EventType.Data:
Console.WriteLine(msg.connectionId + " Data: " + BitConverter.ToString(msg.data));
break;
case Telepathy.EventType.Disconnect:
Console.WriteLine(msg.connectionId + " Disconnected");
break;
}
}
// send a message to client with connectionId = 0 (first one)
server.Send(0, new byte[]{0x42, 0x1337});
// stop the server when you don't need it anymore
server.Stop();
```
# Using the Telepathy Client
```C#
// create and connect the client
Telepathy.Client Client = new Telepathy.Client();
client.Connect("localhost", 1337);
// grab all new messages. do this in your Update loop.
Telepathy.Message msg;
while (client.GetNextMessage(out msg))
{
switch (msg.eventType)
{
case Telepathy.EventType.Connect:
Console.WriteLine("Connected");
break;
case Telepathy.EventType.Data:
Console.WriteLine("Data: " + BitConverter.ToString(msg.data));
break;
case Telepathy.EventType.Disconnect:
Console.WriteLine("Disconnected");
break;
}
}
// send a message to server
client.Send(new byte[]{0xFF});
// disconnect from the server when we are done
client.Disconnect();
```
# Unity Integration
Here is a very simple MonoBehaviour script for Unity. It's really just the above code with logging configured for Unity's Debug.Log:
```C#
using System;
using UnityEngine;
public class SimpleExample : MonoBehaviour
{
Telepathy.Client client = new Telepathy.Client();
Telepathy.Server server = new Telepathy.Server();
void Awake()
{
// update even if window isn't focused, otherwise we don't receive.
Application.runInBackground = true;
// use Debug.Log functions for Telepathy so we can see it in the console
Telepathy.Logger.LogMethod = Debug.Log;
Telepathy.Logger.LogWarningMethod = Debug.LogWarning;
Telepathy.Logger.LogErrorMethod = Debug.LogError;
}
void Update()
{
// client
if (client.Connected)
{
if (Input.GetKeyDown(KeyCode.Space))
client.Send(new byte[]{0x1});
// show all new messages
Telepathy.Message msg;
while (client.GetNextMessage(out msg))
{
switch (msg.eventType)
{
case Telepathy.EventType.Connected:
Console.WriteLine("Connected");
break;
case Telepathy.EventType.Data:
Console.WriteLine("Data: " + BitConverter.ToString(msg.data));
break;
case Telepathy.EventType.Disconnected:
Console.WriteLine("Disconnected");
break;
}
}
}
// server
if (server.Active)
{
if (Input.GetKeyDown(KeyCode.Space))
server.Send(0, new byte[]{0x2});
// show all new messages
Telepathy.Message msg;
while (server.GetNextMessage(out msg))
{
switch (msg.eventType)
{
case Telepathy.EventType.Connected:
Console.WriteLine(msg.connectionId + " Connected");
break;
case Telepathy.EventType.Data:
Console.WriteLine(msg.connectionId + " Data: " + BitConverter.ToString(msg.data));
break;
case Telepathy.EventType.Disconnected:
Console.WriteLine(msg.connectionId + " Disconnected");
break;
}
}
}
}
void OnGUI()
{
// client
GUI.enabled = !client.Connected;
if (GUI.Button(new Rect(0, 0, 120, 20), "Connect Client"))
client.Connect("localhost", 1337);
GUI.enabled = client.Connected;
if (GUI.Button(new Rect(130, 0, 120, 20), "Disconnect Client"))
client.Disconnect();
// server
GUI.enabled = !server.Active;
if (GUI.Button(new Rect(0, 25, 120, 20), "Start Server"))
server.Start(1337);
GUI.enabled = server.Active;
if (GUI.Button(new Rect(130, 25, 120, 20), "Stop Server"))
server.Stop();
GUI.enabled = true;
}
void OnApplicationQuit()
{
// the client/server threads won't receive the OnQuit info if we are
// running them in the Editor. they would only quit when we press Play
// again later. this is fine, but let's shut them down here for consistency
client.Disconnect();
server.Stop();
}
}
```
Make sure to enable 'run in Background' for your project settings, which is a must for all multiplayer games.
Then build it, start the server in the build and the client in the Editor and press Space to send a test message.
# Benchmarks
**Real World**<br/>
Telepathy is constantly tested in production with [uMMORPG](https://www.assetstore.unity3d.com/#!/content/51212).
We [recently tested](https://docs.google.com/document/d/e/2PACX-1vQqf_iqOLlBRTUqqyor_OUx_rHlYx-SYvZWMvHGuLIuRuxJ-qX3s8JzrrBB5vxDdGfl-HhYZW3g5lLW/pub#h.h4wha2mpetsc) 100+ players all broadcasting to each other in the worst case scenario, without issues.
We had to stop the test because we didn't have more players to spawn clients.<br/>
The next huge test will come soon...
**Connections Test**<br/>
We also test only the raw Telepathy library by spawing 1 server and 1000 clients, each client sending 100 bytes 14 times per second and the server echoing the same message back to each client. This test should be a decent example for an MMORPG scenario and allows us to test if the raw Telepathy library can handle it.
Test Computer: 2015 Macbook Pro with a 2,2 GHz Intel Core i7 processor.<br/>
Test Results:<br/>
| Clients | CPU Usage | Ram Usage | Bandwidth Client+Server | Result |
| ------- | ----------| --------- | ------------------------ | ------ |
| 128 | 7% | 26 MB | 1-2 MB/s | Passed |
| 500 | 28% | 51 MB | 3-4 MB/s | Passed |
| 1000 | 42% | 75 MB | 3-5 MB/s | Passed |
_Note: results will be significantly better on a really powerful server. Tests will follow._
The Connections Test can be reproduced with the following code:<br/>
```C#
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Telepathy;
public class Test
{
static void Main()
{
// start server
Server server = new Server();
server.Start(1337);
int serverFrequency = 60;
Thread serverThread = new Thread(() =>
{
Logger.Log("started server");
while (true)
{
// reply to each incoming message
Message msg;
while (server.GetNextMessage(out msg))
{
if (msg.eventType == EventType.Data)
server.Send(msg.connectionId, msg.data);
}
// sleep
Thread.Sleep(1000 / serverFrequency);
}
});
serverThread.IsBackground = false;
serverThread.Start();
// start n clients and get queue messages all in this thread
int clientAmount = 1000;
string message = "Sometimes we just need a good networking library";
byte[] messageBytes = Encoding.ASCII.GetBytes(message);
int clientFrequency = 14;
List<Client> clients = new List<Client>();
for (int i = 0; i < clientAmount; ++i)
{
Client client = new Client();
client.Connect("localhost", 1337);
clients.Add(client);
Thread.Sleep(15);
}
Logger.Log("started all clients");
while (true)
{
foreach (Client client in clients)
{
// send 2 messages each time
client.Send(messageBytes);
client.Send(messageBytes);
// get new messages from queue
Message msg;
while (client.GetNextMessage(out msg))
{
}
}
// client tick rate
Thread.Sleep(1000 / clientFrequency);
}
}
}
```

View File

@ -1,30 +0,0 @@
// a very simple locked 'uint' counter
// (we can't do lock(int) so we need an object and since we also need a max
// check, we might as well put it into a class here)
using System;
namespace Telepathy
{
public class SafeCounter
{
int counter;
public int Next()
{
lock (this)
{
// it's very unlikely that we reach the uint limit of 2 billion.
// even with 1 connection per second, this would take 68 years.
// -> but if it happens, then we should throw an exception
// because the caller probably should stop accepting clients.
// -> it's hardly worth using 'bool Next(out id)' for that case
// because it's just so unlikely.
if (counter == int.MaxValue)
{
throw new Exception("SafeCounter limit reached: " + counter);
}
return counter++;
}
}
}
}

View File

@ -1,66 +0,0 @@
// replaces ConcurrentDictionary which is not available in .NET 3.5 yet.
using System.Collections.Generic;
using System.Linq;
namespace Telepathy
{
public class SafeDictionary<TKey,TValue>
{
Dictionary<TKey,TValue> dict = new Dictionary<TKey,TValue>();
// for statistics. don't call Count and assume that it's the same after the
// call.
public int Count
{
get
{
lock(dict)
{
return dict.Count;
}
}
}
public void Add(TKey key, TValue value)
{
lock(dict)
{
dict[key] = value;
}
}
public void Remove(TKey key)
{
lock(dict)
{
dict.Remove(key);
}
}
// can't check .ContainsKey before Get because it might change inbetween,
// so we need a TryGetValue
public bool TryGetValue(TKey key, out TValue result)
{
lock(dict)
{
return dict.TryGetValue(key, out result);
}
}
public List<TValue> GetValues()
{
lock(dict)
{
return dict.Values.ToList();
}
}
public void Clear()
{
lock(dict)
{
dict.Clear();
}
}
}
}

View File

@ -1,55 +0,0 @@
// replaces ConcurrentQueue which is not available in .NET 3.5 yet.
using System.Collections.Generic;
namespace Telepathy
{
public class SafeQueue<T>
{
Queue<T> queue = new Queue<T>();
// for statistics. don't call Count and assume that it's the same after the
// call.
public int Count
{
get
{
lock(queue)
{
return queue.Count;
}
}
}
public void Enqueue(T item)
{
lock(queue)
{
queue.Enqueue(item);
}
}
// can't check .Count before doing Dequeue because it might change inbetween,
// so we need a TryDequeue
public bool TryDequeue(out T result)
{
lock(queue)
{
result = default(T);
if (queue.Count > 0)
{
result = queue.Dequeue();
return true;
}
return false;
}
}
public void Clear()
{
lock(queue)
{
queue.Clear();
}
}
}
}

View File

@ -1,209 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Telepathy
{
public class Server : Common
{
// listener
TcpListener listener;
Thread listenerThread;
// clients with <connectionId, TcpClient>
SafeDictionary<int, TcpClient> clients = new SafeDictionary<int, TcpClient>();
// connectionId counter
// (right now we only use it from one listener thread, but we might have
// multiple threads later in case of WebSockets etc.)
// -> static so that another server instance doesn't start at 0 again.
static int counter = 0;
// public next id function in case someone needs to reserve an id
// (e.g. if hostMode should always have 0 connection and external
// connections should start at 1, etc.)
public static int NextConnectionId()
{
int id = Interlocked.Increment(ref counter);
// it's very unlikely that we reach the uint limit of 2 billion.
// even with 1 new connection per second, this would take 68 years.
// -> but if it happens, then we should throw an exception because
// the caller probably should stop accepting clients.
// -> it's hardly worth using 'bool Next(out id)' for that case
// because it's just so unlikely.
if (id == int.MaxValue)
{
throw new Exception("connection id limit reached: " + id);
}
return id;
}
// check if the server is running
public bool Active
{
get { return listenerThread != null && listenerThread.IsAlive; }
}
// the listener thread's listen function
void Listen(int port, int maxConnections)
{
// absolutely must wrap with try/catch, otherwise thread
// exceptions are silent
try
{
// start listener
// (NoDelay disables nagle algorithm. lowers CPU% and latency)
listener = new TcpListener(new IPEndPoint(IPAddress.Any, port));
listener.Server.NoDelay = true;
listener.Start();
Logger.Log("Server: listening port=" + port + " max=" + maxConnections);
// keep accepting new clients
while (true)
{
// wait and accept new client
// note: 'using' sucks here because it will try to
// dispose after thread was started but we still need it
// in the thread
TcpClient client = listener.AcceptTcpClient();
// are more connections allowed?
if (clients.Count < maxConnections)
{
// generate the next connection id (thread safely)
int connectionId = NextConnectionId();
// spawn a thread for each client to listen to his
// messages
Thread thread = new Thread(() =>
{
// add to dict immediately
clients.Add(connectionId, client);
// run the receive loop
ReceiveLoop(connectionId, client, messageQueue);
// remove client from clients dict afterwards
clients.Remove(connectionId);
});
thread.IsBackground = true;
thread.Start();
}
// connection limit reached. disconnect the client and show
// a small log message so we know why it happened.
// note: no extra Sleep because Accept is blocking anyway
else
{
client.Close();
Logger.Log("Server too full, disconnected a client");
}
}
}
catch (ThreadAbortException exception)
{
// UnityEditor causes AbortException if thread is still
// running when we press Play again next time. that's okay.
Logger.Log("Server thread aborted. That's okay. " + exception);
}
catch (SocketException exception)
{
// calling StopServer will interrupt this thread with a
// 'SocketException: interrupted'. that's okay.
Logger.Log("Server Thread stopped. That's okay. " + exception);
}
catch (Exception exception)
{
// something went wrong. probably important.
Logger.LogError("Server Exception: " + exception);
}
}
// start listening for new connections in a background thread and spawn
// a new thread for each one.
public void Start(int port, int maxConnections = int.MaxValue)
{
// not if already started
if (Active) return;
// 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 Stop isn't smart because the caller may
// still want to process all the latest messages afterwards
messageQueue.Clear();
// start the listener thread
Logger.Log("Server: Start port=" + port + " max=" + maxConnections);
listenerThread = new Thread(() => { Listen(port, maxConnections); });
listenerThread.IsBackground = true;
listenerThread.Start();
}
public void Stop()
{
// only if started
if (!Active) return;
Logger.Log("Server: stopping...");
// stop listening to connections so that no one can connect while we
// close the client connections
listener.Stop();
// close all client connections
List<TcpClient> connections = clients.GetValues();
foreach (TcpClient client in connections)
{
// close the stream if not closed yet. it may have been closed
// by a disconnect already, so use try/catch
try { client.GetStream().Close(); } catch {}
client.Close();
}
// clear clients list
clients.Clear();
}
// send message to client using socket connection.
public bool Send(int connectionId, byte[] data)
{
// find the connection
TcpClient client;
if (clients.TryGetValue(connectionId, out client))
{
// GetStream() might throw exception if client is disconnected
try
{
NetworkStream stream = client.GetStream();
return SendMessage(stream, data);
}
catch (Exception exception)
{
Logger.LogWarning("Server.Send exception: " + exception);
return false;
}
}
Logger.LogWarning("Server.Send: invalid connectionId: " + connectionId);
return false;
}
// get connection info in case it's needed (IP etc.)
// (we should never pass the TcpClient to the outside)
public bool GetConnectionInfo(int connectionId, out string address)
{
// find the connection
TcpClient client;
if (clients.TryGetValue(connectionId, out client))
{
address = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString();
return true;
}
address = null;
return false;
}
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Telepathy" version="1.0.114" targetFramework="net35" />
</packages>