Я делаю XNA-игру, основанную на клиентском сервере, и у меня есть некоторые трудности с интеграцией механизма Message Framing
в сетевую часть.TcpClient обновляет один раз, а затем застрял в прослушивании
Это сообщение класс протокола кадрирования я получил от here, с небольшим рефакторинга:
using System;
namespace XnaCommonLib.Network
{
// Original source: http://blog.stephencleary.com/2009/04/sample-code-length-prefix-message.html
/// <summary>
/// Maintains the necessary buffers for applying a length-prefix message framing protocol over a stream.
/// </summary>
/// <remarks>
/// <para>Create one instance of this class for each incoming stream, and assign a handler to <see cref="MessageArrived"/>. As bytes arrive at the stream, pass them to <see cref="DataReceived"/>, which will invoke <see cref="MessageArrived"/> as necessary.</para>
/// <para>If <see cref="DataReceived"/> raises <see cref="System.Net.ProtocolViolationException"/>, then the stream data should be considered invalid. After that point, no methods should be called on that <see cref="PacketProtocol"/> instance.</para>
/// <para>This class uses a 4-byte signed integer length prefix, which allows for message sizes up to 2 GB. Keepalive messages are supported as messages with a length prefix of 0 and no message data.</para>
/// <para>This is EXAMPLE CODE! It is not particularly efficient; in particular, if this class is rewritten so that a particular interface is used (e.g., Socket's IAsyncResult methods), some buffer copies become unnecessary and may be removed.</para>
/// </remarks>
public class PacketProtocol
{
private const int LengthBufferSize = sizeof(int);
/// <summary>
/// Wraps a message. The wrapped message is ready to send to a stream.
/// </summary>
/// <remarks>
/// <para>Generates a length prefix for the message and returns the combined length prefix and message.</para>
/// </remarks>
/// <param name="message">The message to send.</param>
public static byte[] WrapMessage(byte[] message)
{
// Get the length prefix for the message
var lengthPrefix = BitConverter.GetBytes(message.Length);
// Concatenate the length prefix and the message
var ret = new byte[lengthPrefix.Length + message.Length];
lengthPrefix.CopyTo(ret, 0);
message.CopyTo(ret, lengthPrefix.Length);
return ret;
}
/// <summary>
/// Wraps a keepalive (0-length) message. The wrapped message is ready to send to a stream.
/// </summary>
public static byte[] WrapKeepaliveMessage()
{
return BitConverter.GetBytes(0);
}
/// <summary>
/// Initializes a new <see cref="PacketProtocol"/>, limiting message sizes to the given maximum size.
/// </summary>
/// <param name="maxMessageBufferSize">The maximum message size supported by this protocol. This may be less than or equal to zero to indicate no maximum message size.</param>
public PacketProtocol(int maxMessageBufferSize)
{
// We allocate the buffer for receiving message lengths immediately
lengthBuffer = new byte[LengthBufferSize];
maxMessageSize = maxMessageBufferSize;
}
/// <summary>
/// The buffer for the length prefix; this is always 4 bytes long.
/// </summary>
private readonly byte[] lengthBuffer;
/// <summary>
/// The buffer for the data; this is null if we are receiving the length prefix buffer.
/// </summary>
private byte[] dataBuffer;
/// <summary>
/// The number of bytes already read into the buffer (the length buffer if <see cref="dataBuffer"/> is null, otherwise the data buffer).
/// </summary>
private int bytesReceived;
/// <summary>
/// The maximum size of messages allowed.
/// </summary>
private readonly int maxMessageSize;
/// <summary>
/// Indicates the completion of a message read from the stream.
/// </summary>
/// <remarks>
/// <para>This may be called with an empty message, indicating that the other end had sent a keepalive message. This will never be called with a null message.</para>
/// <para>This event is invoked from within a call to <see cref="DataReceived"/>. Handlers for this event should not call <see cref="DataReceived"/>.</para>
/// </remarks>
public Action<byte[]> MessageArrived
{
get; set;
}
/// <summary>
/// Notifies the <see cref="PacketProtocol"/> instance that incoming data has been received from the stream. This method will invoke <see cref="MessageArrived"/> as necessary.
/// </summary>
/// <remarks>
/// <para>This method may invoke <see cref="MessageArrived"/> zero or more times.</para>
/// <para>Zero-length receives are ignored. Many streams use a 0-length read to indicate the end of a stream, but <see cref="PacketProtocol"/> takes no action in this case.</para>
/// </remarks>
/// <param name="data">The data received from the stream. Cannot be null.</param>
/// <exception cref="System.Net.ProtocolViolationException">If the data received is not a properly-formed message.</exception>
public void DataReceived(byte[] data)
{
// Process the incoming data in chunks, as the ReadCompleted requests it
// Logically, we are satisfying read requests with the received data, instead of processing the
// incoming buffer looking for messages.
var i = 0;
while (i != data.Length)
{
// Determine how many bytes we want to transfer to the buffer and transfer them
var bytesAvailable = data.Length - i;
if (dataBuffer != null)
{
// We're reading into the data buffer
var bytesRequested = dataBuffer.Length - bytesReceived;
// Copy the incoming bytes into the buffer
var bytesTransferred = Math.Min(bytesRequested, bytesAvailable);
Array.Copy(data, i, dataBuffer, bytesReceived, bytesTransferred);
i += bytesTransferred;
// Notify "read completion"
ReadCompleted(bytesTransferred);
}
else
{
// We're reading into the length prefix buffer
var bytesRequested = lengthBuffer.Length - bytesReceived;
// Copy the incoming bytes into the buffer
var bytesTransferred = Math.Min(bytesRequested, bytesAvailable);
Array.Copy(data, i, lengthBuffer, bytesReceived, bytesTransferred);
i += bytesTransferred;
// Notify "read completion"
ReadCompleted(bytesTransferred);
}
}
}
/// <summary>
/// Called when a read completes. Parses the received data and calls <see cref="MessageArrived"/> if necessary.
/// </summary>
/// <param name="count">The number of bytes read.</param>
/// <exception cref="System.Net.ProtocolViolationException">If the data received is not a properly-formed message.</exception>
private void ReadCompleted(int count)
{
// Get the number of bytes read into the buffer
bytesReceived += count;
if (dataBuffer == null)
{
// We're currently receiving the length buffer
if (bytesReceived != LengthBufferSize)
{
// We haven't gotten all the length buffer yet: just wait for more data to arrive
}
else
{
// We've gotten the length buffer
var length = BitConverter.ToInt32(lengthBuffer, 0);
// Sanity check for length < 0
if (length < 0)
throw new System.Net.ProtocolViolationException("Message length is less than zero");
// Another sanity check is needed here for very large packets, to prevent denial-of-service attacks
if (maxMessageSize > 0 && length > maxMessageSize)
throw new System.Net.ProtocolViolationException("Message length " + length.ToString(System.Globalization.CultureInfo.InvariantCulture) + " is larger than maximum message size " + maxMessageSize.ToString(System.Globalization.CultureInfo.InvariantCulture));
// Zero-length packets are allowed as keepalives
if (length == 0)
{
bytesReceived = 0;
MessageArrived?.Invoke(new byte[0]);
}
else
{
// Create the data buffer and start reading into it
dataBuffer = new byte[length];
bytesReceived = 0;
}
}
}
else
{
if (bytesReceived != dataBuffer.Length)
// We haven't gotten all the data buffer yet: just wait for more data to arrive
return;
// We've gotten an entire packet
MessageArrived?.Invoke(dataBuffer);
// Start reading the length buffer again
dataBuffer = null;
bytesReceived = 0;
}
}
}
}
Я отлажена код, и он, кажется, функционирует должным образом. Проблема в том, как я использую этот код. Из того, что я понимаю, мне нужно звонить PacketProtocol::DataReceived
каждый раз, когда я получаю данные. Но с моей точки зрения, поскольку я использую TCP, было довольно сложно понять, что именно считается Data Received
, так как TCP
использует Stream
, тогда как UDP
, например, использует дейтаграммы, поэтому определение DataReceived
для меня довольно сложно для определения для TCP
.
Я попытался использовать следующий вспомогательный метод, чтобы заставить его работать:
using System.IO;
using System.Net.Sockets;
namespace XnaCommonLib.Network
{
public static class HelperMethods
{
public static void Receive(TcpClient connection, BinaryReader reader, PacketProtocol packetProtocol)
{
var buffer = new byte[connection.ReceiveBufferSize];
while (reader.Read(buffer, 0, buffer.Length) > 0) // this is where it gets stuck
{
packetProtocol.DataReceived(buffer);
buffer = new byte[connection.ReceiveBufferSize];
}
}
}
}
Использование этого метода заключается в следующем:
ConnectionHandler - класс на стороне клиента сетевого управления
private void ConnectionHandler_InteractWithServer()
{
while (Connection.Connected)
{
try
{
HelperMethods.Receive(Connection, Reader, PacketProtocol);
}
catch (Exception)
{
Connection.Close();
break;
}
Thread.Sleep(Constants.Time.UpdateThreadSleepTime);
}
}
Этот метод вызывается в потоке, поэтому он работает постоянно. Клиентская обратного вызова PacketProtocol это:
private void PacketProtocol_MessageRecievedCallback(byte[] data)
{
if (data.Length == 0)
return;
var stringData = Encoding.UTF8.GetString(data);
ProcessServerUpdate(stringData);
WritePlayerData();
}
private void ProcessServerUpdate(string message)
{
UpdatePing();
var incomingUpdate = JsonConvert.DeserializeObject<ServerToClientUpdateMessage>(message);
EmsServerEndpoint.BroadcastIncomingEvents(incomingUpdate.Broadcasts);
foreach (var update in incomingUpdate.PlayerUpdates)
ApplyUpdate(update);
}
private void ApplyUpdate(PlayerUpdate update)
{
var entity = new Entity(update.Guid);
if (!ClientGameManager.EntityPool.Exists(entity))
{
var newGo = ClientGameManager.BeginAllocateRemote(entity.Id);
newGo.Components.Get<NetworkPlayer>().Update(update);
ClientGameManager.EndAllocate(newGo);
}
else
{
var remoteComponents = ClientGameManager.EntityPool.GetComponents(entity);
remoteComponents.Get<NetworkPlayer>().Update(update);
}
}
private void WritePlayerData()
{
var message = new ClientToServerUpdateMessage
{
Broadcasts = EmsServerEndpoint.Flush(),
PlayerUpdate = new PlayerUpdate(GameObject.Components)
};
var messageBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message));
var wrapperMessage = PacketProtocol.WrapMessage(messageBytes);
Writer.Write(wrapperMessage);
}
Это метод обратного вызова рядом методов, называемых им.
Это обратный вызов сервера для PacketProtocol:
private void PacketProtocol_MessageArrivedCallback(byte[] bytes)
{
if (bytes.Length == 0)
return;
var stringData = Encoding.UTF8.GetString(bytes);
ProcessClientUpdate(stringData);
SendServerUpdate();
}
private void SendServerUpdate()
{
var message = JsonConvert.SerializeObject(new ServerToClientUpdateMessage
{
Broadcasts = EmsServerEndpoint.Flush(),
PlayerUpdates = PlayerUpdates()
});
var messageBytes = Encoding.UTF8.GetBytes(message);
Writer.Write(PacketProtocol.WrapMessage(messageBytes));
}
private void ProcessClientUpdate(string clientMessageString)
{
var clientMessage = JsonConvert.DeserializeObject<ClientToServerUpdateMessage>(clientMessageString);
EmsServerEndpoint.BroadcastIncomingEvents(clientMessage.Broadcasts);
UpdateClient(clientMessage.PlayerUpdate);
}
private IList<PlayerUpdate> PlayerUpdates()
{
return GameManager.EntityPool.AllThat(PlayerUpdate.IsPlayer).Select(c => new PlayerUpdate(c)).ToList();
}
private void UpdateClient(PlayerUpdate playerUpdate)
{
var components = GameObject.Components;
components.Get<DirectionalInput>().Update(playerUpdate.Input);
}
Так что теперь для актуальной задачи: Код работает нормально для первого обновления - сообщение получено правильно и обновление происходит. Принимая во внимание, что и клиент, и сервер застревают в строке while (reader.Read(buffer, 0, buffer.Length) > 0)
.
Для начала фактического общения, это то, что ConnectionHandler
делает:
WriteLoginDataToServer(name, team);
ReadLoginResponseFromServer();
WritePlayerData();
UpdateThread.Start();
Перед началом резьбы обновления (который запускает цикл связи с сервером), он называет WritePlayerData
, переводящих первоначальное обновление до сервер. Я бы предположил, что это заставит его позвонить SendServerUpdate
, и клиент подберет его, и цикл продолжится, но этого не произойдет.
Линия, в которой он находится, застрял, это метод блокировки, если данных недостаточно для чтения (buffer.length), он не вернется, но это может и не быть проблемой. Кроме того, для меня ваш код кажется litle messy: 'Something'->' ConnectionHandler_InteractWithServer() '->' HelperMethods'-> 'PacketProtocol'. Слишком большая глубина только для получения данных. – null
'PacketProtocol' принимает обратный вызов для вызова, когда он получает сообщение, поэтому я даю ему метод обратного вызова, который преобразует полученный' byte [] 'в строку и десериализует строку для объекта (я работаю с jsons). 'ConnectionHandler_InteractWithServer()' - это метод потока, то есть его вызываемый один раз и выполняется в цикле до тех пор, пока не произойдет отключение. Я переместил метод «Получить» во внешний класс утилиты, потому что его логика распространена как для клиента, так и для сервера. Я думаю, что это вряд ли сложен и глубже. –
Ну, может быть, это не так уж плохо, комментарии к PacketProtocol заставляют его выглядеть очень уродливо и трудно читать. О определении 'DataReceived': это зависит от того, как вы его реализуете. Здесь, при отправке, по крайней мере, это делается путем создания префикса для каждого «сообщения», которое содержит длину ('length: message'), таким образом, легко идентифицировать каждое сообщение, каждый раз, когда вы получаете« префикс », который вы читаете' length' bytes и обработать это сообщение, а затем повторить. Этот способ позволяет избежать сообщения фиксированной длины и буфера. По какой-то причине это не делается при возврате, размер буфера фиксирован на 8192 байта. – null