Определить размер байтов входящего сообщения
Под каждое входящее сообщение на стороне клиента я выделяю массив байтов (буфер) с размером 256.
Необходимо, чтобы размер буфера сообщений (массив байтов) соотносился с размером входящих сообщений.
Вот так выглядит блок кода, в котором обрабатываются входящие со стороны сервера сообщения:
byte[] bytes = new byte[256];
stream.Read(bytes, 0, bytes.Length);
string answer = Encoding.UTF8.GetString(bytes);
Под каждое сообщение я выделяю массив байтов с размером 256. Получается, даже если входящее сообщение гораздо меньше по размеру - в любом случае это сообщение записывается в массив, размер которого равен 256.
В чём проблема? Проблема в том, что из-за такого подхода у меня нет возможности провести равенство строк. Например, если сервер отправляет сообщение "connect" - мне было бы достаточно размера массива 7 (так как семь символов). Однако, из-за того, что я выделяю под каждое сообщение 256 байтов - используется только 7, а остальные остаются нулевыми.
И следовательно, если я попробую сравнить строки:
byte[] bytes = new byte[256];
stream.Read(bytes, 0, bytes.Length);
string answer = Encoding.UTF8.GetString(bytes);
if (answer == "connect")
{
//не выполнится
}
Условие не выполнится! Так как "connect" имеет размер 7, а answer в свою очередь 256, то есть фактически 249 это просто пустые нули.
Ответы (1 шт):
Сразу предупрежу, что в .NET Framework 4.x ниже показанный код работать не будет. Требуется .NET Core 3.1 или новее. Рекомендую .NET 5 или .NET 6.
Протокол приложения
Чтобы принять TCP сообщение, надо знать его размер, и надо передавать его явно, потому что.
- Сообщение может быть длиннее, чем буфер.
- Сообщение может быть фрагментировано и получено по частям.
Что гарантирует TCP:
- Сообщение будет доставлено, если соединение не будет разорвано.
- Фрагменты сообщения будут получены в том же порядке, в котором были отправлены.
От этого и следует отталкиваться. То есть то, что не гарантирует транспортный протокол, должен гарантировать протокол уровня приложения, то есть ваш протокол.
Самый простой протокол, который можно придумать, это
Length[4] Message[Length]
Где Length - 4 байта длины сообщения, Int32, а Message- это массив байт длиной, указанной перед ним. При этом размер буфера чтения и записи в поток может быть любым, как меньше, так и больше, чем длина сообщения.
Создание сообщения
Для начала можно создать класс, который будет представлять само сообщение.
public class TcpMessage
{
private byte[] _data;
public int Length => _data.Length;
public ReadOnlySpan<byte> Data => _data;
public TcpMessage(byte[] data)
{
_data = new byte[data.Length];
Buffer.BlockCopy(data, 0, _data, 0, data.Length);
}
public static TcpMessage FromString(string message)
{
return new TcpMessage(Encoding.UTF8.GetBytes(message));
}
public string AsString()
{
return Encoding.UTF8.GetString(_data);
}
}
Здесь все очень просто. Данный класс всего-лишь предоставляет данные и их длину и гарантирует, что данные не будут никогда изменены после создания сообщения. При этом сообщение поддерживает только один тип данных - байты. Так мы условились выше в описании протокола уровня приложения.
Отправка сообщения
Создадим сообщение
TcpMessage message = TcpMessage.FromString("Привет");
Теперь отправим
byte[] lengthBytes = BitConverter.GetBytes(message.Length);
stream.Write(lengthBytes);
stream.Write(message.Data);
Сообщение отправлено.
Прием сообщения
Чтение заголовка и создание буфера
byte[] lengthBytes = new byte[4];
stream.Read(lengthBytes);
byte[] buffer = new byte[BitConverter.ToInt32(lengthBytes)];
Упрощенная версия
stream.Read(buffer);
Это сработает только если сообщение придет всё и сразу. Но как я выше писал - гарантии нет.
Полная версия
int offset = 0;
while (offset < buffer.Length)
{
offset += stream.Read(buffer.AsSpan()[offset..]);
}
Вот так можно получить текст из буфера
TcpMessage message = new TcpMessage(buffer);
string text = message.AsString();
Теперь весь код приема можно засунуть в бесконечный цикл и получится нормальный процесс приема пакетов из потока.