【问题标题】:C# best way to implement TCP Client Server ApplicationC# 实现 TCP 客户端服务器应用程序的最佳方式
【发布时间】:2014-10-19 12:00:11
【问题描述】:

我想扩展我在 .NET 框架方面的经验,并想构建一个客户端/服务器应用程序。 实际上,客户端/服务器是一个小型的销售点系统,但首先,我想专注于服务器和客户端之间的通信。 将来,我想让它成为一个 WPF 应用程序,但现在,我只是从一个控制台应用程序开始。

2 个功能:

  • 客户收到一个数据集,每 15/30 分钟更新一次价格/新产品的更新
    (因此代码将在 Thread.sleep 的 Async 方法中持续 15/30 分钟)。

  • 关闭客户端应用程序时,发送一种报告(例如,xml)

在互联网上,我找到了很多示例,但我无法确定哪一种是最好/最安全/性能最好的工作方式,因此我需要一些建议来了解应该实施哪些技术。

客户端/服务器

我想要 1 个服务器应用程序来处理最多 6 个客户端。我读到线程使用大量 mb,也许更好的方法是具有 async/await 功能的任务。

ASYNC/AWAIT 示例

http://bsmadhu.wordpress.com/2012/09/29/simplify-asynchronous-programming-with-c-5-asyncawait/

线程示例

mikeadev.net/2012/07/multi-threaded-tcp-server-in-csharp/

SOCKETS 示例

codereview.stackexchange.com/questions/5306/tcp-socket-server

这似乎是一个很好的套接字示例,但是,修订后的代码不能完全工作,因为并非所有类都包含在内 msdn.microsoft.com/en-us/library/fx6588te(v=vs.110).aspx 这个 MSDN 示例包含更多关于 Buffersize 和消息结束信号的内容。我不知道这是否只是一种“旧方法”,因为在我之前的示例中,它们只是将一个字符串从客户端发送到服务器,仅此而已。

.NET 框架远程处理/WCF 我还发现了一些关于 .NET 和 WCF 的远程处理部分的信息,但不知道我是否需要实现它,因为我认为 Async/Await 的示例还不错。

序列化对象/数据集/XML

在它们之间发送数据的最佳方式是什么?只是一个 XML 序列化程序还是只是二进制文件?

数据集示例 -> XML

stackoverflow.com/questions/8384014/convert-dataset-to-xml

远程处理示例

akadia.com/services/dotnet_dataset_remoting.html

如果我应该使用 Async/Await 方法,在 serverapplication 中这样的事情是否正确:

        while(true)
        {
            string input = Console.ReadLine();
            if(input == "products")
                SendProductToClients(port);
            if(input == "rapport")
            {
                string Example = Console.ReadLine();
            }                                
        }

【问题讨论】:

  • 这是一个 TCP 练习吗?如果没有,请使用更高级别的协议,例如 Web 服务或 HTTP。 TCP 编程非常难。
  • 我只是想以一种高效的方式将数据从服务器发送到客户端。
  • 首先,这似乎是两个完全不同的问题:“使用哪个网络 API?”和“如何序列化数据?”它们是相关的,但不是完全相同的问题。其次,在现代 C# 程序中,我会选择第一个选项:使用 TcpClient/TcpListener 进行异步/等待。你可能想也可能不想精确地遵循这个例子,但这似乎是一个很好的起点。更重要的是,async/await 提供了一种非常干净和简单的方法来处理这个确切的场景。绝对不要使用线程示例;这是效率最低、可扩展性最低的方法。
  • 我推荐SignalR
  • 我认为 SignalR 更适合实时应用程序。感谢@PeterDuniho 的提示,我会制定这个例子!

标签: c# multithreading asynchronous client-server tcpclient


【解决方案1】:

任何编写客户端/服务器应用程序的人都应该考虑以下几点:

  • 应用层数据包可能跨越多个 TCP 数据包。
  • 单个 TCP 数据包中可能包含多个应用层数据包。
  • 加密。
  • 身份验证。
  • 丢失且无响应的客户。
  • 数据序列化格式。
  • 基于线程或异步套接字读取器。

正确检索数据包需要围绕数据的包装协议。该协议可以非常简单。例如,它可以像指定有效载荷长度的整数一样简单。我在下面提供的 sn-p 直接取自 GitHub 上可用的开源客户端/服务器应用程序框架项目DotNetOpenServer。请注意,客户端和服务器都使用此代码:

private byte[] buffer = new byte[8192];
private int payloadLength;
private int payloadPosition;
private MemoryStream packet = new MemoryStream();
private PacketReadTypes readState;
private Stream stream;

private void ReadCallback(IAsyncResult ar)
{
    try
    {
        int available = stream.EndRead(ar);
        int position = 0;

        while (available > 0)
        {
            int lengthToRead;
            if (readState == PacketReadTypes.Header)
            {
                lengthToRead = (int)packet.Position + available >= SessionLayerProtocol.HEADER_LENGTH ?
                        SessionLayerProtocol.HEADER_LENGTH - (int)packet.Position :
                        available;

                packet.Write(buffer, position, lengthToRead);
                position += lengthToRead;
                available -= lengthToRead;

                if (packet.Position >= SessionLayerProtocol.HEADER_LENGTH)
                    readState = PacketReadTypes.HeaderComplete;
            }

            if (readState == PacketReadTypes.HeaderComplete)
            {
                packet.Seek(0, SeekOrigin.Begin);
                BinaryReader br = new BinaryReader(packet, Encoding.UTF8);

                ushort protocolId = br.ReadUInt16();
                if (protocolId != SessionLayerProtocol.PROTOCAL_IDENTIFIER)
                    throw new Exception(ErrorTypes.INVALID_PROTOCOL);

                payloadLength = br.ReadInt32();
                readState = PacketReadTypes.Payload;
            }

            if (readState == PacketReadTypes.Payload)
            {
                lengthToRead = available >= payloadLength - payloadPosition ?
                    payloadLength - payloadPosition :
                    available;

                packet.Write(buffer, position, lengthToRead);
                position += lengthToRead;
                available -= lengthToRead;
                payloadPosition += lengthToRead;

                if (packet.Position >= SessionLayerProtocol.HEADER_LENGTH + payloadLength)
                {
                    if (Logger.LogPackets)
                        Log(Level.Debug, "RECV: " + ToHexString(packet.ToArray(), 0, (int)packet.Length));

                    MemoryStream handlerMS = new MemoryStream(packet.ToArray());
                    handlerMS.Seek(SessionLayerProtocol.HEADER_LENGTH, SeekOrigin.Begin);
                    BinaryReader br = new BinaryReader(handlerMS, Encoding.UTF8);

                    if (!ThreadPool.QueueUserWorkItem(OnPacketReceivedThreadPoolCallback, br))
                        throw new Exception(ErrorTypes.NO_MORE_THREADS_AVAILABLE);

                    Reset();
                }
            }
        }

        stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(ReadCallback), null);
    }
    catch (ObjectDisposedException)
    {
        Close();
    }
    catch (Exception ex)
    {
        ConnectionLost(ex);
    }
}


private void Reset()
{
    readState = PacketReadTypes.Header;
    packet = new MemoryStream();
    payloadLength = 0;
    payloadPosition = 0;
}

如果您要传输销售点信息,则应该对其进行加密。我建议通过 .Net 轻松启用 TLS。代码非常简单,并且有很多示例,因此为简洁起见,我不打算在这里展示它。如果您有兴趣,可以在 DotNetOpenServer 中找到示例实现。

所有连接都应该经过身份验证。有很多方法可以做到这一点。我使用了 Windows 身份验证 (NTLM) 和 Basic。尽管 NTLM 功能强大且自动化,但它仅限于特定平台。基本身份验证只是在套接字加密后传递用户名和密码。但是,基本身份验证仍然可以;针对本地服务器或域控制器验证用户名/密码组合,本质上是模拟 NTLM。后一种方法使开发人员能够轻松创建在 iOS、Mac、Unix/Linux 风格以及 Java 平台上运行的非 Windows 客户端应用程序(尽管一些 Java 实现支持 NTLM)。在会话通过身份验证之前,您的服务器实现不应允许传输应用程序数据。

我们可以依靠的只有几件事:税收、网络故障和客户端应用程序挂起。这只是事物的本质。您的服务器应该实现一种方法来清理丢失和挂起的客户端会话。我已经通过保持活动(AKA 心跳)协议在许多客户端/服务器框架中实现了这一点。在服务器端,我实现了一个计时器,每次客户端发送数据包时都会重置,任何数据包。如果服务器在超时时间内没有收到数据包,则会话关闭。 keep-alive 协议用于在其他应用层协议空闲时发送数据包。由于您的应用程序仅每 15 分钟发送一次 XML,每分钟发送一次保持活动数据包将使服务器端能够在 15 分钟间隔之前丢失连接时向管理员发出警报,这可能使 IT 部门能够解决网络问题更及时。

接下来,数据格式。在您的情况下,XML 很棒。 XML 使您可以随时更改有效负载。如果您真的需要速度,那么二进制将永远胜过字符串表示数据的臃肿性质。

最后,正如@NSFW 所说,线程或异步在您的情况下并不重要。我编写了基于线程和异步回调的可扩展至 10000 个连接的服务器。归根结底,这一切都是一样的。正如@NSFW 所说,我们大多数人现在都在使用异步回调,而我编写的最新服务器实现也遵循该模型。

【讨论】:

    【解决方案2】:

    考虑到现代系统上可用的 RAM 量,线程并不是非常昂贵,因此我认为针对低线程数进行优化没有帮助。特别是如果我们谈论 1 线程和 2-5 线程之间的区别。 (对于成百上千个线程,线程的成本开始变得很重要。)

    但是您确实希望优化您拥有的任何线程的最小阻塞。因此,例如,不要使用 Thread.Sleep 每隔 15 分钟执行一次工作,只需设置一个计时器,让线程返回,并相信系统会在 15 分钟后调用您的代码。并且不要使用阻塞操作通过网络读取或写入信息,而是使用非阻塞操作。

    async/await 模式是 .Net 上异步编程的新热点,它是对可追溯到 .Net 1.0 的 Begin/End 模式的重大改进。使用 async/await 编写的代码仍在使用线程,它只是使用 C# 和 .Net 的特性来向您隐藏线程的许多复杂性——而且在大多数情况下,它隐藏了应该隐藏的东西,因此您可以将注意力集中在应用程序的功能上,而不是多线程编程的细节上。

    所以我的建议是对所有 IO(网络和磁盘)使用 async/await 方法,并使用计时器来处理定期杂务,例如发送您提到的那些更新。

    关于序列化...

    XML 相对于二进制格式的最大优势之一是,您可以将 XML 传输保存到磁盘并使用现成的工具打开它们,以确认有效负载确实包含您认为会存在的数据。所以我倾向于避免使用二进制格式,除非带宽稀缺——即便如此,使用 XML 等文本友好格式开发大部分应用程序是有用的,然后在发送和接收数据的基本机制充实后切换到二进制格式出去。

    所以我投票支持 XML。

    关于您的代码示例,其中没有 async/await...

    但首先,请注意,典型的简单 TCP 服务器将有一个小循环,用于侦听传入连接并启动一个线程来处理每个新连接。然后,连接线程的代码将侦听传入数据,对其进行处理并发送适当的响应。所以listen-for-new-connections代码和handle-a-single-connection代码是完全分开的。

    因此,无论如何,连接线程代码可能看起来与您编写的代码相似,但您不只是调用 ReadLine,而是执行类似“string line = await ReadLine();”之类的操作await 关键字大约是您的代码让一个线程退出(在调用 ReadLine 之后)然后在另一个线程上恢复(当 ReadLine 的结果可用时)的地方。除了可等待方法的名称应以 Async 结尾,例如 ReadLineAsync。从网络读取一行文本并不是一个坏主意,但您必须自己编写 ReadLineAsync,以现有网络 API 为基础。

    我希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 2013-02-25
      • 1970-01-01
      • 2019-09-28
      • 2023-03-14
      • 1970-01-01
      • 2012-06-28
      • 1970-01-01
      • 2019-08-15
      • 1970-01-01
      相关资源
      最近更新 更多