65.9K
CodeProject 正在变化。 阅读更多。
Home

C# TCP 服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (40投票s)

2013年4月8日

CPOL

6分钟阅读

viewsIcon

248565

downloadIcon

29579

一个轻量级、易于使用的 .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 个回调事件:OnConnectOnDataAvailableOnError

OnConnect 在新客户端连接时调用,OnDataAvailable 在先前已连接的客户端的缓冲区中有可读取数据时调用。

OnConnectOnDataAvailable 都会传递一个 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 的函数发送数据,但如果您这样做,建议不要使用此库中的任何发送函数,否则可能会导致数据包发送顺序混乱。

TcpServerOnError 回调直接从操作 TCP 服务器的 2 个线程之一调用。除了极端情况(例如,内存不足)外,您不太可能看到此事件被调用。此事件的调用是由未处理的错误触发的,导致错误的异常将作为参数传递给事件。

TcpServer 的其他属性如下;

MaxSendAttempts;在调用 Send() 函数或 TcpServerConnectionsendData() 函数时,TcpServer 的内部线程之一将处理实际的发送。如果在尝试发送时发生 IOException,它将重试最多 MaxSendAttempts 次,然后丢弃消息。

Connections;(只读)这是一个 TcpServerConnection 的列表,代表所有已连接的客户端。调用此属性将获得内部列表的副本。

IdleTime;当任一内部线程检测到没有工作要做时,它们将等待最多此毫秒数,然后再检查一次。此值应始终较低,没有理由将其增加到高于默认值。如果您有高流量,则应将其设置得非常低。

MaxCallbackThreads;这代表了任何时候允许的最大回调线程数。这取决于您的硬件和 Windows 版本可以处理多少线程,以及服务器上运行的其他多少任务。默认值 100 是保守的,如果您有高流量,应该增加它。

VerifyConnectionInterval;这是 TcpServer 验证客户端是否仍然连接的频率(以毫秒为单位)。验证过程可能需要一些时间,因此如果您有高流量,则应将其设置得高一些。然而,当客户端断开连接并尝试在 TcpServer 验证上一个连接之前重新连接时,会产生问题(通常会导致新连接断开)。也就是说,此值指示客户端在能够成功重新连接之前必须等待多长时间。因此,您应将其设置为可以接受的最小值。

Encoding;每个 TcpServerConnection 都有一个同名的属性,它是在 sendData()Send() 中传递的数据所使用的 System.Text.EncodingTcpServer 上的属性代表所有新连接的默认值。通过直接更改 TcpServer 上的此属性,它将更改所有使用默认编码的客户端的编码为新的编码。要在不更改客户端的情况下更改默认值,请使用 SetEncoding() 函数并将 changeAllClients 参数设置为 false。Encoding 默认为 Encoding.ASCII

如上所述,TcpServer 还有一个 Send() 函数。它会将传递给它的消息发送给所有已连接的客户端(在 TCP 中最接近广播的方式)。调用它相当于在 Connections 的每个项目上调用 sendData() 函数。传递的字符串将根据每个客户端的 Encoding 属性转换为字节数组。

历史

2013-04-08 - 发布到 CodeProject。

2014-03-11 - 修复了调用 close 现在总是能正确清除连接列表(感谢 Bart - Member 10655793)。

© . All rights reserved.