본문 바로가기

Android/Basic

[Android] Socket 통신 예제(New)

하나의 디바이스에서 간단한 서버 앱, 클라이언트 앱을 구성하여 소켓 통신을 수행할 수 있는 예제이다.

자세한 개념설명은 생략하였으며, 개념이해 전 간단한 예제 먼저 작성해보고싶을 경우 해당 포스팅을 읽는 것을 추천한다.

 

같이 알아두면 좋은 내용

  • Socket 의 개념
  • Android Permission
  • Service 생명주기, ForegroundService
  • Android 에서 Thread 를 다루는 방법
  • Notification

 

구현 순서 요약

  1. 서버 앱 구성
    1. 서버를 실행할 Service 클래스 작성
    2. 서버 역할을 하는 ServerThread 클래스 작성
    3. 메인 액티비티에서 구현한 Service 실행(Foreground)
  2. 클라이언트 앱 구성
    1. 화면 구성
    2. 서버에 연결할 ClientThread 클래스 작성

 

서버 앱 구성하기

0. Manifest.xml 파일에 아래 권한을 추가

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

 

1. ServerService 클래스 작성 -> Manifest.xml 의 application 안에 여기서 만든 Service 컴포넌트를 명시해야한다

class ServerService : Service() {

    private var serverThread: Thread? = null

    override fun onBind(intent: Intent?): IBinder? {
        throw UnsupportedOperationException("not implement")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serverThread = ServerThread()

        serverThread?.start()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channelId = "com.delay.test.serverservice"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val mChannel = NotificationChannel(channelId, "Test", importance)

            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(mChannel)

            val notification = Notification.Builder(this, channelId)
                .setContentTitle("테스트 서버")
                .setContentText("테스트 서버 진행중")
                .setSmallIcon(R.mipmap.ic_launcher_round)
                .build()

            startForeground(1111, notification)
        }

        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        serverThread?.interrupt()

        super.onDestroy()
    }

    class ServerThread : Thread() {
        companion object {
            private const val TAG = "ServerThread"
        }

        override fun run() {
            Log.d(TAG, "서버가 실행됨")

            val port = 10001 // 리눅스에서 이미 사용하는 포트를 쓰면 익셉션이 발생함

            val server = ServerSocket(port)

            try {
                while (true) {
                    val socket = server.accept() // 서버 대기상태. 클라이언트 접속 시 소켓 객체 리턴

                    val inputStream = ObjectInputStream(socket.getInputStream())
                    val input = inputStream.readObject() // 클라이언트에서 보낸 데이터
                    Log.d(TAG, "$input -- from client")

                    val outputStream = ObjectOutputStream(socket.getOutputStream())
                    outputStream.writeObject("200 OK from server -- client data: $input")
                    outputStream.flush()
                    Log.d(TAG, "클라이언트로 결과 보내기 완료")

                    socket.close() // 더이상 연결을 유지할 필요가 없다면 끊어줌
                }
            } catch (e: SocketException) {
                e.printStackTrace() // fixme 올바른 에러 처리 방법은 아님
            }
        }
    }

}

 

2. MainActivity 클래스 작성

class MainActivity : AppCompatActivity() {

    private val serviceIntent by lazy { Intent(this, ServerService::class.java) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(serviceIntent)
        } else {
            startService(serviceIntent)
        }
    }

    override fun onDestroy() {
        stopService(serviceIntent)

        super.onDestroy()
    }
    
}

 

3. 서버 앱 실행(위에 노티 떠있는 것 확인)

 

** 주의 : 소켓 객체를 생성할 때 리눅스에서 이미 사용중인 포트를 사용하면 아래와 같은 에러가 발생하는 것을 알아두자!

java.net.ConnectException: failed to connect to localhost/127.0.0.1 (port 1001) from /:: (port 45672): connect failed: ECONNREFUSED (Connection refused)

 

클라이언트 앱 구성하기

0. Manifest.xml 파일에 아래 권한 추가하기

<uses-permission android:name="android.permission.INTERNET"/>

 

1. 간단한 화면 구성

대충 이런 화면

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp">

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="10dp"
        android:lineSpacingExtra="10dp"
        android:text="서버 연결 대기중...\n"
        android:textSize="19sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button"
        app:layout_constraintVertical_bias="0.0" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="서버 전송"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="서버로 보낼 데이터 입력"
        app:layout_constraintBottom_toBottomOf="@+id/button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/button"
        app:layout_constraintTop_toTopOf="@+id/button" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

2. 메인 액티비티 내에 inner 클래스로 서버에 연결할 ClientThread 작성

inner class 로 해야하는 이유는, 메인 액티비티의 멤버변수(handler 나 View들)에 접근하기 위함이다. 이렇게 짜는건 여러가지 이유로 좋지 않은 코드지만, 이 예제에서 중요한 부분이 아니기 때문에 짚지 않을 것이다. (절대 이렇게 짜지 말고 어떻게 해결하면 좋을지 스스로 알아서 공부해보자!!)

class ClientThread : Thread() {
    override fun run() {
        val hostName = "localhost"
        val port = 10001

        try {
            val socket = Socket(hostName, port)

            val outputStream = ObjectOutputStream(socket.getOutputStream())
            outputStream.writeObject(editText.text.toString())
            outputStream.flush()

            val inputStream = ObjectInputStream(socket.getInputStream())
            val input = inputStream.readObject() as String

            handler.post {
                textView.append("$input\n")
            }
        } catch (e: SocketException) {
            handler.post {
                Toast.makeText(applicationContext, "소켓 연결에 실패했습니다.", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

 

3. 메인액티비티에서 서버로 연결하는 코드 추가하기

class MainActivity : AppCompatActivity() {
    private val handler = Handler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button.setOnClickListener {
            ClientThread().start()
        }
    }
    
    // ClientThread inner class 정의
}

 

 

이제 먼저 구현한 서버 앱을 실행시킨 후 클라이언트 앱을 실행하여 내용을 입력한 후 버튼을 누르면 아래와 같이 데이터를 받을 수 있다. 서버 앱에서 서비스가 구동중인지 알 수 있는 방법은 알림채널에 선언한 노티(테스트 서버)가 떠있으면 된다. 클라이언트에서 서버로 제대로된 데이터가 전송됐는지 여부는 로그캣에 찍게 해뒀으리 서버프로젝트 로그캣을 확인해보자.

 

해당 예제 작성 시 문제가 있다면 댓글 부탁드립니다!

'Android > Basic' 카테고리의 다른 글

[Android] CoordinatorLayout 활용  (0) 2018.10.25
[Android] 스플래시 화면  (0) 2018.09.12
[Android] 페이지 슬라이딩  (0) 2018.09.07
[Android] 트윈 애니메이션  (0) 2018.09.07
[Android] 스레드 애니메이션  (0) 2018.09.07