123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- // 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> OnConnected;
- 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> 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($"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("KcpServer: 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($"KcpServer: 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($"KcpServer: RawSend invalid connectionId={connectionId}");
- return;
- }
- try
- {
- socket.SendToNonBlocking(data, connection.remoteEndPoint);
- }
- catch (SocketException e)
- {
- Log.Error($"KcpServer: 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($"KcpServer: 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($"KcpServer: OnConnected({connectionId})");
- OnConnected(connectionId);
- }
- 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($"KcpServer: 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;
- }
- }
- }
|