[안드로이드] 소켓 프로그램

소켓 프로그램

소켓

HTTP 통신 프로그램은 서버와 데이터를 주고받을 때 가장 많이 이용되지만, 서버와 클라이언트 간의 연결이 지속적으로 유지되지 않아서 실시간 서버 푸시를 구현할 수 없습니다.

HTTP 통신 프로그램은 위에서 언급했듯이 실시간 서버 푸시를 구현해야 하는 곳에서는 사용할 수 없습니다. 왜냐하면, 서버와 데이터를 주고받은 다음 응답이 끝나면 자동으로 연결이 끊어지기 때문입니다. 이를 "HTTP의 Stateless"라고 부릅니다. 물론, 클라이언트에서 필요한 순간 다시 연결하여 데이터를 송수신할 수 있습니다. 하지만 실시간 서버 푸시는 서버 측에서 데이터가 발생한 순간 그 데이터를 클라이언트에게 전송해야 하는데, 클라이언트는 연결이 끊어진 상태이므로 클라이언트가 다시 연결을 요청하지않는 한 전송할 방법이 없습니다.

실시간 서버 푸시가 필요한 대표적인 예가 채팅 프로그램입니다. 상대방이 보낸 메시지는 서버로 전송되고, 그 순간 실시간으로 그 메세지가 클라이언트(나)에 전달되어야 하는데, HTTP는 연결이 지속되지 않으므로 서버가 클라이언트에게 데이터를 보낼 방법이 없습니다. 결국, 채팅 같은 실시간 서버 푸시를 구현하려면 클라이언트와 서버 간의 연결을 지속해야 하며, 이를 위해서 소켓 프로그램이 필요합니다.

소켓 프로그램은 클라이언트에서 서버에 연결 요청을 하여 연결이 완성되면 이를 지속하는 구조이므로 클라이언트 또는 서버에서 데이터가 발생할 때마다 이 연결을 이용하여 실시간으로 데이터를 전송할 수 있습니다.


소켓 클래스

퍼미션

소켓 프로그램을 작성하려면 HTTP 통신과 마찬가지로 INTERNET 퍼미션이 필요합니다.

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

클래스

서버와 소켓 연결은 Socket 클래스와 InetSocketAddress 클래스를 이용합니다. InteSocketAddress는 연결 서버 정보를 표현하는 클래스로 생성자에 IP 주소와 포트번호를 지정합니다. InetSocketAddress로 표현된 서버에 연결 요청은 Socket 클래스의 connect() 함수 호출로 이루어지고 connect() 함수의 매개변수로 InetSocketAddress 객체와 타임아웃 시간을 지정하면 됩니다.

// 소켓 객체 생성
socket = Socket()
// InetSocketAddress 생성자에 서버의 IP와 포트번호를 인자로 주고 객체 생성
val remoteAddr = InetSocketAddress(serverIp, serverPort)
// connect 메서드를 사용하고 InetSocketAddress 객체와 타임아웃 시간 지정
socket?.connect(remoteAddr, 10 * 1000)

Socket 클래스로 연결된 서버와 데이터를 송수신하려면 IO 객체를 생성해야 합니다.

// getOutputStream() 메서드를 사용하여 소켓의 output stream을 획득하고
// 그것을 BufferedOutputStream에 연결
bout = BufferedOutputStream(socket?.getOutputStream())
// getInputStream() 메서드를 사용하여 소켓의 input stream을 획득하고
// 그것을 BufferedInputStream에 연결
bin = BufferedInputStream(socket?.getInputStream())

// 데이터를 송신하기 위해 BufferedOutputStream의 write 메서드를 사용
// 매개변수로 전송하고자 하는 데이터 지정
bout.write((msg.obj as String).toByteArray())
bout.flush()

// 데이터를 수신하기 위해 BufferedInputStream의 read 메서드를 사용
var message: String? = null
val size = bin.read(buffer)
if(size > 0) {
    message = String(buffer, 0, size, charset("utf-8"))
}

연결한 서버와 데이터를 송수신 하기 위해 소켓 객체를 이용해 socket.getOutputStream(), socket.getInputStream() 구문으로 IO 객체를 획득합니다. 그리고 이 IO 객체를 이용하여 필요 시 데이터를 전송하거나 수신합니다. 데이터를 전송하기 위해서는 write() 메서드를 사용하고 데이터를 수신하기 위해서는 read() 메서드를 사용합니다.


소켓 작성 시 주의사항

소켓 통신을 위해서는 서버에 연결해야 하고 연결된 서버와 데이터를 송수신해야 합니다. 그런데 이 모든 것들을 스레드에서 처리해야 합니다. 데이터 송수신 작업과 연결 모두 네트워크가 수시로 오프라인 상황이 되어 많은 시간 걸릴 수도 있고 또한 여러 다른 문제가 발생하여 많은 시간이 걸릴 수도 있습니다. 그렇기에 스레드로 처리하지 않으면 ANR이 발생할 수 있습니다(액티비티 상황에서의 예시).

물론 서비스 컴포넌트에서 작성하더라도 연결과 데이터 송수신 모두 스레드로 처리해야 합니다.
소켓 프로그램의 특성상 장시간 서버와 연결을 지속해야 합니다. 서비스가 구동하면서 한 번 서버와 연결에 성공하더라도 수시로 연결이 끊어질 수 있기에 어디선가 지속해서 서버 연결 상태를 파악하여 연결이 안되고 있으면 계속 연결해 주어야 합니다. 결국, 서버 연결 부분을 스레드로 처리해 연결이 안 되어 있는 상황이면 계속 연결을 시도해 주어야 합니다.

연결 스레드

간단한 예제 코드로 isConnected라는 boolean 값을 사용하여 현재 서버와 연결된 상태인지를 표현하여 이 값에 따라 다시 연결을 시도하는 방식입니다.

// 소켓 생성 스레드
inner class SocketThread: Thread() {
    override fun run() {
        while(flagConnection) {
            try {
                // 연결이 안된 경우
                if(!isConnected) {
                    // 소켓 객체 생성
                    socket = Socket()
                    // InetSocketAddress 생성자에 서버의 IP와 포트번호를 인자로 주고 객체 생성
                    val remoteAddr = InetSocketAddress(serverIp, serverPort)
                    // connect 메서드를 사용하고 InetSocketAddress 객체와 타임아웃 시간 지정
                    socket?.connect(remoteAddr, 10 * 1000)
                    
                    // 코드....
                } else { // 연결이 된 경우
                    SystemClock.sleep(10000)
                }
            } catch(e: Exception) {
                // 소켓 연결 시 오류는 여러 가지가 존재합니다.
            }
        }
    }
}

읽기 스레드

서버로부터 데이터가 넘어와야 읽을 수 있기에 read() 메서드가 실행되면 서버로부터 데이터가 넘어오기까지 아랫줄은 실행되지 않고 대기 상태가 됩니다. 이러한 읽기 행위를 스레드로 처리하면 아래와 같습니다.

// 읽기 스레드
inner class ReadThread: Thread() {
    override fun run() {
        // Byte 배열 생성
        var buffer = ByteArray(1024)
        try {
            // read 함수를 사용하여 데이터 수신
            val size = bin?.read(buffer)
            // 받아온 데이터의 크기가 0보다 크다면
            if(size != null && size > 0) {
                // read 후 업무 처리
            }
        } catch(e: IOException) {
            
        }
    }
}

쓰기 스레드

쓰기 행위를 스레드로 처리하면 아래와 같습니다. 아래 코드는 액티비티에서 Write를 하는 코드입니다. 이때 Write 데이터는 Main 스레드가 관리하는 뷰에서 발생하는 데이터이므로 Write 스레드에서 직접 뷰 객체에 접근할 수 없는 문제가 발생합니다. 결국, Main 스레드에서 뷰 데이터를 얻어와 Write 스레드의 Handler를 이용하여 데이터를 전달하고, 전달받은 데이터를 서버에 전송하는 구조로 프로그램을 작성해야 합니다.

// 쓰기 스레드
inner class WriteThread: Thread() {
    override fun run() {
        // 현재 스레드에 Looper 초기화
        Looper.prepare()
        // 핸들러 생성
        writeHandler = object: Handler(Looper.myLooper()!!) {
            // 메세지 처리하기
            override fun handleMessage(msg: Message) {
                try {
                    // 전달된 메세지 객체를 String으로 형변환 후 ByteArray로 변경
                    // 그 후 write 메서드를 통해 데이터 전송
                    bout?.write((msg.obj as String).toByteArray())
                    bout?.flush()
                    // ....
                } catch(e: Exception) {

                }
            }
        }
        // 메세지큐 작동
        Looper.loop()
    }
}

참조
깡쌤의 안드로이드 프로그래밍

틀린 부분 댓글로 남겨주시면 수정하겠습니다..!!

좋은 웹페이지 즐겨찾기