C# TCP 服务器






4.96/5 (40投票s)
一个轻量级、易于使用的 .NET TCP 服务器库
引言
本文描述了一个库,该库为 .NET 环境提供了一个易于使用的 TCP 服务器组件,它没有许多 .NET TCP 服务器教程中存在的限制。
背景
在我参与的一个项目中,客户端要求有多个 TCP 服务器接口(总共 4 个),当我搜索如何在 C# 中实现 TCP 服务器时,我发现 .NET 框架没有一个易于使用的 TCP 服务器组件(就像在 Delphi/C++ Builder 环境中那样)。TcpListener 类可以完成一半的工作,但没有任何东西可以管理已连接的客户端。
我找到的所有 C# TCP 服务器教程都没有考虑在多个 TCP 服务器项目之间共享代码。我还发现所有教程都有各种限制(有些只支持一个已连接的客户端,有些会为每个连接创建一个新线程,这意味着线程限制就是连接限制),因此我决定自己编写一个没有这些限制的版本。
使用代码
源代码包含 2 个项目:TcpServer 和 TestApp(TestApp 作为示例提供,否则可以忽略)。将 TcpServer 项目添加到您的解决方案中,编译后您应该会在工具箱中找到 TcpServer 组件,可以将其放置在窗体、服务、组件或其他容器上。或者,您可以手动将组件添加到工具箱并将其用作组件,或手动添加项目引用,然后直接调用构造函数。
要启动 TCP 服务器,您需要按以下顺序调用代码:
TcpServer tcpServer1 = new TcpServer(); //in constructor (auto added if added as a component)
private void openTcpPort(int port)
{
tcpServer1.Port = port;
tcpServer1.Open();
}
private void closeTcpPort()
{
tcpServer1.Close();
}
调用 Open
()
将打开指定的 TCP 端口进行监听,并启动处理 TCP 服务器主要功能的 2 个内部线程。反之,Close()
会停止 2 个内部线程并关闭端口,释放所有系统资源。
Port
属性是已打开或将要打开的 TCP 端口。请注意,在端口打开时更改 Port
可能会导致一些不良后果。IsOpen
属性会在更改之前告诉您端口是否已打开。
有 3 个回调事件:OnConnect
、OnDataAvailable
和 OnError
。
OnConnect
在新客户端连接时调用,OnDataAvailable
在先前已连接的客户端的缓冲区中有可读取数据时调用。
OnConnect
和 OnDataAvailable
都会传递一个 TcpServerConnection
类(它本质上是对 TcpClient
类的精简包装),该类代表已连接/有可用数据的客户端。
这两个事件都从专门为它们创建的线程调用,在其中一个线程仍然活动时(即,当您同时收到多个回调时,它们将是针对不同客户端的),不会再次调用该事件(针对该客户端)。如果在一个回调返回后缓冲区中仍有任何数据,OnDataAvailable
事件将几乎立即再次被调用。
示例回调 1;
public delegate void invokeDelegate();
private void tcpServer1_OnDataAvailable(tcpServer.TcpServerConnection connection)
{
byte[] data = readStream(connection.Socket);
if (data != null)
{
string dataStr = Encoding.ASCII.GetString(data);
invokeDelegate del = () =>
{
handleInput(dataStr);
};
Invoke(del);
data = null;
}
}
protected byte[] readStream(TcpClient client)
{
NetworkStream stream = client.GetStream();
while (stream.DataAvailable)
{
//call stream.Read(), read until end of packet/stream/other termination indicator
//return data read as byte array
}
return null;
}
示例回调 2;
private void tcpServer1_OnDataAvailable(tcpServer.TcpServerConnection connection)
{
byte[] data = readStream(connection.Socket);
if (data != null)
{
string dataStr = Encoding.ASCII.GetString(data);
string reply;
lock(someKeyObject)
{
reply = getReply(dataStr);
}
connection.sendData(reply);
data = null;
}
}
从这个示例中可以看出,TcpServerConnection
有一个名为 Socket
的属性,这是一个 TcpClient
类,因此您可以使用它,就像编写 TCP 客户端程序一样(不包括通常混乱的重连逻辑)。如上图所示,TcpServerConnection
还有一个 sendData()
函数。它接受一个字符串,该字符串将使用指定的 System.Text.Encoding
编码(默认为 ASCII)进行转换,并排队等待 TcpServer
的内部线程之一将其发送到流中。当然,您可以直接使用 TcpClient
的函数发送数据,但如果您这样做,建议不要使用此库中的任何发送函数,否则可能会导致数据包发送顺序混乱。
TcpServer
的 OnError
回调直接从操作 TCP 服务器的 2 个线程之一调用。除了极端情况(例如,内存不足)外,您不太可能看到此事件被调用。此事件的调用是由未处理的错误触发的,导致错误的异常将作为参数传递给事件。
TcpServer
的其他属性如下;
MaxSendAttempts
;在调用 Send
()
函数或 TcpServerConnection
的 sendData()
函数时,TcpServer
的内部线程之一将处理实际的发送。如果在尝试发送时发生 IOException
,它将重试最多 MaxSendAttempts
次,然后丢弃消息。
Connections
;(只读)这是一个 TcpServerConnection
的列表,代表所有已连接的客户端。调用此属性将获得内部列表的副本。
IdleTime
;当任一内部线程检测到没有工作要做时,它们将等待最多此毫秒数,然后再检查一次。此值应始终较低,没有理由将其增加到高于默认值。如果您有高流量,则应将其设置得非常低。
MaxCallbackThreads
;这代表了任何时候允许的最大回调线程数。这取决于您的硬件和 Windows 版本可以处理多少线程,以及服务器上运行的其他多少任务。默认值 100 是保守的,如果您有高流量,应该增加它。
VerifyConnectionInterval
;这是 TcpServer
验证客户端是否仍然连接的频率(以毫秒为单位)。验证过程可能需要一些时间,因此如果您有高流量,则应将其设置得高一些。然而,当客户端断开连接并尝试在 TcpServer
验证上一个连接之前重新连接时,会产生问题(通常会导致新连接断开)。也就是说,此值指示客户端在能够成功重新连接之前必须等待多长时间。因此,您应将其设置为可以接受的最小值。
Encoding
;每个 TcpServerConnection
都有一个同名的属性,它是在 sendData()
或 Send()
中传递的数据所使用的 System.Text.Encoding
。TcpServer
上的属性代表所有新连接的默认值。通过直接更改 TcpServer
上的此属性,它将更改所有使用默认编码的客户端的编码为新的编码。要在不更改客户端的情况下更改默认值,请使用 SetEncoding()
函数并将 changeAllClients
参数设置为 false。Encoding
默认为 Encoding.ASCII
。
如上所述,TcpServer
还有一个 Send()
函数。它会将传递给它的消息发送给所有已连接的客户端(在 TCP 中最接近广播的方式)。调用它相当于在 Connections
的每个项目上调用 sendData()
函数。传递的字符串将根据每个客户端的 Encoding
属性转换为字节数组。
历史
2013-04-08 - 发布到 CodeProject。
2014-03-11 - 修复了调用 close 现在总是能正确清除连接列表(感谢 Bart - Member 10655793)。