浅谈蓝牙通信流程——(新手上路)

    话不多说我们直接步入正题,下面是一个思维导图:

            首先我们先与另外两种通信方案进行一下对比:


配对流程:

1. 注册适配器打开蓝牙

2.搜索附近设备和已配对设备

3. 选择未配对设备进行配对

4.选择已配对设备进行连接通信


通信工作流程:                                                     

1.服务端先建立一个服务端套接字Socket,然后该套接字开始监听客户端的连接。                        

2 .客户端也建立一个socket,然后向服务端发起连接,这时候如果没有异常就算两个设备连接成功了。  

3.这时候客户端和服务端都会持有一个Socket,利用该Socket可以发送和接收消息。


注册适配器并打开蓝牙

<!-- 蓝牙通讯权限 -->

<uses-permission android:name="android.permission.BLUETOOTH" />//一些配置连接蓝牙的权限

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />//进行操作的权限

<!-- 6.0以上需要的权限 -->

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

在onCreate()方法里,获取蓝牙适配器

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

BluetoothAdapter这个类常用的方法有:

getDefaultAdapter: 获取本地的蓝牙适配器

enable(); 打开蓝牙(不带提示框 但是手机有自带的。。。)

disable(); 关闭蓝牙

isEnabled(): 本地蓝牙是否打开

getAddress(); 获取自己的蓝牙MAC地址

cancelDiscovery() 停止扫描

isDiscovering() 是否正在处于扫描过程中

getState():获取本地蓝牙适配器的状态

getScanMode(): 获取本地蓝牙适配器的当前蓝牙扫描模式。

详细用法表

接下来为了保险起见先判断设备是否支持蓝牙:

if(mBluetoothAdapter == null){

        Toast.makeText(this,"本地蓝牙不可用",Toast.LENGTH_SHORT).show();

        finish(); //退出应用

}

确认支持蓝牙的话就可以调用蓝牙适配器的方法了:

String Address = bluetoothAdapter.getAddress(); //获取本机蓝牙MAC地址

String Name = bluetoothAdapter.getName(); //获取本机蓝牙名称

// 若蓝牙没打开

if(!bluetoothAdapter.isEnabled()){

        bluetoothAdapter.enable(); //打开蓝牙

}

打开蓝牙的两种方式:

第一种打开方法:调用enable

第二种打开方法,调用系统API去打开蓝牙

mBluetoothAdapter.enable();

//不会自动提示用户默认打开 有的手机还是会提示的

Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);

startActivityForResult(intent, REQUEST_OPEN_BT_CODE); //CODE值只是标记可以更改

//会以Dialog样式显示一个Activity , 我们可以在onActivityResult()方法去处理返回值


搜索附近设备和已配对设备

设置可以被搜索到

//设置可以被搜索到

//判断蓝牙适配器的当前蓝牙扫描模式

 if(mBluetoothAdapter.getScanMode()!=BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE){

        Intent discoverableIntent=new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);

        // 设置被发现时间,最大值是3600秒,0表示设备总是可以被发现的(小于0或者大于3600则会被自动设置为120秒)

        discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120);

        startActivity(discoverableIntent);

}

接下来开始搜索附近:

判断是否正在搜索如果是就停止搜索之类的

if (!mBluetoothAdapter.isDiscovering()) {//判断是否正在搜索

        mBluetoothAdapter.startDiscovery();//开始搜索附近设备

        textView2.setText("正在搜索...");

} else {

    mBluetoothAdapter.cancelDiscovery();//停止搜索

}

搜索附近需要先注册个广播来监听查询附近蓝牙设备:

蓝牙扫描时,扫描到任一远程蓝牙设备时,会发送此广播。两种广播用一个就行

静态注册就是在AndroidManifest.xml文件中定义,

注册的广播接收器必须继承BroadReceiver 动态注册就是在程序中使用Context.registerReceiver注册。

动态广播

//注册广播

IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);//搜索到设备

registerReceiver(receiver, filter);

IntentFilter filter2 = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//搜索结束

registerReceiver(receiver, filter2);

静态广播

<!-- 广播接收 -->

<receiver android:name="包名.类名" >

<intent-filter android:priority="1000">

<action android:name="android.bluetooth.adapter.action.DISCOVERY_FINISHED"/>

<action android:name="android.bluetooth.device.action.FOUND" />

</intent-filter>

</receiver>

new个BroadcastReceiver来接收:

广播一旦发送这边就会调用onReceive()方法

BroadcastReceiver receiver = new BroadcastReceiver() {

        @Override

        public void onReceive(Context context, Intent intent) {

                String action = intent.getAction();

                if (action.equals(BluetoothDevice.ACTION_FOUND)) {

                //获取已配对蓝牙设备

                        Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();

                        for (BluetoothDevice bonddevice : devices) {

                                mPeiDuiList.add(bonddevice);

                                peiDuiAdapter.notifyDataSetChanged();

                        }

                        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

                        //判断未配对的设备

                        if (device.getBondState() != BluetoothDevice.BOND_BONDED) {

                                mFuJinList.add(device);

                                fuJinAdapter.notifyDataSetChanged();

                         }

                } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {

                        textView2.setText("搜索完成...");

                }

        }

};

我这边是创建了两个集合分别把已配对的和附近的储存进去

private ArrayList<BluetoothDevice> mPeiDuiList = new ArrayList<>();

private ArrayList<BluetoothDevice> mFuJinList = new ArrayList<>();


选择未配对设备进行配对

这里用到了蓝牙设备BluetoothDevice

BluetoothDevice用于指代某个蓝牙设备,通常表示对方设备

getName:获得该设备的名称; 

getAddress:获得该设备的地址; 

createRfcommSocketToServiceRecord:根据UUID创建并返回一个BluetoothSocket。

1 配对

两个设备建立连接以后,就可以进行一个配对的过程。

配对进行的时候,会产生一个密钥用来加密与鉴别连接。一个典型的情况是,从机设备会要求主机设备提供一个密码来完成一个配对过程。

这个密码可以是固定的例如“000000”,也可以是提供给上层的随机产生的一个值。当主机设备发送一个正确的密码是,这两个设备就会相互交换密钥来加密并鉴别连接。

2 绑定

很多情况下,两个设备会需要经常性的断开连接,连接这样的过程,BLE有一个安全功能,允许两台设备配对的时候给对方一个长期的一套安全密钥。

这种机制称作为绑定。允许两个设备再次连接时不需要再次进行配对就能从新加密与鉴别,只要他们存储了这个安全密钥。

//点击附近的开始配对

mBluetoothAdapter.cancelDiscovery();//停止搜索

String address = mFuJinList.get(position).getAddress();

Toast.makeText(MainActivity.this, mFuJinList.get(position).getName() + "配对中。。。", Toast.LENGTH_SHORT).show();

try {

        //调用工具类与设备配对

        //已知自己的地址 点击获取对方的地址 配对

        remoteDevice = mBluetoothAdapter.getRemoteDevice(address);//通过mac地址获取蓝牙设备

        ClsUtils.createBond(remoteDevice.getClass(), remoteDevice);

} catch (Exception e) {

e.printStackTrace();

}

这里用到了一个配对工具类ClsUtils

package com.example.lin.mylanya.utils;

import java.lang.reflect.Field;

import java.lang.reflect.Method;

import android.bluetooth.BluetoothDevice;

import android.util.Log;

public class ClsUtils {

/**

* 与设备配对 参考源码:platform/packages/apps/Settings.git

* /Settings/src/com/android/settings/bluetooth/CachedBluetoothDevice.java

*/

static public boolean createBond(Class btClass,BluetoothDevice btDevice) throws Exception {

        Method createBondMethod = btClass.getMethod("createBond");

        Boolean returnValue = (Boolean) createBondMethod.invoke(btDevice);

        return returnValue.booleanValue();

}

/**

* 与设备解除配对 参考源码:platform/packages/apps/Settings.git

* /Settings/src/com/android/settings/bluetooth/CachedBluetoothDevice.java

*/

        static public boolean removeBond(Class btClass,BluetoothDevice btDevice) throws Exception {

                Method removeBondMethod = btClass.getMethod("removeBond");

                Boolean returnValue = (Boolean) removeBondMethod.invoke(btDevice);

                return returnValue.booleanValue();

        }

}

 

选择已配对设备进行连接通信

通信需要创建客户端和服务器来建立连接

服务器一般是在打开蓝牙后创建这里的ChatController是通信的工具类 这个方法里面调用了服务器线程(AccepThread)的启动方法

if (!mBluetoothAdapter.isEnabled()) {//判断蓝牙是否打开

        mBluetoothAdapter.enable();//打开蓝牙

}

ChatController.getInstance().waitingForFriends(mBluetoothAdapter, handler);//等待客户端来连接

 服务器端建立套接字,等待客户端连接,

调用BluetoothAdapter的listenUsingRfcommWithServiceRecord方法,产生一个BluetoothServerSocket对象,

然后调用BluetoothServerSocket对象的accept方法,

注意accept方法会产生阻塞,直到一个客户端连接建立,所以服务器端的socket的建立需要在一个子线程中去做,

AccepThread 

public class AccepThread extends Thread {

        /** 连接的名称*/

        private static final String NAME = "BluetoothClass";

        /** UUID*/

        private static final UUID MY_UUID = UUID.fromString(Constant.CONNECTTION_UUID);

        /** 服务端蓝牙Sokcet*/

        private final BluetoothServerSocket mmServerSocket;

        private final BluetoothAdapter mBluetoothAdapter;

        /** 线程中通信的更新UI的Handler*/

        private final Handler mHandler;

        /** 监听到有客户端连接,新建一个线程单独处理,不然在此线程中会堵塞*/

        private ConnectedThread mConnectedThread;

        public AccepThread(BluetoothAdapter adapter, Handler handler) throws IOException {

                mBluetoothAdapter = adapter;

                this.mHandler = handler;

                // 获取服务端蓝牙socket;

                mmServerSocket = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);

          }

@Override

public void run() {

        super.run();

        // 连接的客户端soacket

        BluetoothSocket socket = null;

        // 服务端是不退出的,要一直监听连接进来的客户端,所以是死循环

        while (true){

                try {

                        // 获取连接的客户端socket

                        socket = mmServerSocket.accept();

                } catch (IOException e) {

                       // 通知主线程更新UI, 获取异常

                        mHandler.sendEmptyMessage(Constant.MSG_ERROR);

                        e.printStackTrace();

                        // 服务端退出一直监听线程

                            break;

                }

        if(socket != null) {

                        // 管理连接的客户端socket

                        manageConnectSocket(socket);

        }

    }

}

/**

* 管理连接的客户端socket

* @param socket

*/

private void manageConnectSocket(BluetoothSocket socket) {

        // 只支持同时处理一个连接

        // mConnectedThread不为空,踢掉之前的客户端

        if(mConnectedThread != null) {

                    mConnectedThread.cancle();

        }

        // 新建一个线程,处理客户端发来的数据

        mConnectedThread = new ConnectedThread(socket, mHandler);

        mConnectedThread.start();

}

/**

* 断开服务端,结束监听

*/

public void cancle() {

        try {

                mmServerSocket.close();

                mHandler.sendEmptyMessage(Constant.MSG_FINISH_LISTENING);

        } catch (IOException e) {

                e.printStackTrace();

        }

}

/**

* 发送数据

* @param data

*/

        public void sendData(byte[] data){

                if(mConnectedThread != null) {

                mConnectedThread.write(data);

        }

    }

}

蓝牙客户端套接字BluetoothSocket

BluetoothSocket是客户端的Socket,用于与对方设备进行数据通信。

connect:建立蓝牙的socket连接; 

close:关闭蓝牙的socket连接; 

getInputStream:获取socket连接的输入流对象; 

getOutputStream:获取socket连接的输出流对象; 

getRemoteDevice:获取远程设备信息。

服务器和客户端连接上后都需要新建一个线程进行通信不然会线程阻塞

发送数据需要调用BluetoothSocket的getOutputStream(),接收数据需要调用getInputStream()方法

String uuid = java.util.UUID.randomUUID().toString();

一般在创建Socket时需要UUID作为端口的唯一性,如果两台Android设备互联,则没有什么特殊的,

如果让非Android的蓝牙设备连接Android蓝牙设备,则UUID必须使用某个固定保留的UUID

Android中创建UUID:UUID  uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")

 ConnectedThread

public class ConnectedThread extends Thread{

        /** 当前连接的客户端BluetoothSocket*/

        private final BluetoothSocket mmSokcet;

        /** 读取数据流*/

        private final InputStream mmInputStream;

        /** 发送数据流*/

        private final OutputStream mmOutputStream;

        /** 与主线程通信Handler*/

        private Handler mHandler;

        private String TAG = "ConnectedThread";

        public ConnectedThread(BluetoothSocket socket,Handler handler) {

        mmSokcet = socket;

        mHandler = handler;

        InputStream tmpIn = null;

        OutputStream tmpOut = null;

        try {

                tmpIn = socket.getInputStream();

                tmpOut = socket.getOutputStream();

        } catch (IOException e) {

                e.printStackTrace();

        }

        mmInputStream = tmpIn;

        mmOutputStream = tmpOut;

}

@Override

public void run() {

        super.run();

        byte[] buffer = new byte[1024];

        while (true) {

                try {

                        // 读取数据

                        int bytes = mmInputStream.read(buffer);

                        if(bytes > 0) {

                                        String data = new String(buffer,0,bytes,"utf-8");

                                        // 把数据发送到主线程, 此处还可以用广播

                                        Message message = mHandler.obtainMessage(Constant.MSG_GOT_DATA,data);

                                        mHandler.sendMessage(message);

                        }

                        Log.d(TAG, "messge size :" + bytes);

                } catch (IOException e) {

                        e.printStackTrace();

                }

            }

}

// 踢掉当前客户端

public void cancle() {

        try {

                mmSokcet.close();

        } catch (IOException e) {

                e.printStackTrace();

        }

}

/**

* 服务端发送数据

* @param data

*/

public void write(byte[] data) {

        try {

                mmOutputStream.write(data);

        } catch (IOException e) {

                e.printStackTrace();

        }

    }

}

ChatController

public class ChatController {

        /**

        * 客户端的线程

        */

        private ConnectThread mConnectThread;

        /**

        * 服务端的线程

        */

          private AccepThread mAccepThread;

          private ChatProtocol mProtocol = new ChatProtocol();

        /**

        * 网络协议的处理函数

        */

        private class ChatProtocol {

                private static final String CHARSET_NAME = "utf-8";

                /**

                * 封包(发送数据)

                * 把发送的数据变成 数组 2进制流

                */

                public byte[] encodePackge(String data) {

                           if (data == null) {

                                    return new byte[0];

                            } else {

                                    try {

                                            return data.getBytes(CHARSET_NAME);

                                    } catch (UnsupportedEncodingException e) {

                                            e.printStackTrace();

                                            return new byte[0];

                                    }

                  }

}

/**

* 解包(接收处理数据)

* 把网络上数据变成自己想要的数据体

*/

public String decodePackage(byte[] netData) {

            if (netData == null) {

                return "";

            } else {

                        try {

                                 return new String(netData, CHARSET_NAME);

                        } catch (UnsupportedEncodingException e) {

                                e.printStackTrace();

                               return "";

                        }

           }

        }

}

/**

* 与服务器连接进行聊天

*/

public void startChatWith(BluetoothDevice device, BluetoothAdapter adapter, Handler handler) {

        mConnectThread = new ConnectThread(device, adapter, handler);

        mConnectThread.start();

}

/**

* 等待客户端来连接

* handler : 用来跟主线程通信,更新UI用的

*/

public void waitingForFriends(BluetoothAdapter adapter, Handler handler) {

    try {

                mAccepThread = new AccepThread(adapter, handler);

                mAccepThread.start();

        } catch (IOException e) {

                e.printStackTrace();

        }

}

/**

* 发出消息

*/

public void sendMessage(String msg) {

        // 封包

        byte[] data = mProtocol.encodePackge(msg);

        if (mConnectThread != null) {

                mConnectThread.sendData(data);

        } else if (mAccepThread != null) {

                mAccepThread.sendData(data);

        }

}


/**

* 网络数据解码

*/

public String decodeMessage(byte[] data){

        return mProtocol.decodePackage(data);

}

/**

* 停止聊天

*/

public void stopChart(){

        if(mConnectThread != null) {

                mConnectThread.cancle();

        }

        if(mAccepThread != null) {

                mAccepThread.cancle();

        }

}

/**

* 以下是单例写法

*/

        private static class ChatControlHolder{

                private static ChatController mInstance = new ChatController();

        }

        public static ChatController getInstance(){

                return ChatControlHolder.mInstance;

        }

}


发送数据

//客户端向服务器发送数据

String s = mEdit.getText().toString();

//发出消息

ChatController.getInstance().sendMessage(s);


//handler接收数据

private Handler handler = new Handler() {

@Override

public void handleMessage(Message msg) {

    super.handleMessage(msg);

        switch (msg.what) {

                    case MSG_GOT_DATA:

                    //接收消息

                    String data = (String) msg.obj;

                    Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();

                    break;

       }

    }

};


最后销毁:

@Override

protected void onDestroy() {

        super.onDestroy();

        unregisterReceiver(receiver);//关闭服务

        mBluetoothAdapter.disable();//关闭蓝牙

        ChatController.getInstance().stopChart();//停止聊天

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容