IdTcpServer 部分总结 (用户掉线的检测方法,控制用户量等)

内容整理来自: http://www.delphifans.com/InfoView/Article_6521.html

 

用户掉线的检测方法


如果客户端异常掉线或拔掉网线,那么在服务端会留下一个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 命令查看

 

—建立稳定服务程序之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;

 

 

posted @ 2011-03-07 05:05 陆岛工作室 阅读(...) 评论(...) 编辑 收藏