Keep local version of files
This commit is contained in:
@@ -1,75 +1,75 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Common
|
||||
{
|
||||
// helper function to resolve host to IPAddress
|
||||
public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
|
||||
{
|
||||
try
|
||||
{
|
||||
// NOTE: dns lookup is blocking. this can take a second.
|
||||
addresses = Dns.GetHostAddresses(hostname);
|
||||
return addresses.Length >= 1;
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
Log.Info($"[KCP] Failed to resolve host: {hostname} reason: {exception}");
|
||||
addresses = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if connections drop under heavy load, increase to OS limit.
|
||||
// if still not enough, increase the OS limit.
|
||||
public static void ConfigureSocketBuffers(Socket socket, int recvBufferSize, int sendBufferSize)
|
||||
{
|
||||
// log initial size for comparison.
|
||||
// remember initial size for log comparison
|
||||
int initialReceive = socket.ReceiveBufferSize;
|
||||
int initialSend = socket.SendBufferSize;
|
||||
|
||||
// set to configured size
|
||||
try
|
||||
{
|
||||
socket.ReceiveBufferSize = recvBufferSize;
|
||||
socket.SendBufferSize = sendBufferSize;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
Log.Warning($"[KCP] failed to set Socket RecvBufSize = {recvBufferSize} SendBufSize = {sendBufferSize}");
|
||||
}
|
||||
|
||||
|
||||
Log.Info($"[KCP] RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x)");
|
||||
}
|
||||
|
||||
// generate a connection hash from IP+Port.
|
||||
//
|
||||
// NOTE: IPEndPoint.GetHashCode() allocates.
|
||||
// it calls m_Address.GetHashCode().
|
||||
// m_Address is an IPAddress.
|
||||
// GetHashCode() allocates for IPv6:
|
||||
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699
|
||||
//
|
||||
// => using only newClientEP.Port wouldn't work, because
|
||||
// different connections can have the same port.
|
||||
public static int ConnectionHash(EndPoint endPoint) =>
|
||||
endPoint.GetHashCode();
|
||||
|
||||
// cookies need to be generated with a secure random generator.
|
||||
// we don't want them to be deterministic / predictable.
|
||||
// RNG is cached to avoid runtime allocations.
|
||||
static readonly RNGCryptoServiceProvider cryptoRandom = new RNGCryptoServiceProvider();
|
||||
static readonly byte[] cryptoRandomBuffer = new byte[4];
|
||||
public static uint GenerateCookie()
|
||||
{
|
||||
cryptoRandom.GetBytes(cryptoRandomBuffer);
|
||||
return BitConverter.ToUInt32(cryptoRandomBuffer, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Common
|
||||
{
|
||||
// helper function to resolve host to IPAddress
|
||||
public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
|
||||
{
|
||||
try
|
||||
{
|
||||
// NOTE: dns lookup is blocking. this can take a second.
|
||||
addresses = Dns.GetHostAddresses(hostname);
|
||||
return addresses.Length >= 1;
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
Log.Info($"[KCP] Failed to resolve host: {hostname} reason: {exception}");
|
||||
addresses = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if connections drop under heavy load, increase to OS limit.
|
||||
// if still not enough, increase the OS limit.
|
||||
public static void ConfigureSocketBuffers(Socket socket, int recvBufferSize, int sendBufferSize)
|
||||
{
|
||||
// log initial size for comparison.
|
||||
// remember initial size for log comparison
|
||||
int initialReceive = socket.ReceiveBufferSize;
|
||||
int initialSend = socket.SendBufferSize;
|
||||
|
||||
// set to configured size
|
||||
try
|
||||
{
|
||||
socket.ReceiveBufferSize = recvBufferSize;
|
||||
socket.SendBufferSize = sendBufferSize;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
Log.Warning($"[KCP] failed to set Socket RecvBufSize = {recvBufferSize} SendBufSize = {sendBufferSize}");
|
||||
}
|
||||
|
||||
|
||||
Log.Info($"[KCP] RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x)");
|
||||
}
|
||||
|
||||
// generate a connection hash from IP+Port.
|
||||
//
|
||||
// NOTE: IPEndPoint.GetHashCode() allocates.
|
||||
// it calls m_Address.GetHashCode().
|
||||
// m_Address is an IPAddress.
|
||||
// GetHashCode() allocates for IPv6:
|
||||
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699
|
||||
//
|
||||
// => using only newClientEP.Port wouldn't work, because
|
||||
// different connections can have the same port.
|
||||
public static int ConnectionHash(EndPoint endPoint) =>
|
||||
endPoint.GetHashCode();
|
||||
|
||||
// cookies need to be generated with a secure random generator.
|
||||
// we don't want them to be deterministic / predictable.
|
||||
// RNG is cached to avoid runtime allocations.
|
||||
static readonly RNGCryptoServiceProvider cryptoRandom = new RNGCryptoServiceProvider();
|
||||
static readonly byte[] cryptoRandomBuffer = new byte[4];
|
||||
public static uint GenerateCookie()
|
||||
{
|
||||
cryptoRandom.GetBytes(cryptoRandomBuffer);
|
||||
return BitConverter.ToUInt32(cryptoRandomBuffer, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ced451c2954435f88cf718bcba020cb
|
||||
timeCreated: 1669135138
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 9ced451c2954435f88cf718bcba020cb
|
||||
timeCreated: 1669135138
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// kcp specific error codes to allow for error switching, localization,
|
||||
// translation to Mirror errors, etc.
|
||||
namespace kcp2k
|
||||
{
|
||||
public enum ErrorCode : byte
|
||||
{
|
||||
DnsResolve, // failed to resolve a host name
|
||||
Timeout, // ping timeout or dead link
|
||||
Congestion, // more messages than transport / network can process
|
||||
InvalidReceive, // recv invalid packet (possibly intentional attack)
|
||||
InvalidSend, // user tried to send invalid data
|
||||
ConnectionClosed, // connection closed voluntarily or lost involuntarily
|
||||
Unexpected // unexpected error / exception, requires fix.
|
||||
}
|
||||
// kcp specific error codes to allow for error switching, localization,
|
||||
// translation to Mirror errors, etc.
|
||||
namespace kcp2k
|
||||
{
|
||||
public enum ErrorCode : byte
|
||||
{
|
||||
DnsResolve, // failed to resolve a host name
|
||||
Timeout, // ping timeout or dead link
|
||||
Congestion, // more messages than transport / network can process
|
||||
InvalidReceive, // recv invalid packet (possibly intentional attack)
|
||||
InvalidSend, // user tried to send invalid data
|
||||
ConnectionClosed, // connection closed voluntarily or lost involuntarily
|
||||
Unexpected // unexpected error / exception, requires fix.
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3abbeffc1d794f11a45b7fcf110353f5
|
||||
timeCreated: 1652320712
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 3abbeffc1d794f11a45b7fcf110353f5
|
||||
timeCreated: 1652320712
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,166 +1,166 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
// ArraySegment as HexString for convenience
|
||||
public static string ToHexString(this ArraySegment<byte> segment) =>
|
||||
BitConverter.ToString(segment.Array, segment.Offset, segment.Count);
|
||||
|
||||
// non-blocking UDP send.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool SendToNonBlocking(this Socket socket, ArraySegment<byte> data, EndPoint remoteEP)
|
||||
{
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, SendTo may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
|
||||
|
||||
// send to the the endpoint.
|
||||
// do not send to 'newClientEP', as that's always reused.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3296
|
||||
socket.SendTo(data.Array, data.Offset, data.Count, SocketFlags.None, remoteEP);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, SendTo may throw WouldBlock.
|
||||
// in that case, simply drop the message. it's UDP, it's fine.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP send.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool SendNonBlocking(this Socket socket, ArraySegment<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, SendTo may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
|
||||
|
||||
// SendTo allocates. we used bound Send.
|
||||
socket.Send(data.Array, data.Offset, data.Count, SocketFlags.None);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, SendTo may throw WouldBlock.
|
||||
// in that case, simply drop the message. it's UDP, it's fine.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP receive.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool ReceiveFromNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data, ref EndPoint remoteEP)
|
||||
{
|
||||
data = default;
|
||||
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
|
||||
|
||||
// NOTE: ReceiveFrom allocates.
|
||||
// we pass our IPEndPoint to ReceiveFrom.
|
||||
// receive from calls newClientEP.Create(socketAddr).
|
||||
// IPEndPoint.Create always returns a new IPEndPoint.
|
||||
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761
|
||||
//
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
int size = socket.ReceiveFrom(recvBuffer, 0, recvBuffer.Length, SocketFlags.None, ref remoteEP);
|
||||
data = new ArraySegment<byte>(recvBuffer, 0, size);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP receive.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool ReceiveNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data)
|
||||
{
|
||||
data = default;
|
||||
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
|
||||
|
||||
// ReceiveFrom allocates. we used bound Receive.
|
||||
// returns amount of bytes written into buffer.
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
//
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
int size = socket.Receive(recvBuffer, 0, recvBuffer.Length, SocketFlags.None);
|
||||
data = new ArraySegment<byte>(recvBuffer, 0, size);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
// ArraySegment as HexString for convenience
|
||||
public static string ToHexString(this ArraySegment<byte> segment) =>
|
||||
BitConverter.ToString(segment.Array, segment.Offset, segment.Count);
|
||||
|
||||
// non-blocking UDP send.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool SendToNonBlocking(this Socket socket, ArraySegment<byte> data, EndPoint remoteEP)
|
||||
{
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, SendTo may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
|
||||
|
||||
// send to the the endpoint.
|
||||
// do not send to 'newClientEP', as that's always reused.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3296
|
||||
socket.SendTo(data.Array, data.Offset, data.Count, SocketFlags.None, remoteEP);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, SendTo may throw WouldBlock.
|
||||
// in that case, simply drop the message. it's UDP, it's fine.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP send.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool SendNonBlocking(this Socket socket, ArraySegment<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, SendTo may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
|
||||
|
||||
// SendTo allocates. we used bound Send.
|
||||
socket.Send(data.Array, data.Offset, data.Count, SocketFlags.None);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, SendTo may throw WouldBlock.
|
||||
// in that case, simply drop the message. it's UDP, it's fine.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP receive.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool ReceiveFromNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data, ref EndPoint remoteEP)
|
||||
{
|
||||
data = default;
|
||||
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
|
||||
|
||||
// NOTE: ReceiveFrom allocates.
|
||||
// we pass our IPEndPoint to ReceiveFrom.
|
||||
// receive from calls newClientEP.Create(socketAddr).
|
||||
// IPEndPoint.Create always returns a new IPEndPoint.
|
||||
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761
|
||||
//
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
int size = socket.ReceiveFrom(recvBuffer, 0, recvBuffer.Length, SocketFlags.None, ref remoteEP);
|
||||
data = new ArraySegment<byte>(recvBuffer, 0, size);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// non-blocking UDP receive.
|
||||
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
|
||||
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
|
||||
// => wrapped with try-catch to ignore WouldBlock exception.
|
||||
// make sure to set socket.Blocking = false before using this!
|
||||
public static bool ReceiveNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data)
|
||||
{
|
||||
data = default;
|
||||
|
||||
try
|
||||
{
|
||||
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
|
||||
// in C#, WouldBlock throws a SocketException, which is expected.
|
||||
// unfortunately, creating the SocketException allocates in C#.
|
||||
// let's poll first to avoid the WouldBlock allocation.
|
||||
// note that this entirely to avoid allocations.
|
||||
// non-blocking UDP doesn't need Poll in other languages.
|
||||
// and the code still works without the Poll call.
|
||||
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
|
||||
|
||||
// ReceiveFrom allocates. we used bound Receive.
|
||||
// returns amount of bytes written into buffer.
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
//
|
||||
// throws SocketException if datagram was larger than buffer.
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
|
||||
int size = socket.Receive(recvBuffer, 0, recvBuffer.Length, SocketFlags.None);
|
||||
data = new ArraySegment<byte>(recvBuffer, 0, size);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
|
||||
|
||||
// otherwise it's a real socket error. throw it.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0649195e5ba4fcf8e0e1231fee7d5f6
|
||||
timeCreated: 1641701011
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: c0649195e5ba4fcf8e0e1231fee7d5f6
|
||||
timeCreated: 1641701011
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace kcp2k
|
||||
{
|
||||
// channel type and header for raw messages
|
||||
public enum KcpChannel : byte
|
||||
{
|
||||
// don't react on 0x00. might help to filter out random noise.
|
||||
Reliable = 1,
|
||||
Unreliable = 2
|
||||
}
|
||||
namespace kcp2k
|
||||
{
|
||||
// channel type and header for raw messages
|
||||
public enum KcpChannel : byte
|
||||
{
|
||||
// don't react on 0x00. might help to filter out random noise.
|
||||
Reliable = 1,
|
||||
Unreliable = 2
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e852b2532fb248d19715cfebe371db3
|
||||
timeCreated: 1610081248
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 9e852b2532fb248d19715cfebe371db3
|
||||
timeCreated: 1610081248
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,292 +1,292 @@
|
||||
// kcp client logic abstracted into a class.
|
||||
// for use in Mirror, DOTSNET, testing, etc.
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpClient : KcpPeer
|
||||
{
|
||||
// IO
|
||||
protected Socket socket;
|
||||
public EndPoint remoteEndPoint;
|
||||
|
||||
// expose local endpoint for users / relays / nat traversal etc.
|
||||
public EndPoint LocalEndPoint => socket?.LocalEndPoint;
|
||||
|
||||
// config
|
||||
protected readonly KcpConfig config;
|
||||
|
||||
// raw receive buffer always needs to be of 'MTU' size, even if
|
||||
// MaxMessageSize is larger. kcp always sends in MTU segments and having
|
||||
// a buffer smaller than MTU would silently drop excess data.
|
||||
// => we need the MTU to fit channel + message!
|
||||
// => protected because someone may overwrite RawReceive but still wants
|
||||
// to reuse the buffer.
|
||||
protected readonly byte[] rawReceiveBuffer;
|
||||
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action OnConnectedCallback;
|
||||
protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
|
||||
protected readonly Action OnDisconnectedCallback;
|
||||
protected readonly Action<ErrorCode, string> OnErrorCallback;
|
||||
|
||||
// state
|
||||
bool active = false; // active between when connect() and disconnect() are called
|
||||
public bool connected;
|
||||
|
||||
public KcpClient(Action OnConnected,
|
||||
Action<ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action OnDisconnected,
|
||||
Action<ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
: base(config, 0) // client has no cookie yet
|
||||
{
|
||||
// initialize callbacks first to ensure they can be used safely.
|
||||
OnConnectedCallback = OnConnected;
|
||||
OnDataCallback = OnData;
|
||||
OnDisconnectedCallback = OnDisconnected;
|
||||
OnErrorCallback = OnError;
|
||||
this.config = config;
|
||||
|
||||
// create mtu sized receive buffer
|
||||
rawReceiveBuffer = new byte[config.Mtu];
|
||||
}
|
||||
|
||||
// callbacks ///////////////////////////////////////////////////////////
|
||||
// some callbacks need to wrapped with some extra logic
|
||||
protected override void OnAuthenticated()
|
||||
{
|
||||
Log.Info($"[KCP] Client: OnConnected");
|
||||
connected = true;
|
||||
OnConnectedCallback();
|
||||
}
|
||||
|
||||
protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
|
||||
OnDataCallback(message, channel);
|
||||
|
||||
protected override void OnError(ErrorCode error, string message) =>
|
||||
OnErrorCallback(error, message);
|
||||
|
||||
protected override void OnDisconnected()
|
||||
{
|
||||
Log.Info($"[KCP] Client: OnDisconnected");
|
||||
connected = false;
|
||||
socket?.Close();
|
||||
socket = null;
|
||||
remoteEndPoint = null;
|
||||
OnDisconnectedCallback();
|
||||
active = false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
public void Connect(string address, ushort port)
|
||||
{
|
||||
if (connected)
|
||||
{
|
||||
Log.Warning("[KCP] Client: already connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
// resolve host name before creating peer.
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3361
|
||||
if (!Common.ResolveHostname(address, out IPAddress[] addresses))
|
||||
{
|
||||
// pass error to user callback. no need to log it manually.
|
||||
OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {address}");
|
||||
OnDisconnectedCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
// create fresh peer for each new session
|
||||
// client doesn't need secure cookie.
|
||||
Reset(config);
|
||||
|
||||
Log.Info($"[KCP] Client: connect to {address}:{port}");
|
||||
|
||||
// create socket
|
||||
remoteEndPoint = new IPEndPoint(addresses[0], port);
|
||||
socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
active = true;
|
||||
|
||||
// recv & send are called from main thread.
|
||||
// need to ensure this never blocks.
|
||||
// even a 1ms block per connection would stop us from scaling.
|
||||
socket.Blocking = false;
|
||||
|
||||
// configure buffer sizes
|
||||
Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
|
||||
|
||||
// bind to endpoint so we can use send/recv instead of sendto/recvfrom.
|
||||
socket.Connect(remoteEndPoint);
|
||||
|
||||
// immediately send a hello message to the server.
|
||||
// server will call OnMessage and add the new connection.
|
||||
// note that this still has cookie=0 until we receive the server's hello.
|
||||
SendHello();
|
||||
}
|
||||
|
||||
// io - input.
|
||||
// virtual so it may be modified for relays, etc.
|
||||
// call this while it returns true, to process all messages this tick.
|
||||
// returned ArraySegment is valid until next call to RawReceive.
|
||||
protected virtual bool RawReceive(out ArraySegment<byte> segment)
|
||||
{
|
||||
segment = default;
|
||||
if (socket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment);
|
||||
}
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
catch (SocketException e)
|
||||
{
|
||||
// the other end closing the connection is not an 'error'.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
// for example, his can happen when connecting without a server.
|
||||
// see test: ConnectWithoutServer().
|
||||
Log.Info($"[KCP] Client.RawReceive: looks like the other end has closed the connection. This is fine: {e}");
|
||||
base.Disconnect();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// io - output.
|
||||
// virtual so it may be modified for relays, etc.
|
||||
protected override void RawSend(ArraySegment<byte> data)
|
||||
{
|
||||
// only if socket was connected / created yet.
|
||||
// users may call send functions without having connected, causing NRE.
|
||||
if (socket == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
socket.SendNonBlocking(data);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// SendDisconnect() sometimes gets a SocketException with
|
||||
// 'Connection Refused' if the other end already closed.
|
||||
// this is not an 'error', it's expected to happen.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
Log.Info($"[KCP] Client.RawSend: looks like the other end has closed the connection. This is fine: {e}");
|
||||
// base.Disconnect(); <- don't call this, would deadlock if SendDisconnect() already throws
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(ArraySegment<byte> segment, KcpChannel channel)
|
||||
{
|
||||
if (!connected)
|
||||
{
|
||||
Log.Warning("[KCP] Client: can't send because not connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
SendData(segment, channel);
|
||||
}
|
||||
|
||||
// insert raw IO. usually from socket.Receive.
|
||||
// offset is useful for relays, where we may parse a header and then
|
||||
// feed the rest to kcp.
|
||||
public void RawInput(ArraySegment<byte> segment)
|
||||
{
|
||||
// ensure valid size: at least 1 byte for channel + 4 bytes for cookie
|
||||
if (segment.Count <= 5) return;
|
||||
|
||||
// parse channel
|
||||
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
|
||||
byte channel = segment.Array[segment.Offset + 0];
|
||||
|
||||
// server messages always contain the security cookie.
|
||||
// parse it, assign if not assigned, warn if suddenly different.
|
||||
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
|
||||
if (messageCookie == 0)
|
||||
{
|
||||
Log.Error($"[KCP] Client: received message with cookie=0, this should never happen. Server should always include the security cookie.");
|
||||
}
|
||||
|
||||
if (cookie == 0)
|
||||
{
|
||||
cookie = messageCookie;
|
||||
Log.Info($"[KCP] Client: received initial cookie: {cookie}");
|
||||
}
|
||||
else if (cookie != messageCookie)
|
||||
{
|
||||
Log.Warning($"[KCP] Client: dropping message with mismatching cookie: {messageCookie} expected: {cookie}.");
|
||||
return;
|
||||
}
|
||||
|
||||
// parse message
|
||||
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case (byte)KcpChannel.Reliable:
|
||||
{
|
||||
OnRawInputReliable(message);
|
||||
break;
|
||||
}
|
||||
case (byte)KcpChannel.Unreliable:
|
||||
{
|
||||
OnRawInputUnreliable(message);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// invalid channel indicates random internet noise.
|
||||
// servers may receive random UDP data.
|
||||
// just ignore it, but log for easier debugging.
|
||||
Log.Warning($"[KCP] Client: invalid channel header: {channel}, likely internet noise");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming messages. should be called before updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public override void TickIncoming()
|
||||
{
|
||||
// recv on socket first, then process incoming
|
||||
// (even if we didn't receive anything. need to tick ping etc.)
|
||||
// (connection is null if not active)
|
||||
if (active)
|
||||
{
|
||||
while (RawReceive(out ArraySegment<byte> segment))
|
||||
RawInput(segment);
|
||||
}
|
||||
|
||||
// RawReceive may have disconnected peer. active check again.
|
||||
if (active) base.TickIncoming();
|
||||
}
|
||||
|
||||
// process outgoing messages. should be called after updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public override void TickOutgoing()
|
||||
{
|
||||
// process outgoing while active
|
||||
if (active) base.TickOutgoing();
|
||||
}
|
||||
|
||||
// process incoming and outgoing for convenience
|
||||
// => ideally call ProcessIncoming() before updating the world and
|
||||
// ProcessOutgoing() after updating the world for minimum latency
|
||||
public virtual void Tick()
|
||||
{
|
||||
TickIncoming();
|
||||
TickOutgoing();
|
||||
}
|
||||
}
|
||||
}
|
||||
// kcp client logic abstracted into a class.
|
||||
// for use in Mirror, DOTSNET, testing, etc.
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpClient : KcpPeer
|
||||
{
|
||||
// IO
|
||||
protected Socket socket;
|
||||
public EndPoint remoteEndPoint;
|
||||
|
||||
// expose local endpoint for users / relays / nat traversal etc.
|
||||
public EndPoint LocalEndPoint => socket?.LocalEndPoint;
|
||||
|
||||
// config
|
||||
protected readonly KcpConfig config;
|
||||
|
||||
// raw receive buffer always needs to be of 'MTU' size, even if
|
||||
// MaxMessageSize is larger. kcp always sends in MTU segments and having
|
||||
// a buffer smaller than MTU would silently drop excess data.
|
||||
// => we need the MTU to fit channel + message!
|
||||
// => protected because someone may overwrite RawReceive but still wants
|
||||
// to reuse the buffer.
|
||||
protected readonly byte[] rawReceiveBuffer;
|
||||
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action OnConnectedCallback;
|
||||
protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
|
||||
protected readonly Action OnDisconnectedCallback;
|
||||
protected readonly Action<ErrorCode, string> OnErrorCallback;
|
||||
|
||||
// state
|
||||
bool active = false; // active between when connect() and disconnect() are called
|
||||
public bool connected;
|
||||
|
||||
public KcpClient(Action OnConnected,
|
||||
Action<ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action OnDisconnected,
|
||||
Action<ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
: base(config, 0) // client has no cookie yet
|
||||
{
|
||||
// initialize callbacks first to ensure they can be used safely.
|
||||
OnConnectedCallback = OnConnected;
|
||||
OnDataCallback = OnData;
|
||||
OnDisconnectedCallback = OnDisconnected;
|
||||
OnErrorCallback = OnError;
|
||||
this.config = config;
|
||||
|
||||
// create mtu sized receive buffer
|
||||
rawReceiveBuffer = new byte[config.Mtu];
|
||||
}
|
||||
|
||||
// callbacks ///////////////////////////////////////////////////////////
|
||||
// some callbacks need to wrapped with some extra logic
|
||||
protected override void OnAuthenticated()
|
||||
{
|
||||
Log.Info($"[KCP] Client: OnConnected");
|
||||
connected = true;
|
||||
OnConnectedCallback();
|
||||
}
|
||||
|
||||
protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
|
||||
OnDataCallback(message, channel);
|
||||
|
||||
protected override void OnError(ErrorCode error, string message) =>
|
||||
OnErrorCallback(error, message);
|
||||
|
||||
protected override void OnDisconnected()
|
||||
{
|
||||
Log.Info($"[KCP] Client: OnDisconnected");
|
||||
connected = false;
|
||||
socket?.Close();
|
||||
socket = null;
|
||||
remoteEndPoint = null;
|
||||
OnDisconnectedCallback();
|
||||
active = false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
public void Connect(string address, ushort port)
|
||||
{
|
||||
if (connected)
|
||||
{
|
||||
Log.Warning("[KCP] Client: already connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
// resolve host name before creating peer.
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3361
|
||||
if (!Common.ResolveHostname(address, out IPAddress[] addresses))
|
||||
{
|
||||
// pass error to user callback. no need to log it manually.
|
||||
OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {address}");
|
||||
OnDisconnectedCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
// create fresh peer for each new session
|
||||
// client doesn't need secure cookie.
|
||||
Reset(config);
|
||||
|
||||
Log.Info($"[KCP] Client: connect to {address}:{port}");
|
||||
|
||||
// create socket
|
||||
remoteEndPoint = new IPEndPoint(addresses[0], port);
|
||||
socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
active = true;
|
||||
|
||||
// recv & send are called from main thread.
|
||||
// need to ensure this never blocks.
|
||||
// even a 1ms block per connection would stop us from scaling.
|
||||
socket.Blocking = false;
|
||||
|
||||
// configure buffer sizes
|
||||
Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
|
||||
|
||||
// bind to endpoint so we can use send/recv instead of sendto/recvfrom.
|
||||
socket.Connect(remoteEndPoint);
|
||||
|
||||
// immediately send a hello message to the server.
|
||||
// server will call OnMessage and add the new connection.
|
||||
// note that this still has cookie=0 until we receive the server's hello.
|
||||
SendHello();
|
||||
}
|
||||
|
||||
// io - input.
|
||||
// virtual so it may be modified for relays, etc.
|
||||
// call this while it returns true, to process all messages this tick.
|
||||
// returned ArraySegment is valid until next call to RawReceive.
|
||||
protected virtual bool RawReceive(out ArraySegment<byte> segment)
|
||||
{
|
||||
segment = default;
|
||||
if (socket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment);
|
||||
}
|
||||
// for non-blocking sockets, Receive throws WouldBlock if there is
|
||||
// no message to read. that's okay. only log for other errors.
|
||||
catch (SocketException e)
|
||||
{
|
||||
// the other end closing the connection is not an 'error'.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
// for example, his can happen when connecting without a server.
|
||||
// see test: ConnectWithoutServer().
|
||||
Log.Info($"[KCP] Client.RawReceive: looks like the other end has closed the connection. This is fine: {e}");
|
||||
base.Disconnect();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// io - output.
|
||||
// virtual so it may be modified for relays, etc.
|
||||
protected override void RawSend(ArraySegment<byte> data)
|
||||
{
|
||||
// only if socket was connected / created yet.
|
||||
// users may call send functions without having connected, causing NRE.
|
||||
if (socket == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
socket.SendNonBlocking(data);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// SendDisconnect() sometimes gets a SocketException with
|
||||
// 'Connection Refused' if the other end already closed.
|
||||
// this is not an 'error', it's expected to happen.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
Log.Info($"[KCP] Client.RawSend: looks like the other end has closed the connection. This is fine: {e}");
|
||||
// base.Disconnect(); <- don't call this, would deadlock if SendDisconnect() already throws
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(ArraySegment<byte> segment, KcpChannel channel)
|
||||
{
|
||||
if (!connected)
|
||||
{
|
||||
Log.Warning("[KCP] Client: can't send because not connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
SendData(segment, channel);
|
||||
}
|
||||
|
||||
// insert raw IO. usually from socket.Receive.
|
||||
// offset is useful for relays, where we may parse a header and then
|
||||
// feed the rest to kcp.
|
||||
public void RawInput(ArraySegment<byte> segment)
|
||||
{
|
||||
// ensure valid size: at least 1 byte for channel + 4 bytes for cookie
|
||||
if (segment.Count <= 5) return;
|
||||
|
||||
// parse channel
|
||||
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
|
||||
byte channel = segment.Array[segment.Offset + 0];
|
||||
|
||||
// server messages always contain the security cookie.
|
||||
// parse it, assign if not assigned, warn if suddenly different.
|
||||
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
|
||||
if (messageCookie == 0)
|
||||
{
|
||||
Log.Error($"[KCP] Client: received message with cookie=0, this should never happen. Server should always include the security cookie.");
|
||||
}
|
||||
|
||||
if (cookie == 0)
|
||||
{
|
||||
cookie = messageCookie;
|
||||
Log.Info($"[KCP] Client: received initial cookie: {cookie}");
|
||||
}
|
||||
else if (cookie != messageCookie)
|
||||
{
|
||||
Log.Warning($"[KCP] Client: dropping message with mismatching cookie: {messageCookie} expected: {cookie}.");
|
||||
return;
|
||||
}
|
||||
|
||||
// parse message
|
||||
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case (byte)KcpChannel.Reliable:
|
||||
{
|
||||
OnRawInputReliable(message);
|
||||
break;
|
||||
}
|
||||
case (byte)KcpChannel.Unreliable:
|
||||
{
|
||||
OnRawInputUnreliable(message);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// invalid channel indicates random internet noise.
|
||||
// servers may receive random UDP data.
|
||||
// just ignore it, but log for easier debugging.
|
||||
Log.Warning($"[KCP] Client: invalid channel header: {channel}, likely internet noise");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming messages. should be called before updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public override void TickIncoming()
|
||||
{
|
||||
// recv on socket first, then process incoming
|
||||
// (even if we didn't receive anything. need to tick ping etc.)
|
||||
// (connection is null if not active)
|
||||
if (active)
|
||||
{
|
||||
while (RawReceive(out ArraySegment<byte> segment))
|
||||
RawInput(segment);
|
||||
}
|
||||
|
||||
// RawReceive may have disconnected peer. active check again.
|
||||
if (active) base.TickIncoming();
|
||||
}
|
||||
|
||||
// process outgoing messages. should be called after updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public override void TickOutgoing()
|
||||
{
|
||||
// process outgoing while active
|
||||
if (active) base.TickOutgoing();
|
||||
}
|
||||
|
||||
// process incoming and outgoing for convenience
|
||||
// => ideally call ProcessIncoming() before updating the world and
|
||||
// ProcessOutgoing() after updating the world for minimum latency
|
||||
public virtual void Tick()
|
||||
{
|
||||
TickIncoming();
|
||||
TickOutgoing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa069a28ed24fedb533c102d9742b36
|
||||
timeCreated: 1603786960
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa069a28ed24fedb533c102d9742b36
|
||||
timeCreated: 1603786960
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,97 +1,97 @@
|
||||
// common config struct, instead of passing 10 parameters manually every time.
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
// [Serializable] to show it in Unity inspector.
|
||||
// 'class' so we can set defaults easily.
|
||||
[Serializable]
|
||||
public class KcpConfig
|
||||
{
|
||||
// socket configuration ////////////////////////////////////////////////
|
||||
// DualMode uses both IPv6 and IPv4. not all platforms support it.
|
||||
// (Nintendo Switch, etc.)
|
||||
public bool DualMode;
|
||||
|
||||
// UDP servers use only one socket.
|
||||
// maximize buffer to handle as many connections as possible.
|
||||
//
|
||||
// M1 mac pro:
|
||||
// recv buffer default: 786896 (771 KB)
|
||||
// send buffer default: 9216 (9 KB)
|
||||
// max configurable: ~7 MB
|
||||
public int RecvBufferSize;
|
||||
public int SendBufferSize;
|
||||
|
||||
// kcp configuration ///////////////////////////////////////////////////
|
||||
// configurable MTU in case kcp sits on top of other abstractions like
|
||||
// encrypted transports, relays, etc.
|
||||
public int Mtu;
|
||||
|
||||
// NoDelay is recommended to reduce latency. This also scales better
|
||||
// without buffers getting full.
|
||||
public bool NoDelay;
|
||||
|
||||
// KCP internal update interval. 100ms is KCP default, but a lower
|
||||
// interval is recommended to minimize latency and to scale to more
|
||||
// networked entities.
|
||||
public uint Interval;
|
||||
|
||||
// KCP fastresend parameter. Faster resend for the cost of higher
|
||||
// bandwidth.
|
||||
public int FastResend;
|
||||
|
||||
// KCP congestion window heavily limits messages flushed per update.
|
||||
// congestion window may actually be broken in kcp:
|
||||
// - sending max sized message @ M1 mac flushes 2-3 messages per update
|
||||
// - even with super large send/recv window, it requires thousands of
|
||||
// update calls
|
||||
// best to leave this disabled, as it may significantly increase latency.
|
||||
public bool CongestionWindow;
|
||||
|
||||
// KCP window size can be modified to support higher loads.
|
||||
// for example, Mirror Benchmark requires:
|
||||
// 128, 128 for 4k monsters
|
||||
// 512, 512 for 10k monsters
|
||||
// 8192, 8192 for 20k monsters
|
||||
public uint SendWindowSize;
|
||||
public uint ReceiveWindowSize;
|
||||
|
||||
// timeout in milliseconds
|
||||
public int Timeout;
|
||||
|
||||
// maximum retransmission attempts until dead_link
|
||||
public uint MaxRetransmits;
|
||||
|
||||
// constructor /////////////////////////////////////////////////////////
|
||||
// constructor with defaults for convenience.
|
||||
// makes it easy to define "new KcpConfig(DualMode=false)" etc.
|
||||
public KcpConfig(
|
||||
bool DualMode = true,
|
||||
int RecvBufferSize = 1024 * 1024 * 7,
|
||||
int SendBufferSize = 1024 * 1024 * 7,
|
||||
int Mtu = Kcp.MTU_DEF,
|
||||
bool NoDelay = true,
|
||||
uint Interval = 10,
|
||||
int FastResend = 0,
|
||||
bool CongestionWindow = false,
|
||||
uint SendWindowSize = Kcp.WND_SND,
|
||||
uint ReceiveWindowSize = Kcp.WND_RCV,
|
||||
int Timeout = KcpPeer.DEFAULT_TIMEOUT,
|
||||
uint MaxRetransmits = Kcp.DEADLINK)
|
||||
{
|
||||
this.DualMode = DualMode;
|
||||
this.RecvBufferSize = RecvBufferSize;
|
||||
this.SendBufferSize = SendBufferSize;
|
||||
this.Mtu = Mtu;
|
||||
this.NoDelay = NoDelay;
|
||||
this.Interval = Interval;
|
||||
this.FastResend = FastResend;
|
||||
this.CongestionWindow = CongestionWindow;
|
||||
this.SendWindowSize = SendWindowSize;
|
||||
this.ReceiveWindowSize = ReceiveWindowSize;
|
||||
this.Timeout = Timeout;
|
||||
this.MaxRetransmits = MaxRetransmits;
|
||||
}
|
||||
}
|
||||
}
|
||||
// common config struct, instead of passing 10 parameters manually every time.
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
// [Serializable] to show it in Unity inspector.
|
||||
// 'class' so we can set defaults easily.
|
||||
[Serializable]
|
||||
public class KcpConfig
|
||||
{
|
||||
// socket configuration ////////////////////////////////////////////////
|
||||
// DualMode uses both IPv6 and IPv4. not all platforms support it.
|
||||
// (Nintendo Switch, etc.)
|
||||
public bool DualMode;
|
||||
|
||||
// UDP servers use only one socket.
|
||||
// maximize buffer to handle as many connections as possible.
|
||||
//
|
||||
// M1 mac pro:
|
||||
// recv buffer default: 786896 (771 KB)
|
||||
// send buffer default: 9216 (9 KB)
|
||||
// max configurable: ~7 MB
|
||||
public int RecvBufferSize;
|
||||
public int SendBufferSize;
|
||||
|
||||
// kcp configuration ///////////////////////////////////////////////////
|
||||
// configurable MTU in case kcp sits on top of other abstractions like
|
||||
// encrypted transports, relays, etc.
|
||||
public int Mtu;
|
||||
|
||||
// NoDelay is recommended to reduce latency. This also scales better
|
||||
// without buffers getting full.
|
||||
public bool NoDelay;
|
||||
|
||||
// KCP internal update interval. 100ms is KCP default, but a lower
|
||||
// interval is recommended to minimize latency and to scale to more
|
||||
// networked entities.
|
||||
public uint Interval;
|
||||
|
||||
// KCP fastresend parameter. Faster resend for the cost of higher
|
||||
// bandwidth.
|
||||
public int FastResend;
|
||||
|
||||
// KCP congestion window heavily limits messages flushed per update.
|
||||
// congestion window may actually be broken in kcp:
|
||||
// - sending max sized message @ M1 mac flushes 2-3 messages per update
|
||||
// - even with super large send/recv window, it requires thousands of
|
||||
// update calls
|
||||
// best to leave this disabled, as it may significantly increase latency.
|
||||
public bool CongestionWindow;
|
||||
|
||||
// KCP window size can be modified to support higher loads.
|
||||
// for example, Mirror Benchmark requires:
|
||||
// 128, 128 for 4k monsters
|
||||
// 512, 512 for 10k monsters
|
||||
// 8192, 8192 for 20k monsters
|
||||
public uint SendWindowSize;
|
||||
public uint ReceiveWindowSize;
|
||||
|
||||
// timeout in milliseconds
|
||||
public int Timeout;
|
||||
|
||||
// maximum retransmission attempts until dead_link
|
||||
public uint MaxRetransmits;
|
||||
|
||||
// constructor /////////////////////////////////////////////////////////
|
||||
// constructor with defaults for convenience.
|
||||
// makes it easy to define "new KcpConfig(DualMode=false)" etc.
|
||||
public KcpConfig(
|
||||
bool DualMode = true,
|
||||
int RecvBufferSize = 1024 * 1024 * 7,
|
||||
int SendBufferSize = 1024 * 1024 * 7,
|
||||
int Mtu = Kcp.MTU_DEF,
|
||||
bool NoDelay = true,
|
||||
uint Interval = 10,
|
||||
int FastResend = 0,
|
||||
bool CongestionWindow = false,
|
||||
uint SendWindowSize = Kcp.WND_SND,
|
||||
uint ReceiveWindowSize = Kcp.WND_RCV,
|
||||
int Timeout = KcpPeer.DEFAULT_TIMEOUT,
|
||||
uint MaxRetransmits = Kcp.DEADLINK)
|
||||
{
|
||||
this.DualMode = DualMode;
|
||||
this.RecvBufferSize = RecvBufferSize;
|
||||
this.SendBufferSize = SendBufferSize;
|
||||
this.Mtu = Mtu;
|
||||
this.NoDelay = NoDelay;
|
||||
this.Interval = Interval;
|
||||
this.FastResend = FastResend;
|
||||
this.CongestionWindow = CongestionWindow;
|
||||
this.SendWindowSize = SendWindowSize;
|
||||
this.ReceiveWindowSize = ReceiveWindowSize;
|
||||
this.Timeout = Timeout;
|
||||
this.MaxRetransmits = MaxRetransmits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99692f99c45c4b47b0500e7abbfd12da
|
||||
timeCreated: 1670946969
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 99692f99c45c4b47b0500e7abbfd12da
|
||||
timeCreated: 1670946969
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
// header for messages processed by kcp.
|
||||
// this is NOT for the raw receive messages(!) because handshake/disconnect
|
||||
// need to be sent reliably. it's not enough to have those in rawreceive
|
||||
// because those messages might get lost without being resent!
|
||||
public enum KcpHeaderReliable : byte
|
||||
{
|
||||
// don't react on 0x00. might help to filter out random noise.
|
||||
Hello = 1,
|
||||
// ping goes over reliable & KcpHeader for now. could go over unreliable
|
||||
// too. there is no real difference except that this is easier because
|
||||
// we already have a KcpHeader for reliable messages.
|
||||
// ping is only used to keep it alive, so latency doesn't matter.
|
||||
Ping = 2,
|
||||
Data = 3,
|
||||
}
|
||||
|
||||
public enum KcpHeaderUnreliable : byte
|
||||
{
|
||||
// users may send unreliable messages
|
||||
Data = 4,
|
||||
// disconnect always goes through rapid fire unreliable (glenn fielder)
|
||||
Disconnect = 5,
|
||||
}
|
||||
|
||||
// save convert the enums from/to byte.
|
||||
// attackers may attempt to send invalid values, so '255' may not convert.
|
||||
public static class KcpHeader
|
||||
{
|
||||
public static bool ParseReliable(byte value, out KcpHeaderReliable header)
|
||||
{
|
||||
if (Enum.IsDefined(typeof(KcpHeaderReliable), value))
|
||||
{
|
||||
header = (KcpHeaderReliable)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
header = KcpHeaderReliable.Ping; // any default
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ParseUnreliable(byte value, out KcpHeaderUnreliable header)
|
||||
{
|
||||
if (Enum.IsDefined(typeof(KcpHeaderUnreliable), value))
|
||||
{
|
||||
header = (KcpHeaderUnreliable)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
header = KcpHeaderUnreliable.Disconnect; // any default
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
// header for messages processed by kcp.
|
||||
// this is NOT for the raw receive messages(!) because handshake/disconnect
|
||||
// need to be sent reliably. it's not enough to have those in rawreceive
|
||||
// because those messages might get lost without being resent!
|
||||
public enum KcpHeaderReliable : byte
|
||||
{
|
||||
// don't react on 0x00. might help to filter out random noise.
|
||||
Hello = 1,
|
||||
// ping goes over reliable & KcpHeader for now. could go over unreliable
|
||||
// too. there is no real difference except that this is easier because
|
||||
// we already have a KcpHeader for reliable messages.
|
||||
// ping is only used to keep it alive, so latency doesn't matter.
|
||||
Ping = 2,
|
||||
Data = 3,
|
||||
}
|
||||
|
||||
public enum KcpHeaderUnreliable : byte
|
||||
{
|
||||
// users may send unreliable messages
|
||||
Data = 4,
|
||||
// disconnect always goes through rapid fire unreliable (glenn fielder)
|
||||
Disconnect = 5,
|
||||
}
|
||||
|
||||
// save convert the enums from/to byte.
|
||||
// attackers may attempt to send invalid values, so '255' may not convert.
|
||||
public static class KcpHeader
|
||||
{
|
||||
public static bool ParseReliable(byte value, out KcpHeaderReliable header)
|
||||
{
|
||||
if (Enum.IsDefined(typeof(KcpHeaderReliable), value))
|
||||
{
|
||||
header = (KcpHeaderReliable)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
header = KcpHeaderReliable.Ping; // any default
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ParseUnreliable(byte value, out KcpHeaderUnreliable header)
|
||||
{
|
||||
if (Enum.IsDefined(typeof(KcpHeaderUnreliable), value))
|
||||
{
|
||||
header = (KcpHeaderUnreliable)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
header = KcpHeaderUnreliable.Disconnect; // any default
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91b5edac31224a49bd76f960ae018942
|
||||
timeCreated: 1610081248
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 91b5edac31224a49bd76f960ae018942
|
||||
timeCreated: 1610081248
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs
|
||||
uploadId: 736421
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
|
||||
timeCreated: 1602600432
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
|
||||
timeCreated: 1602600432
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,412 +1,412 @@
|
||||
// kcp server logic abstracted into a class.
|
||||
// for use in Mirror, DOTSNET, testing, etc.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServer
|
||||
{
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action<int, IPEndPoint> OnConnected; // connectionId, address
|
||||
protected readonly Action<int, ArraySegment<byte>, KcpChannel> OnData;
|
||||
protected readonly Action<int> OnDisconnected;
|
||||
protected readonly Action<int, ErrorCode, string> OnError;
|
||||
|
||||
// configuration
|
||||
protected readonly KcpConfig config;
|
||||
|
||||
// state
|
||||
protected Socket socket;
|
||||
EndPoint newClientEP;
|
||||
|
||||
// expose local endpoint for users / relays / nat traversal etc.
|
||||
public EndPoint LocalEndPoint => socket?.LocalEndPoint;
|
||||
|
||||
// raw receive buffer always needs to be of 'MTU' size, even if
|
||||
// MaxMessageSize is larger. kcp always sends in MTU segments and having
|
||||
// a buffer smaller than MTU would silently drop excess data.
|
||||
// => we need the mtu to fit channel + message!
|
||||
protected readonly byte[] rawReceiveBuffer;
|
||||
|
||||
// connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
|
||||
public Dictionary<int, KcpServerConnection> connections =
|
||||
new Dictionary<int, KcpServerConnection>();
|
||||
|
||||
public KcpServer(Action<int, IPEndPoint> OnConnected,
|
||||
Action<int, ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action<int> OnDisconnected,
|
||||
Action<int, ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
{
|
||||
// initialize callbacks first to ensure they can be used safely.
|
||||
this.OnConnected = OnConnected;
|
||||
this.OnData = OnData;
|
||||
this.OnDisconnected = OnDisconnected;
|
||||
this.OnError = OnError;
|
||||
this.config = config;
|
||||
|
||||
// create mtu sized receive buffer
|
||||
rawReceiveBuffer = new byte[config.Mtu];
|
||||
|
||||
// create newClientEP either IPv4 or IPv6
|
||||
newClientEP = config.DualMode
|
||||
? new IPEndPoint(IPAddress.IPv6Any, 0)
|
||||
: new IPEndPoint(IPAddress.Any, 0);
|
||||
}
|
||||
|
||||
public virtual bool IsActive() => socket != null;
|
||||
|
||||
static Socket CreateServerSocket(bool DualMode, ushort port)
|
||||
{
|
||||
if (DualMode)
|
||||
{
|
||||
// IPv6 socket with DualMode @ "::" : port
|
||||
Socket socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
|
||||
|
||||
// enabling DualMode may throw:
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/System.Net.Sockets.Socket.DualMode?view=net-7.0
|
||||
// attempt it, otherwise log but continue
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3358
|
||||
try
|
||||
{
|
||||
socket.DualMode = true;
|
||||
}
|
||||
catch (NotSupportedException e)
|
||||
{
|
||||
Log.Warning($"[KCP] Failed to set Dual Mode, continuing with IPv6 without Dual Mode. Error: {e}");
|
||||
}
|
||||
|
||||
// for windows sockets, there's a rare issue where when using
|
||||
// a server socket with multiple clients, if one of the clients
|
||||
// is closed, the single server socket throws exceptions when
|
||||
// sending/receiving. even if the socket is made for N clients.
|
||||
//
|
||||
// this actually happened to one of our users:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3611
|
||||
//
|
||||
// here's the in-depth explanation & solution:
|
||||
//
|
||||
// "As you may be aware, if a host receives a packet for a UDP
|
||||
// port that is not currently bound, it may send back an ICMP
|
||||
// "Port Unreachable" message. Whether or not it does this is
|
||||
// dependent on the firewall, private/public settings, etc.
|
||||
// On localhost, however, it will pretty much always send this
|
||||
// packet back.
|
||||
//
|
||||
// Now, on Windows (and only on Windows), by default, a received
|
||||
// ICMP Port Unreachable message will close the UDP socket that
|
||||
// sent it; hence, the next time you try to receive on the
|
||||
// socket, it will throw an exception because the socket has
|
||||
// been closed by the OS.
|
||||
//
|
||||
// Obviously, this causes a headache in the multi-client,
|
||||
// single-server socket set-up you have here, but luckily there
|
||||
// is a fix:
|
||||
//
|
||||
// You need to utilise the not-often-required SIO_UDP_CONNRESET
|
||||
// Winsock control code, which turns off this built-in behaviour
|
||||
// of automatically closing the socket.
|
||||
//
|
||||
// Note that this ioctl code is only supported on Windows
|
||||
// (XP and later), not on Linux, since it is provided by the
|
||||
// Winsock extensions. Of course, since the described behavior
|
||||
// is only the default behavior on Windows, this omission is not
|
||||
// a major loss. If you are attempting to create a
|
||||
// cross-platform library, you should cordon this off as
|
||||
// Windows-specific code."
|
||||
// https://stackoverflow.com/questions/74327225/why-does-sending-via-a-udpclient-cause-subsequent-receiving-to-fail
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
const uint IOC_IN = 0x80000000U;
|
||||
const uint IOC_VENDOR = 0x18000000U;
|
||||
const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12));
|
||||
socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null);
|
||||
}
|
||||
|
||||
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
|
||||
return socket;
|
||||
}
|
||||
else
|
||||
{
|
||||
// IPv4 socket @ "0.0.0.0" : port
|
||||
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, port));
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Start(ushort port)
|
||||
{
|
||||
// only start once
|
||||
if (socket != null)
|
||||
{
|
||||
Log.Warning("[KCP] Server: already started!");
|
||||
return;
|
||||
}
|
||||
|
||||
// listen
|
||||
socket = CreateServerSocket(config.DualMode, port);
|
||||
|
||||
// recv & send are called from main thread.
|
||||
// need to ensure this never blocks.
|
||||
// even a 1ms block per connection would stop us from scaling.
|
||||
socket.Blocking = false;
|
||||
|
||||
// configure buffer sizes
|
||||
Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
|
||||
}
|
||||
|
||||
public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
connection.SendData(segment, channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect(int connectionId)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
connection.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// expose the whole IPEndPoint, not just the IP address. some need it.
|
||||
public IPEndPoint GetClientEndPoint(int connectionId)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
return connection.remoteEndPoint as IPEndPoint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// io - input.
|
||||
// virtual so it may be modified for relays, nonalloc workaround, etc.
|
||||
// https://github.com/vis2k/where-allocation
|
||||
// bool return because not all receives may be valid.
|
||||
// for example, relay may expect a certain header.
|
||||
protected virtual bool RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId)
|
||||
{
|
||||
segment = default;
|
||||
connectionId = 0;
|
||||
if (socket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP))
|
||||
{
|
||||
// set connectionId to hash from endpoint
|
||||
connectionId = Common.ConnectionHash(newClientEP);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// NOTE: SocketException is not a subclass of IOException.
|
||||
// the other end closing the connection is not an 'error'.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
Log.Info($"[KCP] Server: ReceiveFrom failed: {e}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// io - out.
|
||||
// virtual so it may be modified for relays, nonalloc workaround, etc.
|
||||
// relays may need to prefix connId (and remoteEndPoint would be same for all)
|
||||
protected virtual void RawSend(int connectionId, ArraySegment<byte> data)
|
||||
{
|
||||
// get the connection's endpoint
|
||||
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
Log.Warning($"[KCP] Server: RawSend invalid connectionId={connectionId}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
socket.SendToNonBlocking(data, connection.remoteEndPoint);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Error($"[KCP] Server: SendTo failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual KcpServerConnection CreateConnection(int connectionId)
|
||||
{
|
||||
// generate a random cookie for this connection to avoid UDP spoofing.
|
||||
// needs to be random, but without allocations to avoid GC.
|
||||
uint cookie = Common.GenerateCookie();
|
||||
|
||||
// create empty connection without peer first.
|
||||
// we need it to set up peer callbacks.
|
||||
// afterwards we assign the peer.
|
||||
// events need to be wrapped with connectionIds
|
||||
KcpServerConnection connection = new KcpServerConnection(
|
||||
OnConnectedCallback,
|
||||
(message, channel) => OnData(connectionId, message, channel),
|
||||
OnDisconnectedCallback,
|
||||
(error, reason) => OnError(connectionId, error, reason),
|
||||
(data) => RawSend(connectionId, data),
|
||||
config,
|
||||
cookie,
|
||||
newClientEP);
|
||||
|
||||
return connection;
|
||||
|
||||
// setup authenticated event that also adds to connections
|
||||
void OnConnectedCallback(KcpServerConnection conn)
|
||||
{
|
||||
// add to connections dict after being authenticated.
|
||||
connections.Add(connectionId, conn);
|
||||
Log.Info($"[KCP] Server: added connection({connectionId})");
|
||||
|
||||
// setup Data + Disconnected events only AFTER the
|
||||
// handshake. we don't want to fire OnServerDisconnected
|
||||
// every time we receive invalid random data from the
|
||||
// internet.
|
||||
|
||||
// setup data event
|
||||
|
||||
// finally, call mirror OnConnected event
|
||||
Log.Info($"[KCP] Server: OnConnected({connectionId})");
|
||||
IPEndPoint endPoint = conn.remoteEndPoint as IPEndPoint;
|
||||
OnConnected(connectionId, endPoint);
|
||||
}
|
||||
|
||||
void OnDisconnectedCallback()
|
||||
{
|
||||
// flag for removal
|
||||
// (can't remove directly because connection is updated
|
||||
// and event is called while iterating all connections)
|
||||
connectionsToRemove.Add(connectionId);
|
||||
|
||||
// call mirror event
|
||||
Log.Info($"[KCP] Server: OnDisconnected({connectionId})");
|
||||
OnDisconnected(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// receive + add + process once.
|
||||
// best to call this as long as there is more data to receive.
|
||||
void ProcessMessage(ArraySegment<byte> segment, int connectionId)
|
||||
{
|
||||
//Log.Info($"[KCP] server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
|
||||
|
||||
// is this a new connection?
|
||||
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
// create a new KcpConnection based on last received
|
||||
// EndPoint. can be overwritten for where-allocation.
|
||||
connection = CreateConnection(connectionId);
|
||||
|
||||
// DO NOT add to connections yet. only if the first message
|
||||
// is actually the kcp handshake. otherwise it's either:
|
||||
// * random data from the internet
|
||||
// * or from a client connection that we just disconnected
|
||||
// but that hasn't realized it yet, still sending data
|
||||
// from last session that we should absolutely ignore.
|
||||
//
|
||||
//
|
||||
// TODO this allocates a new KcpConnection for each new
|
||||
// internet connection. not ideal, but C# UDP Receive
|
||||
// already allocated anyway.
|
||||
//
|
||||
// expecting a MAGIC byte[] would work, but sending the raw
|
||||
// UDP message without kcp's reliability will have low
|
||||
// probability of being received.
|
||||
//
|
||||
// for now, this is fine.
|
||||
|
||||
|
||||
// now input the message & process received ones
|
||||
// connected event was set up.
|
||||
// tick will process the first message and adds the
|
||||
// connection if it was the handshake.
|
||||
connection.RawInput(segment);
|
||||
connection.TickIncoming();
|
||||
|
||||
// again, do not add to connections.
|
||||
// if the first message wasn't the kcp handshake then
|
||||
// connection will simply be garbage collected.
|
||||
}
|
||||
// existing connection: simply input the message into kcp
|
||||
else
|
||||
{
|
||||
connection.RawInput(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming messages. should be called before updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
readonly HashSet<int> connectionsToRemove = new HashSet<int>();
|
||||
public virtual void TickIncoming()
|
||||
{
|
||||
// input all received messages into kcp
|
||||
while (RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId))
|
||||
{
|
||||
ProcessMessage(segment, connectionId);
|
||||
}
|
||||
|
||||
// process inputs for all server connections
|
||||
// (even if we didn't receive anything. need to tick ping etc.)
|
||||
foreach (KcpServerConnection connection in connections.Values)
|
||||
{
|
||||
connection.TickIncoming();
|
||||
}
|
||||
|
||||
// remove disconnected connections
|
||||
// (can't do it in connection.OnDisconnected because Tick is called
|
||||
// while iterating connections)
|
||||
foreach (int connectionId in connectionsToRemove)
|
||||
{
|
||||
connections.Remove(connectionId);
|
||||
}
|
||||
connectionsToRemove.Clear();
|
||||
}
|
||||
|
||||
// process outgoing messages. should be called after updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public virtual void TickOutgoing()
|
||||
{
|
||||
// flush all server connections
|
||||
foreach (KcpServerConnection connection in connections.Values)
|
||||
{
|
||||
connection.TickOutgoing();
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming and outgoing for convenience.
|
||||
// => ideally call ProcessIncoming() before updating the world and
|
||||
// ProcessOutgoing() after updating the world for minimum latency
|
||||
public virtual void Tick()
|
||||
{
|
||||
TickIncoming();
|
||||
TickOutgoing();
|
||||
}
|
||||
|
||||
public virtual void Stop()
|
||||
{
|
||||
// need to clear connections, otherwise they are in next session.
|
||||
// fixes https://github.com/vis2k/kcp2k/pull/47
|
||||
connections.Clear();
|
||||
socket?.Close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// kcp server logic abstracted into a class.
|
||||
// for use in Mirror, DOTSNET, testing, etc.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServer
|
||||
{
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action<int, IPEndPoint> OnConnected; // connectionId, address
|
||||
protected readonly Action<int, ArraySegment<byte>, KcpChannel> OnData;
|
||||
protected readonly Action<int> OnDisconnected;
|
||||
protected readonly Action<int, ErrorCode, string> OnError;
|
||||
|
||||
// configuration
|
||||
protected readonly KcpConfig config;
|
||||
|
||||
// state
|
||||
protected Socket socket;
|
||||
EndPoint newClientEP;
|
||||
|
||||
// expose local endpoint for users / relays / nat traversal etc.
|
||||
public EndPoint LocalEndPoint => socket?.LocalEndPoint;
|
||||
|
||||
// raw receive buffer always needs to be of 'MTU' size, even if
|
||||
// MaxMessageSize is larger. kcp always sends in MTU segments and having
|
||||
// a buffer smaller than MTU would silently drop excess data.
|
||||
// => we need the mtu to fit channel + message!
|
||||
protected readonly byte[] rawReceiveBuffer;
|
||||
|
||||
// connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
|
||||
public Dictionary<int, KcpServerConnection> connections =
|
||||
new Dictionary<int, KcpServerConnection>();
|
||||
|
||||
public KcpServer(Action<int, IPEndPoint> OnConnected,
|
||||
Action<int, ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action<int> OnDisconnected,
|
||||
Action<int, ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
{
|
||||
// initialize callbacks first to ensure they can be used safely.
|
||||
this.OnConnected = OnConnected;
|
||||
this.OnData = OnData;
|
||||
this.OnDisconnected = OnDisconnected;
|
||||
this.OnError = OnError;
|
||||
this.config = config;
|
||||
|
||||
// create mtu sized receive buffer
|
||||
rawReceiveBuffer = new byte[config.Mtu];
|
||||
|
||||
// create newClientEP either IPv4 or IPv6
|
||||
newClientEP = config.DualMode
|
||||
? new IPEndPoint(IPAddress.IPv6Any, 0)
|
||||
: new IPEndPoint(IPAddress.Any, 0);
|
||||
}
|
||||
|
||||
public virtual bool IsActive() => socket != null;
|
||||
|
||||
static Socket CreateServerSocket(bool DualMode, ushort port)
|
||||
{
|
||||
if (DualMode)
|
||||
{
|
||||
// IPv6 socket with DualMode @ "::" : port
|
||||
Socket socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
|
||||
|
||||
// enabling DualMode may throw:
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/System.Net.Sockets.Socket.DualMode?view=net-7.0
|
||||
// attempt it, otherwise log but continue
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3358
|
||||
try
|
||||
{
|
||||
socket.DualMode = true;
|
||||
}
|
||||
catch (NotSupportedException e)
|
||||
{
|
||||
Log.Warning($"[KCP] Failed to set Dual Mode, continuing with IPv6 without Dual Mode. Error: {e}");
|
||||
}
|
||||
|
||||
// for windows sockets, there's a rare issue where when using
|
||||
// a server socket with multiple clients, if one of the clients
|
||||
// is closed, the single server socket throws exceptions when
|
||||
// sending/receiving. even if the socket is made for N clients.
|
||||
//
|
||||
// this actually happened to one of our users:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3611
|
||||
//
|
||||
// here's the in-depth explanation & solution:
|
||||
//
|
||||
// "As you may be aware, if a host receives a packet for a UDP
|
||||
// port that is not currently bound, it may send back an ICMP
|
||||
// "Port Unreachable" message. Whether or not it does this is
|
||||
// dependent on the firewall, private/public settings, etc.
|
||||
// On localhost, however, it will pretty much always send this
|
||||
// packet back.
|
||||
//
|
||||
// Now, on Windows (and only on Windows), by default, a received
|
||||
// ICMP Port Unreachable message will close the UDP socket that
|
||||
// sent it; hence, the next time you try to receive on the
|
||||
// socket, it will throw an exception because the socket has
|
||||
// been closed by the OS.
|
||||
//
|
||||
// Obviously, this causes a headache in the multi-client,
|
||||
// single-server socket set-up you have here, but luckily there
|
||||
// is a fix:
|
||||
//
|
||||
// You need to utilise the not-often-required SIO_UDP_CONNRESET
|
||||
// Winsock control code, which turns off this built-in behaviour
|
||||
// of automatically closing the socket.
|
||||
//
|
||||
// Note that this ioctl code is only supported on Windows
|
||||
// (XP and later), not on Linux, since it is provided by the
|
||||
// Winsock extensions. Of course, since the described behavior
|
||||
// is only the default behavior on Windows, this omission is not
|
||||
// a major loss. If you are attempting to create a
|
||||
// cross-platform library, you should cordon this off as
|
||||
// Windows-specific code."
|
||||
// https://stackoverflow.com/questions/74327225/why-does-sending-via-a-udpclient-cause-subsequent-receiving-to-fail
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
const uint IOC_IN = 0x80000000U;
|
||||
const uint IOC_VENDOR = 0x18000000U;
|
||||
const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12));
|
||||
socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null);
|
||||
}
|
||||
|
||||
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
|
||||
return socket;
|
||||
}
|
||||
else
|
||||
{
|
||||
// IPv4 socket @ "0.0.0.0" : port
|
||||
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, port));
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Start(ushort port)
|
||||
{
|
||||
// only start once
|
||||
if (socket != null)
|
||||
{
|
||||
Log.Warning("[KCP] Server: already started!");
|
||||
return;
|
||||
}
|
||||
|
||||
// listen
|
||||
socket = CreateServerSocket(config.DualMode, port);
|
||||
|
||||
// recv & send are called from main thread.
|
||||
// need to ensure this never blocks.
|
||||
// even a 1ms block per connection would stop us from scaling.
|
||||
socket.Blocking = false;
|
||||
|
||||
// configure buffer sizes
|
||||
Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
|
||||
}
|
||||
|
||||
public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
connection.SendData(segment, channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect(int connectionId)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
connection.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// expose the whole IPEndPoint, not just the IP address. some need it.
|
||||
public IPEndPoint GetClientEndPoint(int connectionId)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
return connection.remoteEndPoint as IPEndPoint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// io - input.
|
||||
// virtual so it may be modified for relays, nonalloc workaround, etc.
|
||||
// https://github.com/vis2k/where-allocation
|
||||
// bool return because not all receives may be valid.
|
||||
// for example, relay may expect a certain header.
|
||||
protected virtual bool RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId)
|
||||
{
|
||||
segment = default;
|
||||
connectionId = 0;
|
||||
if (socket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP))
|
||||
{
|
||||
// set connectionId to hash from endpoint
|
||||
connectionId = Common.ConnectionHash(newClientEP);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// NOTE: SocketException is not a subclass of IOException.
|
||||
// the other end closing the connection is not an 'error'.
|
||||
// but connections should never just end silently.
|
||||
// at least log a message for easier debugging.
|
||||
Log.Info($"[KCP] Server: ReceiveFrom failed: {e}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// io - out.
|
||||
// virtual so it may be modified for relays, nonalloc workaround, etc.
|
||||
// relays may need to prefix connId (and remoteEndPoint would be same for all)
|
||||
protected virtual void RawSend(int connectionId, ArraySegment<byte> data)
|
||||
{
|
||||
// get the connection's endpoint
|
||||
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
Log.Warning($"[KCP] Server: RawSend invalid connectionId={connectionId}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
socket.SendToNonBlocking(data, connection.remoteEndPoint);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Error($"[KCP] Server: SendTo failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual KcpServerConnection CreateConnection(int connectionId)
|
||||
{
|
||||
// generate a random cookie for this connection to avoid UDP spoofing.
|
||||
// needs to be random, but without allocations to avoid GC.
|
||||
uint cookie = Common.GenerateCookie();
|
||||
|
||||
// create empty connection without peer first.
|
||||
// we need it to set up peer callbacks.
|
||||
// afterwards we assign the peer.
|
||||
// events need to be wrapped with connectionIds
|
||||
KcpServerConnection connection = new KcpServerConnection(
|
||||
OnConnectedCallback,
|
||||
(message, channel) => OnData(connectionId, message, channel),
|
||||
OnDisconnectedCallback,
|
||||
(error, reason) => OnError(connectionId, error, reason),
|
||||
(data) => RawSend(connectionId, data),
|
||||
config,
|
||||
cookie,
|
||||
newClientEP);
|
||||
|
||||
return connection;
|
||||
|
||||
// setup authenticated event that also adds to connections
|
||||
void OnConnectedCallback(KcpServerConnection conn)
|
||||
{
|
||||
// add to connections dict after being authenticated.
|
||||
connections.Add(connectionId, conn);
|
||||
Log.Info($"[KCP] Server: added connection({connectionId})");
|
||||
|
||||
// setup Data + Disconnected events only AFTER the
|
||||
// handshake. we don't want to fire OnServerDisconnected
|
||||
// every time we receive invalid random data from the
|
||||
// internet.
|
||||
|
||||
// setup data event
|
||||
|
||||
// finally, call mirror OnConnected event
|
||||
Log.Info($"[KCP] Server: OnConnected({connectionId})");
|
||||
IPEndPoint endPoint = conn.remoteEndPoint as IPEndPoint;
|
||||
OnConnected(connectionId, endPoint);
|
||||
}
|
||||
|
||||
void OnDisconnectedCallback()
|
||||
{
|
||||
// flag for removal
|
||||
// (can't remove directly because connection is updated
|
||||
// and event is called while iterating all connections)
|
||||
connectionsToRemove.Add(connectionId);
|
||||
|
||||
// call mirror event
|
||||
Log.Info($"[KCP] Server: OnDisconnected({connectionId})");
|
||||
OnDisconnected(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// receive + add + process once.
|
||||
// best to call this as long as there is more data to receive.
|
||||
void ProcessMessage(ArraySegment<byte> segment, int connectionId)
|
||||
{
|
||||
//Log.Info($"[KCP] server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
|
||||
|
||||
// is this a new connection?
|
||||
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
// create a new KcpConnection based on last received
|
||||
// EndPoint. can be overwritten for where-allocation.
|
||||
connection = CreateConnection(connectionId);
|
||||
|
||||
// DO NOT add to connections yet. only if the first message
|
||||
// is actually the kcp handshake. otherwise it's either:
|
||||
// * random data from the internet
|
||||
// * or from a client connection that we just disconnected
|
||||
// but that hasn't realized it yet, still sending data
|
||||
// from last session that we should absolutely ignore.
|
||||
//
|
||||
//
|
||||
// TODO this allocates a new KcpConnection for each new
|
||||
// internet connection. not ideal, but C# UDP Receive
|
||||
// already allocated anyway.
|
||||
//
|
||||
// expecting a MAGIC byte[] would work, but sending the raw
|
||||
// UDP message without kcp's reliability will have low
|
||||
// probability of being received.
|
||||
//
|
||||
// for now, this is fine.
|
||||
|
||||
|
||||
// now input the message & process received ones
|
||||
// connected event was set up.
|
||||
// tick will process the first message and adds the
|
||||
// connection if it was the handshake.
|
||||
connection.RawInput(segment);
|
||||
connection.TickIncoming();
|
||||
|
||||
// again, do not add to connections.
|
||||
// if the first message wasn't the kcp handshake then
|
||||
// connection will simply be garbage collected.
|
||||
}
|
||||
// existing connection: simply input the message into kcp
|
||||
else
|
||||
{
|
||||
connection.RawInput(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming messages. should be called before updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
readonly HashSet<int> connectionsToRemove = new HashSet<int>();
|
||||
public virtual void TickIncoming()
|
||||
{
|
||||
// input all received messages into kcp
|
||||
while (RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId))
|
||||
{
|
||||
ProcessMessage(segment, connectionId);
|
||||
}
|
||||
|
||||
// process inputs for all server connections
|
||||
// (even if we didn't receive anything. need to tick ping etc.)
|
||||
foreach (KcpServerConnection connection in connections.Values)
|
||||
{
|
||||
connection.TickIncoming();
|
||||
}
|
||||
|
||||
// remove disconnected connections
|
||||
// (can't do it in connection.OnDisconnected because Tick is called
|
||||
// while iterating connections)
|
||||
foreach (int connectionId in connectionsToRemove)
|
||||
{
|
||||
connections.Remove(connectionId);
|
||||
}
|
||||
connectionsToRemove.Clear();
|
||||
}
|
||||
|
||||
// process outgoing messages. should be called after updating the world.
|
||||
// virtual because relay may need to inject their own ping or similar.
|
||||
public virtual void TickOutgoing()
|
||||
{
|
||||
// flush all server connections
|
||||
foreach (KcpServerConnection connection in connections.Values)
|
||||
{
|
||||
connection.TickOutgoing();
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming and outgoing for convenience.
|
||||
// => ideally call ProcessIncoming() before updating the world and
|
||||
// ProcessOutgoing() after updating the world for minimum latency
|
||||
public virtual void Tick()
|
||||
{
|
||||
TickIncoming();
|
||||
TickOutgoing();
|
||||
}
|
||||
|
||||
public virtual void Stop()
|
||||
{
|
||||
// need to clear connections, otherwise they are in next session.
|
||||
// fixes https://github.com/vis2k/kcp2k/pull/47
|
||||
connections.Clear();
|
||||
socket?.Close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9759159c6589494a9037f5e130a867ed
|
||||
timeCreated: 1603787747
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 9759159c6589494a9037f5e130a867ed
|
||||
timeCreated: 1603787747
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,126 +1,126 @@
|
||||
// server needs to store a separate KcpPeer for each connection.
|
||||
// as well as remoteEndPoint so we know where to send data to.
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServerConnection : KcpPeer
|
||||
{
|
||||
public readonly EndPoint remoteEndPoint;
|
||||
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action<KcpServerConnection> OnConnectedCallback;
|
||||
protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
|
||||
protected readonly Action OnDisconnectedCallback;
|
||||
protected readonly Action<ErrorCode, string> OnErrorCallback;
|
||||
protected readonly Action<ArraySegment<byte>> RawSendCallback;
|
||||
|
||||
public KcpServerConnection(
|
||||
Action<KcpServerConnection> OnConnected,
|
||||
Action<ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action OnDisconnected,
|
||||
Action<ErrorCode, string> OnError,
|
||||
Action<ArraySegment<byte>> OnRawSend,
|
||||
KcpConfig config,
|
||||
uint cookie,
|
||||
EndPoint remoteEndPoint)
|
||||
: base(config, cookie)
|
||||
{
|
||||
OnConnectedCallback = OnConnected;
|
||||
OnDataCallback = OnData;
|
||||
OnDisconnectedCallback = OnDisconnected;
|
||||
OnErrorCallback = OnError;
|
||||
RawSendCallback = OnRawSend;
|
||||
|
||||
this.remoteEndPoint = remoteEndPoint;
|
||||
}
|
||||
|
||||
// callbacks ///////////////////////////////////////////////////////////
|
||||
protected override void OnAuthenticated()
|
||||
{
|
||||
// once we receive the first client hello,
|
||||
// immediately reply with hello so the client knows the security cookie.
|
||||
SendHello();
|
||||
OnConnectedCallback(this);
|
||||
}
|
||||
|
||||
protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
|
||||
OnDataCallback(message, channel);
|
||||
|
||||
protected override void OnDisconnected() =>
|
||||
OnDisconnectedCallback();
|
||||
|
||||
protected override void OnError(ErrorCode error, string message) =>
|
||||
OnErrorCallback(error, message);
|
||||
|
||||
protected override void RawSend(ArraySegment<byte> data) =>
|
||||
RawSendCallback(data);
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// insert raw IO. usually from socket.Receive.
|
||||
// offset is useful for relays, where we may parse a header and then
|
||||
// feed the rest to kcp.
|
||||
public void RawInput(ArraySegment<byte> segment)
|
||||
{
|
||||
// ensure valid size: at least 1 byte for channel + 4 bytes for cookie
|
||||
if (segment.Count <= 5) return;
|
||||
|
||||
// parse channel
|
||||
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
|
||||
byte channel = segment.Array[segment.Offset + 0];
|
||||
|
||||
// all server->client messages include the server's security cookie.
|
||||
// all client->server messages except for the initial 'hello' include it too.
|
||||
// parse the cookie and make sure it matches (except for initial hello).
|
||||
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
|
||||
|
||||
// security: messages after authentication are expected to contain the cookie.
|
||||
// this protects against UDP spoofing.
|
||||
// simply drop the message if the cookie doesn't match.
|
||||
if (state == KcpState.Authenticated)
|
||||
{
|
||||
if (messageCookie != cookie)
|
||||
{
|
||||
// Info is enough, don't scare users.
|
||||
// => this can happen for malicious messages
|
||||
// => it can also happen if client's Hello message was retransmitted multiple times, which is totally normal.
|
||||
Log.Info($"[KCP] ServerConnection: dropped message with invalid cookie: {messageCookie} from {remoteEndPoint} expected: {cookie} state: {state}. This can happen if the client's Hello message was transmitted multiple times, or if an attacker attempted UDP spoofing.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// parse message
|
||||
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case (byte)KcpChannel.Reliable:
|
||||
{
|
||||
OnRawInputReliable(message);
|
||||
break;
|
||||
}
|
||||
case (byte)KcpChannel.Unreliable:
|
||||
{
|
||||
OnRawInputUnreliable(message);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// invalid channel indicates random internet noise.
|
||||
// servers may receive random UDP data.
|
||||
// just ignore it, but log for easier debugging.
|
||||
Log.Warning($"[KCP] ServerConnection: invalid channel header: {channel} from {remoteEndPoint}, likely internet noise");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// server needs to store a separate KcpPeer for each connection.
|
||||
// as well as remoteEndPoint so we know where to send data to.
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServerConnection : KcpPeer
|
||||
{
|
||||
public readonly EndPoint remoteEndPoint;
|
||||
|
||||
// callbacks
|
||||
// even for errors, to allow liraries to show popups etc.
|
||||
// instead of logging directly.
|
||||
// (string instead of Exception for ease of use and to avoid user panic)
|
||||
//
|
||||
// events are readonly, set in constructor.
|
||||
// this ensures they are always initialized when used.
|
||||
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
|
||||
protected readonly Action<KcpServerConnection> OnConnectedCallback;
|
||||
protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
|
||||
protected readonly Action OnDisconnectedCallback;
|
||||
protected readonly Action<ErrorCode, string> OnErrorCallback;
|
||||
protected readonly Action<ArraySegment<byte>> RawSendCallback;
|
||||
|
||||
public KcpServerConnection(
|
||||
Action<KcpServerConnection> OnConnected,
|
||||
Action<ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action OnDisconnected,
|
||||
Action<ErrorCode, string> OnError,
|
||||
Action<ArraySegment<byte>> OnRawSend,
|
||||
KcpConfig config,
|
||||
uint cookie,
|
||||
EndPoint remoteEndPoint)
|
||||
: base(config, cookie)
|
||||
{
|
||||
OnConnectedCallback = OnConnected;
|
||||
OnDataCallback = OnData;
|
||||
OnDisconnectedCallback = OnDisconnected;
|
||||
OnErrorCallback = OnError;
|
||||
RawSendCallback = OnRawSend;
|
||||
|
||||
this.remoteEndPoint = remoteEndPoint;
|
||||
}
|
||||
|
||||
// callbacks ///////////////////////////////////////////////////////////
|
||||
protected override void OnAuthenticated()
|
||||
{
|
||||
// once we receive the first client hello,
|
||||
// immediately reply with hello so the client knows the security cookie.
|
||||
SendHello();
|
||||
OnConnectedCallback(this);
|
||||
}
|
||||
|
||||
protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
|
||||
OnDataCallback(message, channel);
|
||||
|
||||
protected override void OnDisconnected() =>
|
||||
OnDisconnectedCallback();
|
||||
|
||||
protected override void OnError(ErrorCode error, string message) =>
|
||||
OnErrorCallback(error, message);
|
||||
|
||||
protected override void RawSend(ArraySegment<byte> data) =>
|
||||
RawSendCallback(data);
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// insert raw IO. usually from socket.Receive.
|
||||
// offset is useful for relays, where we may parse a header and then
|
||||
// feed the rest to kcp.
|
||||
public void RawInput(ArraySegment<byte> segment)
|
||||
{
|
||||
// ensure valid size: at least 1 byte for channel + 4 bytes for cookie
|
||||
if (segment.Count <= 5) return;
|
||||
|
||||
// parse channel
|
||||
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
|
||||
byte channel = segment.Array[segment.Offset + 0];
|
||||
|
||||
// all server->client messages include the server's security cookie.
|
||||
// all client->server messages except for the initial 'hello' include it too.
|
||||
// parse the cookie and make sure it matches (except for initial hello).
|
||||
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
|
||||
|
||||
// security: messages after authentication are expected to contain the cookie.
|
||||
// this protects against UDP spoofing.
|
||||
// simply drop the message if the cookie doesn't match.
|
||||
if (state == KcpState.Authenticated)
|
||||
{
|
||||
if (messageCookie != cookie)
|
||||
{
|
||||
// Info is enough, don't scare users.
|
||||
// => this can happen for malicious messages
|
||||
// => it can also happen if client's Hello message was retransmitted multiple times, which is totally normal.
|
||||
Log.Info($"[KCP] ServerConnection: dropped message with invalid cookie: {messageCookie} from {remoteEndPoint} expected: {cookie} state: {state}. This can happen if the client's Hello message was transmitted multiple times, or if an attacker attempted UDP spoofing.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// parse message
|
||||
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case (byte)KcpChannel.Reliable:
|
||||
{
|
||||
OnRawInputReliable(message);
|
||||
break;
|
||||
}
|
||||
case (byte)KcpChannel.Unreliable:
|
||||
{
|
||||
OnRawInputUnreliable(message);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// invalid channel indicates random internet noise.
|
||||
// servers may receive random UDP data.
|
||||
// just ignore it, but log for easier debugging.
|
||||
Log.Warning($"[KCP] ServerConnection: invalid channel header: {channel} from {remoteEndPoint}, likely internet noise");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
|
||||
timeCreated: 1602601483
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
|
||||
timeCreated: 1602601483
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace kcp2k
|
||||
{
|
||||
public enum KcpState { Connected, Authenticated, Disconnected }
|
||||
}
|
||||
namespace kcp2k
|
||||
{
|
||||
public enum KcpState { Connected, Authenticated, Disconnected }
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81a02c141a88d45d4a2f5ef68c6da75f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 81a02c141a88d45d4a2f5ef68c6da75f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// 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)
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Log
|
||||
{
|
||||
public static Action<string> Info = Console.WriteLine;
|
||||
public static Action<string> Warning = Console.WriteLine;
|
||||
public static Action<string> Error = Console.Error.WriteLine;
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Log
|
||||
{
|
||||
public static Action<string> Info = Console.WriteLine;
|
||||
public static Action<string> Warning = Console.WriteLine;
|
||||
public static Action<string> Error = Console.Error.WriteLine;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b5e1de98d6d84c3793a61cf7d8da9a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 7b5e1de98d6d84c3793a61cf7d8da9a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs
|
||||
uploadId: 736421
|
||||
|
||||
Reference in New Issue
Block a user