기본 콘텐츠로 건너뛰기

Android 에서 DDP 라이브러리를 사용하자.

Meteor는 자체적으로 Cordova 를 통한 iOS/Android 앱을 지원한다.
하지만, Hybrid 앱은 역시 Native에 비해 예측 못할 변수가 많고 성능도 그다지 좋지 않다.
DDP를 사용하여 Native Android 앱과 Meteor 서버를 연결해보자.

새 프로젝트를 만들고 File/Project Structure (cmd+;) 으로 들어가서

하단 +를 누르면

  1. Library Dependency
  2. File Dependency
  3. Module Dependency
세개 나온다.
Choose Library Dependency 창에서 ddp 하고 엔터쳐보자.
지금 현재는 3개 정도 나오는데

com.keysolutions:android-ddp-client:1.0.2.0
이걸 써서 할거다.
선택하고 OK 해서 추가하자.

activity_main.xml 에 버튼 하나 만들고 
AndroidManifest.xml 에

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
이 두 개의 permission 이 있는지 확인하자. 없으면 추가. <application>이랑 나란히 놓으면 된다.

준비 해야할 class 는 일단 두개인데
하나는 어플리케이션 전체에서 DDP 연결을 책임질 MyApplication (Application을 상속받은 클래스)와 DDP에서 subscription 처리를 해줄 LoginDDPState(DDPStateSingleton을 상속받은 클래스)이다.
물론 이름은 임의로 정했다.

먼저 MyApplication.java 를 보자
package com.appsoulute.meteorddpexam1;
import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
public class MyApplication extends Application {
    private static Context sContext = null;
    @Override
    public void onCreate() {
        super.onCreate();
        MyApplication.sContext = getApplicationContext();
        initSingletons();
    }
    private void initSingletons() {
        LoginDDPState.initInstance(MyApplication.sContext, "192.168.0.11", 4100, false);
    }
    public static Context getAppContext() {
        return MyApplication.sContext;
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
    }
}
대략 이런 구조 되시겠다.
복수의 DDP를 사용할 경우도 마찬가지 initSingletons 부분에 Meteor Server 주소, 포트, SSL 사용여부를 initInstance에 넣어주고 LoginDDPState를 구현해주면 되겠다.

만들고 나서 AndroidManifest.xml에 가서 application에 android:name 속성을 추가한다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.appsoulute.meteorddpexam1">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:name=".MyApplication"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

반드시 자동완성을 사용해서 오타를 피한다.

다음은 LoginDDPState 를 만들어보자.
package com.appsoulute.meteorddpexam1;
import android.content.Context;
import com.keysolutions.ddpclient.DDPClient;
import com.keysolutions.ddpclient.android.DDPStateSingleton;
/** * Created by spectrum on 6/14/16. */
public class LoginDDPState extends DDPStateSingleton {
    private LoginDDPState(Context context, String meteorServer, Integer meteorPort, boolean useSsl) {
        super(context, meteorServer, meteorPort, useSsl);
    }

    public static void initInstance(Context context, String meteorServer, Integer meteorPort, boolean useSsl) {
        if (mInstance == null) {
            mInstance = new LoginDDPState(context, meteorServer, meteorPort, useSsl);        }
    }
    public static LoginDDPState getInstance() {
        // Return the instance
        return (LoginDDPState) mInstance;
    }

    @Override
    public void broadcastSubscriptionChanged(String subscriptionName, String changetype, String docId) {
        if (subscriptionName.equals("getChats")) {
            if (changetype.equals(DDPClient.DdpMessageType.ADDED)) {

            } else if (changetype.equals(DDPClient.DdpMessageType.REMOVED)) {

            } else if (changetype.equals(DDPClient.DdpMessageType.UPDATED)) {

            }
        }
        super.broadcastSubscriptionChanged(subscriptionName, changetype, docId);
    }
}
조금 긴 것 같지만 잘 보면 별거 없다.
MyApplication 에서 호출한 initInstance를 만들어주고
getInstance에 싱글톤인 mInstance를 넘겨주는 것 정도한 뒤

broadcastSubscriptionChanged에서 subscription 처리를 하면 된다.
subscription 명하고 changeType에 따라 Map을 만들거나 해서 처리하면 된다.

이제 문제는 MainActivity인데
그전에 Android 의 LifeCycle을 잠시 언급하면
생성시 : onCreated > onStart > onResume
소멸시 : onPause > onStop > onDestroy
순인 걸 기억하자.

먼저, onResume 에서 할게 좀 많은데 대략 아래와 같다.
protected void onResume() {
    super.onResume();
    LoginBroadcastReceiver();
    DDPBroadcastReceiver();

    LoginDDPState.getInstance().connectIfNeeded();    // start connection process if we're not connected
}

두 개의 처리를 해주는데 DDP상태(LoginDDPState)와 DDPBroadcast(DDPBroadcastReceiver)를 다루는 부분을 처리하면 된다.
마지막으로 LoginDDPState 가 연결이 필요하면 connectIfNeeded()로 연결하도록 하자.

LoginBroadcastReceiver는 
private void LoginBroadcastReceiver() {
    mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Bundle bundle = intent.getExtras();
            if (intent.getAction().equals(LoginDDPState.MESSAGE_ERROR)) {
                String message = bundle.getString(LoginDDPState.MESSAGE_EXTRA_MSG);
            } else if (intent.getAction().equals(LoginDDPState.MESSAGE_CONNECTION)) {
                int state = bundle.getInt(LoginDDPState.MESSAGE_EXTRA_STATE);
                if (state == LoginDDPState.DDPSTATE.LoggedIn.ordinal()) {
                    // login complete, so we can close this login activity and go back
                    Log.d("Login", "LoginServer Completed");
                }
            }
        }
    };
    LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver,
        new IntentFilter(LoginDDPState.MESSAGE_ERROR));
    LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver,
        new IntentFilter(LoginDDPState.MESSAGE_CONNECTION));
    if (LoginDDPState.getInstance().getState() == LoginDDPState.DDPSTATE.Closed) {
        showError("Connection Issue", "Error connecting to server.. try again");
    }
}
이와 같은데 MESSAGE_ERROR와 MESSAGE_CONNECTION을 각각 수신하도록 등록해서 오류처리와 접속 후 상태처리를 해주면 된다.

DDPBroadcastReceiver의 경우는
private void DDPBroadcastReceiver() {
    DDPBroadcastReceiver mDDPReceiver = new DDPBroadcastReceiver(LoginDDPState.getInstance(), this) {
        @Override
        protected void onDDPConnect(DDPStateSingleton ddp) {
            super.onDDPConnect(ddp);
            // add our subscriptions needed for the activity here
            ddp.subscribe("get", new Object[] {});
        }
        @Override
        protected void onSubscriptionUpdate(String changeType,
                                            String subscriptionName, String docId) {
            if (subscriptionName.equals("get")) {
            }
        }
        @Override
        protected void onLogin() {
            // update login/logout action button
            findViewById(R.id.linearLayoutLogin).setVisibility(View.INVISIBLE);
            findViewById(R.id.linearLayoutLogout).setVisibility(View.VISIBLE);
        }
        @Override
        protected void onLogout() {
            // update login/logout action button
            findViewById(R.id.linearLayoutLogin).setVisibility(View.VISIBLE);
            findViewById(R.id.linearLayoutLogout).setVisibility(View.INVISIBLE);
        }
    };
}

이와 같이 DDP연결 시 처리 - 여기선 get 이라는 이름의 subscribe 를 신청함 - 와 subscription의 갱신, 로그인/로그아웃시 처리를 해주면 된다.


반면, onPause의 경우는 간단한데
@Overrideprotected void onPause() {
    super.onPause();
    if (mReceiver != null) {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
        mReceiver = null;
    }
    if (mDDPReceiver != null) {
        // unhook the receiver
        LocalBroadcastManager.getInstance(this)
                .unregisterReceiver(mDDPReceiver);
        mDDPReceiver = null;
    }
}
이와 같이 onResume 시에 등록했던 Receiver들을 unregisterReceiver 해주면 된다.

그러면 실제 구현을 해보자.
onCreate에 login/logout 버튼을 연결하여보면
@Overrideprotected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button btnLogin = (Button)findViewById(R.id.buttonLogin);
    if (btnLogin != null) {
        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoginDDPState.getInstance().login(
        ((TextView)findViewById(R.id.editTextUserName)).getText().toString(),
        ((TextView)findViewById(R.id.editTextPassword)).getText().toString()
                );
            }
}); } Button btnLogout = (Button)findViewById(R.id.buttonLogout); if (btnLogout != null) { btnLogout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { LoginDDPState.getInstance().logout(); } }); } }

이와 같이 구현할 수 있다.
별로 어려운 건 없다. js 의 경우 callback에 해당하는 Receiver 들만 꼼꼼하게 잘 구현하면 Meteor의 풍족한 서버환경을 어려움 없이 사용할 수 있을 것이다.


참고자료;

댓글

이 블로그의 인기 게시물

cURL로 cookie를 다루는 법

http://stackoverflow.com/questions/22252226/passport-local-strategy-and-curl 레거시 소스를 보다보면 인증 관련해서 cookie를 사용하는 경우가 있는데 가령 REST 서버인 경우 curl -H "Content-Type: application/json" -X POST -d '{"email": "aaa@bbb.com", "pw": "cccc"}' "http://localhost/login" 이렇게 로그인이 성공이 했더라도 curl -H "Content-Type: application/json" -X GET -d '' "http://localhost/accounts/" 이런 식으로 했을 때 쿠키를 사용한다면 당연히 인증 오류가 날 것이다. curl의 --cookie-jar 와 --cookie 옵션을 사용해서 cookie를 저장하고 꺼내쓰자. 각각 옵션 뒤엔 저장하고 꺼내쓸 파일이름을 임의로 지정하면 된다. 위의 과정을 다시 수정해서 적용하면 curl -H --cookie-jar jarfile "Content-Type: application/json" -X POST -d '{"email": "aaa@bbb.com", "pw": "cccc"}' "http://localhost/login" curl -H --cookie jarfile "Content-Type: application/json" -X GET -d '' "http://localhost/accounts/" 이렇게 사용하면

MQTT 접속해제 - LWT(Last will and testament)

통신에서 중요하지만 구현이 까다로운 문제로 "상대방이 예상치 못한 상황으로 인하여 접속이 끊어졌을때"의 처리가 있다. 이것이 까다로운 이유는 상대방이 의도적으로 접속을 종료한 경우는 접속 종료 직전에 자신의 종료 여부를 알리고 나갈 수 있지만 프로그램 오류/네트웍 연결 강제 종료와 같은 의도치 않은 상황에선 자신의 종료를 알릴 수 있는 방법 자체가 없기 때문이다. 그래서 전통적 방식으로는 자신의 생존 여부를 계속 ping을 통해 서버가 물어보고 timeout 시간안에 pong이 안올 경우 서버에서 접속 종료를 인식하는 번거로운 방식을 취하는데 MQTT의 경우 subscribe 시점에서 자신이 접속 종료가 되었을 때 특정 topic으로 지정한 메시지를 보내도록 미리 설정할 수 있다. 이를 LWT(Last will and testament) 라고 한다. 선언을 먼저하고 브로커가 처리하게 하는 방식인 것이다. Last Will And Testament 라는 말 자체도 흥미롭다. 법률용어인데  http://www.investopedia.com/terms/l/last-will-and-testament.asp 대략 내가 죽으면 뒷산 xx평은 작은 아들에게 물려주고 어쩌고 하는 상속 문서 같은 내용이다. 즉, 내가 죽었을(연결이 끊어졌을) 때에 변호사(MQTT Broker - ex. mosquitto/mosca/rabbitMQ등)로 하여금 나의 유언(메시지)를 상속자(해당 토픽에 가입한 subscriber)에게 전달한다라는 의미가 된다. MQTT Client 가 있다면 한번 실습해보자. 여러가지가 있겠지만 다른 글에서처럼  https://www.npmjs.com/package/mqtt  을 사용하도록 한다. npm install mqtt --save 로 설치해도 되고 내 경우는 자주 사용하는 편이어서 npm install -g mqtt 로 전역설치를 했다. 호스트는 무료 제공하고 있는 test.mosquitto.org 를

MQTT Broker Mosquitto 설치 후 설정

우분투 기준 $ sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa $ sudo apt-get update 하고 $ sudo apt-get install mosquitto 으로 설치하면 서비스까지 착실하게 올라간다. 설치는 간단한데 사용자를 만들어야한다. /etc/mosquitto/mosquitto.conf 파일에서 권한 설정을 변경하자. allow_anonymous false 를 추가해서 아무나 못들어오게 하자. $ service mosquitto restart 서비스를 재시작. 이제 사용자를 추가하자. mosquitto_passwd <암호파일 경로명> <사용자명> 하면 쉽게 만들 수 있다. # mosquitto_passwd /etc/mosquitto/passwd admin Password:  Reenter password:  암호 넣어준다. 두번 넣어준다. 이제 MQTT 약을 열심히 팔아서 Broker 사글세방 임대업을 하자.