/// <summary> /// Send QoS requests to the given endpoint. /// </summary> /// <param name="remoteEndpoint">Server that QoS requests should be sent to</param> /// <param name="expireTime">When to stop trying to send requests</param> /// <param name="result">Results from the send side of the check (packets sent)</param> /// <returns> /// errorcode - the last error code generated (if any). 0 indicates no error. /// </returns> private int SendQosRequests(NetworkEndPoint remoteEndpoint, ushort identifier, DateTime expireTime, ref QosResult result) { QosRequest request = new QosRequest { Title = m_TitleBytesUtf8.ToArray(), Identifier = identifier }; #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.Log($"Identifier for this QoS check is: 0x{request.Identifier:X4}"); #endif // Send all requests. result.RequestsSent = 0; while (result.RequestsSent < RequestsPerEndpoint && !QosHelper.ExpiredUtc(expireTime)) { request.Timestamp = (ulong)(DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond); request.Sequence = (byte)result.RequestsSent; int errorcode = 0; int sent = 0; (sent, errorcode) = request.Send(m_Socket, ref remoteEndpoint, expireTime); if (errorcode != 0) { Debug.LogError($"Send returned error code {errorcode}, can't continue"); return(errorcode); } else if (sent != request.Length) { Debug.LogWarning($"Sent {sent} of {request.Length} bytes, ignoring this request"); ++result.InvalidRequests; } else { ++result.RequestsSent; } } return(0); }
public void Execute() { if (m_QosServers.Length == 0) { return; // Nothing to do. } m_JobExpireTimeUtc = DateTime.UtcNow.AddMilliseconds(TimeoutMs); // Create the local socket int errorcode = 0; (m_Socket, errorcode) = CreateAndBindSocket(); if (m_Socket == -1 || errorcode != 0) { // Can't run the job Debug.LogError("Failed to create and bind the local socket for QoS Check"); } else { m_Identifier = (ushort)new Random().Next(ushort.MinValue, ushort.MaxValue); for (int i = 0; i < m_QosServers.Length; ++i) { QosResult result = QosResults[i]; InternalQosServer server = m_QosServers[i]; if (QosHelper.ExpiredUtc(m_JobExpireTimeUtc)) { Debug.LogWarning($"Ran out of time to finish remaining QoS Check for endpoint {i}."); break; } // If we've already visited this server, just copy those results here. if (QosServerVisited(server.Id)) { if (TryCopyResult(server.Id, ref result) == false) { Debug.LogError($"Visited server must have a previous result available"); break; } } else if (DateTime.UtcNow > server.BackoffUntilUtc) // Only contact this server if we are allowed { // For each iteration of the loop, give the remaining endpoints an equal fraction of the remaining // overall job time. For example if there are five endpoints that each get 1000ms (5000ms total), // and the first endpoint finishes in 200ms, the remaining endpoints will get 1200ms to complete // (4800ms remaining / 4 endpoints = 1200ms/endpoint). double allottedTimeMs = QosHelper.RemainingUtc(m_JobExpireTimeUtc).TotalMilliseconds / (m_QosServers.Length - i); DateTime startTimeUtc = DateTime.UtcNow; DateTime expireTimeUtc = DateTime.UtcNow.AddMilliseconds(allottedTimeMs); #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.Log($"QoS Check {i} gets {(expireTimeUtc - DateTime.UtcNow).TotalMilliseconds:F0}ms to complete."); #endif ++m_Identifier; int err = SendQosRequests(server.RemoteEndpoint, m_Identifier, expireTimeUtc, ref result); if (err != 0) { Debug.LogError($"Error {err} sending QoS requests. Will attempt to receive responses anyway."); } err = RecvQosResponses(server.RemoteEndpoint, m_Identifier, expireTimeUtc, ref result); if (err != 0) { Debug.LogError($"Error {err} receiving QoS responses. Will attempt to continue anyway."); } Debug.Log($"Received {result.ResponsesReceived}/{result.RequestsSent} responses from endpoint {i} in {(DateTime.UtcNow - startTimeUtc).TotalMilliseconds:F0}ms"); // Mark this server as visited SetQosServerVisited(server.Id); } else { Debug.LogWarning($"Did not contact endpoint {i} due to backoff restrictions."); } // Save the result (even if we didn't contact the server) QosResults[i] = result; } } NativeBindings.network_close(ref m_Socket, ref errorcode); }
/// <summary> /// Receive QoS responses from the given endpoint /// </summary> /// <param name="remoteEndpoint">Where to expect responses to come from</param> /// <param name="identifier">Identifier that will accompany a valid response</param> /// <param name="expireTimeUtc">How long to wait for responses</param> /// <param name="result">Results from the receive side of the check (packets received, latency, packet loss)</param> /// <returns> /// errorcode - the last error code (if any). 0 means no error. /// </returns> private int RecvQosResponses(NetworkEndPoint remoteEndpoint, ushort identifier, DateTime expireTimeUtc, ref QosResult result) { if (result.RequestsSent == 0) { return(0); // Not expecting any responses } NativeArray <int> responseLatency = new NativeArray <int>((int)result.RequestsSent, Allocator.Temp); for (int i = 0; i < responseLatency.Length; ++i) { responseLatency[i] = -1; } QosResponse response = new QosResponse(); int errorcode = 0; while (result.ResponsesReceived < result.RequestsSent && !QosHelper.ExpiredUtc(expireTimeUtc)) { errorcode = 0; int received = 0; (received, errorcode) = response.Recv(m_Socket, remoteEndpoint, expireTimeUtc); if (received == -1) { Debug.LogError($"Invalid or no response received (errorcode = {errorcode})"); } else if (!response.Verify()) { Debug.LogWarning("Ignoring invalid QosResponse"); ++result.InvalidResponses; } else if (response.Identifier != identifier) { Debug.LogWarning($"Identifier 0x{response.Identifier:X4} != expected identifier 0x{identifier:X4}; ignoring..."); ++result.InvalidResponses; } else { if (response.Sequence >= result.RequestsSent) // Sequence can't be more than number of requests sent { Debug.LogWarning($"Ignoring response with sequence {response.Sequence} that is higher than max sequence expected"); ++result.InvalidResponses; } else if (responseLatency[response.Sequence] == -1) { responseLatency[response.Sequence] = response.LatencyMs; ++result.ResponsesReceived; } else { Debug.Log($"Duplicate response {response.Sequence} received for QosCheck identifier 0x{response.Identifier:X4}"); ++result.DuplicateResponses; } // Determine if we've had flow control applied to us. If so, save the most significant result based // on the unit count. In this version, both Ban and Throttle have the same result: client back-off. var fc = response.ParseFlowControl(); if (fc.type != FcType.None && fc.units > result.FcUnits) { result.FcType = fc.type; result.FcUnits = fc.units; } } } // Calculate average latency and log results result.AverageLatencyMs = 0; if (result.ResponsesReceived > 0) { uint validResponses = 0; for (int i = 0, length = responseLatency.Length; i < length; i++) { var latency = responseLatency[i]; if (latency >= 0) { result.AverageLatencyMs += (uint)latency; validResponses++; #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.Log($"Received response {i} for QosCheck identifier 0x{identifier:X4} with latency {latency}ms"); #endif } #if UNITY_EDITOR || DEVELOPMENT_BUILD else { Debug.LogWarning($"Failed to receive response {i} for QosCheck identifier 0x{identifier:X4}"); } #endif } result.AverageLatencyMs /= validResponses; } result.UpdatePacketLoss(); responseLatency.Dispose(); return(errorcode); }
/// <summary> /// Receive a QosResponse if one is available /// </summary> /// <param name="socket">Native socket descriptor</param> /// <param name="endpoint">Remote endpoint from which to receive the response</param> /// <param name="expireTimeUtc">When to stop waiting for a response</param> /// <returns> /// (received, errorcode) where received is the number of bytes received and errorcode is the error code if any. /// 0 means no error. /// </returns> public (int received, int errorcode) Recv(long socket, NetworkEndPoint endpoint, DateTime expireTimeUtc) { m_PacketLength = 0; int errorcode = 0; int received = -1; NetworkEndPoint remote = NetworkEndPoint.AnyIpv4; unsafe { fixed(void *pMagic = &m_Magic, pVerAndFlow = &m_VerAndFlow, pSequence = &m_Sequence, pIdentifier = &m_Identifier, pTimestamp = &m_Timestamp) { var iov = stackalloc network_iovec[5]; iov[0].buf = pMagic; iov[0].len = sizeof(byte); iov[1].buf = pVerAndFlow; iov[1].len = sizeof(byte); iov[2].buf = pSequence; iov[2].len = sizeof(byte); iov[3].buf = pIdentifier; iov[3].len = sizeof(ushort); // Everything below here is user-specified data and not part of the QosResponse header iov[4].buf = pTimestamp; iov[4].len = sizeof(ulong); // TODO: May need to introduce artificial latency here to prevent spinning on WouldBlock() while (!QosHelper.ExpiredUtc(expireTimeUtc)) { errorcode = 0; received = NativeBindings.network_recvmsg(socket, iov, 5, ref remote, ref errorcode); // If we'd block, retry. If we got a response from the wrong endpoint, ignore it and retry. // N.B.: Connecting to loopback at nonstandard (but technically correct) addresses like // 127.0.0.2 will return a remote address of 127.0.0.1, which will cause a mismatch. // Should special-case those, but there is currently no way to get the address information // out of a NetworkEndPoint, so we can't address it. if (received == -1 && QosHelper.WouldBlock(errorcode)) { continue; } if (received != -1 && remote != endpoint) { continue; } break; // Got a response, or a non-retryable error } if (received == -1) { Debug.LogError($"network_recvmsg returned {received} with error code {errorcode}"); return(received, errorcode); } m_PacketLength = (ushort)received; } } m_LatencyMs = (Length >= MinPacketLen) ? (int)((ulong)(DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond) - m_Timestamp) : -1; return(received, errorcode); }