- clientLogin id
- role email account authorized to send c2dm messages (email and password)
If your application has a lot of traffic, you will want to write a windows service or a cron job to send out the messages in batch instead of sending them real time. The source code in this post will be based on a windows service written in C# (C# is very similar to Java).
In my database, I have the following tables:
- c2dm_user - stores the user id and c2dm token pairs
- c2dm_message - stores the messages for the windows service to send out in batch, say each minute 1000 messages
When user A sends a message to user B, a message alert will be inserted into the c2dm_message table if both users A and B exist on c2dm_user table. C2DM is not meant for heavy weighted messages. The intent is to alert the users that there are new messages. What I did is I use the unread message count as the payload. Users will see a notification saying "You got x messages!" in their mobile devices.
Getting the ClientLogin Id
public static String getC2dmAuthToken()
{
String responseFromServer = null;
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(CLIENT_LOGIN_URL);
request.ContentType = "application/x-www-form-urlencoded";
Stream dataStream = null;
request.Method = "POST";
dataStream = request.GetRequestStream();
StringBuilder postData = new StringBuilder();
postData.Append("Email=" + EMAIL);
postData.Append("&Passwd=" + PASSWORD);
postData.Append("&accountType=" + ACCOUNT_TYPE);
postData.Append("&source=" + SOURCE);
postData.Append("&service=" + SERVICE);
byte[] byteArray = Encoding.UTF8.GetBytes(postData.ToString());
dataStream.Write(byteArray, 0, byteArray.Length);
dataStream.Close();
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
dataStream = response.GetResponseStream();
byte[] bytesToRead = new byte[response.ContentLength];
int actuallyRead = 0;
for (; actuallyRead < bytesToRead.Length; )
actuallyRead += dataStream.Read(bytesToRead, actuallyRead, bytesToRead.Length - actuallyRead);
responseFromServer = Encoding.UTF8.GetString(bytesToRead);
dataStream.Close();
response.Close();
}
catch (Exception e)
{
// log exception
}
if (responseFromServer != null && !responseFromServer.Equals(""))
{
if (responseFromServer.Contains("Auth="))
{
responseFromServer = responseFromServer.Substring(responseFromServer.IndexOf("Auth=") + 5);
responseFromServer = responseFromServer.Replace("\n", "");
}
else
responseFromServer = null;
}
return responseFromServer;
}
If your email is a gmail mail, then ACCOUNT_TYPE is "GOOGLE"; service is "ac2dm"; source is a log of what is your app. If you get an error, register here again - http://code.google.com/android/c2dm/signup.html, and make sure it's a gmail account.
Sending a message
public static String SendMessage(string userId, string registrationId, string msg)
{
if (authToken == null || authToken.Equals(""))
{
authToken = getC2dmServerAuthKey();
if (authToken == null || authToken.Equals(""))
{
authToken = getC2dmAuthToken();
}
}
ServicePointManager.ServerCertificateValidationCallback += delegate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
return true;
};
String responseFromServer = null;
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(SEND_MESSAGE_URL);
request.Timeout = 2000;
request.KeepAlive = true;
request.ContentType = "application/x-www-form-urlencoded";
request.Headers.Add("Authorization", "GoogleLogin auth=" + authToken);
Stream dataStream = null;
request.Method = "POST";
StringBuilder postData = new StringBuilder();
postData.Append("registration_id=" + registrationId);
postData.Append("&collapse_key=0");
postData.Append("&data.payload=" + msg);
postData.Append("&data.userid=" + userId);
byte[] byteArray = Encoding.UTF8.GetBytes(postData.ToString());
request.ContentLength = byteArray.Length;
dataStream = request.GetRequestStream();
dataStream.Write(byteArray, 0, byteArray.Length);
dataStream.Close();
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
if (response != null)
{
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
{
// 401 - unauthorized, 403 = forbidden
throw new Exception("401");
}
else if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
{
// 503 - implement exponential backoff
throw new Exception("501");
}
else if (response.StatusCode == HttpStatusCode.OK)
{
string updateClientAuth = response.GetResponseHeader("Update-Client-Auth");
if (!string.IsNullOrEmpty(updateClientAuth))
{
authToken = updateClientAuth;
}
responseFromServer = (new StreamReader(response.GetResponseStream())).ReadToEnd();
}
dataStream.Close();
}
response.Close();
}
catch (Exception e)
{
if (e.Message != null)
{
if (e.Message.IndexOf("401") > 0)
{
throw new Exception("401");
}
else if (e.Message.IndexOf("503") > 0)
{
throw new Exception("503");
}
}
throw new Exception(e.Message);
}
if (responseFromServer != null && !responseFromServer.Equals(""))
{
processSendMessageResponse(responseFromServer, userId, registrationId);
}
return responseFromServer;
}
Note that we are sending the ClientLogin Id in the header for authentication. If you get a 503, implement exponential backoff to avoid getting banned by Google.
Error Processing
public static void processSendMessageResponse(String response, String userId, String registrationId)
{
if (response == null || response.Equals(""))
return;
if (response.StartsWith("Error="))
{
string error = response.Substring(response.IndexOf("Error=") + 6);
switch (error.ToLower().Trim())
{
case "quotaexceeded":
break;
case "devicequotaexceeded":
break;
case "invalidregistration":
break;
case "notregistered":
int s1 = DAL.logServiceStartTime(9999, 0, Thread.CurrentThread.Name + " deleting token");
DAL.deleteToken(registrationId);
break;
case "messagetoobig":
break;
case "missingcollapsekey":
break;
default:
break;
}
}
}
You may want to record the errors.
10 threaded C2DM Windows Service
using System;
using System.Collections.Generic;
using System.Configuration;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using System.Text;
using System.Threading;
namespace C2dmService
{
public partial class C2dmService : ServiceBase
{
private static int BACKOFF_FACTOR = 2;
private static int INITIAL_BACKOFF = 1; // 1 min
private static int DEBUG_TIMEOUT = 30000;
public static bool isRunning = false;
public static bool stopRequested = false;
private static Object queuelock = new Object();
private static int NUM_MSGS_PER_THREAD = 500;
private Queue<ScheduledAlert> mQueue;
private List<Thread> c2dmThreads = new List<Thread>();
public C2dmService()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
Thread.Sleep(DEBUG_TIMEOUT);
stopRequested = false;
while (c2dmThreads.Count < 10)
{
c2dmThreads.Add(null);
}
if (c2dmThreads != null && c2dmThreads.Count > 0)
{
for (int i = 0; i < c2dmThreads.Count; i++)
{
if (c2dmThreads[i] != null)
{
if (c2dmThreads[i].IsAlive)
c2dmThreads[i].Abort();
c2dmThreads[i] = null;
}
c2dmThreads[i] = new Thread(Run);
if (c2dmThreads[i].Name == null)
c2dmThreads[i].Name = "c2dm Thread " + i;
c2dmThreads[i].Start();
}
}
isRunning = true;
}
protected override void OnStop()
{
stopRequested = true;
int counter = 0;
Thread.Sleep(500);
while (isRunning && counter++ < 60)
{
if (c2dmThreads != null && c2dmThreads.Count > 0)
{
for (int i = 0; i < c2dmThreads.Count; i++)
{
if (c2dmThreads[i] != null)
{
c2dmThreads[i].Join(500);
}
}
}
}
}
public void Run()
{
DateTime currentTime = DateTime.Now;
while (true)
{
int counter = 0;
while (counter++ < 60)
{
if (C2dmService.stopRequested)
{
C2dmService.isRunning = false;
return;
}
Thread.Sleep(1000);
}
executeMsgSchedulerReporting();
}
}
private bool executeMsgSchedulerReporting()
{
int backoff = INITIAL_BACKOFF;
int recordId = -1;
try
{
List<ScheduledAlert> alerts = new List<ScheduledAlert>();
lock (queuelock)
{
if (mQueue == null || (mQueue != null && mQueue.Count <= 0))
{
mQueue = DAL.getScheduledAlerts();
}
int numMsgsToFetch = (mQueue.Count > NUM_MSGS_PER_THREAD) ? NUM_MSGS_PER_THREAD : mQueue.Count;
for (int i = 0; i < numMsgsToFetch; i++)
alerts.Add(mQueue.Dequeue());
}
if (alerts.Count > 0)
{
foreach (ScheduledAlert a in alerts)
{
try
{
C2dmMessaging.SendMessage(a.Receiver_id.ToString(), a.Token, a.Count.ToString());
}
catch (Exception e2)
{
if (e2.Message != null)
{
if(e2.Message.Equals("401"))
{
// renew authkey
C2dmMessaging.renewC2dmAuthToken();
}
else if (e2.Message.Equals("503"))
{
// google c2dm service is unavailable - exponential backoff
Thread.Sleep(backoff * 60);
backoff *= BACKOFF_FACTOR;
}
}
}
}
}
}
catch (Exception e)
{
EventLog.WriteEntry(C2DM_SERVICE, C2DM_SERVICE + " an error - " + e.ToString(), EventLogEntryType.Error);
}
EventLog.WriteEntry(C2DM_SERVICE, C2DM_SERVICE + " has finished.", EventLogEntryType.SuccessAudit);
return false;
}
}
}
No comments:
Post a Comment