C/S架构的ERP、CRM程序有的是以并发点(Concurrency)来销售,并发点是指同时在线人数。并发数量大时,理论上程序的运行速度会慢,软件供应商(vendor)也以控制并发的上限以解决客户对系统性能的抱怨。我接触到的一个ERP系统,它的定价策略如下表所示:

序号 并发用户 价格
1 5个以下 每用户20000,总价小于10万
2 5-20 每用户15000,总价小于30万
3 20-50 每用户12000,总价小于60万
4 50个以 每用户10000,总价最小50万

从软件开发的角度,我来分享一下我对并发功能的设计与实现。

需求与设计

1 正常的顺序是先启动服务器,再启动客户端主程序。如果启动客户端主程序时,连接不上服务器,要报错并终止程序。

2 运行过程中,服务器可能因各种情况停止工作。比如杀度软件扫描,停电等原因,这时我们的客户端主程序要能检测到服务器岩机,挂起当前界面。

为了减少这种事情发生的概率,我建议在服务器中安装程序AlwaysUp。

AlwaysUp能将可执行文件、批处理文件及快捷方式作为windows系统服务,并且进行管理和监视确保100%运行。当程序崩溃、挂起、弹出错误对话框时,AlwaysUp 能自动重启程序,并运行自定义的检查功能确保程序一直可用。AlwaysUp 能发送详细的email使你清楚地了解崩溃、重启等事件。

详细信息参考以下地址 http://www.0daydown.com/07/314246.html

3 我们的C/S程序有两种运行模式。第一种是客户端主程序与服务器不在同一台机器上,两个进程运行在物理隔离的两台电脑中,第二种就是客户端主程序与服务器都运行在服务器中,客户端以远程桌面的方式运行。

前一种模式好理解,两台机器之前以.NET通信机制(.NET Remoting,WCF)交互,后一种模式两个进程实际是运行在同一部电脑中,在并发控制上这两者有区别。

我们来看一下C#中的进程(Process)的定义,地址在

https://msdn.microsoft.com/en-us/library/system.diagnostics.process(v=vs.110).aspx

里面有一个SessionId的属性,它的含义如下

Gets the Terminal Services session identifier for the associated process.  获取进程的终端服务的会话标识。

在程序开发时为了识别是否是相同的并发,前者只需要根据IP地址或MAC地址,后者则需要根据SessionId来识别。

这个知识点的重要性在于,用户A已经登录过,在另一台电脑或会话中用户A再次登录时,系统要可以识别出来,要么阻止重复的登录,要么踢出前一个登录,要么刷新登录会话。

4 我们从数据的操作角度对并发用户作两个分组,一组是可编辑数据的用户,另一组是只读用户(readonly)。公司的主管,经理层或是总经理层,常常是查询报表,他们不需要操作数据。由于查询数据对服务器的压力要少很多(事务),所以一般在销售并发用户的时候,还会赠送相应数量的查看用户数。

5 用户之间关系的处理。管理员可以踢出用户,用户之间可以发送消息通知,管理员可以强制所有用户下线(由于系统需要进行重大更新,系统重要业务处理(月结,年结,期末处理等))。

6 运行过程中,客户端意外终止。比如一个耗费时间的操作(MRP运算,工作单发料,产品完工),用户在等待过程中失去耐心,强制杀死运行中的进程。这时因为没有调用Logoff方法清除服务器中的进程会话。如果再次启动登录时,可能会提示会话已经存在,或是登录用户超过最大许可数。

前面提到由于有心跳机制,服务器进程死去,客户端进程要挂起(阻止用户任何输入,暴力一点的方法是退出)。

这一点提到服务器运行正常,客户端意外终结,完全没有时机去通知服务器我已经下线。我们的处理方法是服务器每5分钟轮循一次客户端,检测到会话所在的客户端进程无法回应,则主动清除会话信息,以便于客户端下一次正常登录。

7  我们对许可机制有严格的要求,安装完成ERP后,会给当前机器环境生成一个签名文件,这个文件附注于许可文件中。运行时我们会检测当前运行的机器是否与许可文件中的机器签名匹配。

获取电脑配置可参考下面的方地:

private static string GetDiskDriveSignature()
{
    return WmiHelper.GetWmiPropertyValue("Win32_DiskDrive", "Signature");
}
private static string GetDiskDriveSize()
{
     return WmiHelper.GetWmiPropertyValue("Win32_DiskDrive", "Size");
}
private static string GetDiskDriveTotalTracks()
{
     return WmiHelper.GetWmiPropertyValue("Win32_DiskDrive", "TotalTracks");
}

从代码中可以看出,是使用WMI。

这是服务器中的许可验证方法,客户端程序因为有并发数量控制,不验证许可文件和它的签名。

8 阻止服务器程序被第三方恶意API调用

.NET Remoting的服务端代码例子:

static void Main(string[] args)
{
   TcpChannel channel = new TcpChannel(8080);
   ChannelServices.RegisterChannel(channel, false);
   RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemotingObjects.Person), "RemotingPersonService", WellKnownObjectMode.SingleCall);

   System.Console.WriteLine("Server:Press Enter key to exit");
   System.Console.ReadLine();
}

.NET Remoting客户端程序例子:

TcpChannel channel = new TcpChannel();
ChannelServices.RegisterChannel(channel, false);
IPerson obj = (IPerson)Activator.GetObject(typeof(RemotingObjects.IPerson), "tcp://localhost:8080/RemotingPersonService");
string userName=obj.GetName();

最后一行我们调用了服务器中的程序。如果服务器程序被恶意人员获取,可以很容易的构造出客户端程序进行调用,服务器完全不知觉。为解决这个问题,我们的设计方案是客户端登录时,将当前环境因素(IP地址,电脑名,MAC地址,程序集版本与哈希值等)组合发送到服务器中,经过一个特定的算法,得出一个哈希值,登录完成后(用户名密码正确,权限允许)返回给客户端,客户端也以之前的环境变量进行算法计算,将这两者的值比较,若相等则允许登录。

这为恶意调用服务器程序增加了难度。

9  服务器会话

说穿不值一文钱,其实就是个DataTable对象,当有用户登录(Login)时,增加会话记录。用户注销(Logout)时,清除会画记录。

也可以学习ASP.NET的Session对象的设计思路,参考这里 Exploring Session in ASP.NET

DataTable因为操作上的不方便,后期维护的时候,我们把它完善成强类型对象。

[Serializable]
public class Session
{
  public string SessionId  { get;set;}
  public string UserId  { get;set;}
  public string UserGroup { get;set;}
  public string MachineName { get;set;}
......
}

public class SessionCollection :List<Session>
{

}

//经过OOP的封装,调用时比DataTable要方便
Singleton<SessionCollection>  sessions.....;
sessions.Add(new Session());

10  日志记录

记录客户端登录日志,数据库表设计

1) 日志主表 UserLog(LogNo,LoginTime,LogoutTime,Profile)

纪录登入和注销时间,如果是客户端进程被强制杀死,LogoutTime常常是没有值。对于进程意外终止,用下面的方法不能截获终止前回调事件。

AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CustomExceptionHandler.CurrentDomain_UnhandledException);
Application.ThreadException += new ThreadExceptionEventHandler(eh.OnThreadException);

2)  日志明细表 UserLogDetail(LogNo,SeqNo,FunctionCode,OpenTime,CloseTime)

记录用户登录后,执行了哪些系统功能,持续了多长时间。

3 ) 日志数量表 UserLogDetailAction(LogNo,SeqNo,Remark)

记录用户登录后,操作了哪个功能的哪一笔数据。是做了编辑操作,还是执行过帐。Remark的算法如下

Entity   salesOrder=...
StringBuilder builder=new StringBuilder();
foreach(IField field in salesOrder.Fields)
{
     if(field.IsPrmaryKey)
        builder.Append(field.Name+filed.CurrentValue);
}
string remark=builder.ToString();

UserLog用户日志主表的最后一个字段Profiler,是一个后门,它记录了登录ERP系统的当前登录用户的本机电脑的几乎所有信息,相当于一个隐私收集工具。在审计(audit)的时候,我们可以用于帮忙用户澄清一些不必要的错误。

比如ERP的各部门主管常常是将ERP账户与密码给下面的同事,让他们帮忙获取数据,而自己常常是不进入系统的。

高一级的权限放开给不合理的人员,增加了系统的风险,而这个Profiler可以在一定程度上避免这种情况发生。

大公司的IT审计一看即可知道此登录的用户电脑不具备此高级权限。不过为了维护用户的声誉,我们对此功能做了选择性的处理,在实施时根据自己的实际需要去选择,默认情况下并不会进行隐私收集。

模拟测试

可以通过多开几个虚拟机来模拟测试并发,虚拟机与主机之前的连接方式如下:

1) 主机与虚拟机设为同一个网段的IP地址,比如192.168.1.100,192.168.1.101

2) 虚拟机与主机之间的网络连接方式设置为桥接(Bridge)

在测试并发时,可以将服务器端驻留在物理主机中,启动VS并开启调试模式。如果是以Windows服务存在,可以在程序中添加以下代码来强制附加调试器。

if(!Debugger.IsAttached)
   Debugger.Launch();

设计C/S架构应用程序的并发功能的更多相关文章

  1. 从一般分布式设计看HDFS设计思想与架构

     要想深入学习HDFS就要先了解其设计思想和架构,这样才能继续深入使用HDFS或者深入研究源代码.懂得了"所以然"才能在实际使用中灵活运用.快速解决遇到的问题.下面这篇博文我们就先 ...

  2. 16套java架构师,高并发,高可用,高性能,集群,大型分布式电商项目实战视频教程

    16套Java架构师,集群,高可用,高可扩展,高性能,高并发,性能优化,设计模式,数据结构,虚拟机,微服务架构,日志分析,工作流,Jvm,Dubbo ,Spring boot,Spring cloud ...

  3. 移动App设计之分层架构+MVC

    http://www.cnblogs.com/Logen/archive/2012/11/08/2760638.html 场景分析:我们知道,一个移动设备的应用大多与网络有关,也就是说,我在移动设备上 ...

  4. .NET Core实战项目之CMS 第九章 设计篇-白话架构设计

    前面两篇文章给大家介绍了我们实战的CMS系统的数据库设计,源码也已经上传到服务器上了.今天我们就好聊聊架构设计,在开始之前先给大家分享一下这几天我一直在听的<从零开始学架构>里面关于架构设 ...

  5. IM即时通讯:如何跳出传统思维来设计聊天室架构?

    因为视频直播业务的大规模扩张,聊天室这种功能在最近几年又火了起来.本篇文章将会重点挑选聊天室这个典型场景,和大家分享一下网易云信在实现这个功能时是如何做架构设计的. 相关推荐阅读几十万人同时在线的直播 ...

  6. 从0开发3D引擎(十):使用领域驱动设计,从最小3D程序中提炼引擎(上)

    目录 上一篇博文 下一篇博文 前置知识 回顾上文 最小3D程序完整代码地址 通用语言 将会在本文解决的不足之处 本文流程 解释本文使用的领域驱动设计的一些概念 本文的领域驱动设计选型 设计 引擎名 识 ...

  7. 从0开发3D引擎(十一):使用领域驱动设计,从最小3D程序中提炼引擎(第二部分)

    目录 上一篇博文 本文流程 回顾上文 解释基本的操作 开始实现 准备 建立代码的文件夹结构,约定模块文件的命名规则 模块文件的命名原则 一级和二级文件夹 api_layer的文件夹 applicati ...

  8. TYPESDK手游聚合SDK服务端设计思路与架构之一:应用场景分析

    TYPESDK 服务端设计思路与架构之一:应用场景分析 作为一个渠道SDK统一接入框架,TYPESDK从一开始,所面对的需求场景就是多款游戏,通过一个统一的SDK服务端,能够同时接入几十个甚至几百个各 ...

  9. SOA架构设计经验分享—架构、职责、数据一致性

    阅读目录: 1.背景介绍 2.SOA的架构层次 2.1.应用服务(原子服务) 2.2.组合服务 2.3.业务服务(编排服务) 3.SOA化的重构 3.1.保留服务空间,为了将来服务的组合 4.运用DD ...

随机推荐

  1. 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例

    前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...

  2. VisualVM通过jstatd方式远程监控远程主机

    配置好权限文件 [root@test bin]# cd $JAVA_HOME/bin [root@test bin]# vim jstatd.all.policy grant codebase &qu ...

  3. 菜鸟学Struts2——Struts工作原理

    在完成Struts2的HelloWorld后,对Struts2的工作原理进行学习.Struts2框架可以按照模块来划分为Servlet Filters,Struts核心模块,拦截器和用户实现部分,其中 ...

  4. 04.LoT.UI 前后台通用框架分解系列之——轻巧的弹出框

    LOT.UI分解系列汇总:http://www.cnblogs.com/dunitian/p/4822808.html#lotui LoT.UI开源地址如下:https://github.com/du ...

  5. [C#] C# 知识回顾 - 你真的懂异常(Exception)吗?

    你真的懂异常(Exception)吗? 目录 异常介绍 异常的特点 怎样使用异常 处理异常的 try-catch-finally 捕获异常的 Catch 块 释放资源的 Finally 块 一.异常介 ...

  6. Asp.Net WebApi核心对象解析(上篇)

    生活需要自己慢慢去体验和思考,对于知识也是如此.匆匆忙忙的生活,让人不知道自己一天到晚都在干些什么,似乎每天都在忙,但又好似不知道自己到底在忙些什么.不过也无所谓,只要我们知道最后想要什么就行.不管怎 ...

  7. Solr高级查询Facet

    一.什么是facet solr种以导航为目的的查询结果成为facet,在用户查询的结果上根据分类增加了count信息,然后用户根据count信息做进一步搜索. facet主要用于导航实现渐进式精确搜索 ...

  8. 分页插件--根据Bootstrap Paginator改写的js插件

    刚刚出来实习,之前实习的公司有一个分页插件,和后端的数据字典约定好了的,基本上是看不到内部是怎么实现的,新公司是做WPF的,好像对于ASP.NET的东西不多,导师扔了一个小系统给我和另一个同事,指了两 ...

  9. ABP领域层

    1.实体Entites 1.1 概念 实体是DDD(领域驱动设计)的核心概念之一. 实体是具有唯一标识的ID且存储在数据库总.实体通常被映射成数据库中的一个表. 在ABP中,实体继承自Entity类. ...

  10. 使用HTML5的cavas实现的一个画板

    <!DOCTYPE html><html><head> <meta charset="utf-8"> <meta http-e ...