设计C/S架构应用程序的并发功能

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

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

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

需求与设计

1 正常的顺序是先启动服务器,再启动客户端主程序。若是启动客户端主程序时,链接不上服务器,要报错并终止程序。数据库

2 运行过程当中,服务器可能因各类状况中止工做。好比杀度软件扫描,停电等缘由,这时咱们的客户端主程序要能检测到服务器岩机,挂起当前界面。windows

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

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

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

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

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

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

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();
相关文章
相关标签/搜索