Каков правильный способ чтения из NetworkStream в .NET.
Я боролся с этим и не могу найти причину, по которой мой код не умеет правильно читать с TCP-сервера, который я также написал. Я использую класс TcpClient
и его метод GetStream()
, но что-то не работает должным образом. Либо операция блокируется бесконечно (последняя операция чтения не отключается, как ожидалось), либо данные обрезаны (по какой-либо причине операция чтения возвращает 0 и выходит из цикла, возможно, сервер не отвечает достаточно быстро). Это три попытки реализовать эту функцию:
// this will break from the loop without getting the entire 4804 bytes from the server
string SendCmd(string cmd, string ip, int port)
{
var client = new TcpClient(ip, port);
var data = Encoding.GetEncoding(1252).GetBytes(cmd);
var stm = client.GetStream();
stm.Write(data, 0, data.Length);
byte[] resp = new byte[2048];
var memStream = new MemoryStream();
int bytes = stm.Read(resp, 0, resp.Length);
while (bytes > 0)
{
memStream.Write(resp, 0, bytes);
bytes = 0;
if (stm.DataAvailable)
bytes = stm.Read(resp, 0, resp.Length);
}
return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}
// this will block forever. It reads everything but freezes when data is exhausted
string SendCmd(string cmd, string ip, int port)
{
var client = new TcpClient(ip, port);
var data = Encoding.GetEncoding(1252).GetBytes(cmd);
var stm = client.GetStream();
stm.Write(data, 0, data.Length);
byte[] resp = new byte[2048];
var memStream = new MemoryStream();
int bytes = stm.Read(resp, 0, resp.Length);
while (bytes > 0)
{
memStream.Write(resp, 0, bytes);
bytes = stm.Read(resp, 0, resp.Length);
}
return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}
// inserting a sleep inside the loop will make everything work perfectly
string SendCmd(string cmd, string ip, int port)
{
var client = new TcpClient(ip, port);
var data = Encoding.GetEncoding(1252).GetBytes(cmd);
var stm = client.GetStream();
stm.Write(data, 0, data.Length);
byte[] resp = new byte[2048];
var memStream = new MemoryStream();
int bytes = stm.Read(resp, 0, resp.Length);
while (bytes > 0)
{
memStream.Write(resp, 0, bytes);
Thread.Sleep(20);
bytes = 0;
if (stm.DataAvailable)
bytes = stm.Read(resp, 0, resp.Length);
}
return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}
Последний "работает", но он, безусловно, выглядит уродливым, чтобы помещать жесткий цикл внутри цикла, учитывая, что сокеты уже поддерживают таймауты чтения! Нужно ли настраивать какое-либо свойство (-и) на TcpClient
NetworkStream
? Проблема возникает на сервере? Сервер не закрывает соединения, это зависит от клиента. Вышеупомянутое также работает внутри контекста потока пользовательского интерфейса (тестовая программа), возможно, оно имеет какое-то отношение к этому...
Кто-нибудь знает, как правильно использовать NetworkStream.Read
для чтения данных, пока не появится больше данных? Я предполагаю, что то, что я желаю, - это что-то вроде старых свойств таймаута Win32 winsock... ReadTimeout
и т.д. Он пытается прочитать, пока не будет достигнут тайм-аут, а затем вернется 0... Но иногда кажется, что возвращается 0 когда данные должны быть доступны (или по пути.. может прочитать возврат 0, если он доступен?), и затем он блокируется бесконечно на последнем чтении, когда данные недоступны...
Да, я в недоумении!
Ответы
Ответ 1
Настройка базового сокета ReceiveTimeout
сделала трюк. Вы можете получить доступ к нему следующим образом: yourTcpClient.Client.ReceiveTimeout
. Вы можете прочитать docs для получения дополнительной информации.
Теперь код будет только "спать" до тех пор, пока необходимо, чтобы некоторые данные поступали в сокет, или он вызовет исключение, если в начале операции чтения не поступит никаких данных более чем на 20 мс. Я могу настроить этот таймаут, если это необходимо. Теперь я не плачу за 20 мс на каждой итерации, я плачу только за последнюю операцию чтения. Поскольку у меня есть длина содержимого сообщения в первых байтах, считанных с сервера, я могу использовать его, чтобы настроить его еще больше и не пытаться читать, если все ожидаемые данные уже получены.
Я нахожу использование ReceiveTimeout намного проще, чем реализация асинхронного чтения... Вот рабочий код:
string SendCmd(string cmd, string ip, int port)
{
var client = new TcpClient(ip, port);
var data = Encoding.GetEncoding(1252).GetBytes(cmd);
var stm = client.GetStream();
stm.Write(data, 0, data.Length);
byte[] resp = new byte[2048];
var memStream = new MemoryStream();
var bytes = 0;
client.Client.ReceiveTimeout = 20;
do
{
try
{
bytes = stm.Read(resp, 0, resp.Length);
memStream.Write(resp, 0, bytes);
}
catch (IOException ex)
{
// if the ReceiveTimeout is reached an IOException will be raised...
// with an InnerException of type SocketException and ErrorCode 10060
var socketExept = ex.InnerException as SocketException;
if (socketExept == null || socketExept.ErrorCode != 10060)
// if it not the "expected" exception, let not hide the error
throw ex;
// if it is the receive timeout, then reading ended
bytes = 0;
}
} while (bytes > 0);
return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}
Ответ 2
Сетевой код, как известно, трудно писать, тестировать и отлаживать.
У вас часто есть много вещей, которые нужно учитывать, например:
-
какой "endian" вы будете использовать для обменных данных (Intel x86/x64 основан на малоподобных) - системы, которые используют big-endian, могут читать данные, которые находятся в little-endian (и наоборот), но они должны переупорядочить данные. При документировании своего "протокола" просто дайте понять, какой из них вы используете.
-
Существуют ли какие-либо "настройки", которые были установлены в сокетах, которые могут влиять на поведение "потока" (например, SO_LINGER) - вам может потребоваться включить или отключить некоторые из них, если ваш код очень чувствителен
-
как перегруженность в реальном мире, которая вызывает задержки в потоке, влияет на вашу логику чтения/записи
Если "сообщение", которое обменивается между клиентом и сервером (в любом направлении), может различаться по размеру, тогда часто вам нужно использовать стратегию, чтобы это "сообщение" было обмениваться надежным образом (так называемый протокол).
Вот несколько способов обработки обмена:
-
имеют размер сообщения, закодированный в заголовке, который предшествует данным - это может быть просто "число" в первых отправленных 2/4/8 байтах (в зависимости от вашего максимального размера сообщения) или может быть более экзотический "заголовок"
-
используйте специальный маркер "конец сообщения" (дозорный), с реальными данными, закодированными/экранированными, если есть вероятность того, что реальные данные будут запутаны с "концом маркера"
-
использовать таймаут... т.е. определенный период отсутствия байтов означает, что для сообщения больше нет данных, однако это может быть причиной ошибки с короткими таймаутами, которые могут быть легко удалены по перегруженным потокам.
-
имеют канал "команда" и "данные" на отдельных "соединениях"... это подход, который использует протокол FTP (преимущество - четкое разделение данных от команд... за счет второе соединение)
Каждый подход имеет свои плюсы и минусы для "правильности".
В приведенном ниже коде используется метод "тайм-аут", поскольку он кажется тем, который вам нужен.
См. http://msdn.microsoft.com/en-us/library/bk6w7hs8.aspx. Вы можете получить доступ к NetworkStream
в TCPClient
, чтобы вы могли изменить ReadTimeout
.
string SendCmd(string cmd, string ip, int port)
{
var client = new TcpClient(ip, port);
var data = Encoding.GetEncoding(1252).GetBytes(cmd);
var stm = client.GetStream();
// Set a 250 millisecond timeout for reading (instead of Infinite the default)
stm.ReadTimeout = 250;
stm.Write(data, 0, data.Length);
byte[] resp = new byte[2048];
var memStream = new MemoryStream();
int bytesread = stm.Read(resp, 0, resp.Length);
while (bytesread > 0)
{
memStream.Write(resp, 0, bytesread);
bytesread = stm.Read(resp, 0, resp.Length);
}
return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}
В качестве примечания для других вариантов этого кода написания сети... при выполнении Read
, где вы хотите избежать "блока", вы можете проверить флаг DataAvailable
, а затем ТОЛЬКО читать то, что находится в буфере проверка свойства .Length
, например stm.Read(resp, 0, stm.Length);
Ответ 3
В соответствии с вашим требованием, Thread.Sleep
отлично подходит для использования, потому что вы не уверены, когда данные будут доступны, поэтому вам может потребоваться дождаться появления данных. Я немного изменил логику вашей функции, это может помочь вам немного дальше.
string SendCmd(string cmd, string ip, int port)
{
var client = new TcpClient(ip, port);
var data = Encoding.GetEncoding(1252).GetBytes(cmd);
var stm = client.GetStream();
stm.Write(data, 0, data.Length);
byte[] resp = new byte[2048];
var memStream = new MemoryStream();
int bytes = 0;
do
{
bytes = 0;
while (!stm.DataAvailable)
Thread.Sleep(20); // some delay
bytes = stm.Read(resp, 0, resp.Length);
memStream.Write(resp, 0, bytes);
}
while (bytes > 0);
return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}
Надеюсь, это поможет!