diff --git a/.gitignore b/.gitignore index b8dbc72c..45a8bd51 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ node_modules/ .vs/ settings.json +/PushSharp.Apple/ApnsConnection.cs.bak diff --git a/PushSharp.Apple/ApnsConnection.cs b/PushSharp.Apple/ApnsConnection.cs index 0908b166..d735b4df 100644 --- a/PushSharp.Apple/ApnsConnection.cs +++ b/PushSharp.Apple/ApnsConnection.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Net; using PushSharp.Core; +using System.Collections.Concurrent; namespace PushSharp.Apple { @@ -15,7 +16,7 @@ public class ApnsConnection { static int ID = 0; - public ApnsConnection (ApnsConfiguration configuration) + public ApnsConnection(ApnsConfiguration configuration) { id = ++ID; if (id >= int.MaxValue) @@ -25,34 +26,40 @@ public ApnsConnection (ApnsConfiguration configuration) certificate = Configuration.Certificate; - certificates = new X509CertificateCollection (); + certificates = new X509CertificateCollection(); // Add local/machine certificate stores to our collection if requested - if (Configuration.AddLocalAndMachineCertificateStores) { - var store = new X509Store (StoreLocation.LocalMachine); - certificates.AddRange (store.Certificates); + if (Configuration.AddLocalAndMachineCertificateStores) + { + var store = new X509Store(StoreLocation.LocalMachine); + certificates.AddRange(store.Certificates); - store = new X509Store (StoreLocation.CurrentUser); - certificates.AddRange (store.Certificates); + store = new X509Store(StoreLocation.CurrentUser); + certificates.AddRange(store.Certificates); } // Add optionally specified additional certs into our collection - if (Configuration.AdditionalCertificates != null) { + if (Configuration.AdditionalCertificates != null) + { foreach (var addlCert in Configuration.AdditionalCertificates) - certificates.Add (addlCert); + certificates.Add(addlCert); } // Finally, add the main private cert for authenticating to our collection if (certificate != null) - certificates.Add (certificate); + certificates.Add(certificate); - timerBatchWait = new Timer (new TimerCallback (async state => { + timerBatchWait = new Timer(new TimerCallback(async state => + { - await batchSendSemaphore.WaitAsync (); - try { - await SendBatch ().ConfigureAwait (false); - } finally { - batchSendSemaphore.Release (); + await batchSendSemaphore.WaitAsync(); + try + { + await SendBatch().ConfigureAwait(false); + } + finally + { + batchSendSemaphore.Release(); } }), null, Timeout.Infinite, Timeout.Infinite); @@ -69,26 +76,28 @@ public ApnsConnection (ApnsConfiguration configuration) int id; - SemaphoreSlim connectingSemaphore = new SemaphoreSlim (1); - SemaphoreSlim batchSendSemaphore = new SemaphoreSlim (1); - object notificationBatchQueueLock = new object (); + SemaphoreSlim connectingSemaphore = new SemaphoreSlim(1); + SemaphoreSlim batchSendSemaphore = new SemaphoreSlim(1); + object notificationBatchQueueLock = new object(); //readonly object connectingLock = new object (); - Queue notifications = new Queue (); - List sent = new List (); + ConcurrentQueue notifications = new ConcurrentQueue(); + List sent = new List(); Timer timerBatchWait; - public void Send (CompletableApnsNotification notification) + public void Send(CompletableApnsNotification notification) { - lock (notificationBatchQueueLock) { + lock (notificationBatchQueueLock) + { - notifications.Enqueue (notification); + notifications.Enqueue(notification); - if (notifications.Count >= Configuration.InternalBatchSize) { + if (notifications.Count >= Configuration.InternalBatchSize) + { // Make the timer fire immediately and send a batch off - timerBatchWait.Change (0, Timeout.Infinite); + timerBatchWait.Change(0, Timeout.Infinite); return; } @@ -98,159 +107,211 @@ public void Send (CompletableApnsNotification notification) // if the timer is actually called, it means no more notifications were queued, // so we should flush out the queue instead of waiting for more to be batched as they // might not ever come and we don't want to leave anything stranded in the queue - timerBatchWait.Change (Configuration.InternalBatchingWaitPeriod, Timeout.InfiniteTimeSpan); + timerBatchWait.Change(Configuration.InternalBatchingWaitPeriod, Timeout.InfiniteTimeSpan); } } long batchId = 0; - async Task SendBatch () + async Task SendBatch() { batchId++; if (batchId >= long.MaxValue) batchId = 1; - + // Pause the timer - timerBatchWait.Change (Timeout.Infinite, Timeout.Infinite); + timerBatchWait.Change(Timeout.Infinite, Timeout.Infinite); if (notifications.Count <= 0) return; // Let's store the batch items to send internally - var toSend = new List (); + var toSend = new BlockingCollection(); - while (notifications.Count > 0 && toSend.Count < Configuration.InternalBatchSize) { - var n = notifications.Dequeue (); - toSend.Add (n); + while (notifications.Count > 0 && toSend.Count < Configuration.InternalBatchSize) + { + CompletableApnsNotification n; + if (notifications.TryDequeue(out n)) + toSend.Add(n); } - Log.Info ("APNS-Client[{0}]: Sending Batch ID={1}, Count={2}", id, batchId, toSend.Count); + Log.Info("APNS-Client[{0}]: Sending Batch ID={1}, Count={2}", id, batchId, toSend.Count); - try { + try + { - var data = createBatch (toSend); + var data = createBatch(toSend); - if (data != null && data.Length > 0) { + if (data != null && data.Length > 0) + { - for (var i = 0; i <= Configuration.InternalBatchFailureRetryCount; i++) { + for (var i = 0; i <= Configuration.InternalBatchFailureRetryCount; i++) + { - await connectingSemaphore.WaitAsync (); + await connectingSemaphore.WaitAsync(); - try { + try + { // See if we need to connect - if (!socketCanWrite () || i > 0) - await connect (); - } finally { - connectingSemaphore.Release (); + if (!socketCanWrite() || i > 0) + await connect(); + } + finally + { + connectingSemaphore.Release(); } - - try { + + try + { await networkStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); break; - } catch (Exception ex) when (i != Configuration.InternalBatchFailureRetryCount) { + } + catch (Exception ex) when (i != Configuration.InternalBatchFailureRetryCount) + { Log.Info("APNS-CLIENT[{0}]: Retrying Batch: Batch ID={1}, Error={2}", id, batchId, ex); } } foreach (var n in toSend) - sent.Add (new SentNotification (n)); + sent.Add(new SentNotification(n)); } - } catch (Exception ex) { - Log.Error ("APNS-CLIENT[{0}]: Send Batch Error: Batch ID={1}, Error={2}", id, batchId, ex); + } + catch (Exception ex) + { + Log.Error("APNS-CLIENT[{0}]: Send Batch Error: Batch ID={1}, Error={2}", id, batchId, ex); foreach (var n in toSend) - n.CompleteFailed (new ApnsNotificationException (ApnsNotificationErrorStatusCode.ConnectionError, n.Notification, ex)); + n.CompleteFailed(new ApnsNotificationException(ApnsNotificationErrorStatusCode.ConnectionError, n.Notification, ex)); } - Log.Info ("APNS-Client[{0}]: Sent Batch, waiting for possible response...", id); + Log.Info("APNS-Client[{0}]: Sent Batch, waiting for possible response...", id); - try { - await Reader (); - } catch (Exception ex) { - Log.Error ("APNS-Client[{0}]: Reader Exception: {1}", id, ex); + try + { + await Reader(); + } + catch (Exception ex) + { + Log.Error("APNS-Client[{0}]: Reader Exception: {1}", id, ex); } - Log.Info ("APNS-Client[{0}]: Done Reading for Batch ID={1}, reseting batch timer...", id, batchId); + Log.Info("APNS-Client[{0}]: Done Reading for Batch ID={1}, reseting batch timer...", id, batchId); // Restart the timer for the next batch - timerBatchWait.Change (Configuration.InternalBatchingWaitPeriod, Timeout.InfiniteTimeSpan); + timerBatchWait.Change(Configuration.InternalBatchingWaitPeriod, Timeout.InfiniteTimeSpan); } - byte[] createBatch (List toSend) + byte[] createBatch(BlockingCollection toSend) { if (toSend == null || toSend.Count <= 0) return null; - - var batchData = new List (); + + var batchData = new List(); // Add all the frame data foreach (var n in toSend) - batchData.AddRange (n.Notification.ToBytes ()); - - return batchData.ToArray (); + { + CompletableApnsNotification temp = n; + try + { + if (n != null && n.Notification != null) + batchData.AddRange(n.Notification.ToBytes()); + } + catch(NotificationException ex) + { + toSend.TryTake(out temp); + temp.CompleteFailed(new ApnsNotificationException(GetApnsNotificationErrorStatusCode(ex), n.Notification, ex)); + } + finally + { + if (batchData == null) + batchData = new List(); + } + } + + return batchData.ToArray(); + } + + private ApnsNotificationErrorStatusCode GetApnsNotificationErrorStatusCode(Exception ex) + { + var apnsErrorStatusCode = ApnsNotificationErrorStatusCode.ConnectionError; + if (ex.Message.Contains("Missing DeviceToken")) + apnsErrorStatusCode = ApnsNotificationErrorStatusCode.MissingDeviceToken; + else if (ex.Message.Contains("Invalid DeviceToken")) + apnsErrorStatusCode = ApnsNotificationErrorStatusCode.InvalidToken; + else if (ex.Message.Contains( "Invalid DeviceToken Length")) + apnsErrorStatusCode = ApnsNotificationErrorStatusCode.InvalidTokenSize; + else if (ex.Message.Contains("Payload too large")) + apnsErrorStatusCode = ApnsNotificationErrorStatusCode.InvalidPayloadSize; + return apnsErrorStatusCode; } - async Task Reader () + async Task Reader() { - var readCancelToken = new CancellationTokenSource (); + var readCancelToken = new CancellationTokenSource(); // We are going to read from the stream, but the stream *may* not ever have any data for us to // read (in the case that all the messages sent successfully, apple will send us nothing // So, let's make our read timeout after a reasonable amount of time to wait for apple to tell // us of any errors that happened. - readCancelToken.CancelAfter (750); + readCancelToken.CancelAfter(750); int len = -1; - while (!readCancelToken.IsCancellationRequested) { + while (!readCancelToken.IsCancellationRequested) + { // See if there's data to read - if (client.Client.Available > 0) { - Log.Info ("APNS-Client[{0}]: Data Available...", id); - len = await networkStream.ReadAsync (buffer, 0, buffer.Length).ConfigureAwait (false); - Log.Info ("APNS-Client[{0}]: Finished Read.", id); + if (client != null && client.Client != null && client.Client.Available > 0) + { + Log.Info("APNS-Client[{0}]: Data Available...", id); + len = await networkStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + Log.Info("APNS-Client[{0}]: Finished Read.", id); break; } // Let's not tie up too much CPU waiting... - await Task.Delay (50).ConfigureAwait (false); + await Task.Delay(50).ConfigureAwait(false); } - Log.Info ("APNS-Client[{0}]: Received {1} bytes response...", id, len); + Log.Info("APNS-Client[{0}]: Received {1} bytes response...", id, len); // If we got no data back, and we didn't end up canceling, the connection must have closed - if (len == 0) { + if (len == 0) + { - Log.Info ("APNS-Client[{0}]: Server Closed Connection...", id); + Log.Info("APNS-Client[{0}]: Server Closed Connection...", id); // Connection was closed - disconnect (); + disconnect(); return; - } else if (len < 0) { //If we timed out waiting, but got no data to read, everything must be ok! + } + else if (len < 0) + { //If we timed out waiting, but got no data to read, everything must be ok! - Log.Info ("APNS-Client[{0}]: Batch (ID={1}) completed with no error response...", id, batchId); + Log.Info("APNS-Client[{0}]: Batch (ID={1}) completed with no error response...", id, batchId); //Everything was ok, let's assume all 'sent' succeeded foreach (var s in sent) - s.Notification.CompleteSuccessfully (); + s.Notification.CompleteSuccessfully(); - sent.Clear (); + sent.Clear(); return; } // If we make it here, we did get data back, so we have errors - Log.Info ("APNS-Client[{0}]: Batch (ID={1}) completed with error response...", id, batchId); + Log.Info("APNS-Client[{0}]: Batch (ID={1}) completed with error response...", id, batchId); // If we made it here, we did receive some data, so let's parse the error - var status = buffer [1]; - var identifier = IPAddress.NetworkToHostOrder (BitConverter.ToInt32 (buffer, 2)); + var status = buffer[1]; + var identifier = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buffer, 2)); // Let's handle the failure //Get the index of our failed notification (by identifier) - var failedIndex = sent.FindIndex (n => n.Identifier == identifier); + var failedIndex = sent.FindIndex(n => n.Identifier == identifier); // If we didn't find an index for the failed notification, something is wrong // Let's just return @@ -258,45 +319,46 @@ async Task Reader () return; // Get all the notifications before the failed one and mark them as sent! - if (failedIndex > 0) { + if (failedIndex > 0) + { // Get all the notifications sent before the one that failed // We can assume that these all succeeded - var successful = sent.GetRange (0, failedIndex); //TODO: Should it be failedIndex - 1? + var successful = sent.GetRange(0, failedIndex); //TODO: Should it be failedIndex - 1? // Complete all the successfully sent notifications foreach (var s in successful) - s.Notification.CompleteSuccessfully (); + s.Notification.CompleteSuccessfully(); // Remove all the successful notifications from the sent list // This should mean the failed notification is now at index 0 - sent.RemoveRange (0, failedIndex); + sent.RemoveRange(0, failedIndex); } //Get the failed notification itself - var failedNotification = sent [0]; + var failedNotification = sent[0]; //Fail and remove the failed index from the list - Log.Info ("APNS-Client[{0}]: Failing Notification {1}", id, failedNotification.Identifier); - failedNotification.Notification.CompleteFailed ( - new ApnsNotificationException (status, failedNotification.Notification.Notification)); + Log.Info("APNS-Client[{0}]: Failing Notification {1}", id, failedNotification.Identifier); + failedNotification.Notification.CompleteFailed( + new ApnsNotificationException(status, failedNotification.Notification.Notification)); // Now remove the failed notification from the sent list - sent.RemoveAt (0); + sent.RemoveAt(0); // The remaining items in the list were sent after the failed notification // we can assume these were ignored by apple so we need to send them again // Requeue the remaining notifications foreach (var s in sent) - notifications.Enqueue (s.Notification); + notifications.Enqueue(s.Notification); // Clear our sent list - sent.Clear (); + sent.Clear(); // Apple will close our connection after this anyway - disconnect (); + disconnect(); } - bool socketCanWrite () + bool socketCanWrite() { if (client == null) return false; @@ -307,96 +369,111 @@ bool socketCanWrite () if (!client.Client.Connected) return false; - var p = client.Client.Poll (1000, SelectMode.SelectWrite); + var p = client.Client.Poll(1000, SelectMode.SelectWrite); - Log.Info ("APNS-Client[{0}]: Can Write? {1}", id, p); + Log.Info("APNS-Client[{0}]: Can Write? {1}", id, p); return p; } - async Task connect () - { + async Task connect() + { if (client != null) - disconnect (); - - Log.Info ("APNS-Client[{0}]: Connecting (Batch ID={1})", id, batchId); + disconnect(); - client = new TcpClient (); + Log.Info("APNS-Client[{0}]: Connecting (Batch ID={1})", id, batchId); - try { - await client.ConnectAsync (Configuration.Host, Configuration.Port).ConfigureAwait (false); + client = new TcpClient(); + + try + { + await client.ConnectAsync(Configuration.Host, Configuration.Port).ConfigureAwait(false); //Set keep alive on the socket may help maintain our APNS connection - try { - client.Client.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - } catch { + try + { + client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + } + catch + { } //Really not sure if this will work on MONO.... // This may help windows azure users - try { - SetSocketKeepAliveValues (client.Client, (int)Configuration.KeepAlivePeriod.TotalMilliseconds, (int)Configuration.KeepAliveRetryPeriod.TotalMilliseconds); - } catch { + try + { + SetSocketKeepAliveValues(client.Client, (int)Configuration.KeepAlivePeriod.TotalMilliseconds, (int)Configuration.KeepAliveRetryPeriod.TotalMilliseconds); + } + catch + { } - } catch (Exception ex) { - throw new ApnsConnectionException ("Failed to Connect, check your firewall settings!", ex); + } + catch (Exception ex) + { + throw new ApnsConnectionException("Failed to Connect, check your firewall settings!", ex); } // We can configure skipping ssl all together, ie: if we want to hit a test server - if (Configuration.SkipSsl) { - networkStream = client.GetStream (); - } else { + if (Configuration.SkipSsl) + { + networkStream = client.GetStream(); + } + else + { // Create our ssl stream - stream = new SslStream (client.GetStream (), + stream = new SslStream(client.GetStream(), false, ValidateRemoteCertificate, (sender, targetHost, localCerts, remoteCert, acceptableIssuers) => certificate); - try { - stream.AuthenticateAsClient (Configuration.Host, certificates, System.Security.Authentication.SslProtocols.Tls, false); - } catch (System.Security.Authentication.AuthenticationException ex) { - throw new ApnsConnectionException ("SSL Stream Failed to Authenticate as Client", ex); + try + { + stream.AuthenticateAsClient(Configuration.Host, certificates, System.Security.Authentication.SslProtocols.Tls, false); + } + catch (System.Security.Authentication.AuthenticationException ex) + { + throw new ApnsConnectionException("SSL Stream Failed to Authenticate as Client", ex); } if (!stream.IsMutuallyAuthenticated) - throw new ApnsConnectionException ("SSL Stream Failed to Authenticate", null); + throw new ApnsConnectionException("SSL Stream Failed to Authenticate", null); if (!stream.CanWrite) - throw new ApnsConnectionException ("SSL Stream is not Writable", null); + throw new ApnsConnectionException("SSL Stream is not Writable", null); networkStream = stream; } - Log.Info ("APNS-Client[{0}]: Connected (Batch ID={1})", id, batchId); + Log.Info("APNS-Client[{0}]: Connected (Batch ID={1})", id, batchId); } - void disconnect () - { - Log.Info ("APNS-Client[{0}]: Disconnecting (Batch ID={1})", id, batchId); + void disconnect() + { + Log.Info("APNS-Client[{0}]: Disconnecting (Batch ID={1})", id, batchId); //We now expect apple to close the connection on us anyway, so let's try and close things // up here as well to get a head start //Hopefully this way we have less messages written to the stream that we have to requeue - try { stream.Close (); } catch { } - try { stream.Dispose (); } catch { } + try { stream.Close(); } catch { } + try { stream.Dispose(); } catch { } - try { networkStream.Close (); } catch { } - try { networkStream.Dispose (); } catch { } + try { networkStream.Close(); } catch { } + try { networkStream.Dispose(); } catch { } - try { client.Client.Shutdown (SocketShutdown.Both); } catch { } - try { client.Client.Dispose (); } catch { } + try { client.Client.Shutdown(SocketShutdown.Both); } catch { } + try { client.Client.Dispose(); } catch { } - try { client.Close (); } catch { } + try { client.Close(); } catch { } client = null; networkStream = null; stream = null; - Log.Info ("APNS-Client[{0}]: Disconnected (Batch ID={1})", id, batchId); + Log.Info("APNS-Client[{0}]: Disconnected (Batch ID={1})", id, batchId); } - bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors) + bool ValidateRemoteCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors) { if (Configuration.ValidateServerCertificate) return policyErrors == SslPolicyErrors.None; @@ -408,7 +485,7 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 public class SentNotification { - public SentNotification (CompletableApnsNotification notification) + public SentNotification(CompletableApnsNotification notification) { this.Notification = notification; this.SentAt = DateTime.UtcNow; @@ -424,29 +501,29 @@ public SentNotification (CompletableApnsNotification notification) public class CompletableApnsNotification { - public CompletableApnsNotification (ApnsNotification notification) + public CompletableApnsNotification(ApnsNotification notification) { Notification = notification; - completionSource = new TaskCompletionSource (); + completionSource = new TaskCompletionSource(); } public ApnsNotification Notification { get; private set; } TaskCompletionSource completionSource; - public Task WaitForComplete () + public Task WaitForComplete() { return completionSource.Task; } - public void CompleteSuccessfully () + public void CompleteSuccessfully() { - completionSource.SetResult (null); + completionSource.SetResult(null); } - public void CompleteFailed (Exception ex) + public void CompleteFailed(Exception ex) { - completionSource.SetResult (ex); + completionSource.SetResult(ex); } } @@ -456,22 +533,22 @@ public void CompleteFailed (Exception ex) /// TcpClient /// The keep alive time. (ms) /// The keep alive interval. (ms) - public static void SetSocketKeepAliveValues (Socket socket, int KeepAliveTime, int KeepAliveInterval) + public static void SetSocketKeepAliveValues(Socket socket, int KeepAliveTime, int KeepAliveInterval) { //KeepAliveTime: default value is 2hr //KeepAliveInterval: default value is 1s and Detect 5 times uint dummy = 0; //lenth = 4 - byte[] inOptionValues = new byte[System.Runtime.InteropServices.Marshal.SizeOf (dummy) * 3]; //size = lenth * 3 = 12 + byte[] inOptionValues = new byte[System.Runtime.InteropServices.Marshal.SizeOf(dummy) * 3]; //size = lenth * 3 = 12 - BitConverter.GetBytes ((uint)1).CopyTo (inOptionValues, 0); - BitConverter.GetBytes ((uint)KeepAliveTime).CopyTo (inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf (dummy)); - BitConverter.GetBytes ((uint)KeepAliveInterval).CopyTo (inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf (dummy) * 2); + BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0); + BitConverter.GetBytes((uint)KeepAliveTime).CopyTo(inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf(dummy)); + BitConverter.GetBytes((uint)KeepAliveInterval).CopyTo(inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf(dummy) * 2); // of course there are other ways to marshal up this byte array, this is just one way // call WSAIoctl via IOControl // .net 3.5 type - socket.IOControl (IOControlCode.KeepAliveValues, inOptionValues, null); + socket.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null); } } } diff --git a/README.md b/README.md index b3ae1f9b..ba6acc23 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ PushSharp v4.0 ============== +Same as the original PushSharp only I fixed some bugs while sending thousends of IOS notification. + +--Client null control added + +--Prevent from blocking all notification batch if some of tokens are not valid. (Only invalid tokens are removed from batch) + PushSharp is a server-side library for sending Push Notifications to iOS/OSX (APNS), Android/Chrome (GCM), Windows/Windows Phone, Amazon (ADM) and Blackberry devices!