0 引言
在採用 TCP串連的 C/S結構的系統中,當通訊的一方正常關閉或退出時,另一方能收到相應的串連
斷開的通知,然後進行必要的處理;但如果任意一方發生所謂的“非優雅斷開”,如:意外崩潰、死機、
拔掉網線或路由器故障時,另一方無法得知 TCP 串連已經失效,除非繼續在此串連上不斷地發送資料,
經過若干時間後導致錯誤返回。但在很多時候,更希望伺服器端和用戶端都能及時有效地檢測到網路連
接的非正常斷開,然後完成一些必要的清理工作並把錯誤報表給使用者。
如何及時有效地檢測到通訊一方的非正常斷開,採用的方法是通過通訊的一方或雙方發送心跳包來
告訴對方網路通訊是否正常或已斷開。
1 心跳原理
在基於電路交換的網路中,有專用的控制信令通道,能夠及時發現通路斷開、故障,而 TCP/IP網路
中,鏈路的連通只在串連雙方選項組,物理通道內不存在一個實際的串連鏈路,通訊的雙方只能定時
發送簡單的資訊給另一方,並根據逾時來判斷線路是長時間空閑還是已斷開。這種通過每隔一定時間發
送一個固定資訊給對方,對方收到後回複一個固定資訊,告訴對方“我還在”的方式非常類似於心跳,所
發送的這種簡單資訊就稱為“心跳包”。
心跳包的發送,通常有兩種技術:一種是由使用者在應用程式層實現的心跳包,另一種是由 TCP 協議層提
供的 KeepAlive 。
基於Windows Socket的網路通訊中的心跳機制原理及其實現
18
2 應用程式層自己實現的心跳包
由應用程式自己發送心跳包來檢測串連是否正常,大致的方法是:伺服器在一個 Timer 事件中定時
向用戶端發送一個短小精悍的資料包,然後啟動一個低層級的線程,在該線程中不斷檢測用戶端的回應,
如果在一定時間內沒有收到用戶端的回應,即認為用戶端已經掉線;同樣,如果用戶端在一定時間內沒
有收到伺服器的心跳包,則認為串連不可用。
以下代碼給出在 Delphi 中使用 ServerSocket、ClientSocket 進行網路通訊時,如何在服務端實現應用
層心跳包:
//定義一個 SocketData 記錄類型,用於儲存用戶端資訊
Type SocketData = Record
IP: string; //用戶端 IP
StartTime: Cardinal; //每次向用戶端發送心跳包的目前時間
IsConnected: Boolean; //是否正與服務端保持串連
end;
PSocketData = ^SocketData; //定義一個指向 SocketData 的指標類型
//儲存用戶端資訊,並建立線程,檢測用戶端是否有回應
procedure TForm1.ServerSktClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
var P: PSocketData;
begin
New(p);
p.IP := Socket.RemoteAddress;
p.IsConnected := true;
Socket.Data := p;
if not Timer1.Enabled then
begin
MyThread := TCheckTimeOut.Create(ServerSkt, Panel1);
Timer1.Enabled := true;
end;
end;
//在定時器的 Timer事件中,每隔 2 秒向所有用戶端發送一次心跳包
procedure TForm1.Timer1Timer(Sender: TObject);
var
i, ActConns: integer;
CSocket: TCustomWinSocket;
begin
ActConns := ServerSkt.Socket.ActiveConnections;
caption := '串連數:' + inttostr(ActConns);
for i := 0 to pred(ActConns) do
begin
基於Windows Socket的網路通訊中的心跳機制原理及其實現
19
CSocket := ServerSkt.Socket.Connections[i];
CSocket.SendText('Msgtest');
if PSocketData(CSocket.Data).IsConnected then
begin
PSocketData(CSocket.Data).StartTime := GetTickCount;
PSocketData(CSocket.Data).IsConnected := false;
end;
end;
end;
//如果收到用戶端的特定回應,由表示該客戶處於串連狀態
procedure TForm1.ServerSktClientRead(Sender: TObject;
Socket: TCustomWinSocket);
var RecTxt: string;
begin
RecTxt := Socket.ReceiveText;
if RecTxt = 'OK' then
begin
PSocketData(Socket.Data).IsConnected := true;
Memo1.Lines.Add(RecTxt);
end;
end;
//線程入口,用於檢測用戶端是否在規定時間內向服務端回應
procedure TCheckTimeOut.Execute;
begin
while true do
begin
Synchronize(CheckConnect);
if terminated then exit;
end;
end;
//檢查所有用戶端,是否在 5 000(ms)內向服務端發送回應資訊
procedure TCheckTimeOut.CheckConnect;
var i: integer;
begin
for i := 0 to FServerSocket.Socket.ActiveConnections - 1 do
if not PSocketData(FServerSocket.Socket.Connections[i].Data).IsConnected then
if (GetTickCount - PSocketData(FServerSocket.Socket.Connections[i].Data)^.StartTime) > 5 000
then
begin
基於Windows Socket的網路通訊中的心跳機制原理及其實現
20
Dispose(PSocketData(FServerSocket.Socket.Connections[i].Data));
FServerSocket.Socket.Connections[i].Close;
end;
end;
對於用戶端而言,只要在收到服務端的心跳包後,簡單地發送一個回應資訊即可,代碼略。
3 TCP的 KeepAlive 保活機制
因為要考慮到一個伺服器通常會串連多個用戶端,因此由使用者在應用程式層自己實現心跳包,代碼較多
且稍顯複雜,而利用 TCP/IP 協議層為內建的 KeepAlive 功能來實現心跳功能則簡單得多。
不論是服務端還是用戶端,一方開啟 KeepAlive功能後,就會自動在規定時間內向對方發送心跳包,
而另一方在收到心跳包後就會自動回複,以告訴對方我仍然線上。
因為開啟 KeepAlive 功能需要消耗額外的寬頻和流量,所以 TCP 協議層預設並不開啟 KeepAlive 功
能,儘管這微不足道,但在按流量計費的環境下增加了費用,另一方面,KeepAlive 設定不合理時可能會
因為短暫的網路波動而斷開健康的 TCP串連。並且,預設的 KeepAlive逾時需要 7,200,000 MilliSeconds,
即 2 小時,探測次數為 5 次。對於很多服務端應用程式來說,2 小時的空閑時間太長。因此,我們需要手
工開啟 KeepAlive 功能並設定合理的 KeepAlive 參數。
以下代碼給出在 Delphi 中使用 ServerSocket、ClientSocket 進行網路通訊時,如何在服務端通過添加
KeepAlive 功能來自動實現心跳機制。
//定義心跳常量
Const
IOC_IN = $80000000;
IOC_VENDOR = $18000000;
IOC_out = $40000000;
SIO_KEEPALIVE_VALS = IOC_IN or IOC_VENDOR or 4;
DATA_BUFSIZE = 8192;
//定義 KeepAlive 資料結構
Type
TTCP_KEEPALIVE = packed record
onoff: integer;
keepalivetime: integer;
keepaliveinterval: integer;
end;
// 開啟 KeepAlive 保活機制,每隔 3 秒向用戶端發送一次心跳包
procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket);
var
opt: integer;
klive, outKlive: TTCP_KEEPALIVE;
begin
opt := 1;
21
if setsockopt(Socket.SocketHandle,SOL_SOCKET, SO_KEEPALIVE, @opt, SizeOf(opt)) <> 0 then
begin
Showmessage('setsockopt KeepAlive Error!');
end;
klive.onoff := 1;
klive.keepalivetime := 3000;
klive.keepaliveinterval := 1;
if WSAIoctl( Socket.SocketHandle, SIO_KEEPALIVE_VALS, @klive,
SizeOf(TTCP_KEEPALIVE), @outKlive,
SizeOf(TTCP_KEEPALIVE), @opt,0,nil) = SOCKET_ERROR then
begin
Showmessage('WSAIoctl KeepAlive Error!');
end;
end;
其中,Windows Socket API函數 Setsockopt 用於設定套介面的選項,而此處則用來開啟 KeepAlive功
能,WSAIoctl 函數則用於設定 KeepAlive 逾時及心跳包發送次數。
由於是在 ServerSocket 的 OnClientConnect 事件中使用 Socket 參數來設定每個串連上來的用戶端的
KeepAlive,所以上述代碼同樣支援多客戶串連的情況。
在開啟了 KeepAlive 後, 一旦用戶端死機、 拔網線等“非優雅”退出, 就會觸發服務端的 OnClientError
事件,然後在此事件中完成必要的“善後”處理工作:
procedure TForm1.ServerSocket1ClientError(Sender: TObject;Socket: TCustomWinSocket; ErrorEvent:
TErrorEvent; var ErrorCode: Integer);
begin
ErrorCode:=0;
Showmessage('用戶端 '+Socket.RemoteAddress+'非正常退出,中斷連線');
………… //“善後”處理工作
Socket.Close;
end;
4 結束語
實踐證明,利用 TCP 本身支援的 KeepAlive 功能實現斷線檢測,比使用者自己在應用程式層實現檢測更方
便有效,而且探測時頻寬消耗很小。在沒有資料轉送時依賴 KeepAlive 確保斷線檢測,在傳輸資料時 TCP
會通過逾時判斷是否斷線。
在網路通訊應用系統開發中,根據實際需要也可以在用戶端開啟 KeepAlive,對服務端的非正常斷開
進行及時有效地檢測。