Ответ 1
Нет ничего неправильного в том, чтобы объединить вопросы вместе, но он делает ответ на вопрос более сложным...:)
В статье MSDN, которую вы указали, показано, как выполнить одноразовую TCP-связь, то есть одну отправку и один прием. Вы также заметите, что он использует класс Socket
, где большинство людей, включая меня, предложит использовать TcpClient
. Вы всегда можете получить базовый Socket
через свойство Client
, если вам нужно сконфигурировать определенный сокет, например (например, SetSocketOption()
).
Другой аспект этого примера заключается в том, что, хотя он использует потоки для выполнения делегатов AsyncCallback
для BeginSend()
и BeginReceive()
, это, по сути, однопоточный пример, потому что о том, как используются объекты ManualResetEvent. Для повторного обмена между клиентом и сервером это не то, что вы хотите.
Хорошо, поэтому вы хотите использовать TcpClient
. Подключение к серверу (например, TcpListener
) должно быть простым - используйте Connect()
, если вы хотите операцию блокировки или BeginConnect()
, если вы хотите неблокировать операцию. Как только соединение установлено, используйте метод GetStream()
, чтобы получить NetworkStream
для использования для чтения и записи. Используйте Read()
/Write()
операции для блокировки I/O и BeginRead()
/BeginWrite()
операции для неблокирующего ввода-вывода. Обратите внимание, что BeginRead()
и BeginWrite()
используют тот же механизм AsyncCallback
, используемый методами BeginReceive()
и BeginSend()
класса Socket
.
Одной из ключевых моментов, которые следует отметить на этом этапе, является небольшая публикация в документации MSDN для NetworkStream
:
Операции чтения и записи могут выполняться одновременно на экземпляр класса NetworkStream без необходимости синхронизации. Пока существует один уникальный поток для записи операций и один уникальный поток для операций чтения, там будет не допускать перекрестной помехи между потоками чтения и записи и нет требуется синхронизация.
Короче говоря, поскольку вы планируете читать и писать из одного и того же экземпляра TcpClient
, для этого вам понадобятся два потока. Использование отдельных потоков гарантирует, что никакие данные не будут потеряны при одновременном получении данных, когда кто-то пытается отправить. Способ, которым я подходил к этому в моих проектах, - создать объект верхнего уровня, например Client
, который обертывает TcpClient
и его базовый NetworkStream
. Этот класс также создает и управляет двумя объектами Thread
, передавая объект NetworkStream
каждому в процессе построения. Первый поток - это поток Sender
. Любой, кто хочет отправить данные, делает это с помощью общедоступного метода SendData()
на Client
, который передает данные в Sender
для передачи. Второй поток - это поток Receiver
. Этот поток публикует все полученные данные заинтересованным лицам через публичное мероприятие, открытое Client
. Это выглядит примерно так:
Client.cs
public sealed partial class Client : IDisposable
{
// Called by producers to send data over the socket.
public void SendData(byte[] data)
{
_sender.SendData(data);
}
// Consumers register to receive data.
public event EventHandler<DataReceivedEventArgs> DataReceived;
public Client()
{
_client = new TcpClient(...);
_stream = _client.GetStream();
_receiver = new Receiver(_stream);
_sender = new Sender(_stream);
_receiver.DataReceived += OnDataReceived;
}
private void OnDataReceived(object sender, DataReceivedEventArgs e)
{
var handler = DataReceived;
if (handler != null) DataReceived(this, e); // re-raise event
}
private TcpClient _client;
private NetworkStream _stream;
private Receiver _receiver;
private Sender _sender;
}
Client.Receiver.cs
private sealed partial class Client
{
private sealed class Receiver
{
internal event EventHandler<DataReceivedEventArgs> DataReceived;
internal Receiver(NetworkStream stream)
{
_stream = stream;
_thread = new Thread(Run);
_thread.Start();
}
private void Run()
{
// main thread loop for receiving data...
}
private NetworkStream _stream;
private Thread _thread;
}
}
Client.Sender.cs
private sealed partial class Client
{
private sealed class Sender
{
internal void SendData(byte[] data)
{
// transition the data to the thread and send it...
}
internal Sender(NetworkStream stream)
{
_stream = stream;
_thread = new Thread(Run);
_thread.Start();
}
private void Run()
{
// main thread loop for sending data...
}
private NetworkStream _stream;
private Thread _thread;
}
}
Обратите внимание, что это три отдельных файла .cs, но они определяют разные аспекты одного и того же класса Client
. Я использую трюк Visual Studio, описанный здесь, чтобы вложить соответствующие файлы Receiver
и Sender
в файл Client
. В общем, так, как я это делаю.
Относительно NetworkStream.DataAvailable
/Thread.Sleep()
вопрос. Я бы согласился, что событие будет приятным, но вы можете эффективно достичь этого, используя метод Read()
в сочетании с бесконечным ReadTimeout
, Это не окажет негативного влияния на остальную часть вашего приложения (например, пользовательский интерфейс), поскольку он работает в своем потоке. Однако это затрудняет закрытие потока (например, когда приложение закрывается), поэтому вы, вероятно, захотите использовать что-то более разумное, скажем, 10 миллисекунд. Но тогда вы вернулись к опросу, чего мы пытаемся избежать в первую очередь. Вот как я это делаю, с комментариями для объяснения:
private sealed class Receiver
{
private void Run()
{
try
{
// ShutdownEvent is a ManualResetEvent signaled by
// Client when its time to close the socket.
while (!ShutdownEvent.WaitOne(0))
{
try
{
// We could use the ReadTimeout property and let Read()
// block. However, if no data is received prior to the
// timeout period expiring, an IOException occurs.
// While this can be handled, it leads to problems when
// debugging if we are wanting to break when exceptions
// are thrown (unless we explicitly ignore IOException,
// which I always forget to do).
if (!_stream.DataAvailable)
{
// Give up the remaining time slice.
Thread.Sleep(1);
}
else if (_stream.Read(_data, 0, _data.Length) > 0)
{
// Raise the DataReceived event w/ data...
}
else
{
// The connection has closed gracefully, so stop the
// thread.
ShutdownEvent.Set();
}
}
catch (IOException ex)
{
// Handle the exception...
}
}
}
catch (Exception ex)
{
// Handle the exception...
}
finally
{
_stream.Close();
}
}
}
Что касается "keepalives", то, к сожалению, не существует способа узнать, когда другая сторона отключилась от соединения молча, за исключением попыток отправки некоторых данных. В моем случае, поскольку я контролирую как отправляющую, так и принимающую стороны, я добавил в мой протокол небольшое сообщение KeepAlive
(8 байт). Это отправляется каждые пять секунд с обеих сторон TCP-соединения, если другие данные уже отправлены.
Я думаю, что я затронул все грани, которые вы затронули. Надеюсь, вы сочтете это полезным.