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 件のコメント:
コメントを投稿