設計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();
相關文章
相關標籤/搜索