設計C/S架構應用程序的並發功能


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();


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM