在C#中利用Keep-Alive处理Socket网络异常断开的方法

  最近我负责一个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数组传递给函数:

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);
        }

好了,这样就成功了。

0
0
(请您对文章做出评价)
« 上一篇:在C#中实现Socket端口复用
» 下一篇:.NET Socket开发之异步Socket
posted @ 2007-05-22 08:33 牧野 阅读(10842) 评论(67)  编辑 收藏 网摘 所属分类: Socket, C#

  回复  引用  查看    
#1楼2007-05-22 09:46 | 大石头      
好文章,有空看看对我那程序是否有用。

我做了一个反向代理程序,工作一直很好

  回复  引用  查看    
#2楼2007-05-22 09:47 | 大石头      
但是,一旦时间长了,就会有异常发生,然后程序自动关闭了。

我检查过,这个异常,根本捕获不到。

我全部都是使用异步来通信的。

  回复  引用  查看    
#3楼[楼主]2007-05-22 09:53 | 牧 野       
@大石头
呵呵………………
这是我在项目中使用的代码拷出来的,如果Keep-Alive检测到网络异常的话,就会引发ConnectionReset异常。所以真正的断开后处理还是在BeginReceive和EndReceive里面。只要捕获上面的异常就可以了!

  回复  引用  查看    
#4楼[楼主]2007-05-22 09:54 | 牧 野       
@大石头
至于你说的那个无法捕获的异常,我不知道是什么异常。不过这些代码在我的项目中运行的很正常。

  回复  引用  查看    
#5楼2007-05-22 12:35 | 大石头      
呵呵,要是知道是什么异常,我就不用那么郁闷了。

我在所有函数使用try catch,然后输出异常信息,结果是:所有catch都catch不到,程序还是退出了

  回复  引用  查看    
#6楼[楼主]2007-05-22 13:43 | 牧 野       
@大石头
调试不到这个异常吗?

  回复  引用    
#7楼2007-05-22 21:29 | 阿毅[未注册用户]
@大石头
呵呵,不想分析程序的话,就在调试状态下运行你的程序,直到出错为止。
如果调试也获取不到,那肯定是调用系统底层api时出的问题(出错的是该api调用的某个函数。)我几年前也遇到过这样的问题,想不起来了。

  回复  引用  查看    
#8楼2007-05-22 21:34 | 阿齐      
如果是操作系统底层抛出的异常,有时确实无法捕捉到——我就遇到过这种问题
  回复  引用    
#9楼2007-05-22 23:35 | 热门单曲[未注册用户]
我做了一个反向代理程序,工作一直很好
  回复  引用    
#10楼2007-05-22 23:42 | 热门单曲[未注册用户]
一旦时间长了,就会有异常发生,然后程序自动关闭了。

  回复  引用  查看    
#11楼2007-05-23 01:51 | 大石头      
@阿毅
你说对了。
正是捕获不到的。
请问,你当时是怎么解决的?

  回复  引用    
#12楼2007-05-30 23:09 | 同行[未注册用户]
居然成为老兄你博客的常客了。
不经意间搜索的东西,总会最终在你这里留步。

继续。。。。。。。。。

  回复  引用    
#13楼2007-06-04 13:51 | 老刘头[未注册用户]
各位大侠,对于上诉问题我做了2天的测试,本人使用iocp
1、拔掉客户端网线,客户端不要再继续发送,服务器没有send的时候永远无法检测到连接已经断开,1小时后插上网线,照常通讯,客户端使用telent。
2、服务器每隔15s自动向在线客户端发送数据,如果客户端拔掉网线,大约2分钟后完成端口会抛出read错误 10054
3、使用setsockopt()和 wsaioctl()进行keeplive设置后仍然是上述结果
4、没有测试阻塞模式,在阻塞模式下可能会较早的获得异常(像楼上)。

msdn上说的keepalivetime的默认值是2小时,我没有等上2小时,另外socket默认情况下keepalive选项是关闭的,这样测试下来恐怕要几天的时间。另外,最好不要在注册表上修改keepalive的值。

  回复  引用    
#14楼2007-06-04 13:59 | 老刘头[未注册用户]
本人认为在非阻塞模式下,上述方法可能行的通,但是在非阻塞模式下,上述方法是不行的,可能是因为tcp底层不会主动向应用层抛出异常。不知道各位高见?我会继续关注
  回复  引用  查看    
#15楼[楼主]2007-06-04 15:29 | 牧 野       
@老刘头
你用的是C++还是C#呢??

  回复  引用  查看    
#16楼[楼主]2007-06-04 15:32 | 牧 野       
@老刘头
我也在C++下面弄过IOCP,我这个在C++是可以用的,只不过用的是setSockopt
函数,这个主要的功能就是缩短Keep-Alive的探测的时间。使它在很短的时间内就开始探测。

  回复  引用    
#17楼2007-06-04 18:00 | 老刘头[未注册用户]
又试了1个下午,现在成功了,但是还没有总结出要点,回头发贴
  回复  引用    
#18楼2007-06-04 19:32 | 老刘头[未注册用户]
简单一句话,所有模型均可以在规定的时间内通过tcp keepalive探测到链路中断。不论什么模型均需要调用wsaioctl(),但是在IOCP中必须同时调用setsockopt(),select wsaeventselect等可以不调用setsockopt()。但是在整个测试过程中,我想使用注册表设置而不调用wsaeventselect(),但是没有成功。注册表设置如下:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
"KeepAliveInterval "=dword:000003e8
"KeepAliveTime "=dword:0036ee80
"TcpMaxDataRetransmissions "=dword:00000005

  回复  引用  查看    
#19楼[楼主]2007-06-05 08:36 | 牧 野       
@老刘头
个人不建议使用注册表,因为这样会导致所有的Socket的Keep-Alive都会被修改,而且要重启才能生效。

  回复  引用    
#20楼2007-06-14 11:18 | 老钟[未注册用户]
直接用这句就搞定
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, 1);

  回复  引用  查看    
#21楼[楼主]2007-06-15 19:41 | 牧 野       
@老钟
要是用这句可以搞定的话,我也不用写这么多代码了!

  回复  引用    
#22楼2007-06-21 19:41 | lol[未注册用户]
ППЦ! =))) Вы ещё и говорите! :lol:
  回复  引用    
#23楼2007-06-26 14:57 | yukimatsu[未注册用户]
有点思路了,但上述代码经实验,不行,客户机断开多次后,这招就失效了。
  回复  引用  查看    
#24楼[楼主]2007-06-27 10:10 | 牧 野       
@yukimatsu
不会的,我一直都在用这个代码,没有发现失效的情况!

  回复  引用    
#25楼2007-06-29 18:01 | 红金鱼[未注册用户]
这样做的确可以探测到网络异常断开,但是把探测时间间隔缩短到几秒钟会不会给服务器带来太大的压力从而影响它的性能呢?
  回复  引用  查看    
#26楼[楼主]2007-06-30 09:56 | 牧 野       
@红金鱼
不会的,因为通讯量很小,才几个字节而已。而且这个时间是可以自己定的!

  回复  引用    
#27楼2007-07-09 19:40 | @olind[未注册用户]
IOControlCode 具体的取值都有哪些阿?我是用的.net framework 1.1
socket.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
这一句过不去阿,说是IOControlCode.KeepAliveValues没有这个类型,还需要引用什么么?

  回复  引用    
#28楼2007-08-04 15:52 | 边城浪[未注册用户]
大石头说的异常..我也遇到过....
可以参考这遍文章解决.
http://blog.csdn.net/zhengyun_ustc/archive/2006/09/19/1246115.aspx

  回复  引用  查看    
#29楼2007-08-05 05:04 | 大石头      
楼上的意思是:定时检查异步的执行情况,根据需要关闭异步操作?

我估计引发这个问题的原因也是因为等待过久。

  回复  引用    
#30楼2007-12-21 14:48 | abcefg[未注册用户]
请问楼主,你在文中说到“然后每隔1秒探测一次,一共探测5次,如果5次都没有收到回应的话,就会断开这个连接。”但你的程序中没有断开操作,这个断开是指自动断开还是要另外手动断开?请明示,谢谢
  回复  引用  查看    
#31楼[楼主]2007-12-21 15:11 | 牧 野       
@abcefg
Socket会在Keep-Alive失败后自动断开并引发Socket异常,所以你只要处理好Socket异常就行了。

  回复  引用    
#32楼2007-12-23 10:46 | 闯闯[未注册用户]
我的QQ是:735978202 想知道怎么来写一个ping命名,来判断是否断网,谢谢 !
请那位高手帮帮忙,谢谢!

  回复  引用    
#33楼2007-12-24 00:54 | 看客[未注册用户]
"网络异常断开" 通用的做法是采用心跳包检测!

受不了你们了, 这点儿知识都不知道 还写什么socket服务器程序啊!
鄙视!

  回复  引用  查看    
#34楼[楼主]2007-12-24 08:51 | 牧 野       
@看客
您这话我可就不爱听了,不管是谁都是从不懂到懂的,不懂就学就问嘛。没有人是从生下来就会开发程序的。别人不懂来问的话懂的人应该去帮助他们而不是去挖苦他们,你说对吗?

  回复  引用  查看    
#35楼2007-12-24 09:41 | 阿鸟      
.....如果客户端是采用的长连接, 服务器定期推送一个小包不就行了? 发现socket已经断开就停止了, 为什么要等待keepalive呢?... 我一直理解keepalive是一个辅助判断.. 为什么要让它上升到一个应用判断的层次呀?..
-_-#...

  回复  引用  查看    
#36楼[楼主]2007-12-24 09:49 | 牧 野       
@阿鸟
使用Keep-alive是因为它编程简单,而且准确(使用定期发送的原理和Keep-Alive的原理是一样的,而且自己编程发送会可能因为Socket的算法而导致数据被延迟发送),再说了,能少写一些代码总是一件开心的事。呵呵…………

  回复  引用  查看    
#37楼2007-12-24 13:18 | 蛙蛙池塘      
顶应用层的keep-alive,自己写keep-alive算了,比较保险。
  回复  引用  查看    
#38楼[楼主]2007-12-24 13:35 | 牧 野       
@蛙蛙池塘
同样一个功能,系统已经自带了为什么还要自己去写呢?不解。难道是为了分层而分层?

  回复  引用    
#39楼2007-12-26 08:48 | abcefg[未注册用户]
楼主请问,如果客户端是短连接的形式与服务端相连,每分钟大约三百个连接,如果服务端用.net2.0中的异步通迅方式,那么如果网络不稳定使得服务端没有收到客户端发来的SOCKET关闭信号,那么服务端是不是维持越来越多的无用连接?同步接收倒可以用超时来关闭无用的连接,请问异步接收是不是也可以即时清理无用的连接?
(再请问异步中是将客户端连来的连接放在哪儿的?有何简单方法知道某个连接很长时间不发数据了?)
谢谢。

  回复  引用  查看    
#40楼[楼主]2007-12-26 08:54 | 牧野       
@abcefg
异步Socket中的EndAccept返回的就是客户端的Socket,你可以记录每个客户端最后一次发送数据的时间,然后和当前时间对比就可以知道这个Socket有多长时间没有收发数据了。

网络的异常断开可以用我上面说的方法及时的探测到。

  回复  引用    
#41楼2007-12-26 13:25 | abcefg[未注册用户]
楼主:请问异步socket中的Listen(int backlog)backlog指挂起连接队列的最大长度。 这个长度能设多长?一般异步可以同一时刻并发连多少SOCKET?提高并发的关键是什么?谢谢

  回复  引用  查看    
#42楼2007-12-27 09:04 | 大石头      
◎ abcefg
异步的时候,你自己是不需要去管理连接的,若要管理,就像牧野说那样,在EndAccept那里增加,在程序里面异常的时候移除。
程序的异步方法里面,一般都是EndXXX开头,如果网络异常(包括客户端断开),这里都会返回异常,捕获到异常就可以了。
一般不设置最大挂起连接队列。
提高并发的关键是写好异步方法。

  回复  引用    
#43楼2007-12-27 11:28 | abcefg[未注册用户]
谢谢,写好异步方法关键是什么?总要EndXX吧,我最终分不清问题了,只留下一个EndXX做测试,发现如下:我现在每15秒接收200个并发短连接测试,可是当测试达到14000个短连接后,服务端CPU100%占用,线程数从一开始的500个达到1700个,可我用异步接收并没有在我程序中开启线程,这个线程是不是由内异步内核引起的?并且测试端连接丢失数直线上升。
还望楼主和大石头明示,一般怎么写啊,不胜感激。

  回复  引用    
#44楼2007-12-27 11:32 | abcefg[未注册用户]
我的测试只是单指收进连接,至于建立了连接收数据是没问题的。就是如何提高收接连的效率而又不丢连接,又不增加线程数,因为是线程增加了所以cpu占用才高了,有没有好的方法?
  回复  引用    
#45楼2007-12-29 22:01 | janey[未注册用户]
我的电脑经常上网大概3个小时左右断开了(就是打不开网页),但是QQ之类的可以上。一些游戏也不能玩,“跳出创建socket失败,请重启”。我重装过几次,但是电脑还是这样。请问这是什么原因呢?我电脑水平不是很好,懂的不多,请说一下详细的步骤好吗?谢谢!×0×··等待你的回复。
  回复  引用  查看    
#46楼[楼主]2007-12-30 00:53 | 牧野       
@abcefg
我不清楚你的代码是怎么写,你是否可以把你的代码发给我看看是怎么回事。

  回复  引用  查看    
#47楼[楼主]2007-12-30 00:55 | 牧野       
@janey
像这种情况都是由于中了恶意软件而引起的!

  回复  引用    
#48楼2007-12-30 22:33 | janey[未注册用户]
西西,谢谢回复我!
那么,像这种都是由于中了恶意软件而引起的,情况怎么解决呢?杀毒么?
今天杀毒了,有两类病毒 一种是Js.Ie Frame.kf 清除不掉、还有一种是Worm.whBoy.dh.68778可以清除.不知道是什么毒,我在46楼中说的情况,是不是跟这个毒有关呢? 很想快点解决问题,请高手帮帮忙咯!先谢谢了。。

  回复  引用    
#49楼2008-01-01 22:56 | kellynic[未注册用户]
iocp不知道性能怎样,但知道它最好性能

没多大机会自己写服务器端程序,所以路过了

就算用iocp,最终如果连接数爆增时,还得服务器++

所以,用线程来维持连接也没什么问题

  回复  引用    
#50楼2008-01-01 22:58 | kellynic[未注册用户]
适当的让客户端做服务器,减少流量和服务端通讯次数才是王道

不过对楼主的知识挺佩服的

  回复  引用    
#51楼2008-04-09 19:47 | hel420[未注册用户]
楼主这个方法确实有效,比定时发送数据更有效,代码也短,不过就是重发5次,太多了,我觉得2、3次就足够了,可是不知道哪里可以修改这个次数,GOOGLE也很长时间也找不到,所以请教楼主。
  回复  引用  查看    
#52楼[楼主]2008-04-09 20:40 | 牧野       
@hel420
在Unix等操作系统中确实可以设置重发次数,但在Windows操作系统中,只能通过修改注册表的方式来设置重发次数。无法单独对一个Socket对象设置。

  回复  引用    
#53楼2008-04-09 21:29 | hel420[未注册用户]
谢谢楼主:)楼主回答真及时。
还有,我总觉得这种方法是不是每个客户端会生成2个定时器?如果客户端很多的的话会不会特别占用资源呀?好像采用定时发送只需一个定时器,如果客户端定时发送,服务器只是检测客户端的活跃时间,这样好像更好。我正在写一个文件服务器,真费劲。

  回复  引用    
#54楼2008-04-15 13:41 | paul.Wang[未注册用户]
看过楼主的资料, 确实很不错。 呵呵, 我试过了, 是可以的。
  回复  引用    
#55楼2008-05-15 08:15 | Da.yu[未注册用户]
拜读了
感觉很有用,最近折腾SOCKET,一定试试这个方法

  回复  引用    
#56楼2008-05-20 14:24 | lixz[未注册用户]
请教一下,
我现在仿照楼主这段代码设置了IOControl方法后,检测非阻塞方式发送0字节的异常,需要大概2分钟才能捕获异常,达不到楼主说的5秒以后,这是为什么?

  回复  引用    
#57楼2008-05-23 16:18 | huangqun[未注册用户]
但是如果客户端和服务器端是有防火墙得情况呢。有时候客户端不直接连接到服务器得上,而是连接到了防火墙上。这时候如果客户端和防火墙得socket连接断开了,服务器和防火墙得连接未必被断开。那么服务器只是keepalive得话,是不是永远无法知道客户端真得不在了呢?应用程序自己发心跳消息,可以避免这类情况得发生。
  回复  引用    
#58楼2008-07-29 11:20 | 佳龙[未注册用户]
楼主你好,看到你写的东西感觉很受用。很感谢你的分享

我现在有一个问题希望能帮助分析下:
我在做一个中转程序,程序既要连接多个Internet上的Client,又要连接局域网中的一个Server。所以我的程序有一个Server和一个Client,Client对外是多线程,Server对局域网是单线程。

考虑到容错,如果局域网的Server非法断了的话,正在做此方面的改进。我的程序中的Client可以跳出一个异常,但是很难捕捉,因为只有我远端的Client发送数据时才触发我程序中的Client中的ReceiveCallback回调函数产生异常。

不知道楼主有好的解决办法没,我先考虑试下楼主的方法,看容易实现需求不

  回复  引用  查看    
#59楼2008-07-29 12:43 | 大石头      
哈哈,楼上做的就是反向代理呀。
我正好两年前做了一个。
你现在碰到的问题,也正是我当时的问题。
我解决那个问题的方法,有一部分也正是来自这篇文章。

呵呵
http://www.codeplex.com/XProxy
开源的,你可以看看

  回复  引用    
#60楼2008-07-29 15:11 | 佳龙1[未注册用户]
我用的VS2003,郁闷呢

我的程序基本做出来了,我下了石头兄弟的程序,有时间仔细研究下。

我先用牧野兄弟的方法做做看,看是否能实现

十分感谢两位

  回复  引用    
#61楼2008-08-02 11:10 | 颖[未注册用户]
真的非常谢谢楼主
很受用

  回复  引用    
#62楼2008-11-09 01:28 | 悦悦[未注册用户]
很受用,遇到断了,连续检测5次,这个可以设置么,在哪里设置?
  回复  引用    
#63楼2009-01-04 15:28 | 极光[未注册用户]
很感谢楼住的文章,想问一下,我的服务器端是采用TcpListener,用一个线程处理连接、再用另一个线程处理接收数据,用NetworkStream网络流的读取办法,请问能用你上面所说的方法处理Socket网络异常断开么?按你提供的方法试了一下,没反应哦。
  回复  引用    
#64楼2009-08-21 17:33 | socketabc[未注册用户]
请问获得客户端端口后,在哪里处理的呢?
  回复  引用    
#65楼2009-08-21 17:34 | socketabc[未注册用户]
请问获得客户端断开后,在哪里处理的呢?比如客户端断开后要从哈希表中把断开的客户移除
  回复  引用  查看    
#66楼2010-02-09 23:13 | C雷      
看不明白...也要学习一下!