블루투스 기기 연결

두 기기 간에 연결을 생성하려면 서버 측 메커니즘과 클라이언트 측 메커니즘을 모두 구현해야 합니다. 한 기기는 서버 소켓을 열어야 하고 다른 기기는 서버 기기의 MAC 주소를 사용하여 연결을 시작해야 하기 때문입니다. 서버 기기와 클라이언트 기기는 각각 필요한 BluetoothSocket를 서로 다른 방식으로 얻습니다. 서버는 들어오는 연결이 수락될 때 소켓 정보를 수신합니다. 클라이언트는 서버에 대한 RFCOMM 채널을 열 때 소켓 정보를 제공합니다.

서버와 클라이언트는 각각 동일한 RFCOMM 채널에 연결된 BluetoothSocket가 있을 때 서로 연결된 것으로 간주됩니다. 이 시점에서 각 기기는 입력 및 출력 스트림을 가져올 수 있고 데이터 전송을 시작할 수 있습니다. 이 내용은 블루투스 데이터 전송 섹션에서 설명합니다. 이 섹션에서는 두 기기 간의 연결을 시작하는 방법을 설명합니다.

블루투스 기기를 찾으려고 하기 전에 적절한 블루투스 권한이 있는지 확인하고 앱을 블루투스용으로 설정하세요.

연결 기술

구현 기법 중 하나는 각 기기에 서버 소켓이 열려 있고 연결을 수신 대기하도록 각 기기를 서버로 자동 준비하는 것입니다. 이 경우 어느 한 기기가 다른 기기와의 연결을 시작하고 클라이언트가 될 수 있습니다. 또는 한 기기는 연결을 명시적으로 호스팅하고 요청 시 서버 소켓을 열 수 있고, 다른 기기는 연결을 시작할 수 있습니다.


그림 1. 블루투스 페어링 대화상자

서버로 연결

두 기기를 연결하려면 하나는 열린 BluetoothServerSocket를 눌러 서버 역할을 해야 합니다. 서버 소켓의 목적은 들어오는 연결 요청을 수신 대기하고 요청이 수락된 후 연결된 BluetoothSocket를 제공하는 것입니다. BluetoothServerSocket에서 BluetoothSocket을 가져오는 경우 기기가 추가 연결을 수락하도록 하지 않는 한 BluetoothServerSocket는 삭제될 수 있으며 삭제해야 합니다.

서버 소켓을 설정하고 연결을 수락하려면 다음 단계를 순서대로 완료하세요.

  1. listenUsingRfcommWithServiceRecord(String, UUID)를 호출하여 BluetoothServerSocket를 가져옵니다.

    이 문자열은 식별 가능한 서비스 이름으로, 시스템이 기기의 새 SDP (Service Discovery Protocol) 데이터베이스 항목에 자동으로 기록합니다. 임의의 이름으로 지정되며 단순히 앱 이름일 수도 있습니다. 범용 고유 식별자 (UUID)도 SDP 항목에 포함되며 클라이언트 기기와의 연결 동의의 기반을 형성합니다. 즉, 클라이언트는 이 기기와 연결을 시도할 때 연결하려는 서비스를 고유하게 식별하는 UUID를 제공합니다. 연결이 수락되려면 이러한 UUID가 일치해야 합니다.

    UUID는 정보를 고유하게 식별하는 데 사용되는 표준화된 128비트 문자열 ID 형식입니다. UUID는 반복될 확률은 사실상 0이므로 시스템 또는 네트워크 내에서 고유해야 하는 정보를 식별하는 데 사용됩니다. 중앙 권한을 사용하지 않고 독립적으로 생성됩니다. 이 경우 앱의 블루투스 서비스를 고유하게 식별하는 데 사용됩니다. 앱에서 사용할 UUID를 가져오려면 웹에서 여러 임의의 UUID 생성기 중 하나를 사용한 다음 fromString(String)를 사용하여 UUID를 초기화하면 됩니다.

  2. accept()를 호출하여 연결 요청 리슨을 시작합니다.

    이는 차단 호출이며, 연결이 수락되었거나 예외가 발생한 경우 반환됩니다. 원격 기기가 이 수신 대기 서버 소켓에 등록된 UUID와 일치하는 UUID가 포함된 연결 요청을 보낸 경우에만 연결이 허용됩니다. 성공하면 accept()가 연결된 BluetoothSocket를 반환합니다.

  3. 추가 연결을 수락하지 않으려면 close()를 호출합니다.

    이 메서드를 호출하면 서버 소켓과 모든 리소스가 해제되지만 accept()에서 반환한 연결된 BluetoothSocket는 닫히지 않습니다. TCP/IP와 달리 RFCOMM은 한 번에 채널당 하나의 연결된 클라이언트만 허용하므로 대부분의 경우 연결된 소켓을 수락한 직후에 BluetoothServerSocket에서 close()를 호출하는 것이 좋습니다.

accept() 호출은 차단 호출이므로 기본 활동 UI 스레드에서 실행하지 마세요. 다른 스레드에서 실행하면 앱이 계속 다른 사용자 상호작용에 응답할 수 있습니다. 일반적으로 앱에서 관리하는 새 스레드에서 BluetoothServerSocket 또는 BluetoothSocket와 관련된 모든 작업을 실행하는 것이 좋습니다. accept()와 같은 차단된 호출을 취소하려면 BluetoothServerSocket에서 close() 또는 다른 스레드의 BluetoothSocket를 호출합니다. BluetoothServerSocket 또는 BluetoothSocket의 모든 메서드는 스레드로부터 안전합니다.

다음은 들어오는 연결을 수락하는 서버 구성요소의 단순화된 스레드입니다.

Kotlin

private inner class AcceptThread : Thread() {

   private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
       bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
   }

   override fun run() {
       // Keep listening until exception occurs or a socket is returned.
       var shouldLoop = true
       while (shouldLoop) {
           val socket: BluetoothSocket? = try {
               mmServerSocket?.accept()
           } catch (e: IOException) {
               Log.e(TAG, "Socket's accept() method failed", e)
               shouldLoop = false
               null
           }
           socket?.also {
               manageMyConnectedSocket(it)
               mmServerSocket?.close()
               shouldLoop = false
           }
       }
   }

   // Closes the connect socket and causes the thread to finish.
   fun cancel() {
       try {
           mmServerSocket?.close()
       } catch (e: IOException) {
           Log.e(TAG, "Could not close the connect socket", e)
       }
   }
}

Java

private class AcceptThread extends Thread {
   private final BluetoothServerSocket mmServerSocket;

   public AcceptThread() {
       // Use a temporary object that is later assigned to mmServerSocket
       // because mmServerSocket is final.
       BluetoothServerSocket tmp = null;
       try {
           // MY_UUID is the app's UUID string, also used by the client code.
           tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
       } catch (IOException e) {
           Log.e(TAG, "Socket's listen() method failed", e);
       }
       mmServerSocket = tmp;
   }

   public void run() {
       BluetoothSocket socket = null;
       // Keep listening until exception occurs or a socket is returned.
       while (true) {
           try {
               socket = mmServerSocket.accept();
           } catch (IOException e) {
               Log.e(TAG, "Socket's accept() method failed", e);
               break;
           }

           if (socket != null) {
               // A connection was accepted. Perform work associated with
               // the connection in a separate thread.
               manageMyConnectedSocket(socket);
               mmServerSocket.close();
               break;
           }
       }
   }

   // Closes the connect socket and causes the thread to finish.
   public void cancel() {
       try {
           mmServerSocket.close();
       } catch (IOException e) {
           Log.e(TAG, "Could not close the connect socket", e);
       }
   }
}

이 예에서는 수신 연결이 하나만 있으면 좋으므로, 연결이 수락되고 BluetoothSocket이 획득되는 즉시 앱이 획득한 BluetoothSocket를 별도의 스레드에 전달하고 BluetoothServerSocket를 닫고 루프를 끊습니다.

accept()BluetoothSocket를 반환하면 소켓이 이미 연결된 것입니다. 따라서 클라이언트 측에서 하는 것처럼 connect()를 호출해서는 안 됩니다.

앱별 manageMyConnectedSocket() 메서드는 데이터 전송을 위한 스레드를 시작하도록 설계되었습니다. 이 내용은 블루투스 데이터 전송에 관한 주제에서 설명합니다.

일반적으로 수신 연결 수신 대기를 완료하자마자 BluetoothServerSocket를 닫아야 합니다. 이 예시에서는 BluetoothSocket가 획득되는 즉시 close()가 호출됩니다. 서버 소켓에서 수신 대기를 중지해야 하는 경우 비공개 BluetoothSocket를 닫을 수 있는 공개 메서드를 스레드에 제공하는 것이 좋습니다.

클라이언트로 연결

열린 서버 소켓에서 연결을 수락하는 원격 기기와의 연결을 시작하려면 먼저 원격 기기를 나타내는 BluetoothDevice 객체를 가져와야 합니다. BluetoothDevice를 만드는 방법을 알아보려면 블루투스 기기 찾기를 참고하세요. 그런 다음 BluetoothDevice를 사용하여 BluetoothSocket를 가져오고 연결을 시작해야 합니다.

기본 과정은 다음과 같습니다.

  1. BluetoothDevice를 사용하여 createRfcommSocketToServiceRecord(UUID)를 호출하여 BluetoothSocket를 가져옵니다.

    이 메서드는 클라이언트가 BluetoothDevice에 연결하도록 허용하는 BluetoothSocket 객체를 초기화합니다. 여기에 전달된 UUID는 서버 기기에서 BluetoothServerSocket를 열기 위해 listenUsingRfcommWithServiceRecord(String, UUID)를 호출할 때 사용한 UUID와 일치해야 합니다. 일치하는 UUID를 사용하려면 UUID 문자열을 앱에 하드 코딩한 후 서버 및 클라이언트 코드에서 모두 참조합니다.

  2. connect()를 호출하여 연결을 시작합니다. 이 메서드는 차단 호출입니다.

    클라이언트가 이 메서드를 호출하면 시스템은 SDP 조회를 실행하여 일치하는 UUID가 있는 원격 기기를 찾습니다. 조회에 성공하고 원격 기기가 연결을 수락하면 연결 중에 사용할 RFCOMM 채널을 공유하고 connect() 메서드가 반환됩니다. 연결이 실패하거나 connect() 메서드의 시간이 약 12초 후에 타임아웃되면 메서드에서 IOException이 발생합니다.

connect()는 차단 호출이므로 항상 기본 활동 (UI) 스레드와 분리된 스레드에서 이 연결 절차를 실행해야 합니다.

다음은 블루투스 연결을 시작하는 클라이언트 스레드의 기본 예입니다.

Kotlin

private inner class ConnectThread(device: BluetoothDevice) : Thread() {

   private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
       device.createRfcommSocketToServiceRecord(MY_UUID)
   }

   public override fun run() {
       // Cancel discovery because it otherwise slows down the connection.
       bluetoothAdapter?.cancelDiscovery()

       mmSocket?.let { socket ->
           // Connect to the remote device through the socket. This call blocks
           // until it succeeds or throws an exception.
           socket.connect()

           // The connection attempt succeeded. Perform work associated with
           // the connection in a separate thread.
           manageMyConnectedSocket(socket)
       }
   }

   // Closes the client socket and causes the thread to finish.
   fun cancel() {
       try {
           mmSocket?.close()
       } catch (e: IOException) {
           Log.e(TAG, "Could not close the client socket", e)
       }
   }
}

Java

private class ConnectThread extends Thread {
   private final BluetoothSocket mmSocket;
   private final BluetoothDevice mmDevice;

   public ConnectThread(BluetoothDevice device) {
       // Use a temporary object that is later assigned to mmSocket
       // because mmSocket is final.
       BluetoothSocket tmp = null;
       mmDevice = device;

       try {
           // Get a BluetoothSocket to connect with the given BluetoothDevice.
           // MY_UUID is the app's UUID string, also used in the server code.
           tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
       } catch (IOException e) {
           Log.e(TAG, "Socket's create() method failed", e);
       }
       mmSocket = tmp;
   }

   public void run() {
       // Cancel discovery because it otherwise slows down the connection.
       bluetoothAdapter.cancelDiscovery();

       try {
           // Connect to the remote device through the socket. This call blocks
           // until it succeeds or throws an exception.
           mmSocket.connect();
       } catch (IOException connectException) {
           // Unable to connect; close the socket and return.
           try {
               mmSocket.close();
           } catch (IOException closeException) {
               Log.e(TAG, "Could not close the client socket", closeException);
           }
           return;
       }

       // The connection attempt succeeded. Perform work associated with
       // the connection in a separate thread.
       manageMyConnectedSocket(mmSocket);
   }

   // Closes the client socket and causes the thread to finish.
   public void cancel() {
       try {
           mmSocket.close();
       } catch (IOException e) {
           Log.e(TAG, "Could not close the client socket", e);
       }
   }
}

이 스니펫에서는 연결 시도가 발생하기 전에 cancelDiscovery()이 호출됩니다. 항상 connect() 전에 cancelDiscovery()를 호출해야 합니다. 특히 기기 검색이 현재 진행 중인지 여부와 관계없이 cancelDiscovery()가 성공하기 때문입니다. 앱에서 기기 검색이 진행 중인지 확인해야 하는 경우 isDiscovering()를 사용하여 확인할 수 있습니다.

앱별 manageMyConnectedSocket() 메서드는 데이터 전송을 위한 스레드를 시작하도록 설계되었으며 이 내용은 블루투스 데이터 전송 섹션에서 설명합니다.

BluetoothSocket 작업을 완료하면 항상 close()를 호출하세요. 이렇게 하면 연결된 소켓이 즉시 닫히고 모든 관련 내부 리소스가 해제됩니다.