mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 11:00:32 +00:00
Get Telepathy from nuget instead
This commit is contained in:
parent
507b781250
commit
fdccbe621d
@ -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" />
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
namespace Telepathy
|
||||
{
|
||||
public enum EventType
|
||||
{
|
||||
Connected,
|
||||
Data,
|
||||
Disconnected
|
||||
}
|
||||
}
|
@ -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.
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
4
Mirror/Runtime/packages.config
Normal file
4
Mirror/Runtime/packages.config
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Telepathy" version="1.0.114" targetFramework="net35" />
|
||||
</packages>
|
Loading…
Reference in New Issue
Block a user