【问题标题】:High performance Socket communication between TCPListener and a single Client program (C#)TCPListener 与单个客户端程序 (C#) 之间的高性能 Socket 通信
【发布时间】:2021-08-20 07:02:34
【问题描述】:

我有一个 TCPListener 服务器在专用端口上侦听,并且只有一个客户端与服务器连接。客户端与服务器建立连接,并以 60 秒的间隔继续发送心跳消息以保持其存活。此外,它发送不同类型的请求消息(每个消息的前 4 个字节为消息大小保留),服务器接收这些消息并将套接字传递给 Task(异步),该任务执行包括 DB I/O 在内的漫长过程并使用相同的套接字返回响应。它一直工作正常,但最近客户端已经开始异步发送 1000 个请求。服务器以良好的速度响应初始请求,但在处理了大约 100 个请求后,响应变得缓慢。我征求您的专家意见,以使响应更加有力。

class ServerA
{
    .......
    .......
private void StartServer()
{
    ......
    socket = tcpListener.AcceptSocket();
    Process(socket);
    .......
}

private void Process(Socket clientSocket)
{
    while(true)
    {
       byte[] msg = new byte[1024 * 1024];
       int totalMsgLength = clientSocket.Receive(msg);

       ClientRequest req = new ClientRequest();

       byte[] request = new byte[totalMsgLength];
       Buffer.BlockCopy(msg, 0, request, 0, totalMsgLength);
       req.HandleRequest(clientSocket, request); 

    }
}
}

class ClientRequest
{
byte[] request;
Socket clientSocket;

public void HandleRequest(Socket clientSocket,byte[] request)
{
    try
    {
        this.request = request;
        this.clientSocket = clientSocket;
        Task.Run(() =>
        {
            Entertain(null);
        });                
    }
    catch (Exception ex)
    {
        .....
    }
}

private void Entertain(object callback)
{
    request = admin.PerformAction(ref request); //Long process
    clientSocket.Send(request); 
}

}

【问题讨论】:

  • 附带说明:您没有保护 clientSocket.Send 免受并发写入,这很糟糕
  • 感谢您的专家意见;根据您的建议,如何避免这种不良做法?
  • 因为您使用的是同步写入:lock (clientSocket) { clientSocket.Send(request); }
  • 你觉得有必要吗?每次都从“ClientRequest”类的新对象调用 Entertain() 方法,所以我相信它已经是线程安全的。
  • @jdweng re chunks:我在回答中提出了同样的担忧,但(见评论)显然这只是来自简化代码;另外:参见“巨型帧”——每个数据包可以得到比 1500 字节大得多的数据

标签: c# sockets tcplistener


【解决方案1】:

我会想象,虽然我们不能确定,这在很大程度上是由于 1MiB 缓冲区,然后是您分配的大小合适的缓冲区每次读取时间>;将其扩展到每秒 1000 次,是的:这会造成很大的伤害。有效的缓冲区管理很难,简单的方法只适用于小批量的工作。

不过,老实说,我更关心盲人的接收;意思:你提到了TCP,而TCP是stream协议,不是packet协议;您接收缓冲区的方式与发送的内容之间没有任何关联 - 就每条消息的结束位置而言,尤其是对于高容量/频率而言;你需要一个“框架”协议。否则,您发送给HandleRequest 的内容可能是不完整的消息、多条消息等。

如果totalMsgLength <= 0,您的循环中还应该有一个break;

“管道”API 将是解决这两个问题的非常有效的解决方案;不幸的是,“管道”API 仅针对 服务器 完全实现,但是:客户端可以使用 Pipelines.Sockets.Unofficial (nuget) 在客户端获得相同的功能(顾名思义:它不是官方包)。由于您实际上在这里实现了一个服务器:Kestrel 允许您使用完整的管道 API 实现一个非常高效的任意 TCP 服务器,只需将UseConnectionHandler<T> 用于您的 TCP 处理程序 T。 p>

无论哪种方式,管道 API 都提供:

  • 有效且高效的缓冲区管理
  • 一种无需重新打包缓冲区即可处理不完整帧的出色机制

您可以在我的 here 的 3.5 部分博客系列中阅读有关“管道”API 的更多信息(这是一个方式太大的主题,无法在此处的帖子中涵盖)。

如果重写它以使用管道太多,您可能只需将 1MiB 缓冲区移动到循环之外 就可以实现很多目标,因此它只分配一次。那仍然留下内部分配;可以使用数组池 (ArrayPool<byte>.Shared),它提供 oversized 数组,但您可以将 ReadOnlyMemory<byte>ArraySegment<byte> 传递给 HandleRequest 方法来解决这个问题。

然而,这仍然存在框架问题。这是一个主要问题,需要引起注意。

【讨论】:

  • 感谢您的快速回复。为了保持问题简单,我尝试编写的代码非常少,这显然会造成一些混乱。客户端发送定义良好的 ISO8583 消息,并在每条消息的开头发送消息长度,因此服务器读取套接字并根据消息长度进行分解。但是,我正在研究您对 1MiB 的建议。会及时通知您。再次感谢。
  • 我根据您的建议进行了更改并在 UAT 部署了升级,但遗憾的是性能没有任何改进。相同的程序在我的笔记本电脑(i5+4 核+12 GB RAM)上运行速度很快,因为所有 1000 个请求都在不到 2 分 30 秒内得到响应,而 UAT 服务器上的相同程序(8 个处理器 + 24GB RAM)大约需要 15分钟。有什么其他建议可以隔离问题吗?
  • @S.ATTA.M 并非没有更多上下文和复制能力;但是,问题:本地时间是否基于与本地机器的对话?延迟和/或带宽可能是这里的瓶颈,如果您使用环回,您将不会进行类似的测量
  • 是的,整个开发环境设置在单台机器上,不像 UAT 环境中 3 台机器通过 LAN 连接动作。 (服务器、客户端和 Oracle DB)。如前所述,实际问题是延迟开始良好,但随着时间的推移会增加,同时处理 10 批请求,每批 1000 个请求。这个项目很大,所以我没有解释一切以避免分心并让读者专注于真正的问题。但是,如果您需要更多信息,请随意询问。
  • @S.ATTA.M 您无法通过代码修复延迟;您可以缓解延迟问题,但这需要非常具体的设计方法,例如消息流水线,即客户端可能会触发 5 个请求,而不是“请求、响应、请求、响应”(对于 5 个请求)然后等待 5 个响应。不过,这方式比从示例代码中可以合理回答的要复杂得多;细节真的,真的很重要。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-25
  • 2016-02-29
  • 2012-04-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多