Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

안드로이드 앱 만들기 : TCP/IP 통신을 배워보자 1일차

Billcorea 2023. 4. 10. 15:25
반응형

TCP(Transmission Control Protocol) / IP(Internet Protocol)

 

인터넷 - 나무위키

이 저작물은 CC BY-NC-SA 2.0 KR에 따라 이용할 수 있습니다. (단, 라이선스가 명시된 일부 문서 및 삽화 제외) 기여하신 문서의 저작권은 각 기여자에게 있으며, 각 기여자는 기여하신 부분의 저작권

namu.wiki

인터넷 통신을 하기 위한 프로토콜 중에 하나인 것은 알겠으나, 여태 한 번도 TCP/IP 통신앱을 만들어 보지는 않았습니다. 

최근에 주변장치들과 통신을 하는 앱을 구현해 줄 수 있는 지 물어보는 사용자가 있었습니다.  재택을 하루 종일 하고 있었다면, 자료를 찾아서 앱을 구현해 드리고 싶었으나, 

 

요새는 저녁 시간밖에는 시간을 낼 수 없는 지라... 하루 저녁 자료를 찾아보니 택도 없는 일인 듯하여, 돌려보냈지요. 

그리고 주말에 찾아 보았습니다. 

 

이런저런 코드들을 살펴보다가 참고가 될 듯한 코드를 찾아 앱을 하나 구현해 보았습니다. 

 

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

통신을 해야 하니 일단, manifest 파일에 internet 사용과 관련된 권한을 등록해 보았습니다.

 

Log.e("", "doTcpIpOpen ...")

GlobalScope.launch {

    try{
        socket = Socket( ipAddress, 3000)

        doResponse()

    } catch (e : IOException) {
        Log.e("", "네트워크 응답 없음")
    } catch (e : UnknownHostException) {
        Log.e("", "알 수 없는 호스트 IP")
    } catch (e : SecurityException) {
        Log.e("", "보안 접속 오류 proxy 접속 거부")
    } catch (e : IllegalArgumentException) {
        Log.e("", "server port range 0 ~ 65535 ")
    } catch (e : Exception) {
        Log.e("", "error ${e.localizedMessage}")
    }

    Log.e("", "doTcpIpOpen end...")
}

다음은 소캣을 하나 생성하면서 접속할 서버와 연결을 시도해 보았습니다.  저 코드에서 말하는 ipAddress는 우리가 알고 있는 일반적인 ip 주소 (192.168.0.2 같은)입니다.  저는 PC에서 서버 측 구현을 python을 이용해서 echo 서버를 하나 만들어 놓고 테스트를 진행하였습니다. 

 

아래 코드는 python 을 구동하는 server 코드 예제입니다.  다른 기능은 없고 수신된 값을 다시 보내는 echo 서버입니다.

import socket
from random import random

addr = ("0.0.0.0", 3000)  # 포트번호 4444
with socket.socket() as s:  # 소켓 할당

    s.bind(addr)  # 소켓 바인딩
    s.listen()  # client의 연결요청 대기
    print("Server is started... 3000 ")

    # ------------------------------------서버 개통과정--------------

    conn, addr = s.accept()  # client 연경 요청을 수락
    print("accept {}:{}".format(addr[0], addr[1]))  # 연결된 client 정보 출력
    while (1):  # 무한반복
        data = conn.recv(1024)  # client가 보낸 메시지를 data에 저장
        if data.decode() == "finished":  # data decoding 결과가 finished면
            break  # 반복문 탈출
        respData = data.decode() + "..." + str(random())
        conn.send(respData.encode())  # data를 그대로 client에게 전송
        print(data.decode())  # 보낸 데이터 읽기
    print("Server finished")  # 서버 종료 알리기

print("SOCKET closed... END")

서버와 클라이언트 간의 통신을 하여야 하기 때문에   서버 측 ip와 포트를 열어 줍니다.  잠깐 본 기억이기는 하나, 서버 측에서 선언한 addr 0.0.0.0의 선언에도 주의가 필요해 보이기는 합니다.   0.0.0.0을 선언하는 경우는 클라이언트의 ip 대역은 전체 대역을 어디서나 접근이 가능해지지만, 다르게 선언하는 경우 그 접속 범주가 달라지는 것을 알 수 있었습니다.

 

아무튼 서버를 구동한 상태에서 안드로이드 클라이언트를 실행해 접속을 해 보면 서버 측에 접속이 되는 것을 볼 수 있었습니다.

 

안드로이드에서 소켓 통신을 하면서 주의할 부분은 thread로 감싸 주어야 한다는 것입니다. 안드로이드는 앱이 실행하는 동안에 대기상태가 되는 것을 싫어합니다.  그래서 thread을 이용해 통신을 해 주어야 하는 것 같습니다.

 

GlobalScope.launch {
    try {

        val byteArr = ByteArray(100)

        while(true) {

            val input = withContext(Dispatchers.IO) {
                socket.getInputStream()
            }
            ip = socket.inetAddress.hostAddress as String
            val byte = withContext(Dispatchers.IO) {
                input.read(byteArr)
            }

            val data = String(byteArr, 0, byte, Charset.forName("UTF-8"))

            Log.e("", "ip=$ip readData=$byte $data" )
        }
    } catch (e : IOException) {
        Log.e("", "read error = ${e.localizedMessage}")
    } catch (e : StringIndexOutOfBoundsException) {
        Log.e("", "read error = ${e.localizedMessage}")
        doClose()
    }
}

 

서버 접속이 되었다면, 이제 데이터 수신을 위한 스레드를 하나 구동시켜 둡니다. 그러면 언제든지 수신되는 데이터를 받아서 표시할 수 있을 테니까요..

 

GlobalScope.launch {
    try {
        val writer = OutputStreamWriter(socket.getOutputStream())
        if ("" == strMessage) {
            writer.write("hello server")
        } else {
            writer.write(strMessage)
        }
        writer.flush()
    } catch (e : IOException) {
        Log.e("", "네트워크 응답 없음")
    } catch (e : UnknownHostException) {
        Log.e("", "알 수 없는 호스트 IP")
    } catch (e : SecurityException) {
        Log.e("", "보안 접속 오류 proxy 접속 거부")
    } catch (e : IllegalArgumentException) {
        Log.e("", "server port range 0 ~ 65535 ")
    } catch (e : Exception) {
        Log.e("", "error ${e.localizedMessage}")
    }
}

이제 서버 측을 데이터를 보내 보겠습니다. 

 

서버가 구동되어 있는 상황이라면 정상적으로 데이터를 주고받는 것을 볼 수 있습니다. 그렇지 않다고 하면 여러 종류의 에러(Exception)가 발생할 텐데요. 그 종류는 위에 기술된 정도일 듯합니다.  

 

오늘은 기초 공부를 해 보는 것이니 이 정도 구현을 해 보도록 하겠습니다. 

 

https://github.com/nari4169/TcpIpClientExam

 

GitHub - nari4169/TcpIpClientExam: TcpIp Client Sample Kotlin

TcpIp Client Sample Kotlin. Contribute to nari4169/TcpIpClientExam development by creating an account on GitHub.

github.com

오늘 구현했던 코드는 github에서 참고해 보세요.   다음에는 조금 더 정리된 글을 적어 보도록 하겠습니다.

 

반응형