如要在兩部裝置之間建立連線,您必須同時實作伺服器端和用戶端機制,因為一個裝置必須開啟伺服器通訊端,而另一部裝置必須使用伺服器裝置的 MAC 位址啟動連線。伺服器裝置和用戶端裝置會透過不同方式取得所需的 BluetoothSocket
。伺服器在接受傳入連線時收到通訊端資訊。用戶端會在向伺服器開啟 RFCOMM 管道時提供通訊端資訊。
當伺服器和用戶端都在同一個 RFCOMM 管道上連結 BluetoothSocket
時,系統就會將伺服器和用戶端視為彼此連線。此時,每部裝置都可以取得輸入和輸出串流,並可以開始資料移轉,詳情請參閱「轉移藍牙資料」一節。本節說明如何在兩部裝置之間啟動連線。
嘗試尋找藍牙裝置前,請確認您具備適當的藍牙權限,並設定應用程式的藍牙功能。
連線技巧
一種實作技巧是將每個裝置自動準備為伺服器,讓每部裝置都有一個伺服器通訊端開啟並監聽連線。在這種情況下,任一裝置都可以啟動連線,並成為用戶端。或者,某部裝置可以明確代管連線,並視需求開啟伺服器通訊端,然後另一個裝置就會啟動連線。
圖1. 藍牙配對對話方塊。
以伺服器形式連線
如要連接兩部裝置,一部裝置必須保持開啟中的 BluetoothServerSocket
並做為伺服器使用。伺服器通訊端的用途是監聽傳入的連線要求,並在系統接受要求後提供已連結的 BluetoothSocket
。從 BluetoothServerSocket
取得 BluetoothSocket
時,除非您要讓裝置接受更多連線,否則應該捨棄 BluetoothServerSocket
。
如要設定伺服器通訊端並接受連線,請完成下列步驟:
呼叫
listenUsingRfcommWithServiceRecord(String, UUID)
以取得BluetoothServerSocket
。字串是服務的可識別名稱,系統會自動寫入裝置上的新服務探索通訊協定 (SDP) 資料庫項目。您可以選擇任意名稱,只是應用程式名稱。通用唯一識別碼 (UUID) 也會包含在 SDP 項目中,構成與用戶端裝置連線協議的基礎。也就是說,用戶端嘗試與這部裝置連線時,會傳輸一個 UUID,用於識別它要與其連線的服務。這些 UUID 必須相符,系統才能接受連線。
UUID 是字串 ID 的標準化 128 位元格式,專門用於識別身分資訊。UUID 可用來識別系統或網路內需要的唯一資訊,因為重複使用 UUID 的機率等於零。它是獨立產生,不需使用集中式授權。在這種情況下,這項功能會用於明確識別應用程式的藍牙服務。如要取得可透過應用程式使用的 UUID,您可以使用網路上任何隨機的
UUID
產生器,然後使用fromString(String)
初始化 UUID。呼叫
accept()
即可開始監聽連線要求。此為撥通電話,系統會在接受連線或發生例外狀況時傳回相關資訊。只有在遠端裝置傳送的連線要求內含的 UUID,且該要求與此監聽伺服器通訊端註冊的 UUID 相符時,才會接受連線。成功時,
accept()
會傳回已連結的BluetoothSocket
。除非您要接受其他連線,否則請呼叫
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
並啟動連線。
基本程序如下:
使用
BluetoothDevice
呼叫createRfcommSocketToServiceRecord(UUID)
以取得BluetoothSocket
。這個方法會初始化
BluetoothSocket
物件,讓用戶端連線至BluetoothDevice
。此處傳遞的 UUID 必須符合伺服器裝置在呼叫listenUsingRfcommWithServiceRecord(String, UUID)
以開啟BluetoothServerSocket
時使用的 UUID。如要使用相符的 UUID,請將 UUID 字串硬式編碼到應用程式中,然後從伺服器和用戶端程式碼參照該字串。呼叫
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()
。這麼做會立即關閉已連線的通訊端,並釋出所有相關的內部資源。