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