DELPHI高性能大容量SOCKET并发(九):稳定性问题解决

来源:互联网 发布:疯狂追星 知乎 编辑:程序博客网 时间:2024/05/01 02:36

IOCP接收缓存导致的内存错乱

在用IOCP控件写了一个ERP服务器后,服务器会发生运行3天后,出现莫名的内存错误,用FastMM检测,是本没有内存错误的地方,而且内存错误出现的地方也不固定。这是一个不可重现的Bug,后续通过打日志把错误范围缩小后发现,每次出现内存错误之前都是由于有链接断开释放,因此就加了日志逐步定位到是TSocketHandle释放引起的,具体原因是:在IOCP中,每个Socket连接需要投递一个接收请求,并给出数据存放内存,原来是销毁TSocketHandle的同时,销毁投递接收请求的缓存,这样有可能对象销毁后,IOCP返回一个异步接收消息,会导致写入到已销毁的接收缓存,造成内存被重写,导致内存错误。

解决办法,是用锁和对象分离相同的机制,把接收缓存和对象分离,在释放对象的时候不释放接收缓存,等待超过30分钟后,重新使用这个锁和接受缓存,这样做即可以解决内存错乱问题,也起到了锁和接收缓存的池化处理。

具体代码处理:

投递请求缓存和对象分开,采用是锁和对象分离相同的机制。

  {* 客户端对象和锁 *}  TClientSocket = record    Lock: TCriticalSection;    SocketHandle: TSocketHandle;    IocpRecv: TIocpRecord; //投递请求结构体    IdleDT: TDateTime;  end;  PClientSocket = ^TClientSocket;
在释放TSocketHandle的时候,只释放对象,投递请求缓存不释放,和锁一起保留,加入到空闲列表中。
procedure TSocketHandles.Delete(const AIndex: Integer);var  ClientSocket: PClientSocket;begin  ClientSocket := FList[AIndex];  ClientSocket.Lock.Enter;  try    ClientSocket.SocketHandle.Free;    ClientSocket.SocketHandle := nil;  finally    ClientSocket.Lock.Leave;  end;  FList.Delete(AIndex);  ClientSocket.IdleDT := Now;  FIdleList.Add(ClientSocket);end;
在加入对象的时候,检测空闲列表是否有超过30分钟没使用的,如果有则重复利用。
function TSocketHandles.Add(ASocketHandle: TSocketHandle): Integer;var  ClientSocket, IdleClientSocket: PClientSocket;  i: Integer;begin  ClientSocket := nil;  for i := FIdleList.Count - 1 downto 0 do  begin    IdleClientSocket := FIdleList.Items[i];    if Abs(MinutesBetween(Now, IdleClientSocket.IdleDT)) > 30 then    begin      ClientSocket := IdleClientSocket;      FIdleList.Delete(i);      Break;    end;  end;  if not Assigned(ClientSocket) then  begin    New(ClientSocket);    ClientSocket.Lock := TCriticalSection.Create;    ClientSocket.IocpRecv.WsaBuf.buf := GetMemory(MAX_IOCPBUFSIZE);    ClientSocket.IocpRecv.WsaBuf.len := MAX_IOCPBUFSIZE;  end;  ClientSocket.SocketHandle := ASocketHandle;  ClientSocket.IdleDT := Now;  ASocketHandle.FLock := ClientSocket.Lock;  ASocketHandle.FIocpRecv := @ClientSocket.IocpRecv;  Result := FList.Add(ClientSocket);end;

CheckDisconnectedClient方法加锁及判断是否正在执行

原来检测释放断开连接的方法如下:

procedure TIocpServer.CheckDisconnectedClient;var  i: Integer;begin  FSocketHandles.Lock;  try    for i := FSocketHandles.Count - 1 downto 0 do    begin      if not FSocketHandles.Items[i].SocketHandle.Connected then      begin        FSocketHandles.Delete(i);      end;    end;  finally    FSocketHandles.UnLock;  end;end;
这个方法存在以下问题:

1、对整个FSocketHandles加锁,FSocketHandles.Delete在释放的时候又加了一次锁,如果Delete加锁等待,则导致整个FSocketHandles被锁住,这时再加连接就会等待,造成IOCP无法接收连接,从而存在问题。

2、如果某个TScoketHandle执行很长时间,它的Connected属性为False,则FSocketHandles.Delete会锁住,造成和1相同的问题。

解决办法:

1、不对整个FSocketHandles加锁,我每次查找一个Connected为False的连接,避免一次加两个锁。

2、TSocketHandle增加一个属性标识是否正在执行中,在检测断开连接的时候如果正在执行中则跳过。

具体代码如下:

procedure TIocpServer.CheckDisconnectedClient;var  iCount: Integer;  ClientSocket: PClientSocket;  function GetDisconnectSocket: PClientSocket;  var    i: Integer;  begin    Result := nil;    FSocketHandles.Lock;    try      for i := FSocketHandles.Count - 1 downto 0 do      begin        if (not FSocketHandles.Items[i].SocketHandle.Connected)          and (not FSocketHandles.Items[i].SocketHandle.Executing) then        begin          Result := FSocketHandles.Items[i];          Break;        end;      end;    finally      FSocketHandles.UnLock;    end;  end;begin  ClientSocket := GetDisconnectSocket;  iCount := 0;  while (ClientSocket <> nil) and (iCount < 1024 * 1024) do  begin    ClientSocket.Lock.Enter;    try      if Assigned(ClientSocket.SocketHandle) then        FreeSocketHandle(ClientSocket.SocketHandle);    finally      ClientSocket.Lock.Leave;    end;    ClientSocket := GetDisconnectSocket;    Inc(iCount);  end;end;
主要是使用GetDisconnectSocket来返回一个已经断开的连接。TSocketHandle.Executing的赋值只需要在下面方法中赋值即可,因为他执行的进入口和返回口。

procedure TSocketHandle.ProcessIOComplete(AIocpRecord: PIocpRecord;  const ACount: Cardinal);begin  FExecuting := True;  try    case AIocpRecord.IocpOperate of      ioNone: Exit;      ioRead: //收到数据      begin        FActiveTime := Now;        ReceiveData(AIocpRecord.WsaBuf.buf, ACount);        if FConnected then          PreRecv; //投递请求      end;      ioWrite: //发送数据完成,需要释放AIocpRecord的指针      begin        FActiveTime := Now;        FSendOverlapped.Release(AIocpRecord);      end;      ioStream:      begin        FActiveTime := Now;        FSendOverlapped.Release(AIocpRecord);        WriteStream; //继续发送流      end;    end;  finally    FExecuting := False;  end;end;

解决这两个稳定性问题后,IOCP支持的ERP服务器已经能支持7*24小时运行。一般当服务器出现稳定性问题后,日志就开始发挥作用,但是太多无用的日志不利于定位到问题点,太少的日志又无法定位,一般写日志的原则是在调用顺序关键点上加日志、继承关键点上加日志、数据流输入输出关键点上加日志;这样出现问题后,可以快速把问题定位到一段代码上,能帮助缩短解决问题的周期。如果出现一个不可重现BUG,能控制在3天解决,出现一个内存错乱BUG,能控制在半个月解决,哪你的日志辅助调试就是有效的。

V1版下载地址:http://download.csdn.net/detail/sqldebug_fan/4510076,需要资源10分,有稳定性问题,可以作为研究稳定性用;

V2版下载地址:http://download.csdn.net/detail/sqldebug_fan/5560185,不需要资源分,解决了稳定性问题和提高性能;免责声明:此代码只是为了演示IOCP编程,仅用于学习和研究,切勿用于商业用途。水平有限,错误在所难免,欢迎指正和指导。邮箱地址:fansheng_hx@163.com


原创粉丝点击