前言:Android 藍牙通信,通過BluetoothSocket方式建立長連接并傳輸文本或文件。前段時間有個項目的功能需求是:AR眼鏡通過藍牙的方式連接北斗設備,當北斗設備收到文本/語音/圖片消息時轉發(fā)到AR眼鏡上,AR眼鏡也可以發(fā)送文本/語音/圖片數(shù)據到北斗設備上并轉發(fā)到指定的目標地址。剛開始在百度和github找了許多方法都不盡人意而且大多數(shù)據傳輸都僅僅停留在文字方面,不過好在最后臨近項目deadline時想到了一種傻瓜也簡單的方法實現(xiàn)了這個需求。如果你也恰好遇到了這種 "通過藍牙或其他低效率的方式傳輸文件" 類似的情景可以參考這篇文章,希望這篇文章對你的思路有所啟發(fā),如果有錯漏或可優(yōu)化之處也歡迎提醒。
一、上代碼
DEMO:https://github.com/LXTTTTTT/Android-Bluetooth-Chat-And-Transfer
源碼資源:https://download.csdn.net/download/lxt1292352578/88677601
直接復制就能使用
package com.example.sockettransfer;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Environment;
import android.util.Log;
import org.greenrobot.eventbus.EventBus;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// 藍牙 Socket 工具
public class BluetoothSocketUtil {
private static String TAG = "BluetoothSocketUtil";
private Context context;
private BluetoothAdapter bluetoothAdapter;
private BluetoothSocket bluetoothSocket;
public BluetoothDevice nowDevice;
private InputStream inputStream;
private OutputStream outputStream;
private Set<BluetoothDevice> pairedDeviceList;
private List<BluetoothDevice> devices = new ArrayList();
private ReceiveDataThread receiveDataThread;
private ListenThread listenThread;
private final UUID MY_UUID = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
private int state = 0;
private final int STATE_DISCONNECT = 0;
private final int STATE_CONNECTING = 1;
private final int STATE_CONNECTED = 2;
public boolean isConnectedDevice = false;
public boolean isSendFile = false;
// 單例 ----------------------------------------------------------------
private static BluetoothSocketUtil bluetoothSocketUtil;
public static BluetoothSocketUtil getInstance() {
if (bluetoothSocketUtil == null) {
bluetoothSocketUtil = new BluetoothSocketUtil();
}
return bluetoothSocketUtil;
}
public BluetoothSocketUtil() {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
public void init(Context context){
this.context = context;
registerBroadcast();
listen(); // 開啟設備連接監(jiān)聽
}
public Set<BluetoothDevice> getPairedDeviceList(){
if(bluetoothAdapter!=null){
return bluetoothAdapter.getBondedDevices();
}else {
return null;
}
}
public void searchDevice(){
if(bluetoothAdapter==null){return;}
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
devices.clear();
bluetoothAdapter.startDiscovery();
}
public void stopSearch() {
if (bluetoothAdapter != null && bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
}
public void registerBroadcast() {
IntentFilter filter = new IntentFilter();
filter.addAction("android.bluetooth.device.action.FOUND");
filter.addAction("android.bluetooth.adapter.action.DISCOVERY_FINISHED");
filter.addAction("android.bluetooth.device.action.ACL_CONNECTED");
filter.addAction("android.bluetooth.device.action.ACL_DISCONNECTED");
context.registerReceiver(receiver, filter);
Log.e(TAG, "廣播注冊成功");
}
// 藍牙連接監(jiān)聽廣播
private final BroadcastReceiver receiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.e(TAG, "收到廣播: "+action);
if ("android.bluetooth.device.action.FOUND".equals(action)) {
BluetoothDevice device = (BluetoothDevice) intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
if (device.getName() == null) {return;}
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
if (!devices.contains(device)) {
devices.add(device);
if(onBluetoothSocketWork!=null){onBluetoothSocketWork.onDiscoverNewDevice(devices);}
}
}
}
}
};
public void listen(){
if(state!=STATE_DISCONNECT){return;}
if(listenThread!=null){
listenThread.cancel();
listenThread = null;
}
listenThread = new ListenThread();
listenThread.start();
}
private class ListenThread extends Thread{
private BluetoothServerSocket bluetoothServerSocket;
private boolean listen = false;
public ListenThread(){
try {
bluetoothServerSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord("name", MY_UUID);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
listen = true;
Log.e(TAG, "開啟設備連接監(jiān)聽"+listen+"/"+(state==STATE_DISCONNECT) );
while (listen && state==STATE_DISCONNECT){
try {
bluetoothSocket = bluetoothServerSocket.accept();
} catch (Exception e) {
e.printStackTrace();
}
if (bluetoothSocket != null) {
try {
Log.e(TAG, "監(jiān)聽到設備連接" );
state = STATE_CONNECTING;
if(onBluetoothSocketWork!=null){onBluetoothSocketWork.onConnecting();}
inputStream = bluetoothSocket.getInputStream();
outputStream = bluetoothSocket.getOutputStream();
state = STATE_CONNECTED;
isConnectedDevice = true;
nowDevice = bluetoothSocket.getRemoteDevice();
receiveDataThread = new ReceiveDataThread();
receiveDataThread.start(); // 開啟讀數(shù)據線程
if(onBluetoothSocketWork!=null){onBluetoothSocketWork.onConnected(nowDevice.getName());}
EventMsg msg = new EventMsg();
msg.msgType = EventMsg.CONNECT_DEVICE;
msg.content = nowDevice.getName();
EventBus.getDefault().post(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public void cancel(){
listen = false;
try {
if(bluetoothServerSocket!=null){
bluetoothServerSocket.close();
bluetoothServerSocket=null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public synchronized void connect(BluetoothDevice device) {
Log.e(TAG, "連接設備: " + device.getName()+"/"+state);
if (state == STATE_CONNECTING || state == STATE_CONNECTED) {return;}
new Thread(new Runnable() {
@Override
public void run() {
try {
bluetoothSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
state = STATE_CONNECTING;
if(onBluetoothSocketWork!=null){onBluetoothSocketWork.onConnecting();}
bluetoothSocket.connect();
inputStream = bluetoothSocket.getInputStream();
outputStream = bluetoothSocket.getOutputStream();
state = STATE_CONNECTED;
isConnectedDevice = true;
nowDevice = device;
receiveDataThread = new ReceiveDataThread();
receiveDataThread.start(); // 開啟讀數(shù)據線程
if(onBluetoothSocketWork!=null){onBluetoothSocketWork.onConnected(device.getName());}
EventMsg msg = new EventMsg();
msg.msgType = EventMsg.CONNECT_DEVICE;
msg.content = device.getName();
EventBus.getDefault().post(msg);
}catch(Exception e){
e.printStackTrace();
disconnect();
}
}
}).start();
}
private byte[] readBuffer = new byte[1024];
private class ReceiveDataThread extends Thread{
private boolean receive = false;
byte[] buffer = new byte[1024];
@Override
public void run() {
if(inputStream==null){return;}
receive = true;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while (receive){
try{
int size = inputStream.read(buffer);
if(size>0){
baos.write(buffer, 0, size);
readBuffer = baos.toByteArray();
receiveData(readBuffer);
baos.reset();
}else if(size==-1){
Log.e(TAG, "BluetoothSocket: 斷開了");
cancel();
disconnect();
break;
}
}catch (Exception e){
e.printStackTrace();
// 斷開連接了,通常 inputStream.read 時觸發(fā)這個
Log.e(TAG, "BluetoothSocket: 讀取數(shù)據錯誤");
cancel();
disconnect();
EventMsg msg = new EventMsg();
msg.msgType = EventMsg.DISCONNECT_DEVICE;
EventBus.getDefault().post(msg);
}
}
}
public void cancel(){
receive = false;
}
}
/**
* 自定義一個標識頭來描述發(fā)送的數(shù)據
* 格式:$*x*$0000xxxx
* 前五位 "$*x*$" 中的x為可變數(shù)字,表示發(fā)送數(shù)據的類型,我這里用到的是 "1"-文本,"2"-圖片,根據實際需求自定義
* 后八位 "0000xxxx" 為發(fā)送數(shù)據內容的長度,格式為固定8位的16進制數(shù)據,不足8位則高位補0,最多可以表示 0xFFFFFFFF 個字節(jié),如果發(fā)送的文件超出了這個范圍則需要自行修改
* 例子:
* 發(fā)送文本數(shù)據 "測試" 打包標識 "$*1*$" + 將"測試"以GB18030標準轉化為byte[]后的長度(hex) —— "$*1*$00000004",后續(xù)發(fā)送轉化后的byte[]
* 發(fā)送圖片數(shù)據 打包標識 "$*2*$" + 讀取指定路徑的文件byte[]的長度(hex) —— "$*2*$0033CE27",后續(xù)發(fā)送讀取到的文件byte[]
**/
public void send_text(String data_str){
if(outputStream==null){return;}
if(isSendFile){return;}
// 建議使用線程池
new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] data_bytes = data_str.getBytes("GB18030");
String head = "$*1*$"+String.format("%08X", data_bytes.length);
Log.e(TAG, "發(fā)送文本,打包標識頭: "+head );
outputStream.write(head.getBytes(StandardCharsets.UTF_8));
outputStream.write(data_bytes);
EventMsg msg = new EventMsg();
msg.msgType = EventMsg.SEND_TEXT;
msg.content = data_str;
EventBus.getDefault().post(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
public void send_file(String path){
if(outputStream==null){return;}
if(isSendFile){return;}
new Thread(new Runnable() {
@Override
public void run() {
File file = new File(path);
if (!file.exists() || !file.isFile()) {
Log.e(TAG, "文件不存在");
return;
}else {
Log.e(TAG, "開始發(fā)送文件");
isSendFile = true;
}
byte[] file_byte = fileToBytes(path);
try {
Thread.sleep(100);
String head;
head = "$*2*$"+String.format("%08X", file_byte.length);
Log.e(TAG, "發(fā)送文件,打包標識頭: "+head );
outputStream.write(head.getBytes(StandardCharsets.UTF_8));
outputStream.write(file_byte);
isSendFile = false;
EventMsg msg = new EventMsg();
msg.msgType = EventMsg.SEND_FILE;
msg.content = path;
EventBus.getDefault().post(msg);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "文件發(fā)送失敗", e);
isSendFile = false;
}
}
}).start();
}
private boolean startReceiveFile = false; // 是否開始接收文件數(shù)據
private ByteArrayOutputStream file_bytes_baos = new ByteArrayOutputStream();
private long file_length = 0; // 文件數(shù)據長度
private int message_type = 0; // 消息類型:0-初始狀態(tài) 1-文本 2-圖片
private void receiveData(byte[] data_bytes) {
Log.e(TAG, "處理數(shù)據長度: "+data_bytes.length );
// 還沒收到標識頭,如果一直沒有收到就舍棄直到收到標識頭為止
if(!startReceiveFile){
try{
// 首先判斷收到的數(shù)據是否包含了標識頭
String data_str = new String(data_bytes,StandardCharsets.UTF_8);
int head_index = data_str.indexOf("$*");
// Pattern pattern = Pattern.compile("\\$\\*\\d\\*\\$");
// Matcher matcher = pattern.matcher(data_str);
// 有頭
if(head_index>=0){
startReceiveFile = true;
String head = data_str.substring(head_index,head_index+13); // $*1*$00339433
String msg_type = head.substring(0,5); // $*1*$
if(msg_type.contains("1")){message_type = 1;} else {message_type = 2;}
String length_hex = head.substring(5); // 00339433
file_length = Long.parseLong(length_hex,16); // 解析文件數(shù)據長度
Log.e(TAG, "解析標識頭 head: "+head+" 文件數(shù)據長度:"+file_length);
file_bytes_baos.write(data_bytes,13,data_bytes.length-13); // 存儲標識以外的文件數(shù)據
// 如果文本數(shù)據的話則只有一波,這時要判斷收到的數(shù)據總長度是否文件數(shù)據長度+標識頭數(shù)據長度
if(data_bytes.length==file_length+13){
parseData();
}
}else {
Log.e(TAG, "receiveData: 沒有頭"+data_str );
}
}catch (Exception e){
e.printStackTrace();
}
}
// 后續(xù)的都是文件數(shù)據
else {
try {
file_bytes_baos.write(data_bytes); // 保存文件數(shù)據
Log.e(TAG, "總長度: "+file_length+" /已接收長度: "+file_bytes_baos.size());
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "文件數(shù)據保存失敗");
}
// 每次接收完數(shù)據判斷一下存儲的文件數(shù)據達到數(shù)據長度了嗎
if(file_bytes_baos.size()>=file_length){
parseData();
}
}
}
public void parseData(){
if(message_type==0){return;}
if(message_type==1){
String content = "";
try {
content = new String(file_bytes_baos.toByteArray(),"GB18030"); // 文本消息直接轉碼
Log.e(TAG, "數(shù)據接收完畢,文本:"+content);
} catch (Exception e) {
e.printStackTrace();
}
// 初始化狀態(tài)
startReceiveFile = false;
file_bytes_baos.reset();
file_length = 0;
message_type = 0;
EventMsg msg = new EventMsg();
msg.msgType = EventMsg.RECEIVE_TEXT;
msg.content = content;
EventBus.getDefault().post(msg);
}else if(message_type==2){
Log.e(TAG, "數(shù)據接收完畢,圖片" );
// 保存圖片數(shù)據
new Thread(new Runnable() {
@Override
public void run() {
try {
// 默認保存在系統(tǒng)的 Download 目錄下,自行處理
String imgFilePath= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/receiveImage.jpg";
File imageFile = new File(imgFilePath);
try (FileOutputStream fos = new FileOutputStream(imageFile)) {
fos.write(file_bytes_baos.toByteArray());
}
// 初始化狀態(tài)
startReceiveFile = false;
file_bytes_baos.reset();
file_length = 0;
message_type = 0;
EventMsg msg = new EventMsg();
msg.msgType = EventMsg.RECEIVE_FILE;
msg.content = imgFilePath;
EventBus.getDefault().post(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
// 讀取文件數(shù)據
public static byte[] fileToBytes(String filePath){
File file = new File(filePath);
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024*1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
return bos.toByteArray();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
public void disconnect(){
try {
if(inputStream!=null){
inputStream.close();
inputStream=null;
}
if(outputStream!=null){
outputStream.close();
outputStream=null;
}
if(receiveDataThread!=null){
receiveDataThread.cancel();
receiveDataThread = null;
}
if(bluetoothSocket!=null){
bluetoothSocket.close();
bluetoothSocket = null;
}
if(onBluetoothSocketWork!=null){onBluetoothSocketWork.onDisconnect();}
state = STATE_DISCONNECT;
isConnectedDevice = false;
nowDevice = null;
listen(); // 斷開后重新開啟設備連接監(jiān)聽
} catch (Exception e) {
e.printStackTrace();
}
}
public void destroy(){
disconnect();
if(listenThread!=null){listenThread.cancel();listenThread=null;}
if(context!=null){context.unregisterReceiver(receiver);}
}
// 接口 ---------------------------------------------
public interface OnBluetoothSocketWork{
void onConnecting();
void onConnected(String device_name);
void onDisconnect();
void onDiscoverNewDevice(List<BluetoothDevice> devices);
}
public OnBluetoothSocketWork onBluetoothSocketWork;
public void setOnBluetoothSocketWork(OnBluetoothSocketWork onBluetoothSocketWork){
this.onBluetoothSocketWork = onBluetoothSocketWork;
}
}
二、連接流程說明
1. 連接說明
在 BluetoothSocket 通信中主動發(fā)起連接的一方作為客戶端,被動監(jiān)聽及接受連接的一方作為服務端。他們在連接時需要保證連接/監(jiān)聽過程中設置的 UUID 一致,并且需要先完成系統(tǒng)的配對操作,這里的配對操作有兩種方式:
一種是先在系統(tǒng)的藍牙頁面手動配對,然后在APP里面獲取已配對藍牙并直接連接
第二種則是在APP里面掃描藍牙設備并在首次連接時完成配對操作,但需要注意的一點是設備的藍牙默認是無法被發(fā)現(xiàn)的因此作為接收方的服務端在連接之前需要開啟藍牙可被觀測
new Intent(android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
在完成配對并且 UUID 一致的情況下直接連接即可
2. 客戶端連接
在進行連接之前先要獲取到藍牙設備,獲取藍牙設備的方式有兩種,一種是直接獲取系統(tǒng)的已配對設備
BluetoothAdapter.getDefaultAdapter().getBondedDevices();
另一種則是通過開啟藍牙掃描并注冊廣播監(jiān)聽來獲取掃描到的設備,這種方式則需要照應上文中提到的首次連接配對
BluetoothAdapter.getDefaultAdapter().startDiscovery();
獲取目標設備之后設置UUID創(chuàng)建 BluetoothSocket ,連接并取得對應的輸入/輸出流,同時開啟子線程監(jiān)聽輸入流的數(shù)據輸出情況,在這里當這個讀取數(shù)據線程的輸入流斷開時可視作Socket長連接的斷開。創(chuàng)建BluetoothSocket的方法有兩個 createRfcommSocketToServiceRecord 和?createInsecureRfcommSocketToServiceRecord 他們的主要區(qū)別在于連接的安全性,這里僅以安全連接作為例子
BluetoothSocket bluetoothSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
bluetoothSocket.connect();
InputStream inputStream = bluetoothSocket.getInputStream();
OutputStream outputStream = bluetoothSocket.getOutputStream();
new Thread(new Runnable() {
@Override
public void run() {
byte[] readBuffer = new byte[1024];
byte[] buffer = new byte[1024];
if(inputStream==null){return;}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
state = STATE_CONNECTED;
while (true){
try{
int size = inputStream.read(buffer);
if(size>0){
baos.write(buffer, 0, size);
readBuffer = baos.toByteArray();
receiveData(readBuffer); // 解析數(shù)據
baos.reset();
}else if(size==-1){
Log.e(TAG, "BluetoothSocket: 斷開了");
break;
}
}catch (Exception e){
e.printStackTrace();
// 斷開連接了,通常 inputStream.read 時觸發(fā)這個
Log.e(TAG, "BluetoothSocket: 讀取數(shù)據錯誤");
state = STATE_DISCONNECT;
}
}
}
}).start();
3. 服務端監(jiān)聽
服務端作為接受方不需要主動獲取藍牙設備,只需要開啟監(jiān)聽線程等待連接即可
private BluetoothServerSocket bluetoothServerSocket;
private BluetoothSocket bluetoothSocket;
private InputStream inputStream;
private OutputStream outputStream;
private ListenThread listenThread;
public void listen(){
if(state!=STATE_DISCONNECT){return;}
if(listenThread!=null){
listenThread.cancel();
listenThread = null;
}
listenThread = new ListenThread();
listenThread.start();
}
private class ListenThread extends Thread{
private boolean listen = false;
public ListenThread(){
try {
bluetoothServerSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord("name", MY_UUID);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
listen = true;
Log.e(TAG, "開啟設備連接監(jiān)聽"+listen+"/"+(state==STATE_DISCONNECT) );
while (listen && state==STATE_DISCONNECT){
try {
if(bluetoothSocket==null){
bluetoothSocket = bluetoothServerSocket.accept();
}
} catch (Exception e) {
e.printStackTrace();
}
if (bluetoothSocket != null) {
try {
Log.e(TAG, "監(jiān)聽到設備連接" );
state = STATE_CONNECTING;
inputStream = bluetoothSocket.getInputStream();
outputStream = bluetoothSocket.getOutputStream();
new Thread(new Runnable() {
@Override
public void run() {
byte[] readBuffer = new byte[1024];
byte[] buffer = new byte[1024];
if(inputStream==null){return;}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
state = STATE_CONNECTED;
while (true){
try{
int size = inputStream.read(buffer);
if(size>0){
baos.write(buffer, 0, size);
readBuffer = baos.toByteArray();
receiveData(readBuffer);
baos.reset();
}else if(size==-1){
Log.e(TAG, "BluetoothSocket: 斷開了");
break;
}
}catch (Exception e){
e.printStackTrace();
// 斷開連接了,通常 inputStream.read 時觸發(fā)這個
Log.e(TAG, "BluetoothSocket: 讀取數(shù)據錯誤");
state = STATE_DISCONNECT;
}
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public void cancel(){
listen = false;
try {
if (bluetoothServerSocket != null) {
bluetoothServerSocket.close();
bluetoothServerSocket = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、數(shù)據傳輸
1. 基本思路思路
其實整個連接流程非常簡單,相對復雜的地方僅僅在于對流程的把控,這一點可以參考我的DEMO或者按照實際的需求自行調整。
本文的核心點在于數(shù)據傳輸功能,以往在項目中對數(shù)據傳輸?shù)男枨髢H僅停留在文本數(shù)據的傳輸因此實現(xiàn)起來非常簡單,只需要在發(fā)送時添加標識符并按照特定標準編碼在接收時識別到標識符后轉碼并解析內容就好了。而本次的項目需求除了需要傳輸文本外還需要傳輸音頻格式和圖片格式的文件,按照以往的方法無法實現(xiàn)因此就到網上找了許多方法但是卻發(fā)現(xiàn)大多都不盡人意,最后換了一種思路想到了一種比較簡單的方法,經過驗證也恰好可以滿足本次需求。
總體思路很簡單:傳輸?shù)臄?shù)據類型有三種,并且每次接收/處理數(shù)據之前要知道本次有效數(shù)據的長度和傳輸?shù)臄?shù)據類型是什么,再根據對應的類型對后續(xù)的特定長度的數(shù)據進行不同的處理。因此一個簡單標識頭就定義出來了,我只要在每次發(fā)送數(shù)據之前先封裝一個標識頭再發(fā)送后續(xù)的有效數(shù)據,而在接收時則根據這個標識頭來判斷數(shù)據類型和長度作不同處理即可
我定義的標識頭格式很簡單:"$*x*$"(x為數(shù)據類型:1-文本、2-圖片、3-語音)+"固定8位長度的有效數(shù)據字節(jié)長度(16進制),不足8位則高位補0",以UTF-8標準轉碼
例如:
"$*1*$00000008********":具有8個字節(jié)長度的文本數(shù)據,將標識頭后8位數(shù)據"********"以GB18030標準轉化為字符串即可得到傳輸?shù)奈谋緝热?br> "$*2*$00078A00***...":具有494080個字節(jié)長度的圖片數(shù)據,將標識頭后494080位數(shù)據以jpg格式保存即可的得到傳輸內容
注意:這里定義的長度標識固定8位,最多能夠表示0xFFFFFFFF個字節(jié),按實際需求自行修改即可
2. 發(fā)送文本
發(fā)送文本數(shù)據之前先將文本內容以GB18030標準轉為byte[],封裝文本標識"$*1*$",將前面的文本byte[]長度轉化為16進制并在高位補0封裝長度標識,將封裝好的標識頭以UTF-8標準轉碼并發(fā)送然后再發(fā)送實際的文本數(shù)據即可
public void send_text(String data_str){
if(outputStream==null){return;}
if(isSendFile){return;}
// 建議使用線程池
new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] data_bytes = data_str.getBytes("GB18030");
String head = "$*1*$"+String.format("%08X", data_bytes.length);
Log.e(TAG, "發(fā)送文本,打包標識頭: "+head );
outputStream.write(head.getBytes(StandardCharsets.UTF_8));
outputStream.write(data_bytes);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
3. 發(fā)送文件(以圖片為例)
發(fā)送文件數(shù)據之前先按照文件路徑讀取文件數(shù)據,封裝文件標識"$*2*$",將前面的文件數(shù)據長度轉化為16進制并在高位補0封裝長度標識,將封裝好的標識頭以UTF-8標準轉碼并發(fā)送然后再發(fā)送實際的文件數(shù)據即可
public void send_file(String path){
if(outputStream==null){return;}
if(isSendFile){return;}
new Thread(new Runnable() {
@Override
public void run() {
File file = new File(path);
if (!file.exists() || !file.isFile()) {
Log.e(TAG, "文件不存在");
return;
}else {
Log.e(TAG, "開始發(fā)送文件");
isSendFile = true;
}
byte[] file_byte = fileToBytes(path);
try {
Thread.sleep(100);
String head;
head = "$*2*$"+String.format("%08X", file_byte.length);
Log.e(TAG, "發(fā)送文件,打包標識頭: "+head );
outputStream.write(head.getBytes(StandardCharsets.UTF_8));
outputStream.write(file_byte);
isSendFile = false;
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "文件發(fā)送失敗", e);
isSendFile = false;
}
}
}).start();
}
讀取文件數(shù)據方法
public static byte[] fileToBytes(String filePath){
File file = new File(filePath);
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024*1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
return bos.toByteArray();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
4. 解析數(shù)據
當數(shù)據長度超出最大單次傳輸數(shù)據長度限制時就會以分包的形式進行傳輸,而文本數(shù)據的長度大多都超出了這個限制,因此在接收文件數(shù)據時往往是按照多個分包數(shù)據的形式來處理。因而我們在每次處理新數(shù)據時的第一件事就是判斷本輪數(shù)據是首條包含了標識頭的數(shù)據還是后續(xù)的文件數(shù)據。首先初始化一個開始接收文件數(shù)據的標識變量,每次接收到新數(shù)據時將本輪數(shù)據以UTF-8標準轉化為字符并判斷是否包含了前面定義的標識頭,如果有則表示開始接收新一輪的文件數(shù)據,將標識修改為true,當標識為true時則表示本輪收到的數(shù)據是文件數(shù)據,直接存儲到字節(jié)數(shù)組緩存中,直到存儲的長度達到了解析出來的有效數(shù)據長度才表示本輪文件數(shù)據接收完畢并修改標識為false等待下一個標識頭的到來
private boolean startReceiveFile = false; // 是否開始接收文件數(shù)據
private ByteArrayOutputStream file_bytes_baos = new ByteArrayOutputStream();
private long file_length = 0; // 文件數(shù)據長度
private int message_type = 0; // 消息類型:0-初始狀態(tài) 1-文本 2-圖片
private void receiveData(byte[] data_bytes) {
Log.e(TAG, "處理數(shù)據長度: "+data_bytes.length );
// 還沒收到標識頭,如果一直沒有收到就舍棄直到收到標識頭為止
if(!startReceiveFile){
try{
// 首先判斷收到的數(shù)據是否包含了標識頭
String data_str = new String(data_bytes,StandardCharsets.UTF_8);
int head_index = data_str.indexOf("$*");
// Pattern pattern = Pattern.compile("\\$\\*\\d\\*\\$");
// Matcher matcher = pattern.matcher(data_str);
// 有頭
if(head_index>=0){
startReceiveFile = true;
String head = data_str.substring(head_index,head_index+13); // $*1*$00339433
String msg_type = head.substring(0,5); // $*1*$
if(msg_type.contains("1")){message_type = 1;} else {message_type = 2;}
String length_hex = head.substring(5); // 00339433
file_length = Long.parseLong(length_hex,16); // 解析文件數(shù)據長度
Log.e(TAG, "解析標識頭 head: "+head+" 文件數(shù)據長度:"+file_length);
file_bytes_baos.write(data_bytes,13,data_bytes.length-13); // 存儲標識以外的文件數(shù)據
// 如果是文本數(shù)據的話則只有一波,這時要判斷收到的數(shù)據總長度是否文件數(shù)據長度+標識頭數(shù)據長度
if(data_bytes.length==file_length+13){
parseData(); // 處理數(shù)據
}
}else {
Log.e(TAG, "receiveData: 沒有頭"+data_str );
}
}catch (Exception e){
e.printStackTrace();
}
}
// 后續(xù)的都是文件數(shù)據
else {
try {
file_bytes_baos.write(data_bytes); // 保存文件數(shù)據
Log.e(TAG, "總長度: "+file_length+" /已接收長度: "+file_bytes_baos.size());
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "文件數(shù)據保存失敗");
}
// 每次接收完數(shù)據判斷一下存儲的文件數(shù)據達到數(shù)據長度了嗎
if(file_bytes_baos.size()>=file_length){
parseData(); // 處理數(shù)據
}
}
}
處理數(shù)據 :既然已經知道數(shù)據的類型并且拿到了他的原始數(shù)據那處理起來就很簡單了,如果是文本的話直接將數(shù)據按照GB18030標準轉碼,如果是文件的話直接將數(shù)據按照特定的格式存儲到指定路徑就行了
public void parseData(){
if(message_type==0){return;}
if(message_type==1){
String content = "";
try {
content = new String(file_bytes_baos.toByteArray(),"GB18030"); // 文本消息直接轉碼
Log.e(TAG, "數(shù)據接收完畢,文本:"+content);
} catch (Exception e) {
e.printStackTrace();
}
// 初始化狀態(tài)
startReceiveFile = false;
file_bytes_baos.reset();
file_length = 0;
message_type = 0;
}else if(message_type==2){
Log.e(TAG, "數(shù)據接收完畢,圖片" );
// 保存圖片數(shù)據
new Thread(new Runnable() {
@Override
public void run() {
try {
// 默認保存在系統(tǒng)的 Download 目錄下,自行處理
String imgFilePath=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/receiveImage.jpg";
File imageFile = new File(imgFilePath);
try (FileOutputStream fos = new FileOutputStream(imageFile)) {
fos.write(file_bytes_baos.toByteArray());
}
// 初始化狀態(tài)
startReceiveFile = false;
file_bytes_baos.reset();
file_length = 0;
message_type = 0;
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
四、使用例子
客戶端開啟藍牙可被發(fā)現(xiàn)
服務端掃描/連接目標設備
首次連接需要進行配對操作
連接成功發(fā)送文本消息
發(fā)送圖片
五、小結
總體流程很簡單,相對復雜的部分在于文件數(shù)據的傳輸,但只要找對了思路實現(xiàn)起來也并不難,在這里對連接和發(fā)送數(shù)據的流程做一個小結
1. 連接
服務端(被連接方)開啟藍牙可被偵測并不斷監(jiān)聽指定UUID的客戶端連接操作 → 客戶端(連接方)掃描并連接目標設備?→ 如果雙方未配對則進行配對操作 → 連接成功各自獲取輸入/輸出流 → 開啟子線程監(jiān)聽輸入流的數(shù)據輸出/通過輸出流寫入數(shù)據
2. 通信
發(fā)送方:將需要發(fā)送的文本/文件轉化為byte[] → 將消息類型和數(shù)據長度封裝成特定格式的標識頭 → 將標識頭轉化為byte[]并發(fā)送 → 發(fā)送文本/文件數(shù)據
接收方:收到數(shù)據后轉化為文本判斷是否包含標識頭 → 解析標識頭得到數(shù)據類型和有效數(shù)據長度 → 如果當前已接收的數(shù)據長度未達到有效數(shù)據長度則繼續(xù)接收 → 如果長度達到則根據消息類型處理數(shù)據文章來源:http://www.zghlxwxcb.cn/news/detail-851080.html
3. DEMO
github:https://github.com/LXTTTTTT/Android-Bluetooth-Chat-And-Transfer
DEMO資源:https://download.csdn.net/download/lxt1292352578/88677601文章來源地址http://www.zghlxwxcb.cn/news/detail-851080.html
完
到了這里,關于Android 藍牙通信(通過 BluetoothSocket 傳輸文件/文本)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!