.net 到底行不行!2000 人在线的客服系统真实屏录演示(附技术详解)

news2024/9/25 17:00:41

业余时间用 .net 写了一个免费的在线客服系统:升讯威在线客服与营销系统。

时常有朋友问我性能方面的问题,正好有一个真实客户,在线的访客数量达到了 2000 人。在争得客户同意后,我录了一个视频。

升讯威在线客服系统可以在极低配置的服务器环境下,轻松应对这种情况,依然可以做到:

消息毫秒级送达,操作毫秒级响应

升讯威在线客服系统 2000 人在线的客服系统真实屏录演示

性能

以官方在线使用环境为例,每日处理 HTTPS 请求数大于 16 万次, PV 请求大于 25 万 次的情况下,服务端主程序内存占用小于 300MB,服务器 CPU 占用小于 5%。

每日处理 HTTPS 请求数大于 16 万次:

每日处理 PV 请求大于 25 万 次:

服务端主程序内存占用小于 300MB:

服务器 CPU (Intel Xeon Platinum 8163 / 4 核 2.5 GHz) 占用稳定约 5%:

安全性

  • 访客端使用 https 与 wss 安全连接,数据全程加密传输。
  • 客服端数据报文使用 AES 加密传输。(Advanced Encryption Standard,美国联邦政府区块加密标准)。
  • 可以 100% 私有化部署在您的自有服务器。

拦截的报文中消息以密文传输:


实现效果

客服端

访客端


怎么做到的?技术详解

使用NetworkStream的TCP服务器

在Pipelines之前用.NET编写的典型代码如下所示:

async Task ProcessLinesAsync(NetworkStream stream)
{
    var buffer = new byte[1024];
    await stream.ReadAsync(buffer, 0, buffer.Length);

    // 在buffer中处理一行消息
    ProcessLine(buffer);
}

此代码可能在本地测试时正确工作,但它有几个潜在错误:

一次ReadAsync调用可能没有收到整个消息(行尾)。
它忽略了stream.ReadAsync()返回值中实际填充到buffer中的数据量。(译者注:即不一定将buffer填充满)
一次ReadAsync调用不能处理多条消息。
这些是读取流数据时常见的一些缺陷。为了解决这个问题,我们需要做一些改变:

我们需要缓冲传入的数据,直到找到新的行。
我们需要解析缓冲区中返回的所有行

async Task ProcessLinesAsync(NetworkStream stream)
{
    var buffer = new byte[1024];
    var bytesBuffered = 0;
    var bytesConsumed = 0;

    while (true)
    {
        var bytesRead = await stream.ReadAsync(buffer, bytesBuffered, buffer.Length - bytesBuffered);
        if (bytesRead == 0)
        {
            // EOF 已经到末尾
            break;
        }
        // 跟踪已缓冲的字节数
        bytesBuffered += bytesRead;

        var linePosition = -1;

        do
        {
            // 在缓冲数据中查找找一个行末尾
            linePosition = Array.IndexOf(buffer, (byte)'\n', bytesConsumed, bytesBuffered - bytesConsumed);

            if (linePosition >= 0)
            {
                // 根据偏移量计算一行的长度
                var lineLength = linePosition - bytesConsumed;

                // 处理这一行
                ProcessLine(buffer, bytesConsumed, lineLength);

                // 移动bytesConsumed为了跳过我们已经处理掉的行 (包括\n)
                bytesConsumed += lineLength + 1;
            }
        }
        while (linePosition >= 0);
    }
}

这一次,这可能适用于本地开发,但一行可能大于1KiB(1024字节)。我们需要调整输入缓冲区的大小,直到找到新行。

因此,我们可以在堆上分配缓冲区去处理更长的一行。我们从客户端解析较长的一行时,可以通过使用ArrayPool避免重复分配缓冲区来改进这一点。

async Task ProcessLinesAsync(NetworkStream stream)
{
    byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
    var bytesBuffered = 0;
    var bytesConsumed = 0;

    while (true)
    {
        // 在buffer中计算中剩余的字节数
        var bytesRemaining = buffer.Length - bytesBuffered;

        if (bytesRemaining == 0)
        {
            // 将buffer size翻倍 并且将之前缓冲的数据复制到新的缓冲区
            var newBuffer = ArrayPool<byte>.Shared.Rent(buffer.Length * 2);
            Buffer.BlockCopy(buffer, 0, newBuffer, 0, buffer.Length);
            // 将旧的buffer丢回池中
            ArrayPool<byte>.Shared.Return(buffer);
            buffer = newBuffer;
            bytesRemaining = buffer.Length - bytesBuffered;
        }

        var bytesRead = await stream.ReadAsync(buffer, bytesBuffered, bytesRemaining);
        if (bytesRead == 0)
        {
            // EOF 末尾
            break;
        }

        // 跟踪已缓冲的字节数
        bytesBuffered += bytesRead;

        do
        {
            // 在缓冲数据中查找找一个行末尾
            linePosition = Array.IndexOf(buffer, (byte)'\n', bytesConsumed, bytesBuffered - bytesConsumed);

            if (linePosition >= 0)
            {
                // 根据偏移量计算一行的长度
                var lineLength = linePosition - bytesConsumed;

                // 处理这一行
                ProcessLine(buffer, bytesConsumed, lineLength);

                // 移动bytesConsumed为了跳过我们已经处理掉的行 (包括\n)
                bytesConsumed += lineLength + 1;
            }
        }
        while (linePosition >= 0);
    }
}

这段代码有效,但现在我们正在重新调整缓冲区大小,从而产生更多缓冲区副本。它将使用更多内存,因为根据代码在处理一行行后不会缩缓冲区的大小。为避免这种情况,我们可以存储缓冲区序列,而不是每次超过1KiB大小时调整大小。

此外,我们不会增长1KiB的 缓冲区,直到它完全为空。这意味着我们最终传递给ReadAsync越来越小的缓冲区,这将导致对操作系统的更多调用。

为了缓解这种情况,我们将在现有缓冲区中剩余少于512个字节时分配一个新缓冲区:

public class BufferSegment
{
    public byte[] Buffer { get; set; }
    public int Count { get; set; }

    public int Remaining => Buffer.Length - Count;
}

async Task ProcessLinesAsync(NetworkStream stream)
{
    const int minimumBufferSize = 512;

    var segments = new List<BufferSegment>();
    var bytesConsumed = 0;
    var bytesConsumedBufferIndex = 0;
    var segment = new BufferSegment { Buffer = ArrayPool<byte>.Shared.Rent(1024) };

    segments.Add(segment);

    while (true)
    {
        // Calculate the amount of bytes remaining in the buffer
        if (segment.Remaining < minimumBufferSize)
        {
            // Allocate a new segment
            segment = new BufferSegment { Buffer = ArrayPool<byte>.Shared.Rent(1024) };
            segments.Add(segment);
        }

        var bytesRead = await stream.ReadAsync(segment.Buffer, segment.Count, segment.Remaining);
        if (bytesRead == 0)
        {
            break;
        }

        // Keep track of the amount of buffered bytes
        segment.Count += bytesRead;

        while (true)
        {
            // Look for a EOL in the list of segments
            var (segmentIndex, segmentOffset) = IndexOf(segments, (byte)'\n', bytesConsumedBufferIndex, bytesConsumed);

            if (segmentIndex >= 0)
            {
                // Process the line
                ProcessLine(segments, segmentIndex, segmentOffset);

                bytesConsumedBufferIndex = segmentOffset;
                bytesConsumed = segmentOffset + 1;
            }
            else
            {
                break;
            }
        }

        // Drop fully consumed segments from the list so we don't look at them again
        for (var i = bytesConsumedBufferIndex; i >= 0; --i)
        {
            var consumedSegment = segments[i];
            // Return all segments unless this is the current segment
            if (consumedSegment != segment)
            {
                ArrayPool<byte>.Shared.Return(consumedSegment.Buffer);
                segments.RemoveAt(i);
            }
        }
    }
}

(int segmentIndex, int segmentOffest) IndexOf(List<BufferSegment> segments, byte value, int startBufferIndex, int startSegmentOffset)
{
    var first = true;
    for (var i = startBufferIndex; i < segments.Count; ++i)
    {
        var segment = segments[i];
        // Start from the correct offset
        var offset = first ? startSegmentOffset : 0;
        var index = Array.IndexOf(segment.Buffer, value, offset, segment.Count - offset);

        if (index >= 0)
        {
            // Return the buffer index and the index within that segment where EOL was found
            return (i, index);
        }

        first = false;
    }
    return (-1, -1);
}

此代码只是得到很多更加复杂。当我们正在寻找分隔符时,我们同时跟踪已填充的缓冲区序列。为此,我们此处使用List查找新行分隔符时表示缓冲数据。其结果是,ProcessLine和IndexOf现在接受List作为参数,而不是一个byte[],offset和count。我们的解析逻辑现在需要处理一个或多个缓冲区序列。

我们的服务器现在处理部分消息,它使用池化内存来减少总体内存消耗,但我们还需要进行更多更改:

  • 我们使用的byte[]和ArrayPool的只是普通的托管数组。这意味着无论何时我们执行ReadAsync或WriteAsync,这些缓冲区都会在异步操作的生命周期内被固定(以便与操作系统上的本机IO API互操作)。这对GC有性能影响,因为无法移动固定内存,这可能导致堆碎片。根据异步操作挂起的时间长短,池的实现可能需要更改。
  • 可以通过解耦读取逻辑和处理逻辑来优化吞吐量。这会创建一个批处理效果,使解析逻辑可以使用更大的缓冲区块,而不是仅在解析单个行后才读取更多数据。这引入了一些额外的复杂性
  • 我们需要两个彼此独立运行的循环。一个读取Socket和一个解析缓冲区。
  • 当数据可用时,我们需要一种方法来向解析逻辑发出信号。
  • 我们需要决定如果循环读取Socket“太快”会发生什么。如果解析逻辑无法跟上,我们需要一种方法来限制读取循环(逻辑)。这通常被称为“流量控制”或“背压”。
  • 我们需要确保事情是线程安全的。我们现在在读取循环和解析循环之间共享多个缓冲区,并且这些缓冲区在不同的线程上独立运行。
  • 内存管理逻辑现在分布在两个不同的代码段中,从填充缓冲区池的代码是从套接字读取的,而从缓冲区池取数据的代码是解析逻辑。
  • 我们需要非常小心在解析逻辑完成之后我们如何处理缓冲区序列。如果我们不小心,我们可能会返回一个仍由Socket读取逻辑写入的缓冲区序列。

在线体验或下载完整私有化部署包:

https://kf.shengxunwei.com

希望能够打造: 开放、开源、共享。努力打造 .net 社区的一款优秀开源产品。

钟意的话请给个赞支持一下吧,谢谢~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2164220.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于SSM+Vue+MySQL的农家乐预约管理系统

系统展示 用户前台界面 管理员后台界面 农家乐后台界面 系统背景 随着乡村旅游的兴起&#xff0c;农家乐作为一种结合农业体验与休闲度假的新型旅游模式&#xff0c;受到了广大游客的青睐。然而&#xff0c;传统农家乐在预约管理方面存在效率低下、信息不透明等问题&#xff0c…

头戴式耳机百元测评推荐有哪些?头戴式耳机性价比排名推荐!

在音频设备日益普及的今天&#xff0c;头戴式耳机因其出色的音质和舒适度备受欢迎。然而&#xff0c;面对市场上琳琅满目的百元耳机&#xff0c;消费者常常感到困惑。头戴式耳机百元测评推荐有哪些&#xff1f;在这篇文章中&#xff0c;我们将测评并推荐几款性价比极高的头戴式…

Python爬虫之requests模块(一)

Python爬虫之requests模块&#xff08;一&#xff09; 学完urllib之后对爬虫应该有一定的了解了&#xff0c;随后就来学习鼎鼎有名的requests模块吧。 一、requests简介。 1、什么是request模块&#xff1f; requests其实就是py原生的一个基于网络请求的模块&#xff0c;模拟…

第一届POLARDB数据库性能大赛-亚军0xCC☣☢比赛攻略

关联比赛: 第一届POLARDB数据库性能大赛 1 赛题分析 本次大赛的初赛和复赛的赛题内容是一脉相传的&#xff0c;主要内容都是实现一个KV数据库存储引擎&#xff0c;实现随机插入&#xff0c;随机查询&#xff0c;区间查询这三个功能。赛题的难点主要有两个&#xff1a;1、实现在…

C#常用数据结构栈的介绍

定义 在C#中&#xff0c;Stack<T> 是一个后进先出&#xff08;LIFO&#xff0c;Last-In-First-Out&#xff09;集合类&#xff0c;位于System.Collections.Generic 命名空间中。Stack<T> 允许你将元素压入栈顶&#xff0c;并从栈顶弹出元素。 不难看出&#xff0c;…

图片如何转化为pdf格式?这几种方法超好用!

图片如何转化为pdf格式&#xff1f;在日常工作与学习中&#xff0c;图片与PDF文件作为两种截然不同的文档格式&#xff0c;各自扮演着重要角色&#xff0c;图片以其直观性著称&#xff0c;能够瞬间捕捉并展示视觉信息&#xff0c;无需额外软件即可快速浏览&#xff0c;但其内容…

免费制作证件照的小程序源码

1、效果展示 证件照制作&#xff0c;证件照免费制作&#xff0c;证件照调用api源码&#xff0c;解析代码。证件照制作小程序包&#xff0c;可以下载程序包&#xff0c;最初级版本免费下载。以上是高级版本。如果你有开发能力的话可以自己写前端&#xff0c;然后以下调用以下api…

粉丝精准!小红书卖儿童绘本项目,单月变现近2w(附详细教程)

AI绘本故事以其创新性、个性化、互动性和教育意义&#xff0c;迎合了宝爸宝妈对高质量儿童读物的需求&#xff0c;同时融合科技与教育&#xff0c;满足了他们对孩子全面发展的期待&#xff0c;因此在小红书上备受追捧。 今天给大家分享一个【小红书卖儿童绘本】项目&#xff0…

Spring Boot使用注解方式整合MyBatis

文章目录 实战讲稿&#xff1a;Spring Boot使用注解方式整合MyBatis课程目标课程内容1. 创建员工映射器接口1.1 创建子包1.2 创建接口 2. 测试员工映射器接口2.1 自动装配员工映射器2.2 测试按标识符查询员工方法2.3 测试查询全部员工方法2.4 测试插入员工方法2.5 测试更新员工…

如何保持测试环境的稳定性?

日常自动化测试中最担心的就是环境不稳定问题。不稳定的测试环境&#xff0c;经常可能导致测试失败。 解决方法&#xff1a;尽量保持测试环境的稳定性&#xff0c;包括硬件、软件和网络等方面。 如何保持测试环境的稳定性&#xff1f; 要保持测试环境的稳定性&#xff0c;可…

从零开始,Docker进阶之路(二):Docker安装

Docker 要求 CentOS7 系统的内核版本在 3.10以上 1.通过 uname -r 命令查看你当前的内核版本 uname -r2. 使用 root 权限登录 Centos。确保 yum 包更新到最新。 yum -y update 慢慢等&#xff0c;小编也是等了十分钟之久 3.卸载旧版本(如果安装过旧版本的话) yum remove do…

元宇宙的未来趋势:Web3的潜在影响

元宇宙&#xff0c;一个日益受到关注的概念&#xff0c;代表着一个沉浸式的虚拟世界&#xff0c;其中用户可以进行社交、商业和娱乐活动。随着技术的发展&#xff0c;Web3逐渐成为推动元宇宙演变的重要力量。Web3以去中心化为核心&#xff0c;利用区块链、智能合约和其他创新技…

加密软件巅峰对决:Ping32 vs 天锐绿盾,谁是企业数据安全的守护者之王?

在信息安全日益重要的今天&#xff0c;企业加密软件已成为保护敏感数据的关键工具。在众多加密产品中&#xff0c;Ping32与天锐绿盾&#xff08;简称绿盾&#xff09;凭借其卓越的性能和丰富的功能&#xff0c;成为了企业用户关注的焦点。那么&#xff0c;在这场加密软件的巅峰…

干货分享 | TSMaster—LIN 唤醒与休眠机制

在汽车总线中常见的唤醒方式有硬线唤醒、网络唤醒和特定信号唤醒&#xff0c;而LIN总线则是通过休眠帧与唤醒电平来实现的&#xff0c;本文将介绍LIN的唤醒与休眠机制。 本文关键词&#xff1a;LIN 网络管理&#xff0c;休眠&#xff0c;唤醒 目录 Catalog 1. 网络管理 2. …

vue3开发中易遗漏的常见知识点

文章目录 组件样式的特性Scoped CSS之局部样式的泄露Scoped CSS之深度选择器CSS Modules在CSS中使用v-bind 非props属性继承组件通信父子组件的相互通信props/$emit父组件传递数据给子组件子组件传递数据给父组件 非父子组件的相互通信Provide/inject全局事件总线 组件插槽作用…

用Python与OpenCV的实践:实时面部对称性分析

目录 思路分析 整体代码 效果展示 总结 在当今计算机视觉领域&#xff0c;人脸识别和分析技术得到了广泛应用。无论是安全验证、社交媒体应用&#xff0c;还是美学研究&#xff0c;人脸特征的提取和分析都是关键技术之一。在这篇博客中&#xff0c;我们将深入探讨一个有趣的…

Arco HomeMenu - 无入侵式的个性化菜单配置插件

关于 Arco HomeMenu Arco HomeMenu 插件是一款对 odoo 菜单功能的增强工具&#xff0c;它的主要功能是允许用户个性化菜单收藏。主要通过分类文件夹及布局功能实现。 Arco HomeMenu 插件主要用于优化用户在 odoo 系统中的操作体验。通过插件功能&#xff0c;用户可以根据自己的…

innovus:如何报告SI

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 报告SI首先要设置si aware,报

79、Python之鸭子类型:没有听过鸭子类型?关键在于认知的转变

引言 不同于Java等静态类型的语言&#xff0c;Python基于动态类型系统的设计理念&#xff0c;使得Python在很多应用场景中&#xff0c;显得更急灵活、高效。而在动态类型系统中&#xff0c;有一个很重要的概念&#xff0c;就是“鸭子类型”。鸭子类型的背后&#xff0c;代表的…

一地通过率高达46.43%!为什么都说软考难?

从2023年上半年到2024年上半年&#xff0c;近三次考试&#xff0c;几个考区的软考通过率基本不超过13%。 然而根据近日陕西省科技资源统筹中心公布的数据&#xff0c;从1987年到2024年&#xff0c;陕西软考的总拿证率竟然高达46.43%。软考真的有大家认为的那么难吗&#xff1f;…