마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

안드 Kotlin 소켓 통신 클라 관련 질문드립니다.

0 추천

클릭 리스너

btnConnect.setOnClickListener {
    if (editIp.text.isEmpty() || editPort.text.isEmpty()) {
        Toast.makeText(this@MainActivity, "빈칸을 다 채워주세요", Toast.LENGTH_SHORT).show()
    } else {
        ip = editIp.text.toString()
        port = editPort.text.toString().toInt()
        Log.d(TAG, ip + "")
        Log.d(TAG, port.toString())
        if (socket.isConnected) {
            Toast.makeText(this@MainActivity,
                ip + "에 이미 연결되어 있습니다.",
                Toast.LENGTH_SHORT).show()
        } else {
            Connect().start()
            if (socket.isConnected) {
                txtDevice.setText(ip + "에 연결됨")
                Log.d(TAG, "Connect")
            } else {
                Log.d(TAG, "Not Connect")
            }
        }
    }
}

Connect 클래스

class Connect : Thread() {
    override fun run() {
        try {
            MainActivity.socket = Socket("192.168.0.65", 15136)
            Log.d(MainActivity.TAG, "서버 연결됨")
        } catch (e: Exception) {
            Log.d(MainActivity.TAG, "서버 연결안됨")
            MainActivity.socket.close()
        }
    }
}

버튼 클릭시 서버와 연결리 되도록 하고싶은데 연결이 되지 않습니다. 도와주세요

jjo88413 (180 포인트) 님이 2022년 1월 18일 질문

2개의 답변

+1 추천

왜 소켙을 직접 구현하시는지 여쭤봐도 될까요? 대신할 수 있는 방법이 있다면 굳이 소켙을 직접 구현하지 않는 편이 골치가 많이 덜 아픕니다.

제가 테스트 해 본 결과로는 일단 AndroidManifest.xml에 다음 두가지 permission 을 선언하셔야 합니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="....">

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

<application
   .... >
  ...
</application>
 
</manifest>

 

소켙의 핸들링은 올리신 소스코드 처럼 백그라운드 쓰레드에서 하셔야 합니다. 

그리고 애뮬레이터에서 테스트를 하신다면, 애뮬레이터는 실제 디바이스와 같은 방식으로 동작하도록 되어 있어서 loopback, 즉 localhost, 127.0.0.1 의 주소로는 외부와 연결을 할 수 없습니다. 즉, 앱을 작업하는 동일 컴퓨터에 서버소켙을 띄워놓고 얘뮬레이터에서 접속하려면 공용IP나 내 라우터에서 제공하는 근거리 네트워주소를 사용하셔야 합니다. 아니면 애뮬레이터 자체에 서버 소켙을 띄우셔야 겠죠.

그리고 올리신 코드처럼 MainActivity.socket과 같이 액티비티 안에 있는 멤버를 밖에서 직접 참조해서 사용하는 것 바람직한 방법이 아닙니다. 특히 static 은 상수가 아니라면 더더욱 그렇습니다. 모바일의 특성상 Configuration chnage와  process death라는 게 있습니다. Configuration changes는 디바이스회전, 폰트 변경, 다크모드 세팅, 키보드 설정, 언어 변경 등등 이 발생할 시 현재 실행 중인 액티비티를 종료하고 새로 생성을 해줍니다.  이 때 핸들링이 필요한 부분을 하지 않으면 화면에 변경되었던 정보가 사라지는 경우가 생깁니다.

Process death는 안드로이드가 앱이 백그라운드에 있을 때 상황에 따라 아무 신호없이 앱을 죽일 수 있습니다.  사용자가 task manager에서 앱을 시작하면, 안드로이드 시스템은 앱 전체를 복구해주는 것이 아니라 마지막에 화면에 있던 액티비티만 복구를 해줍니다. 이 경우 Bundle이란 걸 통해서 저장된 것이 아니면, static으로 선언된 글로별 변수까지도 값이 초기화 되게 됩니다.

안드로이드에서 MainActivity.socket과 같은 접근 방법은 좋지않은 접근방법으로 권장하지 않습니다. 필요하면 MainActivity의 멤버나 Connect의 멤버로 사용하시는 것이 낫습니다.

마지막으로, 제가 간단히 테스트해 본 클라이언트 소켙 샘플을 올립니다. 클라이언트 소켙을 안드로이드에서는 사용해 본적이 없어서 좀 급조가 되었습니다. 하지만 테스트 해보시는데 지장은 없을 겁니다.

spark (227,510 포인트) 님이 2022년 1월 18일 답변
spark님이 2022년 1월 18일 수정
+1 추천

 

import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.Socket


class ClientSocket {

    private var socket: Socket? = null
    private var out: PrintWriter? = null
    private var br: BufferedReader? = null

    @Throws(IOException::class)
    fun connect(ip: String?, port: Int) {
        val clientSocket = Socket(ip, port).also { socket = it }
        out = PrintWriter(clientSocket.getOutputStream(), true)
        br = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
    }

    @Throws(IOException::class)
    fun sendMessage(msg: String?): String {
        out!!.println(msg)
        return br!!.readLine()
    }

    fun disconnect() {
        br?.use { }
        out?.use { }
        socket?.use { }
    }

    fun isConnected(): Boolean {
        return socket?.isConnected == true
    }
}

 

import java.io.IOException

class MyClient {

    interface Listener {
        fun onSocketConnected(ip: String, port: Int)
        fun onSocketConnectionFailed(e: Exception)
    }

    private val listeners = hashSetOf<Listener>()
    private val client by lazy { ClientSocket() }

    fun addListener(listener: Listener) {
        listeners.add(listener)
    }

    fun removeListener(listener: Listener) {
        listeners.remove(listener)
    }

    fun connect(ip: String, port: Int) {
        Thread {
            try {
                client.connect(ip = ip, port = port)
                notifyConnected(ip = ip, port = port)
            } catch (e: IOException) {
                e.printStackTrace()
                notifyConnectionError(e)
            }
        }.start()
    }

    fun disconnect() {
        client.disconnect()
    }

    fun isConnected(): Boolean = client.isConnected()

    private fun notifyConnectionError(e: Exception) {
        for (listener in listeners) {
            listener.onSocketConnectionFailed(e)
        }
    }

    private fun notifyConnected(ip: String, port: Int) {
        for (listener in listeners) {
            listener.onSocketConnected(ip = ip, port = port)
        }
    }
}

 

import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity(), MyClient.Listener {

    private val connectBtn: Button by lazy { findViewById(R.id.connectBtn) }
    private val client = MyClient()

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

        setupViews()
    }

    private fun setupViews() {
        connectBtn.setOnClickListener {
            if (client.isConnected()) {
                disconnect()
            } else {
                connectToServer()
            }
        }
    }

    override fun onStart() {
        super.onStart()
        client.addListener(this)
    }

    override fun onStop() {
        super.onStop()
        client.removeListener(this)
    }

    private fun disconnect() {
        client.disconnect()
        connectBtn.text = "Connect"
    }

    private fun connectToServer() {
        val ip = "192.168.1.104"
        val port = 6000
        client.connect(ip = ip, port = port)
    }

    override fun onSocketConnected(ip: String, port: Int) {
        // Background thread이므로, 강제로 Main thread로 전환시켜줘야 함.
        runOnUiThread {
            connectBtn.text = "Disconnect"
            Toast.makeText(this, "Successfully connected", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onSocketConnectionFailed(e: Exception) {
        // Background thread이므로, 강제로 Main thread로 전환시켜줘야 함.
        runOnUiThread {
            Toast.makeText(this, e.localizedMessage, Toast.LENGTH_SHORT).show()
        }
    }
}

 

 

spark (227,510 포인트) 님이 2022년 1월 18일 답변
이 예제를 따라 하여도 서버와 연결이 되지않을 때는 서버가 문제인가요?
어떤 Exception이 발생하는지 체크하셔야 해요.
...