C# socket连接超时等待问题(转载)
转自:http://www.cnblogs.com/weidagang2046/archive/2009/02/07/1385977.html
作者:RazanPaul
译者:Todd Wei
原文:http://www.codeproject.com/KB/IP/TimeOutSocket.aspx
介绍
您可能注意到了,.Net的System.Net.Sockets.TcpClient和System.Net.Sockets.Socket都没有直接为Connect/BeginConnect提供超时控制机制。因此,当服务器未处于监听状态,或者发生网络故障时,客户端连接请求会被迫等待很长一段时间,直到抛出异常。默认的等待时间长达20~30s。.Net Socket库的SocketOptionName.SendTimeout提供了控制发送数据的超时时间,但并非本文讨论的连接请求的超时时间。
背景
这个问题最初源于我的某个项目,在解决以后,我曾将关键代码发表在自己的博客上。我注意到不少人对此表示感谢,所以我想这是一个常见的问题,或许很多人都需要解决它。
实现
下面是实现的关键代码:
class TimeOutSocket
{
private static bool IsConnectionSuccessful= false;
private static Exception socketexception;
private static ManualResetEvent TimeoutObject= new ManualResetEvent(false);
public static TcpClient Connect(IPEndPoint remoteEndPoint,int timeoutMSec)
{
TimeoutObject.Reset();
socketexception = null;
string serverip= Convert.ToString(remoteEndPoint.Address);
int serverport= remoteEndPoint.Port;
TcpClient tcpclient = new TcpClient();
tcpclient.BeginConnect(serverip, serverport,
new AsyncCallback(CallBackMethod), tcpclient);
if (TimeoutObject.WaitOne(timeoutMSec,false))
{
if (IsConnectionSuccessful)
{
return tcpclient;
}
else
{
throw socketexception;
}
}
else
{
tcpclient.Close();
throw new TimeoutException("TimeOut Exception");
}
}
private static void CallBackMethod(IAsyncResult asyncresult)
{
try
{
IsConnectionSuccessful = false;
TcpClient tcpclient = asyncresult.AsyncStateas TcpClient;
if (tcpclient.Client!= null)
{
tcpclient.EndConnect(asyncresult);
IsConnectionSuccessful = true;
}
}
catch (Exception ex)
{
IsConnectionSuccessful = false;
socketexception = ex;
}
finally
{
TimeoutObject.Set();
}
}
}
这里,ManualResetEvent的WaitOne(TimeSpan, Boolean)起到了主要的作用。它将阻止当前线程,直到ManualResetEvent对象被Set或者超过timeout时间。上面的代码中,调用BeginConnect后通过WaitOne方法阻止当前线程,如果在timeoutMSec时间内连接成功,将在CallBackMethod回调中调用TimeoutObject.Set,解除被阻塞的连接线程并返回;否则,连接线程会在等待超时后,主动关闭连接并抛出TimeoutException。
总结
虽然实现非常简单,但或许很多人都需要连接请求超时机制,如果有任何问题,我会尽力为您解答。
[译注]
作者介绍了一种异步连接+WaitOne的连接请求超时机制。其中的实现细节有值得商榷的地方,比如:a.static成员带来的线程安全性问题;b.可以考虑利用IAsyncResult.AsyncWaitHandle,不必另行创建ManualResetEvent。但瑕不掩瑜,感谢作者的解决思路。
------------------------ gdjlc备注-----------------------------------------
如果不用TcpClient,直接用Socket,可改为:
class TimeOutSocket
{
private static bool IsConnectionSuccessful = false;
private static Exception socketexception;
private static System.Threading.ManualResetEvent TimeoutObject = new System.Threading.ManualResetEvent(false);
public static Socket Connect(IPEndPoint remoteEndPoint, int timeoutMSec)
{
TimeoutObject.Reset();
socketexception = null;
Socket socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketClient.BeginConnect(remoteEndPoint, new AsyncCallback(CallBackMethod), socketClient);
if (TimeoutObject.WaitOne(timeoutMSec, false))
{
if (IsConnectionSuccessful)
return socketClient;
else
throw socketexception;
}
else
{
socketClient.Close();
throw new TimeoutException("TimeOut Exception");
}
}
private static void CallBackMethod(IAsyncResult asyncresult)
{
try
{
IsConnectionSuccessful = false;
Socket socketClient = asyncresult.AsyncState as Socket;
if (socketClient != null)
{
socketClient.EndConnect(asyncresult);
IsConnectionSuccessful = true;
}
}
catch (Exception ex)
{
IsConnectionSuccessful = false;
socketexception = ex;
}
finally
{
TimeoutObject.Set();
}
}
}
自用修改了一下:
public class TimeoutSocket
{
private static bool IsConnectionSuccessful = false;
private static Exception socketexception;
private static System.Threading.ManualResetEvent TimeoutObject = new System.Threading.ManualResetEvent(false);
public static Socket Connect(Socket socketClient, IPEndPoint remoteEndPoint, int timeoutMSec)
{
TimeoutObject.Reset();
socketexception = null;
//Socket socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketClient.BeginConnect(remoteEndPoint, new AsyncCallback(CallBackMethod), socketClient);
if (TimeoutObject.WaitOne(timeoutMSec, false))
{
if (IsConnectionSuccessful)
return socketClient;
else
throw socketexception;
}
else
{
socketClient.Close();
throw new TimeoutException("socket连接超时,请检查网络状态");
}
}
private static void CallBackMethod(IAsyncResult asyncresult)
{
try
{
IsConnectionSuccessful = false;
Socket socketClient = asyncresult.AsyncState as Socket;
if (socketClient != null)
{
socketClient.EndConnect(asyncresult);
IsConnectionSuccessful = true;
}
}
catch (Exception ex)
{
IsConnectionSuccessful = false;
socketexception = ex;
}
finally
{
TimeoutObject.Set();
}
}
}
调用:
/// <summary>
/// 建立连接
/// </summary>
public void Connect(ref bool isOk,ref string message)
{
//绑定主机
//IPHostEntry iPHostEntry = Dns.GetHostEntry(Host);
//_IPAddress = iPHostEntry.AddressList[iPHostEntry.AddressList.Length - 1];
_IPAddress = IPAddress.Parse(Host);
IPEndPoint ep = new IPEndPoint(_IPAddress, Port);
switch (ep.AddressFamily)
{
//同时支持IP6,IP4
case AddressFamily.InterNetwork:
_SocketControl = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
break;
case AddressFamily.InterNetworkV6:
_SocketControl = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
break;
default:
_SocketControl = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
break;
}
// 链接
try
{
//在此处调用,可起到阻塞连接的作用
_SocketControl=TimeoutSocket.Connect(_SocketControl,ep,1500);
}
catch (Exception e)
{
IsConnected = false;
isOk = false;
message = e.ToString();
return;
}
FetchReply(ref isOk,ref message); // 获取应答码
if (!isOk)
{
IsConnected = false;
return;
}
if (ReplyCode != 220) //在连接到一台FTP服务器的端口21并接收到一个由代码220打头的行,表示服务器已准备好你向它发USER和PASS命令,以登录进此FTP服务器之后,紧跟着发送USER命令。
{
IsConnected = false;
isOk = false;
message = "ftp连接失败";
return;
}
SendCommand("USER " + _UserName,ref isOk,ref message);
if (!(ReplyCode == 331 || ReplyCode == 230))
{
CloseSocketConnect();//关闭连接
isOk = false;
message = "ftp连接失败:"+ ReplyMessage.Substring(4);
return;
//throw new IOException(ReplyMessage.Substring(4));
}
if (ReplyCode != 230)
{
SendCommand("PASS " + _Password,ref isOk ,ref message);
if (!(ReplyCode == 230 || ReplyCode == 202))
{
CloseSocketConnect();//关闭连接
isOk = false;
message = "ftp连接失败:" + ReplyMessage.Substring(4);
return;
//throw new IOException(ReplyMessage.Substring(4));
}
}
IsConnected = true; // 切换到目录
isOk = true;
message = "ftp连接成功";
ChangeWorkDirectory(WorkDirectory,ref isOk,ref message);
}