一、為DataSnap系統服務程式添加描述
這幾天一直在研究Delphi 2010的DataSnap,感覺功能真是很強大,現在足有理由證明Delphi7該下崗了。
DataSnap有三種服務模式,其中Service Application方式建立的Windows服務沒有描述,描述部分是空的,可用如下方法添加服務描述:
procedure TServerContainer.ServiceAfterInstall(Sender: TService);
var
reg: TRegistry;
begin
reg := TRegistry.Create;
try
with reg do
begin
RootKey := HKEY_LOCAL_MACHINE;
if OpenKey('SYSTEM/CurrentControlSet/Services/' + Self.Name, false) then
begin
WriteString('Description', '機房管理系統核心服務');
end;
CloseKey;
end;
finally
reg.Free;
end;
end;
二、DataSnap服務端和用戶端釋出分發方法
伺服器釋出方法:
1.在unit ServerMethodsUnit1單元中,添加uses MidasLib;(添加MidasLib的目的是省去釋出Midas.dll)
2.如果用的是火鳥資料庫,隻需拷貝dbxfb.dll和fbclient.dll,如果用的是SQLite,則什麼都不用拷貝。
分發的伺服器軟體隻需三個檔案:你的伺服器程式、dbxfb.dll 和 fbclient.dll
用戶端釋出方法:
1.在用戶端程式中加上uses MidasLib;(添加MidasLib的目的是省去釋出Midas.dll)
2.如果伺服器使用了http協定作為DataSnap通訊的話,還需在用戶端程式中加上users DSHTTPLayer,如果使用tcp協定,無需此步驟。
分發的用戶端軟體隻需一個檔案:你的用戶端程式
伺服器和用戶端無需Midas.dll,也不需要注冊regsvr32 Midas.dll,看來Delphi2010的datasnap抛棄使用COM真是進步不少!
三、DataSnap伺服器如何得到用戶端的IP和端口
作為一個伺服器軟體,必須做到對用戶端強有力的控制,想要控制,就必須得到用戶端的網絡基本資訊,比如用戶端IP和端口。有了用戶端IP就能随心所欲操控用戶端,比如終止某些用戶端的連接配接、限制功能等等。
在Delphi2010中的DataSnap伺服器如何獲得用戶端ip,的确花了我點時間,奇怪為什麼這個功能不做的更人性化點呢,功能總是藏着掖着。還得讓程式員像尋寶一樣摸索,浪費時間。現在把我整理的結果奉獻給大家,免得大家在花時間研究這個。
另外,通過研究發現,DSConnectEventObject.ChannelInfo.Id屬性實際上是記憶體位址,并不是一個簡單的數字。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cGcq5SNzMDO3cDM0EzM2MWOwUTNzYzX2QDMzcTMzIzLcBTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.jpg)
以下代碼中if .. then 裡面的内容是關鍵。
uses IdTCPConnection;
//......
procedure TServerContainer1.DSServer1Connect(DSConnectEventObject: TDSConnectEventObject);
var
ClientConnection: TIdTCPConnection;
begin
with Form1 do
begin
dsShowDataSet.Append;
dsShowDataSet['ClientConnectTime'] := Now;
if DSConnectEventObject.ChannelInfo <> nil then
begin
ClientConnection := TIdTCPConnection(DSConnectEventObject.ChannelInfo.Id);
dsShowDataSet['ClientID'] := DSConnectEventObject.ChannelInfo.Id;
dsShowDataSet['ClientIP'] := ClientConnection.Socket.Binding.PeerIP +
':' + IntToStr(ClientConnection.Socket.Binding.PeerPort);
dsShowDataSet['ServerIP'] := ClientConnection.Socket.Binding.IP + ':' +
IntToStr(ClientConnection.Socket.Binding.Port);
end;
dsShowDataSet['ClientUserName'] := DSConnectEventObject.ConnectProperties
[TDBXPropertyNames.UserName];
dsShowDataSet['ClientUserPassword'] :=
DSConnectEventObject.ConnectProperties[TDBXPropertyNames.Password];
dsShowDataSet['ServerInfo'] := DSConnectEventObject.ConnectProperties
[TDBXPropertyNames.ServerConnection];
dsShowDataSet.Post;
end;
end;
或者也可以這樣:
procedure TServerContainer1.DSServer1Connect(DSConnectEventObject: TDSConnectEventObject);
var
ClientConnection: TDBXClientInfo;
Val: TTCP_KeepAlive;
Ret: DWord;
begin
ClientConnection := DSConnectEventObject.ChannelInfo.ClientInfo;
AddLog(
ClientConnection.IpAddress
+':'+
ClientConnection.ClientPort
+'登入伺服器');
UpdateLinkToList(ClientConnection.IpAddress
+':'+
ClientConnection.ClientPort
,IntToStr(DSConnectEventObject.ChannelInfo.Id)
,0);
end;
四、DataSnap中的TCP keepAlive和KeepAliveInterval(心跳包)參數詳解
Delphi2010中DataSnap,如果用戶端異常掉線或拔掉網線,那麼在服務端會留下一個TCP連接配接,這個連接配接會變成死連接配接(經過測試,如果windows的TCP保持連接配接禁用的話,三個小時該死連接配接還不消失)。如果大量用戶端并發,出現的死TCP連接配接過多,伺服器記憶體和端口将會增加,直到占滿伺服器的端口和耗盡記憶體為止。如果這樣的話,伺服器無法健壯穩定的運作。
大家可以另開線程來監控用戶端連接配接,但是今天要給大家講解的不是這個方法,而是使用TCP協定自帶的心跳包功能解決這個問題。
大家先了解一下 TCP keep-alive原理
一個TCP keep-alive 包是一個簡單的ACK,該ACK包内容為一個比目前連接配接sequence number 小于一的包。主機接受到這些ACKs會返
回一個包含目前sequence number 的ACK包。
Keep-alives一般被用來驗證遠端連接配接是否有效。如果該連接配接上沒有其他資料被傳輸,或者更高level 的 keep-alives被傳送,keep-alives 在每個KeepAliveTime被發送。(預設是 7,200,000 milliseconds ,也就是2個小時)。
如果沒有收到 keep-alive 應答,keep-alive 将在每 KeepAliveInterval 秒重發一次。KeepAliveInterval 預設為1秒。如 Microsoft 網絡功能中很多部分中采用的 NETBT 連接配接,更常見的是發送 NETBios keep-alives,是以,在 NetBios 連接配接中通常不發送TCP keep-alives。
TCP保持連接配接預設被禁用,但是微軟Sockets應用程式可以使用SetSockOpt函數去啟用他們。
請看下面的類
type
TCP_KeepAlive = record
OnOff: Cardinal;
KeepAliveTime: Cardinal; // 多長時間(ms)沒有資料就開始send心跳包
KeepAliveInterval: Cardinal // 每隔多長時間(ms)send一個心跳包,發5次(系統值)
end;
KeepAliveTime: TCP連接配接多長時間(毫秒)沒有資料就開始發送心跳包,有資料傳遞的時候不發送心跳包
KeepAliveInterval: 每隔多長時間(毫秒)發送一個心跳包,發5次(系統預設值)
如果用戶端網絡中斷,伺服器系統發送心跳包後,伺服器會自動解除TCP連接配接。這一點,大家可以使用 netstat -p -tcp 指令檢視
接下來我們将結合Delphi2010 DataSnap技術使用心跳包功能!敬請關注
五、建立穩定服務程式之TCP心跳包的使用
為了能讓我們的服務程式更加穩定,有些細節問題必須解決。就如上一講中提到的用戶端拔掉網線,造成伺服器上TCP變成死連接配接,如果死連接配接數量過多,對伺服器能長期穩定運作是一個巨大的威脅。
另外,經過測試,如果伺服器上有TCP死連接配接,那麼服務程式連接配接資料庫,也會産生那個一個死連接配接。這樣的話,給資料庫伺服器也造成威脅。是以,伺服器程式編寫的好壞,直接影響系統的穩定性!
如何解決TCP死連接配接的問題,有多種方法,其中最有效的就是心跳包技術。
我們在DSServer的OnConnect事件中加入心跳包代碼
uses IdTCPConnection,IdWinsock2
//........
type
TCP_KeepAlive = record
OnOff: Cardinal;
KeepAliveTime: Cardinal;
KeepAliveInterval: Cardinal;
end;
//........
procedure TServerContainer1.DSServer1Connect
(DSConnectEventObject: TDSConnectEventObject);
var
Val: TCP_KeepAlive;
Ret: DWord;
ClientConnection: TIdTCPConnection;
begin
ClientConnection := TIdTCPConnection(DSConnectEventObject.ChannelInfo.Id);
Val.OnOff := 1;
Val.KeepAliveTime := 5000;
Val.KeepAliveInterval := 3000;
WSAIoctl(ClientConnection.Socket.Binding.Handle, IOC_IN or IOC_VENDOR or 4,
@Val, SizeOf(Val), nil, 0, @Ret, nil, nil);
end;
觀察上述代碼,我們把心跳包放到服務端上執行,如果伺服器的某個TCP連接配接在5秒鐘沒有收到資料,将會發送向對端發送心跳包,間隔3秒鐘,連續發送5次(參數詳解見上一講進階技術4)。如果5次以後對端還沒有應答,伺服器将結束該TCP連接配接。TCP的連接配接可以使用 netstat -p tcp 指令檢視。
當該TCP結束後,delphi編寫的服務程式會自動結束和資料庫的連接配接。我用的是FireBird資料庫,大家可以使用指令檢視 SELECT MON$USER, MON$REMOTE_ADDRESS,
MON$REMOTE_PID,
MON$TIMESTAMP
FROM MON$ATTACHMENTS
現在伺服器的tcp死連接配接和資料庫的死連接配接都清除了,我們的系統将能長期穩定的運作。
六、加強服務程式對通路者的控制能力
1)作為一個服務程式,如果不限制用戶端通路數量,後果将是很可怕的。如果有人惡搞,伺服器不堪重負,記憶體将耗盡,最終伺服器将當機。如何限制通路者的數量呢?
我們可以設定一個變量,來記錄來訪者的數量,如果超過我們既定的數字,那麼後續的連接配接伺服器請求,都将被斷掉。
2)限制了通路數量,但是如果不做密碼身份認證,無關的人員也将能登陸伺服器!解決辦法是用戶端傳入使用者名和密碼,如果使用者名和密碼不正确,連接配接将被挂斷。
在用戶端的SQLConnection1中Driver分類的username和password屬性設定好使用者名和密碼。
3)盡量不要設定DSTCPServerTransport1的Maxthreads屬性,還有資料庫連接配接池也不要設定,delphi2010會有記憶體洩露,這兩個參數儲存預設即可。
在DSServer1控件的OnConnect事件中加入如下代碼(使用的是tcp/ip連接配接):
procedure TMainForm.DSServer1Connect
(DSConnectEventObject: TDSConnectEventObject);
var
val: TCP_KeepAlive;
Ret: Integer;
ClientConnection: TIdTCPConnection;
begin
// 最大連接配接數量,驗證來訪者密碼
if (DSConnectEventObject.ChannelInfo = nil) or (Connections >= 500) or
(DSConnectEventObject.ConnectProperties[TDBXPropertyNames.UserName]
<> 'sunstone') or (DSConnectEventObject.ConnectProperties
[TDBXPropertyNames.Password] <> 'mypassword') then
begin
DSConnectEventObject.DbxConnection.Destroy;
// ClientConnection.Disconnect;
end
else
begin
// 擷取socket連接配接
ClientConnection := TIdTCPConnection(DSConnectEventObject.ChannelInfo.Id);
ClientConnection.OnDisconnected := ClientDisconnectEvent;
// 記錄來訪者數量
inc(Connections);
lblShowConnections.Caption := IntToStr(Connections);
if Trim(ShowConnections.Cells[0, 1]) <> '' then
ShowConnections.RowCount := ShowConnections.RowCount + 1;
ShowConnections.Cells[0, ShowConnections.RowCount - 1] := IntToStr
(DSConnectEventObject.ChannelInfo.Id);
ShowConnections.Cells[1, ShowConnections.RowCount - 1] :=
ClientConnection.Socket.Binding.PeerIP + ':' + IntToStr
(ClientConnection.Socket.Binding.PeerPort);
ShowConnections.Cells[2, ShowConnections.RowCount - 1] :=
DSConnectEventObject.ConnectProperties[TDBXPropertyNames.UserName];
ShowConnections.Cells[3, ShowConnections.RowCount - 1] :=
DSConnectEventObject.ConnectProperties[TDBXPropertyNames.Password];
ShowConnections.Cells[4, ShowConnections.RowCount - 1] := FormatDateTime
('yyyy-mm-dd hh:nn:ss', Now);
// ShowConnections.Cells[6, ShowConnections.RowCount - 1] :=
// DSConnectEventObject.ConnectProperties
// [TDBXPropertyNames.ServerConnection];
// 設定心跳包
val.OnOff := 1;
val.KeepAliveTime := 5000;
val.KeepAliveInterval := 1000;
WSAIoctl(ClientConnection.Socket.Binding.Handle, IOC_IN or IOC_VENDOR or 4,
@val, SizeOf(val), nil, 0, @Ret, nil, nil);
end;
end;