将问题组合在一起并没有什么错,但它确实使回答问题更具挑战性...... :)
您链接的 MSDN 文章显示了如何进行一次性 TCP 通信,即一次发送和一次接收。您还会注意到它直接使用Socket 类,而大多数人(包括我自己)会建议改用TcpClient 类。如果您需要配置某个套接字(例如,SetSocketOption()),您始终可以通过Client 属性获取底层Socket。
关于该示例需要注意的另一个方面是,虽然它使用线程来执行 BeginSend() 和 BeginReceive() 的 AsyncCallback 委托,但它本质上是一个单线程示例,因为 ManualResetEvent 对象被使用。对于客户端和服务器之间的重复交换,这不是您想要的。
好的,所以你想使用TcpClient。连接到服务器(例如,TcpListener)应该很简单——如果你想要一个阻塞操作,使用Connect(),如果你想要一个非阻塞操作,使用BeginConnect()。建立连接后,使用GetStream() 方法获取NetworkStream 对象用于读写。对阻塞 I/O 使用 Read()/Write() 操作,对非阻塞 I/O 使用 BeginRead()/BeginWrite() 操作。请注意,BeginRead() 和 BeginWrite() 使用与 Socket 类的 BeginReceive() 和 BeginSend() 方法相同的 AsyncCallback 机制。
此时要注意的关键事项之一是 NetworkStream 的 MSDN 文档中的这个小简介:
读写操作可以同时在一个
NetworkStream 类的实例,而不需要
同步。 只要有一个唯一的线程可以写
操作和一个用于读取操作的唯一线程,将
读写线程之间没有交叉干扰并且没有
需要同步。
简而言之,因为您计划从同一个 TcpClient 实例读取和写入,所以您需要两个线程来执行此操作。使用单独的线程将确保在有人尝试发送的同时接收数据时不会丢失数据。我在我的项目中处理这个问题的方法是创建一个顶级对象,比如Client,它包装了TcpClient 及其底层NetworkStream。该类还创建和管理两个Thread 对象,在构造过程中将NetworkStream 对象传递给每个对象。第一个线程是Sender 线程。任何想要发送数据的人都可以通过Client 上的公共SendData() 方法发送数据,该方法将数据路由到Sender 进行传输。第二个线程是Receiver 线程。此线程通过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 类的不同方面。我使用 here 描述的 Visual Studio 技巧将各自的 Receiver 和 Sender 文件嵌套在 Client 文件下。简而言之,我就是这样做的。
关于NetworkStream.DataAvailable/Thread.Sleep()的问题。我同意一个事件会很好,但是您可以通过结合使用Read() 方法和无限ReadTimeout 来有效地实现这一点。这不会对应用程序的其余部分(例如 UI)产生不利影响,因为它在自己的线程中运行。但是,这会使关闭线程变得复杂(例如,当应用程序关闭时),因此您可能希望使用更合理的时间,比如 10 毫秒。但是你又回到了投票,这是我们一开始就试图避免的。下面是我的做法,用 cmets 来解释:
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 连接的两端发送一次。
我想我已经解决了你提到的所有方面。希望对您有所帮助。