Android C2DM의 구조

Android 2011. 11. 2. 16:31

이번엔 C2DM에 관하여 포스팅하겠다.

앱 개발을 하다보면, 필요에 따라 Server에서 Client로 Push 방식으로 msg를 보내야 할 때가 생긴다.
상식적으로 Server는 고정된 ip(uri)를 가지고 항시 켜져있기 때문에 client가 server로 데이터를 보내는 건 아무때나 가능하다. 그러나 client는 항상 켜져있지 않고 고정된 ip가 아니기 때문에 server 입장에서 client에 msg를 보내는건 거의 불가능에 가깝다.

이러한 문제는 다각도에서 고민되어 왔고 지금도 회자되고 있지만 100% 완벽한 push는 아직 존재하지 않는 듯 하다.
APNS (애플), C2DM (구글), SKT&NHN 에서 제공하는 push 서비스들이 존재하지만 이 모든것의 기본 개념은 어찌 되었건 client와 server의 주기적인 접속 유지가 되어야만 가능하다.

비록, 얼마나 빈번하게 접속 유지를 해야 할지, 또 응답속도와 신뢰할 수 있는 security등은 세부 구현과정에서 조금씩 다르겠지만 근본적인 방식은 TLS (transport layer security) 위에서 동작한다. 결론은 100% 완벽한 push는 없다는 것.

슬프지만 어쩔 수 없다.
아이폰 개발자는 APNS를 이용해서 push를 날리고,
안드로이드 개발자는 C2DM을 통해 push를 날리면 된다.

본인의 실력이 뛰어나 MQTT (IBM push 공개 솔루션) 보다 더 좋은 효율로 push를 구현할 수 있다면,
그렇게 하면 된다. 아마도 카카오톡 급의 push 성능은 나와야 하겠지만..

우리는 안드로이드 개발자이니, C2DM에 관하여 정리하겠다.
1. C2DM (Cloud to Device Messaging Framework)의 구조
2. C2DM 등록
3. push Server 예제
4. client app 에서의 처리 예제

1. C2DM의 구조
C2DM의 구조는 다음 그림을 참고하자. 그냥 보면 복잡해 보이나, 2,3,4 단계 까지 훑어 본 뒤 다시 아래 그림을 보게되면 깨닳음을 얻을 수 있다.

출처 : http://www.adamrocker.com/blog/311/ore-ore-c2dm.html



2. C2DM 등록
우선 구글에 요청을 하여 인증 메일을 받아야 한다.
http://code.google.com/intl/ko-KR/android/c2dm/signup.html 
위 경로로 들어가서 요청을 하면 되는데 google ID는 반드시 있어야 한다.
세부 항목에 대한 자세한 설명은 구글링하면 좀 나온다.. 참고 하길 바람.

등록이 완료되면 구글로 부터 메일이 온다.
Thank you for your interest in Android Cloud to Device Messaging (C2DM).
We've accepted your application into the trial group. The Google account
you requested as the sender account for your application:

기본적으로 developer-level을 제공하는데 하루에 약 10만건 정도 요청이 가능하다.
추가적인 요청이 필요할 경우엔 따로 요청을 해야 한다. 
받은 메일 중간에 보면 production-level의 요청 방법이 설명되어 있다. (억단위까지 가능)

** 주의사항 Android 2.2 이상의 단말에서만 동작 가능함.

3. push server 예제
push msg를 만들고 해당 push를 보낼 서버를 만들어 보자.
우선 해당 서버는 보낼 내용인 msg 분만 아니라 C2DM 서버로부터 인증 받은 client의 id 즉, 위 그림에서 본다면 registeration_id 를 반드시 알고 있어야 한다. registeration_id와 client 는 한 쌍을 이뤄 Map 형태로 저장되어 있어야 하며, registeration_id는 client app이 최초 실행 될 때 C2DM으로 부터 발급 받아 server에 전달 되어야 한다.
client 코드 참고 바람.

기본적인 WebProject를 생성 후 Servlet을 이용해서 간단히 구현해 보았다.
관심을 가지고 볼 부분은 C2DM 서버로 데이터를 보내는 부분이 되곘으며,
user의 정보와 registeration_id 는 별도 저장하지 않고 configuration file에 하드코딩하여 보내는 것으로 구현 하였다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Properties;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class PushServlet
 */
public class PushServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	private String registerID;

	/* 서버측 아이디와 비밀번호 (구글에 등록 필요) */
	private String devID;
	private String devPasswd;

	/**
	 * @see HttpServlet#HttpServlet()
	 */
	public PushServlet() {
		super();
		// TODO Auto-generated constructor stub
		loadProperties();
	}

	private void loadProperties() {
		Properties props = new Properties();
		try {
			props.load(new FileInputStream("D:\\test.properties"));
			registerID = props.getProperty("URID");
			devID = props.getProperty("DID");
			devPasswd = props.getProperty("DPW");
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		doPost(request, response);
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {

		String inputData = request.getParameter("input");
		if (inputData != null && !"".equals(inputData)) {
			/* push할 데이터를 C2DM (구글) 서버로 전송 */
			sendMsg(registerID, getAuthToken(), inputData);
		}
	}

	public void sendMsg(String regId, String authToken, String msg) {

		StringBuffer postDataBuilder = new StringBuffer();

		/* registration_id, collapse_key, delay_while_idle 대소문자 구분 정확해야 함 */
		postDataBuilder.append("registration_id=" + registerID); // client가 얻은
																	// regID
		postDataBuilder.append("&collapse_key=1");
		postDataBuilder.append("&delay_while_idle=1");

		try {
			postDataBuilder.append("&data.msg="
					+ URLEncoder.encode(msg, "UTF-8"));

			byte[] postData = postDataBuilder.toString().getBytes("UTF8");

			/* 대소문자 구분 반드시 일치 해야 함 */
			URL url = new URL("https://android.apis.google.com/c2dm/send");

			/* SSL 인증 에러 방지 */
			HttpsURLConnection.setDefaultHostnameVerifier(new CustomVF());
			HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
			conn.setDoOutput(true);
			conn.setUseCaches(false);
			conn.setRequestMethod("POST");
			conn.setRequestProperty("Content-Type",
					"application/x-www-form-urlencoded");
			conn.setRequestProperty("Content-Length", Integer
					.toString(postData.length));
			conn.setRequestProperty("Authorization", "GoogleLogin auth="
					+ authToken);

			OutputStream out = conn.getOutputStream();
			out.write(postData);
			out.close();

			System.out.println(postDataBuilder.toString());
			conn.getInputStream();

		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ProtocolException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public String getAuthToken() {
		String authToken = "";

		StringBuffer postDataBuilder = new StringBuffer();
		postDataBuilder.append("accountType=HOSTED_OR_GOOGLE"); // 똑같이 써주셔야 합니다.
		postDataBuilder.append("&Email=" + devID); // 서버측 구글 id
		postDataBuilder.append("&Passwd=" + devPasswd); // 서버측 구글 비빌번호
		postDataBuilder.append("&service=ac2dm");
		postDataBuilder.append("&source=test-1.0");

		byte[] postData;
		try {
			postData = postDataBuilder.toString().getBytes("UTF8");
			URL url = new URL("https://www.google.com/accounts/ClientLogin");

			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setDoOutput(true);
			conn.setUseCaches(false);
			conn.setRequestMethod("POST");
			conn.setRequestProperty("Content-Type",
					"application/x-www-form-urlencoded");
			conn.setRequestProperty("Content-Length", Integer
					.toString(postData.length));

			OutputStream out = conn.getOutputStream();
			out.write(postData);
			out.close();

			BufferedReader br = new BufferedReader(new InputStreamReader(conn
					.getInputStream()));

			String sidLine = br.readLine();
			String lsidLine = br.readLine();
			String authLine = br.readLine();

			System.out.println("sidLine----------->>>" + sidLine);
			System.out.println("lsidLine----------->>>" + lsidLine);
			System.out.println("authLine----------->>>" + authLine);
			System.out.println("AuthKey----------->>>"
					+ authLine.substring(5, authLine.length()));
			authToken = authLine.substring(5, authLine.length());

		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ProtocolException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return authToken;
	}

	/**
	 * SSL 인증 에러를 방지
	 * 
	 * 
	 */
	private static class CustomVF implements HostnameVerifier {
		@Override
		public boolean verify(String arg0, SSLSession arg1) {
			// TODO Auto-generated method stub
			return true;
		}
	}

}


4. client 예제
C2DMActivity.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.infraware.test.c2dm;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

public class C2DMActivity extends Activity {
	
	public static final String TAG = "C2DMActivity";
	
	public static final String GOOGLE_APP = "app";
	public static final String GOOGLE_SENDER = "sender";
	
	//public static final String MY_GMAIL_ACCOUNT = "bigsail2@gmail.com";
	
	private String mUserGoogleAccount;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        Log.d(TAG, "onCreate");
        
        /*사용자의 구글 계정 ID 얻기*/
        Account[] account = AccountManager.get(this).getAccounts();
        
        for ( int i = 0 ; i < account.length; ++i){
        	
        	Log.d(TAG, "account[" +i+"]" + " : " + account[i]);
        	
        	if ( "com.google".equals(account[i].type)){
        		mUserGoogleAccount = account[i].name;
        		Log.d(TAG, "google account : " + mUserGoogleAccount);
        	}
        }

        if ( mUserGoogleAccount == null ){
        	//TODO 구글 계정 등록 요청 팝업
        	
        }else{
        	//C2DM 등록ID 발급
        	Intent registrationIntent = new Intent("com.google.android.c2dm.intent.REGISTER");
        	registrationIntent.putExtra(GOOGLE_APP, PendingIntent.getBroadcast(this, 0, new Intent(), 0)); // 어플
        	registrationIntent.putExtra(GOOGLE_SENDER, mUserGoogleAccount);
        	startService(registrationIntent);         	
        }
    }
       
}

* 사용자 (client)는 반드시 구글 계정이 활성화 되어있어야 한다. 구글 계정이 없으면 C2DM 등록 자체가 안됨.
* 사용자 ID를 얻어 startService를 C2DM에 보낸다. C2DM은 Android 2.2 버전 이상의 폰에선 기본적인 Service로 내장 되어있다.

C2DMReceiver.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.infraware.test.c2dm;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.view.Gravity;
import android.widget.RemoteViews;
import android.widget.Toast;

public class C2DMReceiver extends BroadcastReceiver{

	public static final String TAG = "C2DMReceiver";
	
	String mRegistration_id = null;
	String mReceivedMsg = "";
	
	@Override
	public void onReceive(Context context, Intent intent) {
		// TODO Auto-generated method stub
		String action = intent.getAction();
		Log.d(TAG, "onReceive : " + action);
		
		
		if ("com.google.android.c2dm.intent.REGISTRATION".equals(action)) {
			handleRegistration(context, intent);
		}else if ("com.google.android.c2dm.intent.RECEIVE".equals(action)) {
			mReceivedMsg = intent.getExtras().getString("msg");
//			noti(context);    		
    		
    		Toast toast = Toast.makeText(context, "메시지 도착!\n"+mReceivedMsg, Toast.LENGTH_SHORT );
 			toast.setGravity( Gravity.TOP | Gravity.CENTER, 0, 0);
 			toast.show();
    	}
	}
	
	private void handleRegistration(Context context, Intent intent) {
    	
		mRegistration_id = intent.getStringExtra("registration_id");
    	Log.d(TAG, "registration_id : " + mRegistration_id);
    	
    	//TODO server에 mRegistration_id 를 전달 해야 함.
    	// server는 mRegistration_id와 user 정보를 함꼐 매핑하여 관리 해야 함.
    }
	
	private void noti(Context context){
		NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
		Notification noti = new Notification(R.drawable.icon, mReceivedMsg, 3000);
		
		RemoteViews contentView = new RemoteViews(context.getPackageName(), R.layout.status_bar_balloon);
		
		PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
	               new Intent(context, C2DMActivity.class).putExtra("moodimg", R.drawable.stat_happy),
	                PendingIntent.FLAG_UPDATE_CURRENT);
		
		noti.contentIntent = contentIntent;
		noti.defaults = Notification.DEFAULT_VIBRATE;
		noti.icon = R.drawable.icon; 
		noti.contentView = contentView;
		notificationManager.notify(1, noti);
	}
}

* noti는 추가 구현해야 할 사항이 많아, 일단 막아놨다.
* server에 얻은 registeration_id를 보내는 작업은 서버와 연동 규격이 있어야 하고 그건 서버개발시 고려되어야 할 사항이 되겠다. 여기선 생략. 



*** 참고 소스 : http://aldehyde7.tistory.com/155
 
 

'Android' 카테고리의 다른 글

Android C2DM의 구조  (1) 2011.11.02
Android ImageDownloader  (0) 2011.11.02
Android coding 규칙  (0) 2011.10.04
Android Custom View의 생성시 주의사항  (0) 2011.10.04
android에서 View 의 효율적인 Event 처리 방법  (0) 2011.09.21
Posted by sail2

댓글을 달아 주세요

  1. aldehyde7 2011.11.28 15:49 신고  댓글주소  수정/삭제  댓글쓰기

    반갑습니다. 제글이 링크가 있어서 흔적 남기고 갑니다 ^-^