#if UNITY_2018_3_OR_NEWER #define UNITY_SOCKET_FIX #endif using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Threading; using LiteNetLib.Layers; using LiteNetLib.Utils; namespace LiteNetLib { public sealed class NetPacketReader : NetDataReader { private NetPacket _packet; private readonly NetManager _manager; private readonly NetEvent _evt; internal NetPacketReader(NetManager manager, NetEvent evt) { _manager = manager; _evt = evt; } internal void SetSource(NetPacket packet, int headerSize) { if (packet == null) return; _packet = packet; SetSource(packet.RawData, headerSize, packet.Size); } internal void RecycleInternal() { Clear(); if (_packet != null) _manager.PoolRecycle(_packet); _packet = null; _manager.RecycleEvent(_evt); } public void Recycle() { if (_manager.AutoRecycle) return; RecycleInternal(); } } internal sealed class NetEvent { public NetEvent Next; public enum EType { Connect, Disconnect, Receive, ReceiveUnconnected, Error, ConnectionLatencyUpdated, Broadcast, ConnectionRequest, MessageDelivered, PeerAddressChanged } public EType Type; public NetPeer Peer; public IPEndPoint RemoteEndPoint; public object UserData; public int Latency; public SocketError ErrorCode; public DisconnectReason DisconnectReason; public ConnectionRequest ConnectionRequest; public DeliveryMethod DeliveryMethod; public byte ChannelNumber; public readonly NetPacketReader DataReader; public NetEvent(NetManager manager) { DataReader = new NetPacketReader(manager, this); } } /// /// Main class for all network operations. Can be used as client and/or server. /// public partial class NetManager : IEnumerable { public struct NetPeerEnumerator : IEnumerator { private readonly NetPeer _initialPeer; private NetPeer _p; public NetPeerEnumerator(NetPeer p) { _initialPeer = p; _p = null; } public void Dispose() { } public bool MoveNext() { _p = _p == null ? _initialPeer : _p.NextPeer; return _p != null; } public void Reset() { throw new NotSupportedException(); } public NetPeer Current => _p; object IEnumerator.Current => _p; } private struct IncomingData { public NetPacket Data; public IPEndPoint EndPoint; public DateTime TimeWhenGet; } private readonly List _pingSimulationList = new List(); private struct OutboundDelayedPacket { public byte[] Data; public int Start; public int Length; public IPEndPoint EndPoint; public DateTime TimeWhenSend; } private readonly List _outboundSimulationList = new List(); private readonly Random _randomGenerator = new Random(); private const int MinLatencyThreshold = 5; private Thread _logicThread; private bool _manualMode; private readonly AutoResetEvent _updateTriggerEvent = new AutoResetEvent(true); private NetEvent _pendingEventHead; private NetEvent _pendingEventTail; private NetEvent _netEventPoolHead; private readonly INetEventListener _netEventListener; private readonly IDeliveryEventListener _deliveryEventListener; private readonly INtpEventListener _ntpEventListener; private readonly IPeerAddressChangedListener _peerAddressChangedListener; private readonly Dictionary _requestsDict = new Dictionary(); private readonly ConcurrentDictionary _ntpRequests = new ConcurrentDictionary(); private long _connectedPeersCount; private readonly List _connectedPeerListCache = new List(); private readonly PacketLayerBase _extraPacketLayer; private int _lastPeerId; private ConcurrentQueue _peerIds = new ConcurrentQueue(); private byte _channelsCount = 1; private readonly object _eventLock = new object(); private volatile bool _isRunning; /// /// Used with and to tag packets that /// need to be dropped. Only relevant when DEBUG is defined. /// private bool _dropPacket; //config section /// /// Enable messages receiving without connection. (with SendUnconnectedMessage method) /// public bool UnconnectedMessagesEnabled = false; /// /// Enable nat punch messages /// public bool NatPunchEnabled = false; /// /// Library logic update and send period in milliseconds /// Lowest values in Windows doesn't change much because of Thread.Sleep precision /// To more frequent sends (or sends tied to your game logic) use /// public int UpdateTime = 15; /// /// Interval for latency detection and checking connection (in milliseconds) /// public int PingInterval = 1000; /// /// If NetManager doesn't receive any packet from remote peer during this time (in milliseconds) then connection will be closed /// (including library internal keepalive packets) /// public int DisconnectTimeout = 5000; /// /// Simulate packet loss by dropping random amount of packets. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) /// public bool SimulatePacketLoss = false; /// /// Simulate latency by holding packets for random time. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) /// public bool SimulateLatency = false; /// /// Chance of packet loss when simulation enabled. value in percents (1 - 100). /// public int SimulationPacketLossChance = 10; /// /// Minimum simulated round-trip latency (in milliseconds). Actual latency applied per direction is half of this value. /// public int SimulationMinLatency = 30; /// /// Maximum simulated round-trip latency (in milliseconds). Actual latency applied per direction is half of this value. /// public int SimulationMaxLatency = 100; /// /// Events automatically will be called without PollEvents method from another thread /// public bool UnsyncedEvents = false; /// /// If true - receive event will be called from "receive" thread immediately otherwise on PollEvents call /// public bool UnsyncedReceiveEvent = false; /// /// If true - delivery event will be called from "receive" thread immediately otherwise on PollEvents call /// public bool UnsyncedDeliveryEvent = false; /// /// Allows receive broadcast packets /// public bool BroadcastReceiveEnabled = false; /// /// Delay between initial connection attempts (in milliseconds) /// public int ReconnectDelay = 500; /// /// Maximum connection attempts before client stops and call disconnect event. /// public int MaxConnectAttempts = 10; /// /// Enables socket option "ReuseAddress" for specific purposes /// public bool ReuseAddress = false; /// /// UDP Only Socket Option /// Normally IP sockets send packets of data through routers and gateways until they reach the final destination. /// If the DontRoute flag is set to True, then data will be delivered on the local subnet only. /// public bool DontRoute = false; /// /// Statistics of all connections /// public readonly NetStatistics Statistics = new NetStatistics(); /// /// Toggles the collection of network statistics for the instance and all known peers /// public bool EnableStatistics = false; /// /// NatPunchModule for NAT hole punching operations /// public readonly NatPunchModule NatPunchModule; /// /// Returns true if socket listening and update thread is running /// public bool IsRunning => _isRunning; /// /// Local EndPoint (host and port) /// public int LocalPort { get; private set; } /// /// Automatically recycle NetPacketReader after OnReceive event /// public bool AutoRecycle; /// /// IPv6 support /// public bool IPv6Enabled = true; /// /// Override MTU for all new peers registered in this NetManager, will ignores MTU Discovery! /// public int MtuOverride = 0; /// /// Automatically discovery mtu starting from. Use at own risk because some routers can break MTU detection /// and connection in result /// public bool MtuDiscovery = false; /// /// First peer. Useful for Client mode /// public NetPeer FirstPeer => _headPeer; /// /// Experimental feature mostly for servers. Only for Windows/Linux /// use direct socket calls for send/receive to drastically increase speed and reduce GC pressure /// public bool UseNativeSockets = false; /// /// Disconnect peers if HostUnreachable or NetworkUnreachable spawned (old behaviour 0.9.x was true) /// public bool DisconnectOnUnreachable = false; /// /// Allows peer change it's ip (lte to wifi, wifi to lte, etc). Use only on server /// public bool AllowPeerAddressChange = false; /// /// QoS channel count per message type (value must be between 1 and 64 channels) /// public byte ChannelsCount { get => _channelsCount; set { if (value < 1 || value > 64) throw new ArgumentException("Channels count must be between 1 and 64"); _channelsCount = value; } } /// /// Returns connected peers list (with internal cached list) /// public List ConnectedPeerList { get { GetPeersNonAlloc(_connectedPeerListCache, ConnectionState.Connected); return _connectedPeerListCache; } } /// /// Returns connected peers count /// public int ConnectedPeersCount => (int)Interlocked.Read(ref _connectedPeersCount); public int ExtraPacketSizeForLayer => _extraPacketLayer?.ExtraPacketSizeForLayer ?? 0; /// /// NetManager constructor /// /// Network events listener (also can implement IDeliveryEventListener) /// Extra processing of packages, like CRC checksum or encryption. All connected NetManagers must have same layer. #if UNITY_SOCKET_FIX public NetManager(INetEventListener listener, PacketLayerBase extraPacketLayer = null, bool useSocketFix = true) { _useSocketFix = useSocketFix; #else public NetManager(INetEventListener listener, PacketLayerBase extraPacketLayer = null) { #endif _netEventListener = listener; _deliveryEventListener = listener as IDeliveryEventListener; _ntpEventListener = listener as INtpEventListener; _peerAddressChangedListener = listener as IPeerAddressChangedListener; NatPunchModule = new NatPunchModule(this); _extraPacketLayer = extraPacketLayer; } internal void ConnectionLatencyUpdated(NetPeer fromPeer, int latency) { CreateEvent(NetEvent.EType.ConnectionLatencyUpdated, fromPeer, latency: latency); } internal void MessageDelivered(NetPeer fromPeer, object userData) { if (_deliveryEventListener != null) CreateEvent(NetEvent.EType.MessageDelivered, fromPeer, userData: userData); } internal void DisconnectPeerForce(NetPeer peer, DisconnectReason reason, SocketError socketErrorCode, NetPacket eventData) { DisconnectPeer(peer, reason, socketErrorCode, true, null, 0, 0, eventData); } private void DisconnectPeer( NetPeer peer, DisconnectReason reason, SocketError socketErrorCode, bool force, byte[] data, int start, int count, NetPacket eventData) { var shutdownResult = peer.Shutdown(data, start, count, force); if (shutdownResult == ShutdownResult.None) return; if (shutdownResult == ShutdownResult.WasConnected) Interlocked.Decrement(ref _connectedPeersCount); CreateEvent( NetEvent.EType.Disconnect, peer, errorCode: socketErrorCode, disconnectReason: reason, readerSource: eventData); } private void CreateEvent( NetEvent.EType type, NetPeer peer = null, IPEndPoint remoteEndPoint = null, SocketError errorCode = 0, int latency = 0, DisconnectReason disconnectReason = DisconnectReason.ConnectionFailed, ConnectionRequest connectionRequest = null, DeliveryMethod deliveryMethod = DeliveryMethod.Unreliable, byte channelNumber = 0, NetPacket readerSource = null, object userData = null) { NetEvent evt; bool unsyncEvent = UnsyncedEvents; if (type == NetEvent.EType.Connect) Interlocked.Increment(ref _connectedPeersCount); else if (type == NetEvent.EType.MessageDelivered) unsyncEvent = UnsyncedDeliveryEvent; lock (_eventLock) { evt = _netEventPoolHead; if (evt == null) evt = new NetEvent(this); else _netEventPoolHead = evt.Next; } evt.Next = null; evt.Type = type; evt.DataReader.SetSource(readerSource, readerSource?.GetHeaderSize() ?? 0); evt.Peer = peer; evt.RemoteEndPoint = remoteEndPoint; evt.Latency = latency; evt.ErrorCode = errorCode; evt.DisconnectReason = disconnectReason; evt.ConnectionRequest = connectionRequest; evt.DeliveryMethod = deliveryMethod; evt.ChannelNumber = channelNumber; evt.UserData = userData; if (unsyncEvent || _manualMode) { ProcessEvent(evt); } else { lock (_eventLock) { if (_pendingEventTail == null) _pendingEventHead = evt; else _pendingEventTail.Next = evt; _pendingEventTail = evt; } } } private void ProcessEvent(NetEvent evt) { NetDebug.Write("[NM] Processing event: " + evt.Type); bool emptyData = evt.DataReader.IsNull; switch (evt.Type) { case NetEvent.EType.Connect: _netEventListener.OnPeerConnected(evt.Peer); break; case NetEvent.EType.Disconnect: var info = new DisconnectInfo { Reason = evt.DisconnectReason, AdditionalData = evt.DataReader, SocketErrorCode = evt.ErrorCode }; _netEventListener.OnPeerDisconnected(evt.Peer, info); break; case NetEvent.EType.Receive: _netEventListener.OnNetworkReceive(evt.Peer, evt.DataReader, evt.ChannelNumber, evt.DeliveryMethod); break; case NetEvent.EType.ReceiveUnconnected: _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.BasicMessage); break; case NetEvent.EType.Broadcast: _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.Broadcast); break; case NetEvent.EType.Error: _netEventListener.OnNetworkError(evt.RemoteEndPoint, evt.ErrorCode); break; case NetEvent.EType.ConnectionLatencyUpdated: _netEventListener.OnNetworkLatencyUpdate(evt.Peer, evt.Latency); break; case NetEvent.EType.ConnectionRequest: _netEventListener.OnConnectionRequest(evt.ConnectionRequest); break; case NetEvent.EType.MessageDelivered: _deliveryEventListener.OnMessageDelivered(evt.Peer, evt.UserData); break; case NetEvent.EType.PeerAddressChanged: _peersLock.EnterUpgradeableReadLock(); IPEndPoint previousAddress = null; if (ContainsPeer(evt.Peer)) { _peersLock.EnterWriteLock(); RemovePeerFromSet(evt.Peer); previousAddress = new IPEndPoint(evt.Peer.Address, evt.Peer.Port); evt.Peer.FinishEndPointChange(evt.RemoteEndPoint); AddPeerToSet(evt.Peer); _peersLock.ExitWriteLock(); } _peersLock.ExitUpgradeableReadLock(); if (previousAddress != null && _peerAddressChangedListener != null) _peerAddressChangedListener.OnPeerAddressChanged(evt.Peer, previousAddress); break; } //Recycle if not message if (emptyData) RecycleEvent(evt); else if (AutoRecycle) evt.DataReader.RecycleInternal(); } internal void RecycleEvent(NetEvent evt) { evt.Peer = null; evt.ErrorCode = 0; evt.RemoteEndPoint = null; evt.ConnectionRequest = null; lock (_eventLock) { evt.Next = _netEventPoolHead; _netEventPoolHead = evt; } } //Update function private void UpdateLogic() { var peersToRemove = new List(); var stopwatch = new Stopwatch(); stopwatch.Start(); while (_isRunning) { try { ProcessDelayedPackets(); float elapsed = (float)(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0); elapsed = elapsed <= 0.0f ? 0.001f : elapsed; stopwatch.Restart(); for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { if (netPeer.ConnectionState == ConnectionState.Disconnected && netPeer.TimeSinceLastPacket > DisconnectTimeout) { peersToRemove.Add(netPeer); } else { netPeer.Update(elapsed); } } if (peersToRemove.Count > 0) { _peersLock.EnterWriteLock(); for (int i = 0; i < peersToRemove.Count; i++) RemovePeer(peersToRemove[i], false); _peersLock.ExitWriteLock(); peersToRemove.Clear(); } ProcessNtpRequests(elapsed); int sleepTime = UpdateTime - (int)stopwatch.ElapsedMilliseconds; if (sleepTime > 0) _updateTriggerEvent.WaitOne(sleepTime); } catch (ThreadAbortException) { return; } catch (Exception e) { NetDebug.WriteError("[NM] LogicThread error: " + e); } } stopwatch.Stop(); } [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void ProcessDelayedPackets() { if (!SimulateLatency) return; var time = DateTime.UtcNow; lock (_pingSimulationList) { for (int i = 0; i < _pingSimulationList.Count; i++) { var incomingData = _pingSimulationList[i]; if (incomingData.TimeWhenGet <= time) { HandleMessageReceived(incomingData.Data, incomingData.EndPoint); _pingSimulationList.RemoveAt(i); i--; } } } lock (_outboundSimulationList) { for (int i = 0; i < _outboundSimulationList.Count; i++) { var outboundData = _outboundSimulationList[i]; if (outboundData.TimeWhenSend <= time) { // Send the delayed packet directly to socket layer bypassing simulation SendRawCore(outboundData.Data, outboundData.Start, outboundData.Length, outboundData.EndPoint); _outboundSimulationList.RemoveAt(i); i--; } } } } private void ProcessNtpRequests(float elapsedMilliseconds) { if (_ntpRequests.IsEmpty) { return; } List requestsToRemove = null; foreach (var ntpRequest in _ntpRequests) { ntpRequest.Value.Send(_udpSocketv4, elapsedMilliseconds); if (ntpRequest.Value.NeedToKill) { if (requestsToRemove == null) requestsToRemove = new List(); requestsToRemove.Add(ntpRequest.Key); } } if (requestsToRemove != null) { foreach (var ipEndPoint in requestsToRemove) { _ntpRequests.TryRemove(ipEndPoint, out _); } } } /// /// Update and send logic. Use this only when NetManager started in manual mode /// /// elapsed milliseconds since last update call public void ManualUpdate(float elapsedMilliseconds) { if (!_manualMode) return; for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { if (netPeer.ConnectionState == ConnectionState.Disconnected && netPeer.TimeSinceLastPacket > DisconnectTimeout) { RemovePeer(netPeer, false); } else { netPeer.Update(elapsedMilliseconds); } } ProcessNtpRequests(elapsedMilliseconds); } internal NetPeer OnConnectionSolved(ConnectionRequest request, byte[] rejectData, int start, int length) { NetPeer netPeer = null; if (request.Result == ConnectionRequestResult.RejectForce) { NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject force."); if (rejectData != null && length > 0) { var shutdownPacket = PoolGetWithProperty(PacketProperty.Disconnect, length); shutdownPacket.ConnectionNumber = request.InternalPacket.ConnectionNumber; FastBitConverter.GetBytes(shutdownPacket.RawData, 1, request.InternalPacket.ConnectionTime); if (shutdownPacket.Size >= NetConstants.PossibleMtu[0]) NetDebug.WriteError("[Peer] Disconnect additional data size more than MTU!"); else Buffer.BlockCopy(rejectData, start, shutdownPacket.RawData, 9, length); SendRawAndRecycle(shutdownPacket, request.RemoteEndPoint); } lock (_requestsDict) _requestsDict.Remove(request.RemoteEndPoint); } else lock (_requestsDict) { if (TryGetPeer(request.RemoteEndPoint, out netPeer)) { //already have peer } else if (request.Result == ConnectionRequestResult.Reject) { netPeer = new NetPeer(this, request.RemoteEndPoint, GetNextPeerId()); netPeer.Reject(request.InternalPacket, rejectData, start, length); AddPeer(netPeer); NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject."); } else //Accept { netPeer = new NetPeer(this, request, GetNextPeerId()); AddPeer(netPeer); CreateEvent(NetEvent.EType.Connect, netPeer); NetDebug.Write(NetLogLevel.Trace, $"[NM] Received peer connection Id: {netPeer.ConnectTime}, EP: {netPeer}"); } _requestsDict.Remove(request.RemoteEndPoint); } return netPeer; } private int GetNextPeerId() { return _peerIds.TryDequeue(out int id) ? id : _lastPeerId++; } private void ProcessConnectRequest( IPEndPoint remoteEndPoint, NetPeer netPeer, NetConnectRequestPacket connRequest) { //if we have peer if (netPeer != null) { var processResult = netPeer.ProcessConnectRequest(connRequest); NetDebug.Write($"ConnectRequest LastId: {netPeer.ConnectTime}, NewId: {connRequest.ConnectionTime}, EP: {remoteEndPoint}, Result: {processResult}"); switch (processResult) { case ConnectRequestResult.Reconnection: DisconnectPeerForce(netPeer, DisconnectReason.Reconnect, 0, null); RemovePeer(netPeer, true); //go to new connection break; case ConnectRequestResult.NewConnection: RemovePeer(netPeer, true); //go to new connection break; case ConnectRequestResult.P2PLose: DisconnectPeerForce(netPeer, DisconnectReason.PeerToPeerConnection, 0, null); RemovePeer(netPeer, true); //go to new connection break; default: //no operations needed return; } //ConnectRequestResult.NewConnection //Set next connection number if (processResult != ConnectRequestResult.P2PLose) connRequest.ConnectionNumber = (byte)((netPeer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); //To reconnect peer } else { NetDebug.Write($"ConnectRequest Id: {connRequest.ConnectionTime}, EP: {remoteEndPoint}"); } ConnectionRequest req; lock (_requestsDict) { if (_requestsDict.TryGetValue(remoteEndPoint, out req)) { req.UpdateRequest(connRequest); return; } req = new ConnectionRequest(remoteEndPoint, connRequest, this); _requestsDict.Add(remoteEndPoint, req); } NetDebug.Write($"[NM] Creating request event: {connRequest.ConnectionTime}"); CreateEvent(NetEvent.EType.ConnectionRequest, connectionRequest: req); } private void OnMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) { if (packet.Size == 0) { PoolRecycle(packet); return; } _dropPacket = false; HandleSimulateLatency(packet, remoteEndPoint); HandleSimulatePacketLoss(); if (_dropPacket) { return; } // ProcessEvents HandleMessageReceived(packet, remoteEndPoint); } [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void HandleSimulateLatency(NetPacket packet, IPEndPoint remoteEndPoint) { if (!SimulateLatency) { return; } int roundTripLatency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); int inboundLatency = roundTripLatency / 2; if (inboundLatency > MinLatencyThreshold) { lock (_pingSimulationList) { _pingSimulationList.Add(new IncomingData { Data = packet, EndPoint = remoteEndPoint, TimeWhenGet = DateTime.UtcNow.AddMilliseconds(inboundLatency) }); } // hold packet _dropPacket = true; } } [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void HandleSimulatePacketLoss() { if (SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance) { _dropPacket = true; } } #if DEBUG || SIMULATE_NETWORK private bool HandleSimulateOutboundLatency(byte[] data, int start, int length, IPEndPoint remoteEndPoint) { if (!SimulateLatency) { return false; } int roundTripLatency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); int outboundLatency = roundTripLatency / 2; if (outboundLatency > MinLatencyThreshold) { // Create a copy of the data to avoid issues with recycled packets byte[] dataCopy = new byte[length]; Array.Copy(data, start, dataCopy, 0, length); lock (_outboundSimulationList) { _outboundSimulationList.Add(new OutboundDelayedPacket { Data = dataCopy, Start = 0, Length = length, EndPoint = remoteEndPoint, TimeWhenSend = DateTime.UtcNow.AddMilliseconds(outboundLatency) }); } return true; } return false; } #endif #if DEBUG || SIMULATE_NETWORK private bool HandleSimulateOutboundPacketLoss() { bool shouldDrop = SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance; return shouldDrop; } #endif private void HandleMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) { var originalPacketSize = packet.Size; if (EnableStatistics) { Statistics.IncrementPacketsReceived(); Statistics.AddBytesReceived(originalPacketSize); } if (_ntpRequests.Count > 0 && _ntpRequests.TryGetValue(remoteEndPoint, out var request)) { if (packet.Size < 48) { NetDebug.Write(NetLogLevel.Trace, $"NTP response too short: {packet.Size}"); return; } byte[] copiedData = new byte[packet.Size]; Buffer.BlockCopy(packet.RawData, 0, copiedData, 0, packet.Size); NtpPacket ntpPacket = NtpPacket.FromServerResponse(copiedData, DateTime.UtcNow); try { ntpPacket.ValidateReply(); } catch (InvalidOperationException ex) { NetDebug.Write(NetLogLevel.Trace, $"NTP response error: {ex.Message}"); ntpPacket = null; } if (ntpPacket != null) { _ntpRequests.TryRemove(remoteEndPoint, out _); _ntpEventListener?.OnNtpResponse(ntpPacket); } return; } if (_extraPacketLayer != null) { _extraPacketLayer.ProcessInboundPacket(ref remoteEndPoint, ref packet.RawData, ref packet.Size); if (packet.Size == 0) return; } if (!packet.Verify()) { NetDebug.WriteError("[NM] DataReceived: bad!"); PoolRecycle(packet); return; } switch (packet.Property) { //special case connect request case PacketProperty.ConnectRequest: if (NetConnectRequestPacket.GetProtocolId(packet) != NetConstants.ProtocolId) { SendRawAndRecycle(PoolGetWithProperty(PacketProperty.InvalidProtocol), remoteEndPoint); return; } break; //unconnected messages case PacketProperty.Broadcast: if (!BroadcastReceiveEnabled) return; CreateEvent(NetEvent.EType.Broadcast, remoteEndPoint: remoteEndPoint, readerSource: packet); return; case PacketProperty.UnconnectedMessage: if (!UnconnectedMessagesEnabled) return; CreateEvent(NetEvent.EType.ReceiveUnconnected, remoteEndPoint: remoteEndPoint, readerSource: packet); return; case PacketProperty.NatMessage: if (NatPunchEnabled) NatPunchModule.ProcessMessage(remoteEndPoint, packet); return; } //Check normal packets bool peerFound = remoteEndPoint is NetPeer netPeer || TryGetPeer(remoteEndPoint, out netPeer); if (peerFound && EnableStatistics) { netPeer.Statistics.IncrementPacketsReceived(); netPeer.Statistics.AddBytesReceived(originalPacketSize); } switch (packet.Property) { case PacketProperty.ConnectRequest: var connRequest = NetConnectRequestPacket.FromData(packet); if (connRequest != null) ProcessConnectRequest(remoteEndPoint, netPeer, connRequest); break; case PacketProperty.PeerNotFound: if (peerFound) //local { if (netPeer.ConnectionState != ConnectionState.Connected) return; if (packet.Size == 1) { //first reply //send NetworkChanged packet netPeer.ResetMtu(); SendRaw(NetConnectAcceptPacket.MakeNetworkChanged(netPeer), remoteEndPoint); NetDebug.Write($"PeerNotFound sending connection info: {remoteEndPoint}"); } else if (packet.Size == 2 && packet.RawData[1] == 1) { //second reply DisconnectPeerForce(netPeer, DisconnectReason.PeerNotFound, 0, null); } } else if (packet.Size > 1) //remote { //check if this is old peer bool isOldPeer = false; if (AllowPeerAddressChange) { NetDebug.Write($"[NM] Looks like address change: {packet.Size}"); var remoteData = NetConnectAcceptPacket.FromData(packet); if (remoteData != null && remoteData.PeerNetworkChanged && remoteData.PeerId < _peersArray.Length) { _peersLock.EnterUpgradeableReadLock(); var peer = _peersArray[remoteData.PeerId]; _peersLock.ExitUpgradeableReadLock(); if (peer != null && peer.ConnectTime == remoteData.ConnectionTime && peer.ConnectionNum == remoteData.ConnectionNumber) { if (peer.ConnectionState == ConnectionState.Connected) { peer.InitiateEndPointChange(); CreateEvent(NetEvent.EType.PeerAddressChanged, peer, remoteEndPoint); NetDebug.Write("[NM] PeerNotFound change address of remote peer"); } isOldPeer = true; } } } PoolRecycle(packet); //else peer really not found if (!isOldPeer) { var secondResponse = PoolGetWithProperty(PacketProperty.PeerNotFound, 1); secondResponse.RawData[1] = 1; SendRawAndRecycle(secondResponse, remoteEndPoint); } } break; case PacketProperty.InvalidProtocol: if (peerFound && netPeer.ConnectionState == ConnectionState.Outgoing) DisconnectPeerForce(netPeer, DisconnectReason.InvalidProtocol, 0, null); break; case PacketProperty.Disconnect: if (peerFound) { var disconnectResult = netPeer.ProcessDisconnect(packet); if (disconnectResult == DisconnectResult.None) { PoolRecycle(packet); return; } DisconnectPeerForce( netPeer, disconnectResult == DisconnectResult.Disconnect ? DisconnectReason.RemoteConnectionClose : DisconnectReason.ConnectionRejected, 0, packet); } else { PoolRecycle(packet); } //Send shutdown SendRawAndRecycle(PoolGetWithProperty(PacketProperty.ShutdownOk), remoteEndPoint); break; case PacketProperty.ConnectAccept: if (!peerFound) return; var connAccept = NetConnectAcceptPacket.FromData(packet); if (connAccept != null && netPeer.ProcessConnectAccept(connAccept)) CreateEvent(NetEvent.EType.Connect, netPeer); break; default: if (peerFound) netPeer.ProcessPacket(packet); else SendRawAndRecycle(PoolGetWithProperty(PacketProperty.PeerNotFound), remoteEndPoint); break; } } internal void CreateReceiveEvent(NetPacket packet, DeliveryMethod method, byte channelNumber, int headerSize, NetPeer fromPeer) { NetEvent evt; if (UnsyncedEvents || UnsyncedReceiveEvent || _manualMode) { lock (_eventLock) { evt = _netEventPoolHead; if (evt == null) evt = new NetEvent(this); else _netEventPoolHead = evt.Next; } evt.Next = null; evt.Type = NetEvent.EType.Receive; evt.DataReader.SetSource(packet, headerSize); evt.Peer = fromPeer; evt.DeliveryMethod = method; evt.ChannelNumber = channelNumber; ProcessEvent(evt); } else { lock (_eventLock) { evt = _netEventPoolHead; if (evt == null) evt = new NetEvent(this); else _netEventPoolHead = evt.Next; evt.Next = null; evt.Type = NetEvent.EType.Receive; evt.DataReader.SetSource(packet, headerSize); evt.Peer = fromPeer; evt.DeliveryMethod = method; evt.ChannelNumber = channelNumber; if (_pendingEventTail == null) _pendingEventHead = evt; else _pendingEventTail.Next = evt; _pendingEventTail = evt; } } } /// /// Send data to all connected peers (channel - 0) /// /// DataWriter with data /// Send options (reliable, unreliable, etc.) public void SendToAll(NetDataWriter writer, DeliveryMethod options) { SendToAll(writer.Data, 0, writer.Length, options); } /// /// Send data to all connected peers (channel - 0) /// /// Data /// Send options (reliable, unreliable, etc.) public void SendToAll(byte[] data, DeliveryMethod options) { SendToAll(data, 0, data.Length, options); } /// /// Send data to all connected peers (channel - 0) /// /// Data /// Start of data /// Length of data /// Send options (reliable, unreliable, etc.) public void SendToAll(byte[] data, int start, int length, DeliveryMethod options) { SendToAll(data, start, length, 0, options); } /// /// Send data to all connected peers /// /// DataWriter with data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options) { SendToAll(writer.Data, 0, writer.Length, channelNumber, options); } /// /// Send data to all connected peers /// /// Data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options) { SendToAll(data, 0, data.Length, channelNumber, options); } /// /// Send data to all connected peers /// /// Data /// Start of data /// Length of data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options) { try { _peersLock.EnterReadLock(); for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) netPeer.Send(data, start, length, channelNumber, options); } finally { _peersLock.ExitReadLock(); } } /// /// Send data to all connected peers (channel - 0) /// /// DataWriter with data /// Send options (reliable, unreliable, etc.) /// Excluded peer public void SendToAll(NetDataWriter writer, DeliveryMethod options, NetPeer excludePeer) { SendToAll(writer.Data, 0, writer.Length, 0, options, excludePeer); } /// /// Send data to all connected peers (channel - 0) /// /// Data /// Send options (reliable, unreliable, etc.) /// Excluded peer public void SendToAll(byte[] data, DeliveryMethod options, NetPeer excludePeer) { SendToAll(data, 0, data.Length, 0, options, excludePeer); } /// /// Send data to all connected peers (channel - 0) /// /// Data /// Start of data /// Length of data /// Send options (reliable, unreliable, etc.) /// Excluded peer public void SendToAll(byte[] data, int start, int length, DeliveryMethod options, NetPeer excludePeer) { SendToAll(data, start, length, 0, options, excludePeer); } /// /// Send data to all connected peers /// /// DataWriter with data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) /// Excluded peer public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) { SendToAll(writer.Data, 0, writer.Length, channelNumber, options, excludePeer); } /// /// Send data to all connected peers /// /// Data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) /// Excluded peer public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) { SendToAll(data, 0, data.Length, channelNumber, options, excludePeer); } /// /// Send data to all connected peers /// /// Data /// Start of data /// Length of data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) /// Excluded peer public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) { try { _peersLock.EnterReadLock(); for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { if (netPeer != excludePeer) netPeer.Send(data, start, length, channelNumber, options); } } finally { _peersLock.ExitReadLock(); } } #if LITENETLIB_SPANS || NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1 || NETCOREAPP3_1 || NET5_0 || NETSTANDARD2_1 /// /// Send data to all connected peers (channel - 0) /// /// Data /// Send options (reliable, unreliable, etc.) public void SendToAll(ReadOnlySpan data, DeliveryMethod options) { SendToAll(data, 0, options, null); } /// /// Send data to all connected peers (channel - 0) /// /// Data /// Send options (reliable, unreliable, etc.) /// Excluded peer public void SendToAll(ReadOnlySpan data, DeliveryMethod options, NetPeer excludePeer) { SendToAll(data, 0, options, excludePeer); } /// /// Send data to all connected peers /// /// Data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) /// Excluded peer public void SendToAll(ReadOnlySpan data, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) { try { _peersLock.EnterReadLock(); for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { if (netPeer != excludePeer) netPeer.Send(data, channelNumber, options); } } finally { _peersLock.ExitReadLock(); } } /// /// Send message without connection /// /// Raw data /// Packet destination /// Operation result public bool SendUnconnectedMessage(ReadOnlySpan message, IPEndPoint remoteEndPoint) { int headerSize = NetPacket.GetHeaderSize(PacketProperty.UnconnectedMessage); var packet = PoolGetPacket(message.Length + headerSize); packet.Property = PacketProperty.UnconnectedMessage; message.CopyTo(new Span(packet.RawData, headerSize, message.Length)); return SendRawAndRecycle(packet, remoteEndPoint) > 0; } #endif /// /// Start logic thread and listening on available port /// public bool Start() { return Start(0); } /// /// Start logic thread and listening on selected port /// /// bind to specific ipv4 address /// bind to specific ipv6 address /// port to listen public bool Start(IPAddress addressIPv4, IPAddress addressIPv6, int port) { return Start(addressIPv4, addressIPv6, port, false); } /// /// Start logic thread and listening on selected port /// /// bind to specific ipv4 address /// bind to specific ipv6 address /// port to listen public bool Start(string addressIPv4, string addressIPv6, int port) { IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); return Start(ipv4, ipv6, port); } /// /// Start logic thread and listening on selected port /// /// port to listen public bool Start(int port) { return Start(IPAddress.Any, IPAddress.IPv6Any, port); } /// /// Start in manual mode and listening on selected port /// In this mode you should use ManualReceive (without PollEvents) for receive packets /// and ManualUpdate(...) for update and send packets /// This mode useful mostly for single-threaded servers /// /// bind to specific ipv4 address /// bind to specific ipv6 address /// port to listen public bool StartInManualMode(IPAddress addressIPv4, IPAddress addressIPv6, int port) { return Start(addressIPv4, addressIPv6, port, true); } /// /// Start in manual mode and listening on selected port /// In this mode you should use ManualReceive (without PollEvents) for receive packets /// and ManualUpdate(...) for update and send packets /// This mode useful mostly for single-threaded servers /// /// bind to specific ipv4 address /// bind to specific ipv6 address /// port to listen public bool StartInManualMode(string addressIPv4, string addressIPv6, int port) { IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); return StartInManualMode(ipv4, ipv6, port); } /// /// Start in manual mode and listening on selected port /// In this mode you should use ManualReceive (without PollEvents) for receive packets /// and ManualUpdate(...) for update and send packets /// This mode useful mostly for single-threaded servers /// /// port to listen public bool StartInManualMode(int port) { return StartInManualMode(IPAddress.Any, IPAddress.IPv6Any, port); } /// /// Send message without connection /// /// Raw data /// Packet destination /// Operation result public bool SendUnconnectedMessage(byte[] message, IPEndPoint remoteEndPoint) { return SendUnconnectedMessage(message, 0, message.Length, remoteEndPoint); } /// /// Send message without connection. WARNING This method allocates a new IPEndPoint object and /// synchronously makes a DNS request. If you're calling this method every frame it will be /// much faster to just cache the IPEndPoint. /// /// Data serializer /// Packet destination IP or hostname /// Packet destination port /// Operation result public bool SendUnconnectedMessage(NetDataWriter writer, string address, int port) { IPEndPoint remoteEndPoint = NetUtils.MakeEndPoint(address, port); return SendUnconnectedMessage(writer.Data, 0, writer.Length, remoteEndPoint); } /// /// Send message without connection /// /// Data serializer /// Packet destination /// Operation result public bool SendUnconnectedMessage(NetDataWriter writer, IPEndPoint remoteEndPoint) { return SendUnconnectedMessage(writer.Data, 0, writer.Length, remoteEndPoint); } /// /// Send message without connection /// /// Raw data /// data start /// data length /// Packet destination /// Operation result public bool SendUnconnectedMessage(byte[] message, int start, int length, IPEndPoint remoteEndPoint) { //No need for CRC here, SendRaw does that NetPacket packet = PoolGetWithData(PacketProperty.UnconnectedMessage, message, start, length); return SendRawAndRecycle(packet, remoteEndPoint) > 0; } /// /// Triggers update and send logic immediately (works asynchronously) /// public void TriggerUpdate() { _updateTriggerEvent.Set(); } /// /// Receive "maxProcessedEvents" pending events. Call this in game update code /// In Manual mode it will call also socket Receive (which can be slow) /// 0 - receive all events /// /// Max events that will be processed (called INetEventListener Connect/Receive/Etc), 0 - receive all events public void PollEvents(int maxProcessedEvents = 0) { if (_manualMode) { if (_udpSocketv4 != null) ManualReceive(_udpSocketv4, _bufferEndPointv4, maxProcessedEvents); if (_udpSocketv6 != null && _udpSocketv6 != _udpSocketv4) ManualReceive(_udpSocketv6, _bufferEndPointv6, maxProcessedEvents); ProcessDelayedPackets(); return; } if (UnsyncedEvents) return; NetEvent pendingEvent; lock (_eventLock) { pendingEvent = _pendingEventHead; _pendingEventHead = null; _pendingEventTail = null; } int counter = 0; while (pendingEvent != null) { var next = pendingEvent.Next; ProcessEvent(pendingEvent); pendingEvent = next; counter++; if (counter == maxProcessedEvents) break; } } /// /// Connect to remote host /// /// Server IP or hostname /// Server Port /// Connection key /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting /// Manager is not running. Call public NetPeer Connect(string address, int port, string key) { return Connect(address, port, NetDataWriter.FromString(key)); } /// /// Connect to remote host /// /// Server IP or hostname /// Server Port /// Additional data for remote peer /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting /// Manager is not running. Call public NetPeer Connect(string address, int port, NetDataWriter connectionData) { IPEndPoint ep; try { ep = NetUtils.MakeEndPoint(address, port); } catch { CreateEvent(NetEvent.EType.Disconnect, disconnectReason: DisconnectReason.UnknownHost); return null; } return Connect(ep, connectionData); } /// /// Connect to remote host /// /// Server end point (ip and port) /// Connection key /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting /// Manager is not running. Call public NetPeer Connect(IPEndPoint target, string key) { return Connect(target, NetDataWriter.FromString(key)); } /// /// Connect to remote host /// /// Server end point (ip and port) /// Additional data for remote peer /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting /// Manager is not running. Call public NetPeer Connect(IPEndPoint target, NetDataWriter connectionData) { if (!_isRunning) throw new InvalidOperationException("Client is not running"); lock (_requestsDict) { if (_requestsDict.ContainsKey(target)) return null; byte connectionNumber = 0; if (TryGetPeer(target, out var peer)) { switch (peer.ConnectionState) { //just return already connected peer case ConnectionState.Connected: case ConnectionState.Outgoing: return peer; } //else reconnect connectionNumber = (byte)((peer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); RemovePeer(peer, true); } //Create reliable connection //And send connection request peer = new NetPeer(this, target, GetNextPeerId(), connectionNumber, connectionData); AddPeer(peer); return peer; } } #if LITENETLIB_SPANS || NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1 || NETCOREAPP3_1 || NET5_0 || NETSTANDARD2_1 /// /// Connect to remote host /// /// Server end point (ip and port) /// Additional data for remote peer /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting /// Manager is not running. Call public NetPeer Connect(IPEndPoint target, ReadOnlySpan connectionData) { if (!_isRunning) throw new InvalidOperationException("Client is not running"); lock (_requestsDict) { if (_requestsDict.ContainsKey(target)) return null; byte connectionNumber = 0; if (TryGetPeer(target, out var peer)) { switch (peer.ConnectionState) { //just return already connected peer case ConnectionState.Connected: case ConnectionState.Outgoing: return peer; } //else reconnect connectionNumber = (byte)((peer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); RemovePeer(peer, true); } //Create reliable connection //And send connection request peer = new NetPeer(this, target, GetNextPeerId(), connectionNumber, connectionData); AddPeer(peer); return peer; } } #endif /// /// Force closes connection and stop all threads. /// public void Stop() { Stop(true); } /// /// Force closes connection and stop all threads. /// /// Send disconnect messages public void Stop(bool sendDisconnectMessages) { if (!_isRunning) return; NetDebug.Write("[NM] Stop"); //Send last disconnect for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) netPeer.Shutdown(null, 0, 0, !sendDisconnectMessages); //Stop CloseSocket(); #if UNITY_SOCKET_FIX if (_useSocketFix) { _pausedSocketFix.Deinitialize(); _pausedSocketFix = null; } #endif _updateTriggerEvent.Set(); if (!_manualMode) { _logicThread.Join(); _logicThread = null; } //clear peers ClearPeerSet(); _peerIds = new ConcurrentQueue(); _lastPeerId = 0; ClearPingSimulationList(); ClearOutboundSimulationList(); _connectedPeersCount = 0; _pendingEventHead = null; _pendingEventTail = null; } [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void ClearPingSimulationList() { lock (_pingSimulationList) _pingSimulationList.Clear(); } [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void ClearOutboundSimulationList() { lock (_outboundSimulationList) _outboundSimulationList.Clear(); } /// /// Return peers count with connection state /// /// peer connection state (you can use as bit flags) /// peers count public int GetPeersCount(ConnectionState peerState) { int count = 0; _peersLock.EnterReadLock(); for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { if ((netPeer.ConnectionState & peerState) != 0) count++; } _peersLock.ExitReadLock(); return count; } /// /// Get copy of peers (without allocations) /// /// List that will contain result /// State of peers public void GetPeersNonAlloc(List peers, ConnectionState peerState) { peers.Clear(); _peersLock.EnterReadLock(); for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { if ((netPeer.ConnectionState & peerState) != 0) peers.Add(netPeer); } _peersLock.ExitReadLock(); } /// /// Disconnect all peers without any additional data /// public void DisconnectAll() { DisconnectAll(null, 0, 0); } /// /// Disconnect all peers with shutdown message /// /// Data to send (must be less or equal MTU) /// Data start /// Data count public void DisconnectAll(byte[] data, int start, int count) { //Send disconnect packets _peersLock.EnterReadLock(); for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { DisconnectPeer( netPeer, DisconnectReason.DisconnectPeerCalled, 0, false, data, start, count, null); } _peersLock.ExitReadLock(); } /// /// Immediately disconnect peer from server without additional data /// /// peer to disconnect public void DisconnectPeerForce(NetPeer peer) { DisconnectPeerForce(peer, DisconnectReason.DisconnectPeerCalled, 0, null); } /// /// Disconnect peer from server /// /// peer to disconnect public void DisconnectPeer(NetPeer peer) { DisconnectPeer(peer, null, 0, 0); } /// /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) /// /// peer to disconnect /// additional data public void DisconnectPeer(NetPeer peer, byte[] data) { DisconnectPeer(peer, data, 0, data.Length); } /// /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) /// /// peer to disconnect /// additional data public void DisconnectPeer(NetPeer peer, NetDataWriter writer) { DisconnectPeer(peer, writer.Data, 0, writer.Length); } /// /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) /// /// peer to disconnect /// additional data /// data start /// data length public void DisconnectPeer(NetPeer peer, byte[] data, int start, int count) { DisconnectPeer( peer, DisconnectReason.DisconnectPeerCalled, 0, false, data, start, count, null); } /// /// Create the requests for NTP server /// /// NTP Server address. public void CreateNtpRequest(IPEndPoint endPoint) { _ntpRequests.TryAdd(endPoint, new NtpRequest(endPoint)); } /// /// Create the requests for NTP server /// /// NTP Server address. /// port public void CreateNtpRequest(string ntpServerAddress, int port) { IPEndPoint endPoint = NetUtils.MakeEndPoint(ntpServerAddress, port); _ntpRequests.TryAdd(endPoint, new NtpRequest(endPoint)); } /// /// Create the requests for NTP server (default port) /// /// NTP Server address. public void CreateNtpRequest(string ntpServerAddress) { IPEndPoint endPoint = NetUtils.MakeEndPoint(ntpServerAddress, NtpRequest.DefaultPort); _ntpRequests.TryAdd(endPoint, new NtpRequest(endPoint)); } public NetPeerEnumerator GetEnumerator() { return new NetPeerEnumerator(_headPeer); } IEnumerator IEnumerable.GetEnumerator() { return new NetPeerEnumerator(_headPeer); } IEnumerator IEnumerable.GetEnumerator() { return new NetPeerEnumerator(_headPeer); } } }