ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] Application을 이용해 Socket Service 관리하기
    programing/Mobile 2019. 1. 12. 19:00

     

    안녕하세요, Einere입니다.

    (ADblock을 꺼주시면 감사하겠습니다.)


    오늘은 Application객체를 이용하여 앱 내의 어떤 Activity에서도 socket을 이용할 수 있게 하는 방법을 알려드리겠습니다.

    (단, 이 포스트는 제가 봐도 딱히 좋은 예제는 아닙니다. 다른 좋은 글을 참고하시는 게 더 좋을 것 같습니다.)

     

    우선 지금 진행중인 프로젝트는, 기본적으로 소켓통신을 이용합니다.

    따라서 모든 액티비티에서 소켓을 이용한 통신이 가능해야 합니다.

    이를 위해서, 액티비티와 독립적인 Service내부에 socket과 관련된 변수와 메소드들을 정의합니다.

    Service를 이용하기 위해서는 binding이 필요하기 때문에, Application class에서 바인딩합니다.

    Application class에는 Service를 이용할 수 있게 하는 binder가 있습니다.

    binder를 통해서 Service가 제공하는 method를 호출합니다.

     

    정리하자면, 아래의 과정을 통해 소켓 통신을 이용합니다.

    1. 각 Activity에서 static으로 선언된 Application class instance에 접근한다.
    2. Application class instance의 binder에 접근한다.
    3. binder를 통해 Service의 method를 호출한다.
    그럼 이제 차례대로 코드를 설명하겠습니다.
     
     
     

    Directory Structure

     

    디렉토리 구조는 위와 같습니다.

    해당 포스트에서 다룰 파일들은 AndroidManifest.xml, ConnectionService, MainActivity와 activity_main.xml, IConnectionService.aidl, SocketManager입니다.

     
     

    AndroidManifest.xml

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package=mypackage.myapplication">
        <uses-permission android:name="android.permission.INTERNET" />
        <application
            android:name=".SocketManager"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            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>
            <activity android:name=".ConnectToServerActivity" />
    
            <service
                android:name=".ConnectionService"
                android:enabled="true"
                android:exported="true">
            </service>
        </application>
    
    </manifest>

     

    Application class는 android studio에서 자동으로 생성해주는 기능이 없기 때문에, 직접 New - Java Class를 이용해 생성한 뒤, manifest에 등록하셔야 합니다.

    즉, application의 name속성을 ".SocketManager"으로 설정합니다.

     

    service는 New - Service - Service를 이용해 만들면 자동으로 manifest에 등록이 됩니다. 이름은 물론 "ConnectionService"입니다.

     

     

     

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        android:orientation="vertical">
    
        <Button
            android:id="@+id/btn_connectToServer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="서버 연결"
            android:onClick="connectToServer"
            />
        <Button
            android:id="@+id/btn_sendData"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="발신"
            android:onClick="sendData"
            />
        <Button
            android:id="@+id/btn_receiveData"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="수신"
            android:onClick="receiveData"
            />
    
    </LinearLayout>

     

    MainActivity의 레이아웃은 위와 같이 설정합니다.

     

     

     

    MainActivity

    package mypackage.myapplication;
    
    import android.os.RemoteException;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Toast;
    
    public class MainActivity extends AppCompatActivity {
        // 소켓의 상태를 표현하기 위한 상수
        final int STATUS_DISCONNECTED = 0;
        final int STATUS_CONNECTED = 1;
        // 서버의 ip
        String ip = "127.0.0.1";
        // ConnectionService의 binder를 가지고 있는 SocketManager instance
        SocketManager manager = null;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Log.i("MainActivity", "onCreate()");
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            Log.i("MainActivity", "onResume()");
    
            // get SocketManager instance
            manager = SocketManager.getInstance();
        }
    
        @Override
        protected void onPause() {
            super.onPause();
        }
    
        public void connectToServer(View v) throws RemoteException {
            manager.setSocket(ip);
            manager.connect();
        }
    
        public void sendData(View v) throws RemoteException {
            if(manager.getStatus() == STATUS_CONNECTED){
                manager.send();
            }
            else {
                Toast.makeText(this, "not connected to server", Toast.LENGTH_SHORT).show();
            }
        }
    
        public void receiveData(View v) throws RemoteException {
            if(manager.getStatus() == STATUS_CONNECTED){
                manager.receive();
            }
            else {
                Toast.makeText(this, "not connected to server", Toast.LENGTH_SHORT).show();
            }
    
        }
    }
    MainActivity는 위와 같이 작성합니다.
    socket의 연결상태를 확인하기 위한 상수가 필요합니다.
    서버의 ip도 필요합니다. 원래는 다른 view를 통해 사용자에게서 입력받아야 합니다.
    SocketManager instance를 통해 소켓통신을 합니다.
     
    Activity가 전환될 수 있으므로, onResume()내에서 SocketManager의 instance를 얻습니다.
     
    connectToServer()는 ip주소를 이용해 서버에 접속하는 메소드입니다.
    sendData()는 소켓을 통해 간단한 문자열을 전송하는 메소드입니다. 원래라면 필요한 데이터를 입력받아야 합니다.
    receiveData()는 서버로부터 데이터를 받는 메소드입니다.
     
     
     

    IConnectionService.aidl

    // IConnectionService.aidl
    package mypackage.myapplication;
    
    // Declare any non-default types here with import statements
    
    interface IConnectionService {
        /**
         * Demonstrates some basic types that you can use as parameters
         * and return values in AIDL.
         */
        int getStatus();
        void setSocket(String ip);
        void connect();
        void disconnect();
        void send();
        void receive();
    }

    New - AIDL - AIDL file을 클릭하여 생성해줍니다.

    현재로서는 위와 같이 몇개 안되는 메소드만 구현하도록 합니다.

    aidl을 android studio가 추적하게 하려면 rebuild를 해야 합니다.

     

     

     

    ConnectionService

    package mypackage.myapplication;
    
    import android.app.Service;
    import android.content.Intent;
    import android.os.IBinder;
    import android.os.RemoteException;
    import android.util.Log;
    
    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    import java.net.InetSocketAddress;
    import java.net.Socket;
    import java.net.SocketAddress;
    
    public class ConnectionService extends Service {
        // 소켓의 상태를 표현하기 위한 상수
        final int STATUS_DISCONNECTED = 0;
        final int STATUS_CONNECTED = 1;
        // 소켓연결 대기시간 (5초)
        final int TIME_OUT = 5000;
    
        private int status = STATUS_DISCONNECTED;
        private Socket socket = null;
        private SocketAddress socketAddress = null;
        private BufferedReader reader = null;
        private BufferedWriter writer = null;
        private int port = 8080;
    
        IConnectionService.Stub binder = new IConnectionService.Stub() {
            @Override
            public int getStatus() throws RemoteException {
                return status;
            }
    
            @Override
            public void setSocket(String ip) throws RemoteException {
                mySetSocket(ip);
            }
    
            @Override
            public void connect() throws RemoteException {
                myConnect();
            }
    
            @Override
            public void disconnect() throws RemoteException {
                myDisconnect();
            }
    
            @Override
            public void send() throws RemoteException {
                mySend();
            }
    
            @Override
            public void receive() throws RemoteException {
                myReceive();
            }
        };
    
        public ConnectionService() {
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            Log.i("ConnectionService", "onCreate()");
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            Log.i("ConnectionService", "onStartCommand()");
    
            return START_STICKY;
        }
    
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.i("ConnectionService", "onDestroy()");
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            // TODO: Return the communication channel to the service.
            Log.i("ConnectionService", "onBind()");
    
            return binder;
        }
    
        @Override
        public boolean onUnbind(Intent intent) {
            Log.i("ConnectionService", "onUnbind()");
            return super.onUnbind(intent);
        }
    
        void mySetSocket(String ip) {
            socketAddress = new InetSocketAddress(ip, port);
            Log.i("ConnectionService", "mySetSocket()");
        }
    
        void myConnect() {
            Log.i("ConnectionService", "myConnect1()");
            socket = new Socket();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        socket.connect(socketAddress, TIME_OUT);
                        writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                        reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        Log.i("ConnectionService", "myConnect2()");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    status = STATUS_CONNECTED;
                }
            }).start();
        }
    
        void myDisconnect() {
            try {
                reader.close();
                writer.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            status = STATUS_DISCONNECTED;
        }
    
        void mySend() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String msg = "hello, world!";
                    try {
                        writer.write(msg, 0, msg.length());
                        writer.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    
        void myReceive() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Log.i("ConnectionService", reader.readLine());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    ConnectionService입니다.

    소켓 연결 및 관리를 해야 하므로 필요한 필드가 많습니다.

    포트번호도 8080으로 고정해놨습니다. 필요하다면, aidl의 setSocket()을 수정하시면 됩니다.

     

    binder도 구현합니다. binder는 aidl에서 정의한 interface를 구현하시면 됩니다. 저같은 경우에는 각 메소드의 body를 따로 밖으로 빼내어, 유지보수가 용이하게 했습니다.

     

    onStartCommand()에서는 START_STICKY를 반환하시는 것 잊지 마세요.

     

    Service가 바인딩되면 onBind()를 통해 binder를 Application에 넘겨줘야 하니, binder를 반환합니다.

     

    그 밑으로는 ip와 포트를 설정하는 mySetSocket(), 서버에 연결을 시도하는 myConnect(), 연결을 끊는 myDisconnect(), 데이터를 송신하는 mySend(), 데이터를 수신하는 myReceive()를 구현하면 됩니다.

    이때, 각종 네트워크 관련 메소드는 ANR을 유발할 가능성이 있으므로, thread를 이용하여 작업해야 합니다.

    myRecieve()같은 경우, thread가 무한히 살아있어야 하지만, 일단 임시로 위와 같이 코딩하였습니다. (추후 업데이트 예정)

     

     

     

    SocketManager

    package mypacakge.myapplication;
    
    import android.app.Application;
    import android.content.ComponentName;
    import android.content.Context;
    import android.content.Intent;
    import android.content.ServiceConnection;
    import android.os.IBinder;
    import android.os.RemoteException;
    import android.util.Log;
    
    public class SocketManager extends Application {
        private static final SocketManager instance = new SocketManager();
        private static Context context = null;
        private ServiceConnection connection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Log.i("SocketManager", "onServiceConnected()");
                binder = IConnectionService.Stub.asInterface(service);
                // we use instance, need to set instance's binder
                instance.setBinder(binder);
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
                Log.i("SocketManager", "onServiceDisconnected()");
            }
        };
        private IConnectionService binder = null;
    
    
        public SocketManager() {
            Log.i("SocketManager", "SocketManager()");
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            Log.i("SocketManager", "onCreate()");
    
            // get context
            context = getApplicationContext();
    
            // bind service
            Intent intent = new Intent(context, ConnectionService.class);
            context.bindService(intent, connection, BIND_AUTO_CREATE);
        }
    
        public static SocketManager getInstance() {
            return instance;
        }
    
        public void setBinder(IConnectionService binder) {
            this.binder = binder;
        }
    
        int getStatus() throws RemoteException {
            return binder.getStatus();
        }
    
        void setSocket(String ip) throws RemoteException {
            binder.setSocket(ip);
        }
    
        void connect() throws RemoteException {
            binder.connect();
    
        }
    
        void disconnect() throws RemoteException {
            binder.disconnect();
        }
    
        void send() throws RemoteException {
            binder.send();
        }
    
        void receive() throws RemoteException {
            binder.receive();
        }
    }

    SocketManager입니다.

    Application을 상속하여 어느 Activity에서도 접근할 수 있습니다.

     

    모든 Activity에서 동일한 SocketManager instance에 접근해야 하니, 기본적으로 singleton패턴을 사용합니다만, 생성자가 private인 경우 빌드시 에러가 나므로 public으로 고쳐줍니다. (따라서, 개발자가 new를 이용한 동적할당을 하면 안됩니다.)

     

    Service를 이용하므로, connection를 구현해줍니다.

     

    onCreate()에서는 context를 얻고, context를 이용해 Service를 binding합니다.

    Application class는 context가 기본적으로 null이므로, getApplicationContext()를 이용해 context를 얻은 후, binding해야 합니다.

    생성자에서 binding을 시도하면, Service가 초기화되기도 전에 binding을 시도하기 때문에 에러가 납니다. 따라서 onCreate()내부에서 binding을 합니다.

     

    SocketManager의 instance의 binder에 직접 접근해서 binder의 method를 호출해도 되지만, 필드에 직접적으로 접근하는 것은 지양해야 하므로, 따로 method를 정의합니다.

     

     

     

    위의 예제는 아직 고쳐야 할 부분이 많습니다.

    포트 설정도 할 수 있게 해야 하며, myReceive()의 thread도 무한히 작동하도록 수정해야 합니다.

    그러나 해당 포스트의 목적은 Application을 이용하여 모든 Activity에서 Service를 이용할 수 있는 방법을 소개하는 것입니다.

    구조만 이해한다면, 여러분의 목적에 맞게 수정하여 사용할 수 있을 것입니다.

     

    댓글

Designed by black7375.