Mono’s .NET Asynchronous Socket API may block!
.NET’s Asynchronous Socket API has below methods to provide Socket connection.
BeginConnect(EndPoint, AsyncCallback, Object)
BeginConnect(IPAddress, Int32, AsyncCallback, Object)
BeginConnect(IPAddress[], Int32, AsyncCallback, Object)
BeginConnect(String, Int32, AsyncCallback, Object)
They are asynchronous and does not block. If you want to connect using an URL (string), you need to use the 4th one. Its docs say: Begins an asynchronous request for a remote host connection. The host is specified by a host name and a port number.
However, Mono’s implementation of .NET’s Socket.cs is a little bit different.
BeginConnect method, before beginning its asynchronous flow, resolves the URL you provided.
return BeginConnect (Dns.GetHostAddresses (host), port, requestCallback, state);
Dns.GetHostAddresses (host) as the first parameter is a blocking method. Which may block your calling thread unexpectedly (I’ve seen it blocking calling thread up to 10 seconds).
On the other side, decompiled source of Microsoft’s .NET Socket class shows that it is indeed asynchronous.
IAsyncResult hostAddresses = Dns.UnsafeBeginGetHostAddresses(host, new AsyncCallback(Socket.DnsCallback), (object) context);
I don’t why Mono’s implementation is different. If you need a true asynchronous socket connection you need to use BeginConnect with an IPAddress or an EndPoint.
Below, there’s a simple test to confirm this. Just disconnect your internet or use some kind of network throttling tool (Network Link Conditioner on MacOS).
var timer = Stopwatch.StartNew();
client.BeginConnect("www.google.com", 80, Completed, client);
timer.Stop();
Console.Writeline(timer.ElapsedMilliseconds);
There may be a couple of ways to fix this. Here is a simple solution:
...
Dns.BeginGetHostAddresses(host, OnDnsResolved, null);
OnDnsResolved(IAsyncResult ar)
{
var ipAddresses = Dns.EndGetHostAddresses(ar);
client.BeginConnect(ipAddresses, port, OnConnectionComplete, null);
}
OnConnectionComplete(IAsyncResult ar)
{
try
{
client.EndConnect();
}
catch (Exception ex)
{
// handle exceptions separately
}
}
...
Actually if you also want to define a user defined timeout. Here is the combined solution:
Have the connection logic within a separate thread. After both Dns.BeginGetHostAddresses and BeginConnect methods, use AsyncWaitHandle and block until they signal. In the end we make connection asynchronous limited by an external timeout.
new Thread(() =>
{
try
{
var timer = Stopwatch.StartNew();
var dnsResult = Dns.BeginGetHostAddresses(host, OnConnectionComplete, null);
dnsResult.AsyncWaitHandle.WaitOne(connectionTimeoutDuration);
timer.Stop();
var remainingTime = connectionTimeoutDuration - (int) timer.ElapsedMilliseconds;
if (remainingTime < 0)
{
ConnectionTimeout();
return;
}
var ipAddresses = Dns.EndGetHostAddresses(dnsResult);
if (ipAddresses == null)
{
logger.E("DNS Result: IPAddresses is null!");
return;
}
var connectResult = client.BeginConnect(ipAddresses, port, null, null);
var connectHandleSignalled = connectResult.AsyncWaitHandle.WaitOne(remainingTime);
if (!client.Connected && !connectHandleSignalled)
{
ConnectionTimeout();
return;
}
OnConnectionComplete(connectResult);
}
catch (Exception e)
{
OnConnectionError(e);
}
}).Start();
Leave a Comment