Android Advent Calendar 2012に参加したdommyです。
2012年もあと2日、今年も山あり谷ありの1年でした。
12月30日はすでにAdvent Calendarに載るべき日付でも無いですが、
Androidに関する記事ならなんでも良いとの事だったので、
リアルタイム通信関連の記事を書いてみたいと思います。
前提として、Androidでの通信といえばHTTPになります。
これはクライアントからのリクエストに対して、サーバがレスポンスを返すという形で、
サーバからリクエストする事は出来ないので、
リアルタイム通信とは違います。
socket通信
AndroidはNDKを使えばC言語でも実装可能なので、サーバ側にレスポンスを返す部分を用意して、
Cでサーバへ接続する部分も掛けるが、
ルート的にもソース的にも面倒臭い。
レイヤーは高いが、もっと簡単な方法があるはず。
WebSocket
HTML5の技術になるのですが、Androidでより簡単にリアルタイム通信を実装するには、
WebSocketが楽かなぁと実装プロトコルに選びました。
PCでのMMORPG等はUDPが主流なので、
UDPの実装出来ないWebSocketはどうなの?
との話もありますが、
まぁ、TCP上でソケット通信するならWebSocketが
ライブラリが充実していて実装が楽です。
サーバ、アプリの両方で実装する必要があるので、
まずはサーバを準備しました。
node.js - express - socket.io
サーバの実装方法です。CentOS 6.0をメインで使っているので、
node.js - express - socket.ioで実装しました。
Web側のインターフェースも用意し、
アプリからとウェブからのsocket.ioのメソッドを準備しました。
このあたりはAndroidのプログラムには関係ないので、
そのうち解説します。
分散処理はこの記事を参考にしました。
node.js アプリの負荷分散構成を考える
Android側のライブラリ
最初はjWebSocketを選びました。はい、ここで間違っていました。
socket.ioとWebSocketは厳密には違います。
socket.ioはWebSocketのラッパーなので、
socket.ioと通信する場合は、
クライアントもsocket.ioじゃないとダメです。
と、いう訳でライブラリの選び直し。
socket.io-java-clientを選びました。
早速jarファイルの作成です。
$ git clone https://github.com/Gottox/socket.io-java-client.git $ cd socket.io-java-client $ ant jar
jar/socketio.jarが作成されました。
Androidの実装
socket.ioのCallbackはもうライブラリで指定されており、implements IOCallbackとすれば大体の作業は終わってしまいます。
Service化
socket.ioでは、Acknowledgementが10秒に1回ぐらいあったので、メインスレッドで実装させるには抵抗がありました。
かと言って、Threadを利用しても、Acitivityの遷移があったらどうする?
となってしまいますので、サービスを作って、そちらに任せます。
ついでに、ライブラリ化出来ればいいなぁと考えていました。
とりあえずはService化に着手。
/** * Service for Socket.IO connection * * @author morodomi */ class SMService extends Service implements IOCallback { protected static final String MESSAGE_INTENT = "socket messaging message intent"; protected static final String ERROR_INTENT = "socket messaging error intent"; protected static final int INTENT_ACTIVITY = 38427; private static SocketIO socket; /** * Service Binder * * @author morodomi * */ class ServiceBinder extends Binder { SMService getService() { return SMService.this; } } /** * {@inheritDoc} */ @Override public void onCreate() { super.onCreate(); } /** * Trying to connect with server. * {@inheritDoc} */ @Override public int onStartCommand(Intent intent, int flags, int startId) { if (socket == null) { try { socket = new SocketIO(SMConfig.URL); } catch (MalformedURLException e) { e.printStackTrace(); } } if (!socket.isConnected()) { try { socket.connect(this); } catch (Exception e) { Log.e(SMConfig.TAG, e.getMessage(), e); } } // TODO: create a task which connecting if disconnected; return START_STICKY_COMPATIBILITY; } /** * {@inheritDoc} */ @Override public void onDestroy() { socket.disconnect(); super.onDestroy(); } /** * {@inheritDoc} */ @Override public IBinder onBind(Intent intent) { return new ServiceBinder(); } /** * {@inheritDoc} */ @Override public void onRebind(Intent intent) { super.onRebind(intent); } /** * {@inheritDoc} */ @Override public boolean onUnbind(Intent intent) { return super.onUnbind(intent); } /** * {@inheritDoc} */ @Override public void on(String event, IOAcknowledge ack, Object... args) { if (SMConfig.DEBUG) { Log.d(SMConfig.TAG, "SMService.on(" + event + ", " + (String) args[0] + ")"); } Intent intent = new Intent(); intent.putExtra("event", event); intent.putExtra("message", (String) args[0]); intent.setAction(MESSAGE_INTENT); sendBroadcast(intent); } /** * {@inheritDoc} */ @Override public void onConnect() { } /** * {@inheritDoc} */ @Override public void onDisconnect() { if (SMConfig.DEBUG) { Log.d(SMConfig.TAG, "SMService.onDisconnect()"); } } /** * {@inheritDoc} */ @Override public void onError(SocketIOException error) { if (SMConfig.DEBUG) { Log.d(SMConfig.TAG, error.getMessage(), error); } Intent intent = new Intent(); intent.setAction(ERROR_INTENT); sendBroadcast(intent); } /** * {@inheritDoc} */ @Override public void onMessage(String data, IOAcknowledge ack) { if (SMConfig.DEBUG) { Log.d(SMConfig.TAG, "SMService.onMessage(" + data + ")"); } } /** * {@inheritDoc} */ @Override public void onMessage(JSONObject json, IOAcknowledge ack) { if (SMConfig.DEBUG) { try { Log.d(SMConfig.TAG, "SMService.onMessage(" + json.toString(2) + ")"); } catch (JSONException e) { e.printStackTrace(); } } } /** * Send string message to the server; * * @param event * key for the message * @param message * value of the event */ protected void send(String event, String message) { socket.emit(event, message); } /** * Sending json object to the server * * @param event * @param json */ protected void send(String event, JSONObject json) { socket.emit(event, json.toString()); } }
と、こんな感じで書いてみました。
基本的にサーバとやりとりするのはこのクラスですが、
ライブラリ化する為に、このサービスはpublicにしてありません。
ServiceとActivity間をやりとりするServiceConnectionを作成します。
/** * client manager for server push * * @author morodomi */ public class SMManager implements ServiceConnection { private static Context mContext; private static SMService mService; private BroadcastReceiver mReceiver; private SMManagerListener listener; /** * Constructor for SMManager * * @param context * Application Context */ private SMManager(Context context) { mContext = context; mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { /** * message from service */ if (SMConfig.DEBUG) { Log.d(SMConfig.TAG, "onReceive:" + intent.getAction()); } if (intent.getAction() != null) { if (intent.getAction().equals(SMService.MESSAGE_INTENT)) { if (listener != null) { listener.onMessage(intent.getStringExtra("event"), intent.getStringExtra("message")); } } } } }; } /** * {@inheritDoc} */ @Override public void onServiceConnected(ComponentName componentName, IBinder service) { if (SMConfig.DEBUG) { Log.d(SMConfig.TAG, "WebSocketManager.onServiceConnect"); } mService = ((SMService.ServiceBinder) service).getService(); if (listener != null) { listener.onConnected(); } } /** * {@inheritDoc} */ @Override public void onServiceDisconnected(ComponentName componentName) { mService = null; if (listener != null) { listener.onDisconnected(); } } /** * SMManagerの初期化 * * @param context * @param reciever * @return */ public static SMManager init(Context context) { return new SMManager(context); } /** * Setter for SMManagerListener * * @param listener */ public void setListener(SMManagerListener listener) { this.listener = listener; } /** * bind service to application */ public void connect() { Intent intent = new Intent(mContext, SMService.class); mContext.startService(intent); if (mReceiver != null) { IntentFilter filter = new IntentFilter(SMService.MESSAGE_INTENT); mContext.registerReceiver(mReceiver, filter); } mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); } /** * call when the activity destroy */ public void disconnect() { mContext.unbindService(this); mContext.unregisterReceiver(mReceiver); Intent it = new Intent(); it.putExtra("data", "disconnect"); mReceiver.onReceive(mContext, it); // mService.stopSelf(); } public void send(String event, String message) { if (mService != null) { mService.send(event, message); } } /** * emit message on server * * @param event * @param json */ public void send(String event, JSONObject json) { if (mService != null) { mService.send(event, json.toString()); } } }
一旦、これで動きました。
何かまだ動きがぎこちないので改良が必要です。
最後にServiceからのコールバックを受け取るListenerインターフェースを作成します。
/** * Event Listener for SMManager * * @author morodomi */ public interface SMManagerListener { /** * called when the SMService is connected to the activity. */ public void onConnected(); /** * called when the SMService is disconnected from the activity. */ public void onDisconnected(); /** * called when the server pushes event * * @param event * event key for push * @param message * data for the event */ public void onMessage(String event, String message); }
あと、SMConfig.も書いてますが、4行ぐらいのもんです。
4つのクラスをjarファイルにまとめればライブラリの完成です。
最後に
ライブラリの実装方法ですが、このSocketMessagingライブラリと、socket.io.jarをlibsへ入れます。
で、Activityで、下記のように実装
public class MainActivity extends Activity implements SMManagerListener { private SMManager manager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); /** Web Socket */ manager = SMManager.init(this); manager.setListener(this); manager.connect(); } @Override protected void onDestroy() { manager.disconnect(); super.onDestroy(); } @Override public void onConnected() { // 接続完了 manager.send("msg", "is_connected"); } @Override public void onDisconnected() { } @Override public void onMessage(String event, String message) { // メッセージ処理 } }
こんな感じで動かしました。
あとは、受け取ったメッセージをHandler使ってメインスレで処理するとか、
実はいろいろと問題があったりします。
本当は12月30日までには完了させるつもりだったんですが、
なかなか時間が無く、中途半端な感じになってしまいました。
メッセージのやりとりをチャネリングして、1:1にするか、
ブロードキャストして、1:多にするかはサーバ側で調整可能です。
1:1にするのであれば、アプリ側で相手のID的なものを保持する必要があります。
今年のブログ記事はこれで最後になります。
来年も頑張ってアプリ開発しましょう!
それでは、良いお年を!!!
0 件のコメント:
コメントを投稿