2013年11月27日 星期三

[Android]Android程式BluetoothChat範例程式學藍芽連線原理

一、前言
Bluetooth藍芽裝置在手機界已經存在很久了,
也早就成為連低階Android手機都有的基本配備。
官方也提供了一個利用藍芽連線互相對話的範例程式
讓我們能快速地了解藍芽在Android中的使用方式。 
  
二、文章開始 
首先,
我們先建立一個觀念︰ 
藍芽一定分成2個端點,
分別為被動的Server端主動的Client端

底下是BluetoothChat的Sample Code程式流程︰

=========現在在BluetoothChat.java底下==============

1.在onCreate()時,呼叫
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();


一起來看看官方文件裡怎麼說BluetoothAdapter
BluetoothAdapter represents the local Bluetooth adapter (Bluetooth radio).
The BluetoothAdapter is the entry-point for all Bluetooth interaction.
Using this, you can discover other Bluetooth devices, query a list of bonded (paired) devices, instantiate a BluetoothDevice using a known MAC address, and create a BluetoothServerSocket to listen for communications from other devices.
BluetoothAdapter是區域藍芽接口(藍芽廣播)。BluetoothAdapter也是所有藍芽交易互動的啟始點。用這個接口,我們可以偵 測區域內有哪些其它的藍芽裝置、查詢已配對過的藍芽列表、用已知的MAC地址建立一個BluetoothDevice實體、建立一個 BluetoothServerSocket來監聽是否有其它藍芽裝置傳來的通訊…等。

2-1.得到一個BluetoothAdapter實體之後,
在onStart()裡, 
如果沒有啟動藍芽,則要求使用者開啟藍芽。
指令是︰
if (!mBluetoothAdapter.isEnabled()) {
    Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
// Otherwise, setup the chat session
}else {
    if (mChatService == null) setupChat();
}

2-2.透過setupChat()建立起基本的對話視窗和BluetoothChatService背景服務,並把主Thread的Handler傳給Service以供日後傳回message。
mChatService = new BluetoothChatService(this, mHandler);


3.在onResume()裡,也做一樣的事,如果檢查沒有開啟藍芽BluetoothChatService背景服務,則再次開始該服務。 
if (mChatService.getState() == BluetoothChatService.STATE_NONE) {
// Start the Bluetooth chat services
mChatService.start();
}

=========現在進入BluetoothChatService.java裡==============

程式碼才剛開出來,
馬上就看到這個Service的程式架構中,塞了3個執行緒, 
分別為︰
(1)AcceptThread
(2)ConnectThread
(3)ConnectedThread

馬上來看看它們在Service裡,分別擔任什麼樣的任務︰

// Cancel any thread attempting to make a connection
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
 
// Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
 
// Start the thread to listen on a BluetoothServerSocket
if (mAcceptThread == null) {
mAcceptThread = new AcceptThread();
mAcceptThread.start();
}

1.在onStart()中,
檢查如果ConnectThread和ConnectedThread存在,則將他們關掉。
2.啟動一個AcceptThread(現在的流程是在藍芽開啟中的狀態,開啟了一個AcceptThread待命)。
這個AcceptThread存在的目的,是因為程式先假設每臺裝置都有可能想要跟它做藍芽連線。

來看一下這個程式一啟動後就執行的AcceptThread裡面做了些什麼︰

/**
   * This thread runs while listening for incoming connections. It behaves
   * like a server-side client. It runs until a connection is accepted
   * (or until cancelled).
   */
  private class AcceptThread extends Thread {
      // The local server socket
      private final BluetoothServerSocket mmServerSocket;
 
      public AcceptThread() {
          BluetoothServerSocket tmp = null;
 
          // Create a new listening server socket
          try {
              tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
          } catch (IOException e) {
              Log.e(TAG, "listen() failed", e);
          }
          mmServerSocket = tmp;
      }
 
      public void run() {
          if (D) Log.d(TAG, "BEGIN mAcceptThread" + this);
          setName("AcceptThread");
          BluetoothSocket socket = null;
 
          // Listen to the server socket if we're not connected
          while (mState != STATE_CONNECTED) {
              try {
                  // This is a blocking call and will only return on a
                  // successful connection or an exception
                  socket = mmServerSocket.accept();
              } catch (IOException e) {
                  Log.e(TAG, "accept() failed", e);
                  break;
              }
 
              // If a connection was accepted
              if (socket != null) {
                  synchronized (BluetoothChatService.this) {
                      switch (mState) {
                      case STATE_LISTEN:
                      case STATE_CONNECTING:
                          // Situation normal. Start the connected thread.
                          connected(socket, socket.getRemoteDevice());
                          break;
                      case STATE_NONE:
                      case STATE_CONNECTED:
                          // Either not ready or already connected. Terminate new socket.
                          try {
                              socket.close();
                          } catch (IOException e) {
                              Log.e(TAG, "Could not close unwanted socket", e);
                          }
                          break;
                      }
                  }
              }
          }
          if (D) Log.i(TAG, "END mAcceptThread");
      }
 
      public void cancel() {
          if (D) Log.d(TAG, "cancel " + this);
          try {
              mmServerSocket.close();
          } catch (IOException e) {
              Log.e(TAG, "close() of server failed", e);
          }
      }
  }

我們看到了在AcceptThread裡面,埋入了一個BluetoothServerSocket。
看一下Bluetooth Android官方對它的解釋︰
BluetoothServerSocket represents an open server socket that listens for incoming requests (similar to a TCP ServerSocket). In order to connect two Android devices, one device must open a server socket with this class. When a remote Bluetooth device makes a connection request to the this device, the BluetoothServerSocket will return a connectedBluetoothSocket when the connection is accepted.
BluetoothServerSocket是一個開放式的server socket,用來監聽任何傳進來的請求(原理類似TCP ServerSocket)。為了讓2隻Android devices能夠連線,其中一隻裝置必須開啟server socket。當遠端的藍芽裝置向手上這隻裝備請求連線後,這隻裝置上的BluetoothServerSocket會回傳一個accepted的 BluetoothSocket給呼叫那一方。

因此我們知道,上面程式碼中

BluetoothSocket  socket = mmServerSocket.accept();


就是應證了BluetoothServerSocket會吐BluetoothSocket出來這件事。

回到一開始呼叫AcceptThread.start()的那個時間點,
也就是說,
程式在一啟動時,
都先要求使用者開啟藍芽,
然後隨時準備接收別臺藍芽裝置會傳送連線請求的事件。

我們取到了BluetoothSocket後,
看看這個BluetoothSocket能做些什麼。

首先,
在官方技術文件提到︰
BluetoothSocket represents the interface for a Bluetooth socket (similar to a TCP Socket). This is the connection point that allows an application to exchange data with another Bluetooth device via InputStream and OutputStream.
BluetoothSocket是一個Bluetooth socket的接口(原理類似TCP Socket)。這個連結點允許APP透過InputStream和OutpusStream互相交換資料。

因此我們得知,
BluetoothSocket可以讓我們做到資料交換的功能。

因為在Service onStart()呼叫AcceptThread.start()後,
馬上將藍芽狀態設定成setState(STATE_LISTEN);
因此,在switch迴圈中,
程式執行了connected()函式。

這段程式碼如下︰

case STATE_LISTEN:
case STATE_CONNECTING:
    // Situation normal. Start the connected thread.
    connected(socket, socket.getRemoteDevice());
    break;

馬上來看看connected()函式做了哪些事

/**
 * Start the ConnectedThread to begin managing a Bluetooth connection
 * @param socket  The BluetoothSocket on which the connection was made
 * @param device  The BluetoothDevice that has been connected
 */
public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
    if (D) Log.d(TAG, "connected");
 
    // Cancel the thread that completed the connection
    if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
 
    // Cancel any thread currently running a connection
    if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
 
    // Cancel the accept thread because we only want to connect to one device
    if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}
 
    // Start the thread to manage the connection and perform transmissions
    mConnectedThread = new ConnectedThread(socket);
    mConnectedThread.start();
 
    // Send the name of the connected device back to the UI Activity
    Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_DEVICE_NAME);
    Bundle bundle = new Bundle();
    bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());
    msg.setData(bundle);
    mHandler.sendMessage(msg);
 
    setState(STATE_CONNECTED);
}

為了避免重覆連線,
先檢查有沒有已存在的ConectThread、ConnectedThrad和AcceptThread。
如果有,一律先關掉。
然後,啟動ConnectedThread,
並將MESSAGE_DEVICE_NAME用handler(mHandler,還記得我們前面有提到在BluetoothChat.java傳了一個主Thread的Handler給Service嗎?)傳訊的方式,
將Client端的裝置資料傳回BluetoothChat.java,
讓Server端知道是誰在跟它做連結。

前面提到,
一旦取得了BluetoothSocket之後,
就可以開始執行互相傳遞資料的工作了 。
這個被啟動的ConnectedThread就是在做資料互傳的監聽工作
我們看看ConnectedThread做了些什麼

/**
  * This thread runs during a connection with a remote device.
  * It handles all incoming and outgoing transmissions.
  */
 private class ConnectedThread extends Thread {
     private final BluetoothSocket mmSocket;
     private final InputStream mmInStream;
     private final OutputStream mmOutStream;
 
     public ConnectedThread(BluetoothSocket socket) {
         Log.d(TAG, "create ConnectedThread");
         mmSocket = socket;
         InputStream tmpIn = null;
         OutputStream tmpOut = null;
 
         // Get the BluetoothSocket input and output streams
         try {
             tmpIn = socket.getInputStream();
             tmpOut = socket.getOutputStream();
         } catch (IOException e) {
             Log.e(TAG, "temp sockets not created", e);
         }
 
         mmInStream = tmpIn;
         mmOutStream = tmpOut;
     }
 
     public void run() {
         Log.i(TAG, "BEGIN mConnectedThread");
         byte[] buffer = new byte[1024];
         int bytes;
 
         // Keep listening to the InputStream while connected
         while (true) {
             try {
                 // Read from the InputStream
                 bytes = mmInStream.read(buffer);
 
                 // Send the obtained bytes to the UI Activity
                 mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
                         .sendToTarget();
             } catch (IOException e) {
                 Log.e(TAG, "disconnected", e);
                 connectionLost();
                 break;
             }
         }
     }
 
     /**
      * Write to the connected OutStream.
      * @param buffer  The bytes to write
      */
     public void write(byte[] buffer) {
         try {
             mmOutStream.write(buffer);
 
             // Share the sent message back to the UI Activity
             mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer)
                     .sendToTarget();
         } catch (IOException e) {
             Log.e(TAG, "Exception during write", e);
         }
     }
 
     public void cancel() {
         try {
             mmSocket.close();
         } catch (IOException e) {
             Log.e(TAG, "close() of connect socket failed", e);
         }
     }
 }

是的!
有看到嗎?
ConnectedThread正在用BluetoothSocket取得InputStream和OutputStream,
並透過旗下的write()和read()在做2隻藍芽裝置的溝通!

現在剩下ConnectThread還沒有去理解了,
查了一下ConnectThread被start的時間是發生在一開始對話頁面的menu鍵中!

原來ConnectThread的目的是要主動連接其它已開啟藍芽的裝置。 
當使用者點擊Connect a device時,
會啟動ConnectThread,
開始尋找附近的藍芽裝置,
並對對方發出連線訊號,
對方監聽到你的配對要求後,
對方手機裡原本程式就開啟中的AcceptThread便答應你的請求,
然後開啟ConnectedThread, 
並利用連結成功後得到的BluetoothSocket和你做藍芽傳輸溝通。

主動連線端是Client端,被動接收端是Server端,
就好像精子與受精卵… 

三、總結
在這裡我把整個程式流程重覆敍述一次︰

在連線的一開始,兩隻手機的程式一開始都先建立一個AcceptThread
(因為誰都不知道誰最後會成為被動接收的Server端,誰又是主動的Client端),
然後都跟RFCOMM頻道索取這隻app專屬的BluetoothServerSocket實體。

Server方做了些什麼︰ 
用BluetoothServerSocket這個實體去等待Client端用ConnectThread發出的請求連線事件
連線若成功會得到這次藍芽溝通專用的BluetoothSocket。

Client方做了些什麼︰ 
Client端執行ConnectThread
1.Client端在與Server方連線(Connect a deivce)之前,
會先取得到Server端的身份證MAC address,
並用該address得到Server端的BluetoothDevice實體。
2.Client端藉由自己的MY_UUID和Server端的BluetoothDevice實體,
從RFCOMM頻道拿到這次藍芽溝通專用的BluetoothSocket。

兩方在這個時候都拿到這次藍芽溝通專用的BluetootheSocket, 
也都在此時知道了對方的BluetoothDevice實體(知道對方的身份)。
這時候雙方都同時開啟ConnectedThread, 
彼此利用BluetoothSocket互相做資料傳輸。

註︰資料傳輸利用 InputStream和OutputStream。 

轉載:小鰻的Android學習筆記




















其它文章

沒有留言:

張貼留言

標籤

Oracle (150) Oracle DB (144) Oracle_DB (143) Oracle SQL (135) JAVA (84) css-基本類 (65) MySQL (59) CSS Selector (58) jQuery (49) JavaScript-基本類 (39) Spring Boot (38) JavaScript (37) JavaScript HTML DOM (37) JavaScript-HTML_DOM (36) CSS3 (30) JAVA-基本類 (28) jQuery UI (27) Apache (23) Oracle GROUP BY (20) datepicker (20) Android (18) Oracle Date (17) c (17) JAVA-lang套件 (16) Linux (16) Oracle Sub Query (16) Spring-基本類 (16) jQuery-基本類 (16) MySQL-進階系列教學 (15) Android基本類 (14) Grails (14) Oracle join (14) SQLite (13) Spring (13) WIN7-基本類 (13) grails-基本類 (13) linux cent os (13) CKEditor (12) JAVA-流程控制類 (12) JAVA_Spring (12) PHP (11) Spring MVC (11) MySQL-基本系列教學 (10) Notepad (10) Notepad++ (10) SQLite for java (10) Windows (10) c/c++ (10) eclipse (9) jQuery-Selector (9) sqldeveloper (9) DB_Toad (8) JAVA_IDE_Eclipse (8) JavaScript-String類 (8) MySQL DB Toad (8) MySQL-DATE相關 (8) MySQL-函式相關 (8) Spring Bean (8) Android Studio (7) HTML5 (7) Hibernate (7) JAVA-OCWCD (7) JavaScript-陣列類 (7) Docker (6) JAVA-程式分享 (6) JAVA.util套件 (6) JavaScript-數學類 (6) MinGw (6) MySQL-其它類 (6) Servlet (6) centos (6) Apache_Tomcat (5) Apache套件_POI (5) CSS (5) JavaScript-Date物件 (5) JavaScript-其它類 (5) PostgreSQL (5) httpd (5) log4j (5) 基本資訊 (5) 開發工具 (5) CSS Properties (4) Dev-C++ (4) IntelliJ IDEA (4) Oracle DDL (4) Sublime (4) TortoiseSVN (4) apache_Maven (4) Android NDK (3) Eclipse IDE for C/C++ (3) Hibernate-基本類 (3) JAVA-問題 (3) JAVA-綀習分享 (3) JVM (3) Linux 指令 (3) Proxy Server (3) Spring Mobile (3) Spring web (3) Squid (3) VirtualBox (3) maven (3) zk (3) 生活其它 (3) Bootstrap (2) Filter (2) JAVA_IO (2) JAVA_其它_itext套件 (2) JBoss-問題 (2) JSP (2) Jboss (2) Listener (2) MySQL-語法快速查詢 (2) Spring AOP (2) Spring Batch (2) Spring Boot Actuator (2) Spring i18n (2) Subversive (2) Tomcat 8 (2) UML (2) WebJars (2) WinMerge (2) c++ (2) c語言綀習題 (2) jQuery Mobile (2) jQuery-事件處理 (2) jQuery-套件類 (2) putty (2) svn (2) weblogic (2) Apache_JMeter (1) Apache套件_BeanUtils (1) Apache套件_StringUtils (1) Base64 (1) Google API (1) HTML5-基本類 (1) Heap (1) JAVA 7 (1) JAVA SE 、JAVA EE、JAVA ME (1) JAVA 日期 (1) JAVA-OCJP (1) JAVA-WEB (1) JAVA_IDE (1) JAVA其它 (1) JBoss Server (1) JDK (1) JMX (1) JRE (1) Java RMI (1) Java String (1) Joda Time (1) Linux_其它 (1) MySQL教學 (1) Oracle_VirtualBox (1) SQL Server (1) SWT (1) Session (1) Stack (1) Struts 2 (1) Tool (1) ZK Studio (1) csv (1) grails-其它類 (1) jQuery-進階 (1) java mail (1) java web (1) java8 (1) jsoup (1) mockmvc (1) modules (1) tomcat (1) win10 (1) 其它類 (1) 圖片工具 (1) 模擬器 (1) 讀書分享 (1) 開發資訊 (1)

精選文章

初學 Java 的 HelloWorld 程式

撰寫一個JAVA程式 public class HelloWorld{ public static void main(String[ ] args){ System.out.println("我第一支Java程式!!"); } } ...