2009年6月10日星期三

解决TCP-socket -掉线问题的总结(转)

直接用socket做的服务器和客户端,程序做的差不多了,实际使用时,发现经常"掉线",而程序不知道,找了许多解决办法,最后全都用上了,总结一下。

1:在连接时,设置他的avalie保活定时器.

Type
TCP_KeepAlive = record
OnOff: Cardinal;
KeepAliveTime: Cardinal;
KeepAliveInterval: Cardinal;
end;

procedure TFrm_Client.Sck_MainConnect(Sender: TObject;
Socket: TCustomWinSocket);
var val:TCP_KeepAlive;
Ret:DWord;
begin
inherited;
try
val.OnOff:=1;
val.KeepAliveTime:=1000 * 60 * 60 * 24; //24小时
val.KeepAliveInterval:=100;
Ret:=WSAIoctl(socket.SocketHandle, IOC_IN or IOC_VENDOR or 4,
@val, SizeOf(Val), nil, 0, @Ret, nil, nil)
finally
showInfo('已经连接到服务器.TCP_KeepAlive定时器'+intToStr(val.KeepAliveTime)+'结果:'+intToStr(Ret));
end;
end;

含义一目了然,不再多说。

2:在出错中检查10053之类错误,遇到就直接close,主动触发关闭事件,在关闭事件中触发自动重新连接的timer的自动重连:

procedure TFrm_Client.Sck_MainError(Sender: TObject;
Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
var ErrorCode: Integer);
Var nCode:Cardinal;
begin
inherited;
showInfo('||[Error]出错:'+intToStr(ErrorCode)+';自动重连:'+boolToStr(lAskForReConnect,
true));
nCode:=ErrorCode; //GetLastError;
ErrorCode:=0;
//如果出错,关闭,触发断开事件
if (nCode=EConnAborted) //10053 EConnAborted WSAEConnAborted
or (nCode=ENetDown) //10050 ENetDown
or (nCode=ETimedOut) //10060
or (nCode=EConnRefused) //10061
or (nCode=EHostUnReach) //10065
then begin
if lAskForReConnect and (not lInReConnecting) then
Timer_ReConnect.enabled:=true; //ReConnectServer;
if not lInReConnecting then begin
sck_Main.active:=false;
sck_Main.close; //ReConnectServer and (not sck_Main.active)
end;
showInfo('
连接状态:'+boolToStr(sck_Main.active,true)+';重连设置:'+boolToStr(lAskForReConnect,true)+';重连状态:'+boolToStr(timer_ReConnect.enabled,true));
end;
end;
//断开连接,断开时自动尝试再次连接,连接不上才断开
procedure TFrm_Client.Sck_MainDisconnect(Sender: TObject; Socket:
TCustomWinSocket);
begin
inherited;
if (not lAskForReConnect) or (lInReConnecting) then exit;
Timer_ReConnect.enabled:=true; //ReConnectServer;
end;
//重新连接
Function TFrm_Client.ReConnectServer:Boolean;
const nMax=6;
var Qry:TAdoQuery; L,L2:Boolean; i:Integer; s:String;
begin
Result:=False; i:=1; if not lAskForReConnect then exit;
L:=frm_MDI.ASysConfig.lAutoLogon;
qry:=nil;
with sck_Main do
try
if not lInCreatting then //or (not lLoginOK)
try
if not lAskForReConnect then exit;
sck_Main.Socket.Disconnect(sck_Main.Socket.Handle);
sck_Main.Close;
if L then begin
showInfo('断线,开始尝试自动重新连接服务器,最大重试次数['+intToStr(nMax)+']...');
&nsp; i:=1; //尝试3次重新连接
while (not sck_Main.active) and (i<=nMax) do
try
if not lAskForReConnect then break;
showInfo('
*尝试自动重新连接['+intToStr(i)+'/'+intToStr(nMax)+']...');
sck_Main.Open;
delay(2000);
L2:= sck_Main.active;
//connectToServer(sck_Main, txt_svrIP.text,
strToIntDef(txt_SvrPort.text,65180));
if not L2 then delay(1000); //sck_Main.active
inc(i);
except
on x:Exception do ;
end;
end;
//
except
on x:exception do ;
end;
finally
lLoginOK:=sck_Main.Active;
if not lLoginOK then begin
showInfo('已经断开服务器的连接,正在尝试重新连接');
lbl_Move.caption:='已经断开连接,正在尝试重新连接';
end;
Result:=lLoginOK;
s:='失败';
if lLoginOK then
Try
s:='[第'+intToStr(i)+'次重试成功]';
qry:=tAdoQuery.create(self);
qry.connection:=dmain.conn_Main;
clsAskForNewMessages(sck_Main.Socket, qry);
finally
if assigned(qry) then qry.free;
End;
if L then showInfo('断线重新连接结果:'+s);
end;
End;

procedure TFrm_Client.Timer_ReconnectTimer(Sender: TObject);
var msg:_msgIdle;
begin
inherited;
Try
//已连接了或用户不要求自动重连,则退出
showInfo('[##Timer_ReconnectTimer]断线自动重连。连接状态:'+boolToStr(sck_Main.active,true)+';重连设置:'+boolToStr(lAskForReConnect,true)+';重连状态:'+boolToStr(timer_ReConnect.enabled,true));
if (sck_Main.active) or (not lAskForReConnect) then exit;
//
lInReConnecting:=True; //处于连接中
timer_ReConnect.enabled:=False;
timer_ReConnect.enabled:=(not ReConnectServer) and
lAskForReConnect; //连上就停止
//
finally
lInReConnecting:=False;
end;
end;

3:服务器上。信息发送都用一个sendMessage方法,在他里边给socket加上错误处理,也时遇到10053错误直接就直接认为已经断开了。

//发送聊天信息
function sendMessage(socket:TCustomWinSocket; nType:Integer;
pMessage:Pointer):boolean;
const sFile='d:\aa.txt';
var tp:_msgHead; buf, pMsg, p:pByte; f,m,hBuf:THandle;
pOldErrorEvent:TSocketErrorEvent;
s:String; n,i,nLen,nMsgLen:integer;
begin
result:=false; buf:=nil; m:=0;
if lGlobalTerminate then exit;
//
try
pOldErrorEvent:=nil;
if assigned(pSocketErrProcedure) then begin
pOldErrorEvent:=socket.OnErrorEvent;
socket.OnErrorEvent:=pSocketErrProcedure;
end;
try
if not socket.Connected then raise
exception.Create('连接未打开或不存在');
//
tp.msgType:=nType;
nMsgLen:=getMessageTypeLen(nType);
if nMsgLen<0 then exit;
tp.nBagSize:=sizeof(tp)+nMsgLen;
tp.Version.dwMajorVersion:=frm_MDI.Version.dwMajorVersion; //1
tp.Version.dwMinorVersion:=frm_MDI.Version.dwMinorVersion; //0
//
nLen:=nMsgLen+sizeOf(tp)+0;
//分配内存区、复制头 globalAlloc(gmem_MoveAble, n+sizeOf(tp));
hBuf:=globalAlloc(GMEM_MOVEABLE + GMem_Share, nLen);
buf:=globalLock(hBuf); //getMem(buf, nLen);
fillChar( buf^, nLen, #0);
copyMemory(buf, @tp, sizeOf(tp)); //strCopy(buf, @tp);
//复制内容到全部内存区
if (nMsgLen<>0) and (pMessage<>nil) then begin
n:=integer(buf)+sizeOf(tp);
p:=ptr(n);
copyMemory(p, pMessage, nMsgLen); //strLCopy(p, pMessage,
nMsgLen); //copyMemor(p, pMessage, nMsgLen);
end;
//发送,重试n次,最多150ms
n:=-1; i:=1;
while (n=-1) and (i<=5) do begin
if lGlobalTerminate or (not socket.Connected) then break;
n:=socket.SendBuf(buf^, nLen);
if n=-1 then delay(10*i); //socket没有空间,则等待后重发
inc(i);
end;
//
result:=lGlobalTerminate or (n>-1);
except
on x:exception do begin
s:='[sendMessage]发送信息出错!Type='+intToStr(nType)+#13+#10+'
'+x.Message;
//if assigned(buf) then freeMem(buf);
insShowInfo(s);
raise eSocketError.Create(s);
end;
end;
finally
if assigned(pOldErrorEvent) then socket.OnErrorEvent:=pOldErrorEvent;
if assigned(buf) then begin
globalUnlock(hBuf);
globalFree(hBuf);
end;
end;
end;
//服务器端初始化时:

pSocketErrProcedure:=defaultSocketErrProcedure;

//socket出错的处理
procedure TFrm_Service.defaultSocketErrProcedure(Sender: TObject; Socket:
TCustomWinSocket;
ErrorEvent: TErrorEvent; var ErrorCode: Integer);
var nCode:Integer;
begin
inherited; nCode:=errorCode;
//若出错,关闭
if nCode=10053 then begin
socket.Close;
raise exception.create('客户端已经断开连接了!');
end;
end;

4:心跳包,可能这个最有效,心跳包的意思,就是像心跳一样,每隔多长事件跳一次,跳自然会触发错误了。

//保持连接,心跳包
procedure TFrm_Client.Timer_KeepConnectTimer(Sender: TObject);
var msg:_msgIdle; H:TSocket; n:Integer; L:Boolean;
begin
inherited;
if (not lLoginOK) or (lInReConnecting) then exit;
L:=False;
try
if lGlobalTerminate then exit;
Timer_KeepConnect.enabled:=false;
//
//fillChar(msg.sComment,length(msg.sComment), #0);
msg.Version.dwMajorVersion:=frm_MDI.Version.dwMajorVersion;
msg.Version.dwMinorVersion:=frm_MDI.Version.dwMinorVersion;
msg.nTickCount:=0; //GetTickCount;
msg.nHandle:=0; //Socket.Handle;
msg.sComment:='你好!有空常联系!'; //sComment;
if not sendMessage(sck_Main.Socket, msgTypeIdle, @msg) then ;
//
//sendIdleMsg(sck_Main.socket);
{n:=timer_KeepConnect.interval;
H:=sck_Main.Socket.SocketHandle;
waitForData(n-300); }
L:=True;
finally
Timer_KeepConnect.enabled:=true;
showInfo('发送联系服务器消息IDLE'+intToStr(Timer_KeepConnect.interval)+',结果:'+boolToStr(L,true)+'...');
end;
end;

没有评论: