為了在兩台設備間創建一個連接,必須實現服務器端和客戶端的機制,因為一個設備必須打開一個Server Socket,而另一個必須發起連接(使用服務器端設備的MAC地址發起連接)。當服務器端和客戶端在同一個RFCOMM信道上都有一個BluetoothSocket時,則兩端就建立了連接。此刻,每個設備都能獲得一個輸入輸出流,進行數據傳輸。
服務器端和客戶端獲得BluetoothSocket的方法是不同的,服務器端是在客戶端的連接被接受時才產生一個BluetoothSocket,客戶端是在打開一個到服務器端的RFCOMM信道時獲得BluetoothSocket的。
藍牙連接的一種實現技術是,每一個設備都自動准備作為一個服務器,所以每個設備都有一個Server Socket並監聽連接。然后每個設備都能作為客戶端建立一個到另一個設備的連接。
另外一種替代方法是,一個設備按需打開一個Server Socket,另外一個設備僅作為客戶端建立與這個設備的連接。
1.作為服務器連接
如果要連接兩個設備,其中一個必須充當服務器,它擁有BluetoothServerSocket。服務器Socket的作用是偵聽進來的連接,且在一個連接被接受時返回一個BluetoothSocket對象。從BluetoothServerSocket獲取到BluetoothSocket對象之后,BluetoothServerSocket就可以(也應該)丟棄了,除非還要用它接收更多的連接。
下面是建立服務器Socket和接受一個連接的基本步驟:
步驟1 通過調用listenUsingRfcommWithServiceRecord(String, UUID)方法得到一個BluetoothServerSocket對象。字符串參數為服務的標識名稱,名字是任意的,可以簡單地是應用程序的名稱。當客戶端試圖連接本設備時,它將攜帶一個UUID用來唯一標識它要連接的服務,UUID必須匹配,連接才會被接受。
步驟2 通過調用accept()來偵聽連接請求。這是一個阻塞線程,直到接受一個連接或者產生異常才會返回。當客戶端攜帶的UUID與偵聽它Socket注冊的UUID匹配,連接請求才會被接受。如果成功,accept()將返回一個BluetoothSocket對象。
步驟3 除非需要再接受另外的連接,否則的話調用close()。close()釋放Server Socket及其資源,但不會關閉accept()返回的BluetoothSocket對象。與TCP/IP不同,RFCOMM同一時刻一個信道只允許一個客戶端連接,因此大多是情況下意味着在BluetoothServerSocket接受一個連接請求后應該立即調用close()。
accept()調用不應該在主Activity UI線程中進行,因為它是個阻塞線程,會妨礙應用中其他的交互。通常在一個新線程中做BluetoothServerSocket或BluetoothSocket的所有工作來避免線程阻塞。如果需要放棄阻塞線程,可以調用close()方法。
下面是一個服務器組件接受連接的線程示例。
//定義接受線程 private class AcceptThread extends Thread { //創建BluetoothServerSocket類 private final BluetoothServerSocket mmServerSocket; public AcceptThread() { BluetoothServerSocket tmp = null; try { //MY_UUID是應用的UUID標識 tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID); } catch(IOException e) {} mmServerSocket = tmp; } //線程啟動時候運行 public void run() { BluetoothSocket socket = null; //保持偵聽 while(true) { try { //接受 socket = mmServerSocket.accept(); } catch(IOException e) { break; } //連接被接受 if(socket!=null) { //管理連接 manageConnectedSocket(socket); //關閉連接 mmServerSocket.close(); break; } } } //關閉連接 public void cancel() { try { //關閉BluetoothServerSocket mmServerSocket.close(); } catch(IOException e) {} } }
本例中,只接受一個進來的連接,一旦連接被接受並獲取BluetoothSocket,應用就發送獲取到的BluetoothSocket給一個單獨的線程,然后關閉BluetoothServerSocket並跳出循環。
---------------------
注意 accept()返回BluetoothSocket后,Socket就建立了連接,所以在客戶端就不應該再調用connect()。
---------------------
2.作為客戶端連接
為了實現與遠程服務器設備的連接,必須首先獲得一個代表遠程設備BluetoothDevice的對象。然后使用BluetoothDevice對象來獲取一個BluetoothSocket以實現連接。
基本步驟如下:
步驟1 使用BluetoothDevice調用方法createRfcommSocketToServiceRecord(UUID)獲取一個BluetoothSocket對象。
步驟2 調用connect()建立連接。當調用這個方法的時候,系統會在遠程設備上完成一個SDP查找來匹配UUID。如果查找成功並且遠程設備接受連接,就共享RFCOMM信道,connect()會返回。這個方法也是一個阻塞的調用。如果連接失敗或者超時(12秒)都會拋出異常。
---------------------
注意 要確保在調用connect()時沒有同時做設備搜索,如果在搜索設備,該連接嘗試會顯著變慢,容易導致連接失敗。
---------------------
下面是一個發起Bluetooth連接的線程示例。
//定義Bluetooth連接線程 private class ConnectThread extends Thread { //新建BluetoothSocket類 private final BluetoothSocket mmSocket; //新建BluetoothDevice對象 private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { BluetoothSocket tmp = null; //賦值給設備 mmDevice = device; try { //根據UUID創建並返回一個BluetoothSocket tmp = device.createRfcommSocketToServiceRecord(MY_UUID); } catch(IOException e) {} //賦值給BluetoothSocket mmSocket = tmp; } public void run() { //取消發現設備 mBluetoothAdapter.cancelDiscovery(); try { //連接到設備 mmSocket.connect(); } catch(IOException connectException) { //無法連接,關閉Socket try { mmSocket.close(); } catch (IOException connectException) {} return; } //管理連接 manageConnectedSocket(mmSocket); } //取消連接 public void cancel() { try { //關閉BluetoothSocket mmSocket.close(); } catch(IOException e) {} } }
注意:cancelDiscovery()應用連接操作前被調用。在連接之前,不管搜索有沒有進行。該調用都是安全的,不需要確認(當然如果要確認,可以調用isDiscovering()方法)。處理完后別忘了調用close()方法來關閉連接的Socket和釋放所有的內部資源。
3.管理連接
如果兩個設備成功建立了連接,各自會有一個BluetoothSocket,此時可以在設備間共享數據了。使用BluetoothSocket,傳輸任何數據通常來說都比較容易,通常如下進行:
+分別使用getInputStream()和getOutputStream()獲取輸入輸出流來處理傳輸。
+調用read(byte[])和write(byte[])來實現數據讀寫。
當然,要注意一些實現細節。比如,需要用一個專門的線程來實現流的讀寫,因為方法read(byte[])和write(byte[])都是阻塞調用。read(byte[])會阻塞,直到流中有數據可讀。write(byte[])雖然通常不會阻塞,但是如果遠程設備調用read(byte[])不夠快而導致中間緩沖區滿,它也可能阻塞。所以線程中的主循環應該用於讀取InputStream。線程中也應該有單獨的方法用來完成寫OutputStream。
請看下面的示例:
//連接管理線程 private class ConnectThread extends Thread { //新建BluetoothSocket類 private final BluetoothSocket mmSocket; //新建輸入流對象 private final InputStream mmInStream; //新建輸出流對象 private final OutputStream mmOutStream; public ConnectThread(BluetoothSocket socket) { //為BluetoothSocket賦初始值 mmSocket = socket; //輸入流賦值為null InputStream tmpIn = null; //輸出流賦值為null OutputStream tmpOut = null; try { //從BluetoothSocket中獲取輸入流 tmpIn = socket.getInputStream(); //從BluetoothSocket中獲取輸出流 tmpOut = socket.getOutputStream(); } catch(IOException e) {} //為輸入流賦值 mmInStream = temIn; //為輸出流賦值 mmOutStream = temOut; } public void run() { //流的緩沖大小 byte[] buffer = new byte[1024]; //用於保存read()所讀取的字節數 int bytes; while(true) { try { //從輸入流中讀取數據 bytes = mmInStream.read(buffer); //發送數據到界面 mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer).sendToTarget(); } catch () { break; } } } //發送數據到遠程設備 public void write(byte[] bytes) { try { //寫數據到輸出流中 mmOutStream.write(bytes); } catch(IOException e) {} } //取消 public void cancel() { try { //關閉B連接 mmSocket.close(); } catch(IOException e) {} } }
構造函數中得到需要的流,一旦執行,線程會等待從InputStream來的數據,當read(byte[])返回從流中讀到的字節后,數據通過父類的成員Handler被送到主Activity,然后繼續等待讀取流中的數據。向外發送數據只需簡單地調用線程的write()方法。線程的cancel()方法是很重要的,以便連接可以在任何時候通過關閉BluetoothSocket來終止。它總在處理完Bluetooth連接后被調用。