这在某种程度上是“TCP的基础”问题,但与此同时我还没有在其他地方找到令人信服的答案,并相信我对TCP的基础知识有一个好的/很好的理解。我不确定问题的组合(或者一个问题,而我在确认几个要点的时候)是违反规则的。希望不是。
我正在尝试编写TCP客户端的C#实现,它与包含TCP服务器的现有应用程序通信(我无法访问其代码,因此没有WCF)。当新信息进入或退出时,如何连接到它,发送和接收,最终断开连接。使用the following MSDN code作为示例,列出“发送”和“接收”异步方法(或只是TcpClient),并忽略连接和断开连接,我怎样才能最好地继续检查接收的新数据包和同时在需要时发送?
我最初使用的是TCPClient和GetStream(),并且msdn代码似乎仍然需要稍微描述一下循环和睡眠(反直觉),我在一个单独的线程中以睡眠形式在循环中运行receive方法( 10)毫秒,并根据需要在主(或第三)线程中发送。这允许我发送罚款,并且接收方法有效地定期轮询以查找新数据包。然后将收到的数据包添加到队列中。
这真的是最好的解决方案吗?不应该有一个DataAvailable事件等价物(或者我在msdn代码中缺少的东西),它允许我们在有新数据时才会收到它们吗?
作为一个事后的想法,我注意到套接字可以从另一边切断而客户端不知道直到下一次拙劣的发送。为了澄清,客户端有义务发送常规Keepalive(并且接收不充分,仅发送)以确定套接字是否仍然存活。 keepalive的频率决定了我知道链接断开的时间。那是对的吗?我试过Poll,socket.connected等只是为了发现为什么每个都没有帮助。
最后,为了确认(我认为不是很好确定),在上面的按需发送和接收tcpclient.DataAvailable每十秒的情况下,如果同时发送和接收可能会丢失数据吗?如果我在接收的同时尝试发送一个会失败,覆盖另一个或任何其他此类不良行为?
答案 0 :(得分:79)
将问题分组在一起并没有什么不妥,但它确实使回答问题更具挑战性......:)
您链接的MSDN文章显示了如何进行一次性完成的TCP通信,即一次发送和一次接收。您还会注意到它直接使用Socket
类,大多数人(包括我自己)会建议使用TcpClient
类。如果您需要配置某个套接字(例如Client
),则可以始终通过SetSocketOption()
属性获取基础Socket
。
关于要注意的示例的另一个方面是,虽然它使用线程来执行AsyncCallback
和BeginSend()
的BeginReceive()
委托,但它本质上是一个单线程示例,因为如何使用ManualResetEvent个对象。对于客户端和服务器之间的重复交换,这不是您想要的。
好的,所以你想使用TcpClient
。连接到服务器(例如TcpListener
)应该很简单 - 如果要进行阻止操作,请使用Connect()
;如果需要非阻塞操作,请使用BeginConnect()
。建立连接后,使用GetStream()
方法获取用于读写的NetworkStream
对象。使用Read()
/ Write()
操作阻止I / O以及非阻塞I / O的BeginRead()
/ BeginWrite()
操作。请注意,BeginRead()
和BeginWrite()
使用AsyncCallback
类的BeginReceive()
和BeginSend()
方法使用的相同Socket
机制。
此时需要注意的一个关键事项是NetworkStream
的MSDN文档中的一点点模糊:
读取和写入操作可以同时执行 不需要NetworkStream类的实例 同步。 只要有一个唯一的写入线程 操作和一个用于读取操作的唯一线程,会有 是读写线程之间没有交叉干扰和否 同步是必需的。
简而言之,因为您计划从同一个TcpClient
实例读取和写入,所以您需要两个线程才能执行此操作。使用单独的线程将确保在有人尝试发送的同时接收数据时不会丢失任何数据。我在项目中处理此问题的方法是创建一个顶级对象,比如Client
,它包裹TcpClient
及其基础NetworkStream
。此类还创建和管理两个Thread
对象,在构造期间将NetworkStream
对象传递给每个对象。第一个线程是Sender
线程。想要发送数据的任何人都可以通过SendData()
上的公共Client
方法进行发送,该方法将数据路由到Sender
进行传输。第二个线程是Receiver
线程。该主题通过Client
公开的公共事件将所有收到的数据发布给感兴趣的各方。它看起来像这样:
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;
}
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;
}
}
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毫秒。但接下来你回到民意调查,这是我们首先要避免的。以下是我的工作方式,并附有解释说明:
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();
}
}
}
就“保持活动”而言,遗憾的是,除了尝试发送一些数据之外,很难解决另一方何时以静默方式退出连接的问题。在我的情况下,因为我控制发送方和接收方,所以我在协议中添加了一条很小的KeepAlive
消息(8个字节)。除非已经发送其他数据,否则每隔五秒从TCP连接的两端发送一次。
我想我已经解决了你所涉及的所有方面。我希望你觉得这很有帮助。