在c#中利用keep-alive处理socket网络异常断开的方法

本文摘自

http://www.z6688.com/info/57987-1.htm

最近我负责一个IM项目的开发,服务端和客户端采用TCP协议连接。服务端采用C#开发,客户端采用Delphi开发。在服务端开发中我碰到了各种各样的网络异常断开现象。在处理这些异常的时候有了一些心得,现在写出来和大家分享一下。

那网络异常断开原因主要有那些呢?归纳起来主要有以下两种:

1、客户端程序异常。

  对于这种情况,我们很好处理,因为客户端程序异常退出会在服务端引发ConnectionReset的Socket异常(就是WinSock2中的10054异常)。只要在服务端处理这个异常就可以了。

2、网络链路异常。

  如:网线拔出、交换机掉电、客户端机器掉电。当出现这些情况的时候服务端不会出现任何异常。这样的话上面的代码就不能处理这种情况了。对于这种情况在MSDN里面是这样处理的,我在这里贴出MSDN的原文:

如果您需要确定连接的当前状态,请进行非阻止、零字节的 Send 调用。如果该调用成功返回或引发 WAEWOULDBLOCK 错误代码 (10035),则该套接字仍然处于连接状态;否则,该套接字不再处于连接状态。

  但是我在实际应用中发现,MSDN说的这种处理方法在很多时候根本无效,无法检测出网络已经异常断开了。那我们该怎么办呢?

  我们知道,TCP有一个连接检测机制,就是如果在指定的时间内(一般为2个小时)没有数据传送,会给对端发送一个Keep-Alive数据报,使用的序列号是曾经发出的最后一个报文的最后一个字节的序列号,对端如果收到这个数据,回送一个TCP的ACK,确认这个字节已经收到,这样就知道此连接没有被断开。如果一段时间没有收到对方的响应,会进行重试,重试几次后,向对端发一个reset,然后将连接断掉。

  在Windows中,第一次探测是在最后一次数据发送的两个小时,然后每隔1秒探测一次,一共探测5次,如果5次都没有收到回应的话,就会断开这个连接。但两个小时对于我们的项目来说显然太长了。我们必须缩短这个时间。那么我们该如何做呢?我要利用Socket类的IOControl()函数。我们来看看这个函数能干些什么:

使用 IOControlCode 枚举指定控制代码,为 Socket 设置低级操作模式。

命名空间:System.Net.Sockets 
程序集:System(在 system.dll 中)

语法

C# 
public int IOControl ( 
IOControlCode ioControlCode, 
byte[] optionInValue, 
byte[] optionOutValue 
)


参数 
ioControlCode 
一个 IOControlCode 值,它指定要执行的操作的控制代码。

optionInValue 
Byte 类型的数组,包含操作要求的输入数据。

optionOutValue 
Byte 类型的数组,包含由操作返回的输出数据。

返回值 
optionOutValue 参数中的字节数。

如:

socket.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
我们要搞清楚的就是inOptionValues的定义,在C++里它是一个结构体。我们来看看这个结构体:

struct tcp_keepalive 
...{ 
    u_long  onoff; //是否启用Keep-Alive
    u_long  keepalivetime; //多长时间后开始第一次探测(单位:毫秒)
    u_long  keepaliveinterval; //探测时间间隔(单位:毫秒)
};

在C#中,我们直接用一个Byte数组传递给函数:http://www.devdao.com/

uint dummy = 0;
byte[] inOptionValues = new byte[Marshal.SizeOf(dummy) * 3];
BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0);//是否启用Keep-Alive
BitConverter.GetBytes((uint)5000).CopyTo(inOptionValues, Marshal.SizeOf(dummy));//多长时间开始第一次探测
BitConverter.GetBytes((uint)5000).CopyTo(inOptionValues, Marshal.SizeOf(dummy) * 2);//探测时间间隔

具体实现代码:

        public static void AcceptThread()
        ...{
            Thread.CurrentThread.IsBackground = true;
            while (true)
            ...{
                uint dummy = 0;
                byte[] inOptionValues = new byte[Marshal.SizeOf(dummy) * 3];
                BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0);
                BitConverter.GetBytes((uint)5000).CopyTo(inOptionValues, Marshal.SizeOf(dummy));
                BitConverter.GetBytes((uint)5000).CopyTo(inOptionValues, Marshal.SizeOf(dummy) * 2);
                try
                ...{
                    Accept(inOptionValues);
                }
                catch ...{ }
            }
        }

        private static void Accept(byte[] inOptionValues)
        ...{
            Socket socket = Public.s_socketHandler.Accept();
            socket.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
            UserInfo info = new UserInfo();
            info.socket = socket;
            int id = GetUserId();
            info.Index = id;
            Public.s_userList.Add(id, info);
            socket.BeginReceive(info.Buffer, 0, info.Buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallBack), info);
        }

好了,这样就成功了。

自我注:

使用此方法的确能够解决一些网络状况较差的情况下的资源管理问题,远端Socket异常断开后如果不及时回收资源,在.Net下资源耗费是很大的,使用此方法能较快的发现远端的连接状态,所以能及时回收一些无效连接以达到资源管理的目的.

附:

.Net2003下没有IOControlCode的枚举

.Net2005下IOControlCode枚举是这样的

 // 摘要:
    //     指定 System.Net.Sockets.Socket.IOControl(System.Int32,System.Byte[],System.Byte[])
    //     方法支持的 IO 控制代码。
    public enum IOControlCode
    {
        // 摘要:
        //     当传入的消息队列已满时,将时间最久的已排队数据报替换为传入的数据报。此值等于 Winsock 2 SIO_ENABLE_CIRCULAR_QUEUEING
        //     常数。
        EnableCircularQueuing = 671088642,
        //
        // 摘要:
        //     放弃发送队列的内容。此值等于 Winsock 2 SIO_FLUSH 常数。
        Flush = 671088644,
        //
        // 摘要:
        //     当套接字协议族的本地接口列表更改时启用接收通知。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2 SIO_ADDRESS_LIST_CHANGE
        //     常数。
        AddressListChange = 671088663,
        //
        // 摘要:
        //     返回可读取的字节数。此值等于 Winsock 2 FIONREAD 常数。
        DataToRead = 1074030207,
        //
        // 摘要:
        //     返回有关等待要接收的带外数据的信息。在流式套接字上使用此控制代码时,返回值指示可用的字节数。
        OobDataRead = 1074033415,
        //
        // 摘要:
        //     返回包含当前套接字地址族的广播地址的 SOCKADDR 结构。返回的地址可与 Overload:System.Net.Sockets.Socket.SendTo
        //     方法一起使用。此值等于 Winsock 2 SIO_GET_BROADCAST_ADDRESS 常数。此值只能在用户数据报协议 (UDP) 套接字上使用。
        GetBroadcastAddress = 1207959557,
        //
        // 摘要:
        //     返回套接字可绑定到的本地接口列表。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2 SIO_ADDRESS_LIST_QUERY
        //     常数。
        AddressListQuery = 1207959574,
        //
        // 摘要:
        //     检索基础提供程序的 SOCKET 句柄。此句柄可用于接收即插即用事件通知。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于
        //     Winsock 2 SIO_QUERY_TARGET_PNP_HANDLE 常数。
        QueryTargetPnpHandle = 1207959576,
        //
        // 摘要:
        //     当数据等待接收时启用通知。此值等于 Winsock 2 FIOASYNC 常数。
        AsyncIO = 2147772029,
        //
        // 摘要:
        //     控制套接字的阻止行为。如果使用此控制代码指定的参数为零,套接字将置于阻止模式下。如果参数不为零,套接字将置于非阻止模式下。此值等于 Winsock 2
        //     FIONBIO 常数。
        NonBlockingIO = 2147772030,
        //
        // 摘要:
        //     将此套接字与附带接口的指定句柄关联。有关其他详细信息,请参考 Winsock 2 参考或文档中特定附带接口的相应协议特定附录。建议使用组件对象模型
        //     (COM) 代替此 IOCTL,以发现并跟踪套接字可能支持的其他接口。此控制代码是为了与某些系统保持向后兼容而提供的,在这些系统中,COM 不可用或由于某些其他原因而无法使用。此值等于
        //     Winsock 2 SIO_ASSOCIATE_HANDLE 常数。
        AssociateHandle = 2281701377,
        //
        // 摘要:
        //     控制套接字发送的多路广播数据是否在套接字接收队列中显示为传入数据。此值等于 Winsock 2 SIO_MULTIPOINT_LOOPBACK 常数。
        MultipointLoopback = 2281701385,
        //
        // 摘要:
        //     控制路由器可以转发多路广播数据包的次数,也称作生存时间 (TTL) 或跃点计数。此值等于 Winsock 2 SIO_MULTICAST_SCOPE
        //     常数。
        MulticastScope = 2281701386,
        //
        // 摘要:
        //     设置套接字的服务质量 (QOS) 属性。QOS 用于定义套接字的带宽要求。Windows Me、Windows 2000 及更高版本的操作系统支持此控制代码。此值等于
        //     Winsock 2 SIO_SET_QOS 常数。
        SetQos = 2281701387,
        //
        // 摘要:
        //     设置套接字组的服务质量 (QOS) 属性。此值保留供将来使用,并且等于 Winsock 2 SIO_SET_GROUP_QOS 常数。
        SetGroupQos = 2281701388,
        //
        // 摘要:
        //     当用于访问远程终结点的本地接口更改时启用接收通知。此值等于 Winsock 2 SIO_ROUTING_INTERFACE_CHANGE 常数。
        RoutingInterfaceChange = 2281701397,
        //
        // 摘要:
        //     控制套接字是否在命名空间查询无效时接收通知。Windows XP 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2 SIO_NSP_NOTIFY_CHANGE
        //     常数。
        NamespaceChange = 2281701401,
        //
        // 摘要:
        //     启用对网络上的所有 IPv4 数据包的接收。套接字必须有 System.Net.Sockets.AddressFamily.InterNetwork
        //     地址族,套接字类型必须是 System.Net.Sockets.SocketType.Raw,并且协议类型必须为 System.Net.Sockets.ProtocolType.IP。当前用户必须属于本地计算机上的
        //     Administrators 组,并且套接字必须绑定到特定端口。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2
        //     SIO_RCVALL 常数。
        ReceiveAll = 2550136833,
        //
        // 摘要:
        //     启用对网络上的所有多路广播 IPv4 数据包的接收。这些数据包的目标地址范围介于 224.0.0.0 到 239.255.255.255 之间。套接字必须有
        //     System.Net.Sockets.AddressFamily.InterNetwork 地址族,套接字类型必须是 System.Net.Sockets.SocketType.Raw,并且协议类型必须为
        //     System.Net.Sockets.ProtocolType.Udp。当前用户必须属于本地计算机上的 Administrators 组,并且套接字必须绑定到特定端口。Windows
        //     2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2 SIO_RCVALL_MCAST 常数。
        ReceiveAllMulticast = 2550136834,
        //
        // 摘要:
        //     启用对网络上的所有 Internet 组管理协议 (IGMP) 数据包的接收。套接字必须有 System.Net.Sockets.AddressFamily.InterNetwork
        //     地址族,套接字类型必须是 System.Net.Sockets.SocketType.Raw,并且协议类型必须为 System.Net.Sockets.ProtocolType.Igmp。当前用户必须属于本地计算机上的
        //     Administrators 组,并且套接字必须绑定到特定端口。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2
        //     SIO_RCVALL_IGMPMCAST 常数。
        ReceiveAllIgmpMulticast = 2550136835,
        //
        // 摘要:
        //     控制 TCP keep-alive 数据包的发送以及发送间隔。Windows 2000 及更高版本的操作系统支持此控制代码。有关附加信息,请参见
        //     RFC 1122 的 4.2.3.6 节。此值等于 Winsock 2 SIO_KEEPALIVE_VALS 常数。
        KeepAliveValues = 2550136836,
        //
        // 摘要:
        //     此值等于 Winsock 2 SIO_ABSORB_RTRALERT 常数。
        AbsorbRouterAlert = 2550136837,
        //
        // 摘要:
        //     设置用于输出的单播数据包的接口。此值等于 Winsock 2 SIO_UCAST_IF 常数。
        UnicastInterface = 2550136838,
        //
        // 摘要:
        //     此值等于 Winsock 2 SIO_LIMIT_BROADCASTS 常数。
        LimitBroadcasts = 2550136839,
        //
        // 摘要:
        //     将套接字绑定到指定的接口索引。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2 SIO_INDEX_BIND
        //     常数。
        BindToInterface = 2550136840,
        //
        // 摘要:
        //     设置用于输出的多路广播数据包的接口。该接口通过其索引进行标识。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2
        //     SIO_INDEX_MCASTIF 常数。
        MulticastInterface = 2550136841,
        //
        // 摘要:
        //     使用按索引标识的接口联接多路广播组。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2 SIO_INDEX_ADD_MCAST
        //     常数。
        AddMulticastGroupOnInterface = 2550136842,
        //
        // 摘要:
        //     将套接字从多路广播组中移除。Windows 2000 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2 SIO_INDEX_ADD_MCAST
        //     常数。
        DeleteMulticastGroupFromInterface = 2550136843,
        //
        // 摘要:
        //     获取提供程序特定的函数,这类函数不是 Winsock 规范的一部分。它们使用其提供程序分配的 GUID 进行指定。此值等于 Winsock 2 SIO_GET_EXTENSION_FUNCTION_POINTER
        //     常数。
        GetExtensionFunctionPointer = 3355443206,
        //
        // 摘要:
        //     检索与套接字关联的 QOS 结构。只有能提供 QOS 传输的平台(Windows Me、Windows 2000 和更高版本)才支持此控件。此值等于
        //     Winsock 2 SIO_GET_QOS 常数。
        GetQos = 3355443207,
        //
        // 摘要:
        //     返回套接字组的服务质量 (QOS) 属性。此值保留供将来使用,并且等于 Winsock 2 SIO_GET_GROUP_QOS 常数。
        GetGroupQos = 3355443208,
        //
        // 摘要:
        //     返回附带接口上下文中有效的套接字的句柄。此值等于 Winsock 2 SIO_TRANSLATE_HANDLE 常数。
        TranslateHandle = 3355443213,
        //
        // 摘要:
        //     返回可用于连接到指定远程地址的接口地址。此值等于 Winsock 2 SIO_ROUTING_INTERFACE_QUERY 常数。
        RoutingInterfaceQuery = 3355443220,
        //
        // 摘要:
        //     对 System.Net.Sockets.IOControlCode.AddressListQuery 字段返回的结构进行排序,并为 IPv6 地址添加范围
        //     ID 信息。Windows XP 及更高版本的操作系统支持此控制代码。此值等于 Winsock 2 SIO_ADDRESS_LIST_SORT 常数。
        AddressListSort = 3355443225,
    }

值为Uint型,在调用的时候要转换为int型