对Socket网络编程有一定的了解,应该对KeepAlive都不陌生。毫不夸张地说,如果不懂KeepAlive,你所编写的TCP服务一定不堪重负。甚至会出现许多在你看来非常莫名其妙的问题。
本文不再对基础的KeepAlive知识作重点介绍。而是重点介绍在对低功耗穿戴产品提供TCP服务时,如何高效使用KeepAlive。
以下我将通过一段文字简单介绍一下Socket KeepAlive相关知识。然后我们再切入正题。
TCP KeepAlive,从字面上看,它能够检测你的 TCP socket 并检测连接是否在运行或者是否已经被破坏。keepalive概念很简单:当建立一个TCP连接时,你将一系列的定时器与该连接相关联。这些定时器中某些用于处理keepalive过程。当keepalive定时器变为0时,你给你的同伴(也就是对方)发送一个keepalive 探针包(probe packet),包内没有数据并且ACK标识打开。另一方面,你会收到一个来自远方主机的回应,该回应没有数据并且设置ACK标识。如果你收到一个对于你的keepalive探针的响应,那么就说明连接正在运行,不必担心用户级别的实现。事实上,TCP允许你控制流,没有包及零长度的数据包对于用户程序而言没有危险。这个过程是有用的。因为如果其他主机失去连接,你就可以注意到连接时断开的。如果keepalive探针没有被响应,那么就可以断言连接不能被认为是有效的,那么就需要采取正确的操作。我们之所以会使用TCP KeepAlive,主要是:1. 服务端,检测dead peers,以便节省服务开销;2. 客户端,阻止因网络连接不活跃而被服务端踢掉。
事实上,TCP KeepAlive并非绝无风险的。在使用时,仍需小心谨慎。
如上文所述,虽然keepalive是无攻击性的,但是它会产生多余的网络带宽,这就会对于路由和防火墙产生影响。而将这个影响放在<TCP Server - 低功耗智能穿戴设备>这对组合上。其影响绝非小事。过于频繁的KeepAlive探测会造成低功耗智能穿戴设备网络通讯一直处于活跃状态,造成设备功耗大幅度升高。
在服务端,我们该如何妥善使用KeepAlive呢?
KeepAlive有3个参数。从以下函数的注释代码,可以知道onOff是KeepAlive的开关,keepAliveTime是开始首次KeepAlive探测前的TCP空闭时间,keepAliveInterval是 两次KeepAlive探测间的时间间隔。结合智能穿戴设备的心跳间隔特点。如何在服务端既要能做到检测dead peers,又要不引起智能穿戴设备的功耗升高。此时,最好将keepAliveTime时间设置成大于智能穿戴设备的心跳间隔,但为了保证检测dead peer的时效性,又不能大过头。如:设备的心跳间隔是5min,则keepAliveTime最好设置成6min。详见以下代码:
/*设置Keep-Alive参数
*
* KeepAlive函数参数说明:
* onOff:是否开启KeepAlive,0表示停止,1表示开启
* keepAliveTime:开始首次KeepAlive探测前的TCP空闭时间,单位ms
* keepAliveInterval: 两次KeepAlive探测间的时间间隔,单位ms
*/
private byte[] KeepAlive(int onOff, int keepAliveTime, int keepAliveInterval)
{
byte[] buffer = new byte[12];
BitConverter.GetBytes(onOff).CopyTo(buffer, 0);
BitConverter.GetBytes(keepAliveTime).CopyTo(buffer, 4);
BitConverter.GetBytes(keepAliveInterval).CopyTo(buffer, 8);
return buffer;
}
/*在接受客户端连接处理时,我们按如下方法调用KeepAlive。
* 客户端连接处理函数
* 欲建立服务器连接的Socket对象
*/
protected virtual void AcceptConn(IAsyncResult iar)
{
//如果服务器停止了服务,就不能再接收新的客户端
if (!_isRun)
{
return;
}
//接受一个客户端的连接请求
Socket oldserver = (Socket)iar.AsyncState;
Socket client = oldserver.EndAccept(iar);
//检查是否达到最大的允许的客户端数目
if (_clientCount == _maxClient)
{
//服务器已满,发出通知
if (ServerFull != null)
{
ServerFull(this, new NetEventArgs(new Session(client)));
}
}
else
{
client.IOControl(IOControlCode.KeepAliveValues, KeepAlive(1, 3600000, 1000), null);
Session newSession = new Session(client);
_sessionTable.Add(newSession.ID, newSession);
//客户端引用计数+1
_clientCount++;
//开始接受来自该客户端的数据
client.BeginReceive(_recvDataBuffer, 0, _recvDataBuffer.Length, SocketFlags.None,
new AsyncCallback(ReceiveData), client);
//新的客户段连接,发出通知
if (ClientConn != null)
{
ClientConn(this, new NetEventArgs(newSession));
}
}
//继续接受客户端
_svrSock.BeginAccept(new AsyncCallback(AcceptConn), _svrSock);
}