連結藍牙裝置

如要在兩部裝置之間建立連線,您必須同時實作伺服器端和用戶端機制,因為一個裝置必須開啟伺服器通訊端,而另一部裝置必須使用伺服器裝置的 MAC 位址啟動連線。伺服器裝置和用戶端裝置會透過不同方式取得所需的 BluetoothSocket。伺服器在接受傳入連線時收到通訊端資訊。用戶端會在向伺服器開啟 RFCOMM 管道時提供通訊端資訊。

當伺服器和用戶端都在同一個 RFCOMM 管道上連結 BluetoothSocket 時,系統就會將伺服器和用戶端視為彼此連線。此時,每部裝置都可以取得輸入和輸出串流,並可以開始資料移轉,詳情請參閱「轉移藍牙資料」一節。本節說明如何在兩部裝置之間啟動連線。

嘗試尋找藍牙裝置前,請確認您具備適當的藍牙權限,並設定應用程式的藍牙功能

連線技巧

一種實作技巧是將每個裝置自動準備為伺服器,讓每部裝置都有一個伺服器通訊端開啟並監聽連線。在這種情況下,任一裝置都可以啟動連線,並成為用戶端。或者,某部裝置可以明確代管連線,並視需求開啟伺服器通訊端,然後另一個裝置就會啟動連線。


圖1. 藍牙配對對話方塊。

以伺服器形式連線

如要連接兩部裝置,一部裝置必須保持開啟中的 BluetoothServerSocket 並做為伺服器使用。伺服器通訊端的用途是監聽傳入的連線要求,並在系統接受要求後提供已連結的 BluetoothSocket。從 BluetoothServerSocket 取得 BluetoothSocket 時,除非您要讓裝置接受更多連線,否則應該捨棄 BluetoothServerSocket

如要設定伺服器通訊端並接受連線,請完成下列步驟:

  1. 呼叫 listenUsingRfcommWithServiceRecord(String, UUID) 以取得 BluetoothServerSocket

    字串是服務的可識別名稱,系統會自動寫入裝置上的新服務探索通訊協定 (SDP) 資料庫項目。您可以選擇任意名稱,只是應用程式名稱。通用唯一識別碼 (UUID) 也會包含在 SDP 項目中,構成與用戶端裝置連線協議的基礎。也就是說,用戶端嘗試與這部裝置連線時,會傳輸一個 UUID,用於識別它要與其連線的服務。這些 UUID 必須相符,系統才能接受連線。

    UUID 是字串 ID 的標準化 128 位元格式,專門用於識別身分資訊。UUID 可用來識別系統或網路內需要的唯一資訊,因為重複使用 UUID 的機率等於零。它是獨立產生,不需使用集中式授權。在這種情況下,這項功能會用於明確識別應用程式的藍牙服務。如要取得可透過應用程式使用的 UUID,您可以使用網路上任何隨機的 UUID 產生器,然後使用 fromString(String) 初始化 UUID。

  2. 呼叫 accept() 即可開始監聽連線要求。

    此為撥通電話,系統會在接受連線或發生例外狀況時傳回相關資訊。只有在遠端裝置傳送的連線要求內含的 UUID,且該要求與此監聽伺服器通訊端註冊的 UUID 相符時,才會接受連線。成功時,accept() 會傳回已連結的 BluetoothSocket

  3. 除非您要接受其他連線,否則請呼叫 close()

    這個方法呼叫會釋放伺服器通訊端及其所有資源,但不會關閉 accept() 傳回的已連線 BluetoothSocket。與 TCP/IP 不同,RFCOMM 一次只能每個管道一個連線的用戶端,因此在大多數情況下,在接受已連線的通訊端後,立即在 BluetoothServerSocket 上呼叫 close()

accept() 呼叫屬於封鎖呼叫,因此請勿在主要活動 UI 執行緒中執行。在其他執行緒中執行,可確保應用程式仍可以回應其他使用者的使用者互動。在應用程式管理的新執行緒中執行所有涉及 BluetoothServerSocketBluetoothSocket 的工作,通常很合理。如要取消已封鎖的呼叫 (例如 accept()),請在 BluetoothServerSocket 上呼叫 close(),或從其他執行緒呼叫 BluetoothSocket。請注意,BluetoothServerSocketBluetoothSocket 上的所有方法都屬於執行緒安全。

範例

以下是接受傳入連線的伺服器元件的簡化執行緒:

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

    這個方法會初始化 BluetoothSocket 物件,讓用戶端連線至 BluetoothDevice。此處傳遞的 UUID 必須符合伺服器裝置在呼叫 listenUsingRfcommWithServiceRecord(String, UUID) 以開啟 BluetoothServerSocket 時使用的 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()。這麼做會立即關閉已連線的通訊端,並釋出所有相關的內部資源。