1 //------------------------------------------------------------------------------
2 // <copyright file="_MultipleConnectAsync.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //------------------------------------------------------------------------------
6 
7 using System.Threading;
8 
9 namespace System.Net.Sockets
10 {
11     // This object is used to wrap a bunch of ConnectAsync operations
12     // on behalf of a single user call to ConnectAsync with a DnsEndPoint
13     internal abstract class MultipleConnectAsync
14     {
15         protected SocketAsyncEventArgs userArgs;
16         protected SocketAsyncEventArgs internalArgs;
17 
18         protected DnsEndPoint endPoint;
19         protected IPAddress[] addressList;
20         protected int nextAddress;
21 
22         private enum State
23         {
24             NotStarted,
25             DnsQuery,
26             ConnectAttempt,
27             Completed,
28             Canceled,
29         }
30 
31         private State state;
32 
33         private object lockObject = new object();
34 
35         // Called by Socket to kick off the ConnectAsync process.  We'll complete the user's SAEA
36         // when it's done.  Returns true if the operation will be asynchronous, false if it has failed synchronously
StartConnectAsync(SocketAsyncEventArgs args, DnsEndPoint endPoint)37         public bool StartConnectAsync(SocketAsyncEventArgs args, DnsEndPoint endPoint)
38         {
39             lock (lockObject)
40             {
41                 GlobalLog.Assert(endPoint.AddressFamily == AddressFamily.Unspecified ||
42                      endPoint.AddressFamily == AddressFamily.InterNetwork ||
43                      endPoint.AddressFamily == AddressFamily.InterNetworkV6,
44                      "MultipleConnectAsync.StartConnectAsync(): Unexpected endpoint address family - " + endPoint.AddressFamily.ToString());
45 
46                 this.userArgs = args;
47                 this.endPoint = endPoint;
48 
49                 // If Cancel() was called before we got the lock, it only set the state to Canceled: we need to
50                 // fail synchronously from here.  Once State.DnsQuery is set, the Cancel() call will handle calling AsyncFail.
51                 if (state == State.Canceled)
52                 {
53                     SyncFail(new SocketException(SocketError.OperationAborted));
54                     return false;
55                 }
56 
57                 GlobalLog.Assert(state == State.NotStarted, "MultipleConnectAsync.StartConnectAsync(): Unexpected object state");
58 
59                 state = State.DnsQuery;
60 
61                 IAsyncResult result = Dns.BeginGetHostAddresses(endPoint.Host, new AsyncCallback(DnsCallback), null);
62                 if (result.CompletedSynchronously)
63                 {
64                     return DoDnsCallback(result, true);
65                 }
66                 else
67                 {
68                     return true;
69                 }
70             }
71         }
72 
73         // Callback which fires when the Dns Resolve is complete
DnsCallback(IAsyncResult result)74         private void DnsCallback(IAsyncResult result)
75         {
76             if (!result.CompletedSynchronously)
77             {
78                 DoDnsCallback(result, false);
79             }
80         }
81 
82         // Called when the DNS query completes (either synchronously or asynchronously).  Checks for failure and
83         // starts the first connection attempt if it succeeded.  Returns true if the operation will be asynchronous,
84         // false if it has failed synchronously.
DoDnsCallback(IAsyncResult result, bool sync)85         private bool DoDnsCallback(IAsyncResult result, bool sync)
86         {
87             Exception exception = null;
88 
89             lock (lockObject)
90             {
91                 // If the connection attempt was canceled during the dns query, the user's callback has already been
92                 // called asynchronously and we simply need to return.
93                 if (state == State.Canceled)
94                 {
95                     return true;
96                 }
97 
98                 GlobalLog.Assert(state == State.DnsQuery, "MultipleConnectAsync.DoDnsCallback(): Unexpected object state");
99 
100                 try
101                 {
102                     addressList = Dns.EndGetHostAddresses(result);
103                     GlobalLog.Assert(addressList != null, "MultipleConnectAsync.DoDnsCallback(): EndGetHostAddresses returned null!");
104                 }
105                 catch (Exception e)
106                 {
107                     state = State.Completed;
108                     exception = e;
109                 }
110 
111                 // If the dns query succeeded, try to connect to the first address
112                 if (exception == null)
113                 {
114                     state = State.ConnectAttempt;
115 
116                     internalArgs = new SocketAsyncEventArgs();
117                     internalArgs.Completed += InternalConnectCallback;
118                     internalArgs.SetBuffer(userArgs.Buffer, userArgs.Offset, userArgs.Count);
119 
120                     exception = AttemptConnection();
121 
122                     if (exception != null)
123                     {
124                         // There was a synchronous error while connecting
125                         state = State.Completed;
126                     }
127                 }
128             }
129 
130             // Call this outside of the lock because it might call the user's callback.
131             if (exception != null)
132             {
133                 return Fail(sync, exception);
134             }
135             else
136             {
137                 return true;
138             }
139         }
140 
141         // Callback which fires when an internal connection attempt completes.
142         // If it failed and there are more addresses to try, do it.
InternalConnectCallback(object sender, SocketAsyncEventArgs args)143         private void InternalConnectCallback(object sender, SocketAsyncEventArgs args)
144         {
145             Exception exception = null;
146 
147             lock (lockObject)
148             {
149                 if (state == State.Canceled)
150                 {
151                     // If Cancel was called before we got the lock, the Socket will be closed soon.  We need to report
152                     // OperationAborted (even though the connection actually completed), or the user will try to use a
153                     // closed Socket.
154                     exception = new SocketException(SocketError.OperationAborted);
155                 }
156                 else
157                 {
158                     GlobalLog.Assert(state == State.ConnectAttempt, "MultipleConnectAsync.InternalConnectCallback(): Unexpected object state");
159 
160                     if (args.SocketError == SocketError.Success)
161                     {
162                         // the connection attempt succeeded; go to the completed state.
163                         // the callback will be called outside the lock
164                         state = State.Completed;
165                     }
166                     else if (args.SocketError == SocketError.OperationAborted)
167                     {
168                         // The socket was closed while the connect was in progress.  This can happen if the user
169                         // closes the socket, and is equivalent to a call to CancelConnectAsync
170                         exception = new SocketException(SocketError.OperationAborted);
171                         state = State.Canceled;
172                     }
173                     else
174                     {
175                         // Try again, if there are more IPAddresses to be had.
176 
177                         // Keep track of this because it will be overwritten by AttemptConnection
178                         SocketError currentFailure = args.SocketError;
179                         Exception connectException = AttemptConnection();
180 
181                         if (connectException == null)
182                         {
183                             // don't call the callback, another connection attempt is successfully started
184                             return;
185                         }
186                         else
187                         {
188                             SocketException socketException = connectException as SocketException;
189                             if (socketException != null && socketException.SocketErrorCode == SocketError.NoData)
190                             {
191                                 // If the error is NoData, that means there are no more IPAddresses to attempt
192                                 // a connection to.  Return the last error from an actual connection instead.
193                                 exception = new SocketException(currentFailure);
194                             }
195                             else
196                             {
197                                 exception = connectException;
198                             }
199 
200                             state = State.Completed;
201                         }
202                     }
203                 }
204             }
205 
206             if (exception == null)
207             {
208                 Succeed();
209             }
210             else
211             {
212                 AsyncFail(exception);
213             }
214         }
215 
216         // Called to initiate a connection attempt to the next address in the list.  Returns an exception
217         // if the attempt failed synchronously, or null if it was successfully initiated.
AttemptConnection()218         private Exception AttemptConnection()
219         {
220             try
221             {
222                 Socket attemptSocket = null;
223                 IPAddress attemptAddress = GetNextAddress(out attemptSocket);
224 
225                 if (attemptAddress == null)
226                 {
227                     return new SocketException(SocketError.NoData);
228                 }
229 
230                 GlobalLog.Assert(attemptSocket != null, "MultipleConnectAsync.AttemptConnection: attemptSocket is null!");
231 
232                 internalArgs.RemoteEndPoint = new IPEndPoint(attemptAddress, endPoint.Port);
233 
234                 if (!attemptSocket.ConnectAsync(internalArgs))
235                 {
236                     return new SocketException(internalArgs.SocketError);
237                 }
238             }
239             catch (ObjectDisposedException)
240             {
241                 // This can happen if the user closes the socket, and is equivalent to a call
242                 // to CancelConnectAsync
243                 return new SocketException(SocketError.OperationAborted);
244             }
245             catch (Exception e)
246             {
247                 return e;
248             }
249 
250             return null;
251         }
252 
OnSucceed()253         protected abstract void OnSucceed();
254 
Succeed()255         protected void Succeed()
256         {
257             OnSucceed();
258             userArgs.FinishWrapperConnectSuccess(internalArgs.ConnectSocket, internalArgs.BytesTransferred, internalArgs.SocketFlags);
259             internalArgs.Dispose();
260         }
261 
OnFail(bool abortive)262         protected abstract void OnFail(bool abortive);
263 
Fail(bool sync, Exception e)264         private bool Fail(bool sync, Exception e)
265         {
266             if (sync)
267             {
268                 SyncFail(e);
269                 return false;
270             }
271             else
272             {
273                 AsyncFail(e);
274                 return true;
275             }
276         }
277 
SyncFail(Exception e)278         private void SyncFail(Exception e)
279         {
280             OnFail(false);
281             if (internalArgs != null)
282             {
283                 internalArgs.Dispose();
284             }
285 
286             SocketException socketException = e as SocketException;
287             if (socketException != null)
288             {
289                 userArgs.FinishConnectByNameSyncFailure(socketException, 0, SocketFlags.None);
290             }
291             else
292             {
293                 throw e;
294             }
295         }
296 
AsyncFail(Exception e)297         private void AsyncFail(Exception e)
298         {
299             OnFail(false);
300             if (internalArgs != null)
301             {
302                 internalArgs.Dispose();
303             }
304 
305             userArgs.FinishOperationAsyncFailure(e, 0, SocketFlags.None);
306         }
307 
Cancel()308         public void Cancel()
309         {
310             bool callOnFail = false;
311 
312             lock (lockObject)
313             {
314                 switch (state)
315                 {
316                     case State.NotStarted:
317                         // Cancel was called before the Dns query was started.  The dns query won't be started
318                         // and the connection attempt will fail synchronously after the state change to DnsQuery.
319                         // All we need to do here is close all the sockets.
320                         callOnFail = true;
321                         break;
322 
323                     case State.DnsQuery:
324                         // Cancel was called after the Dns query was started, but before it finished.  We can't
325                         // actually cancel the Dns query, but we'll fake it by failing the connect attempt asynchronously
326                         // from here, and silently dropping the connection attempt when the Dns query finishes.
327                         ThreadPool.QueueUserWorkItem(CallAsyncFail);
328                         callOnFail = true;
329                         break;
330 
331                     case State.ConnectAttempt:
332                         // Cancel was called after the Dns query completed, but before we had a connection result to give
333                         // to the user.  Closing the sockets will cause any in-progress ConnectAsync call to fail immediately
334                         // with OperationAborted, and will cause ObjectDisposedException from any new calls to ConnectAsync
335                         // (which will be translated to OperationAborted by AttemptConnection).
336                         callOnFail = true;
337                         break;
338 
339                     case State.Completed:
340                         // Cancel was called after we locked in a result to give to the user.  Ignore it and give the user
341                         // the real completion.
342                         break;
343 
344                     default:
345                         GlobalLog.Assert("MultipleConnectAsync.Cancel(): Unexpected object state");
346                         break;
347                 }
348 
349                 state = State.Canceled;
350             }
351 
352             // Call this outside the lock because Socket.Close may block
353             if (callOnFail)
354             {
355                 OnFail(true);
356             }
357         }
358 
359         // Call AsyncFail on a threadpool thread so it's asynchronous with respect to Cancel().
CallAsyncFail(object ignored)360         private void CallAsyncFail(object ignored)
361         {
362             AsyncFail(new SocketException(SocketError.OperationAborted));
363         }
364 
GetNextAddress(out Socket attemptSocket)365         protected abstract IPAddress GetNextAddress(out Socket attemptSocket);
366     }
367 
368     // Used when the instance ConnectAsync method is called, or when the DnsEndPoint specified
369     // an AddressFamily.  There's only one Socket, and we only try addresses that match its
370     // AddressFamily
371     internal class SingleSocketMultipleConnectAsync : MultipleConnectAsync
372     {
373         private Socket socket;
374         private bool userSocket;
375 
SingleSocketMultipleConnectAsync(Socket socket, bool userSocket)376         public SingleSocketMultipleConnectAsync(Socket socket, bool userSocket)
377         {
378             this.socket = socket;
379             this.userSocket = userSocket;
380         }
381 
GetNextAddress(out Socket attemptSocket)382         protected override IPAddress GetNextAddress(out Socket attemptSocket)
383         {
384             attemptSocket = socket;
385 
386             IPAddress rval = null;
387             do
388             {
389                 if (nextAddress >= addressList.Length)
390                 {
391                     return null;
392                 }
393 
394                 rval = addressList[nextAddress];
395                 ++nextAddress;
396             }
397             while (!socket.CanTryAddressFamily(rval.AddressFamily));
398 
399             return rval;
400         }
401 
OnFail(bool abortive)402         protected override void OnFail(bool abortive)
403         {
404             // Close the socket if this is an abortive failure (CancelConnectAsync)
405             // or if we created it internally
406             if (abortive || !userSocket)
407             {
408                 socket.Close();
409             }
410         }
411 
412         // nothing to do on success
OnSucceed()413         protected override void OnSucceed() { }
414     }
415 
416     // This is used when the static ConnectAsync method is called.  We don't know the address family
417     // ahead of time, so we create both IPv4 and IPv6 sockets.
418     internal class MultipleSocketMultipleConnectAsync : MultipleConnectAsync
419     {
420         private Socket socket4;
421         private Socket socket6;
422 
MultipleSocketMultipleConnectAsync(SocketType socketType, ProtocolType protocolType)423         public MultipleSocketMultipleConnectAsync(SocketType socketType, ProtocolType protocolType)
424         {
425             if (Socket.OSSupportsIPv4)
426             {
427                 socket4 = new Socket(AddressFamily.InterNetwork, socketType, protocolType);
428             }
429             if (Socket.OSSupportsIPv6)
430             {
431                 socket6 = new Socket(AddressFamily.InterNetworkV6, socketType, protocolType);
432             }
433         }
434 
GetNextAddress(out Socket attemptSocket)435         protected override IPAddress GetNextAddress(out Socket attemptSocket)
436         {
437             IPAddress rval = null;
438             attemptSocket = null;
439 
440             while (attemptSocket == null)
441             {
442                 if (nextAddress >= addressList.Length)
443                 {
444                     return null;
445                 }
446 
447                 rval = addressList[nextAddress];
448                 ++nextAddress;
449 
450                 if (rval.AddressFamily == AddressFamily.InterNetworkV6)
451                 {
452                     attemptSocket = socket6;
453                 }
454                 else if (rval.AddressFamily == AddressFamily.InterNetwork)
455                 {
456                     attemptSocket = socket4;
457                 }
458             }
459 
460             return rval;
461         }
462 
463         // on success, close the socket that wasn't used
OnSucceed()464         protected override void OnSucceed()
465         {
466             if (socket4 != null && !socket4.Connected)
467             {
468                 socket4.Close();
469             }
470             if (socket6 != null && !socket6.Connected)
471             {
472                 socket6.Close();
473             }
474         }
475 
476         // close both sockets whether its abortive or not - we always create them internally
OnFail(bool abortive)477         protected override void OnFail(bool abortive)
478         {
479             if (socket4 != null)
480             {
481                 socket4.Close();
482             }
483             if (socket6 != null)
484             {
485                 socket6.Close();
486             }
487         }
488     }
489 }
490