Чтобы установить соединение между двумя устройствами, необходимо реализовать механизмы как на стороне сервера, так и на стороне клиента, поскольку одно устройство должно открыть серверный сокет, а другое – инициировать соединение, используя MAC-адрес серверного устройства. Серверное и клиентское устройства получают необходимый BluetoothSocket разными способами. Сервер получает информацию о сокете, когда принимает входящее подключение. Клиент предоставляет информацию о сокете, когда открывает канал RFCOMM к серверу.
Сервер и клиент считаются подключенными друг к другу, когда у каждого из них есть подключенный BluetoothSocket на одном и том же канале RFCOMM. На этом этапе каждое устройство может получать входные и выходные потоки, и может начаться передача данных, которая обсуждается в разделе о передаче данных Bluetooth. В этом разделе описывается, как инициировать соединение между двумя устройствами.
Прежде чем искать устройства Bluetooth, убедитесь, что у вас есть необходимые разрешения Bluetooth и вы настроили приложение для работы с Bluetooth.
Техники установления контакта
Один из способов реализации – автоматически подготовить каждое устройство в качестве сервера, чтобы на каждом устройстве был открыт серверный сокет, ожидающий подключений. В этом случае любое устройство может инициировать подключение к другому и стать клиентом. Другой вариант – одно устройство может явно разместить подключение и открыть серверный сокет по запросу, а другое устройство инициирует подключение.
Рисунок 1. Диалоговое окно подключения по Bluetooth.
Подключиться как сервер
Чтобы подключить два устройства, одно из них должно действовать как сервер, удерживая открытый сокет BluetoothServerSocket.
Серверный сокет прослушивает входящие запросы на подключение и предоставляет подключенный объект BluetoothSocket после принятия запроса. После того как BluetoothSocket будет получен от BluetoothServerSocket, BluetoothServerSocket можно и нужно удалить, если вы не хотите, чтобы устройство принимало больше подключений.
Чтобы настроить серверный сокет и принять подключение, выполните следующие действия:
Чтобы получить
BluetoothServerSocket, позвоните по номеруlistenUsingRfcommWithServiceRecord(String, UUID).Строка – это идентифицируемое название вашего сервиса, которое система автоматически записывает в новую запись базы данных протокола обнаружения сервисов (SDP) на устройстве. Название может быть произвольным, например совпадать с названием приложения. Универсальный уникальный идентификатор (UUID) также включен в запись SDP и формирует основу для соглашения о подключении с клиентским устройством. То есть, когда клиент пытается подключиться к этому устройству, он несет UUID, который однозначно идентифицирует службу, с которой он хочет подключиться. Чтобы подключение было принято, эти UUID должны совпадать.
UUID – это стандартизированный 128-битный формат строкового идентификатора, который используется для уникальной идентификации информации. UUID используется для идентификации информации, которая должна быть уникальной в системе или сети, поскольку вероятность повторения UUID фактически равна нулю. Она генерируется независимо, без использования централизованного органа. В данном случае он используется для уникальной идентификации Bluetooth-сервиса вашего приложения. Чтобы получить UUID для приложения, можно воспользоваться одним из многочисленных генераторов случайных
UUIDв интернете, а затем инициализировать UUID с помощьюfromString(String).Начните прослушивание запросов на подключение, вызвав
accept().Это блокирующий вызов. Возвращает значение, когда подключение принято или возникло исключение. Подключение принимается, только когда удаленное устройство отправляет запрос на подключение, содержащий UUID, который совпадает с UUID, зарегистрированным на этом прослушивающем серверном сокете. В случае успеха
accept()возвращает подключенныйBluetoothSocket.Если вы не хотите принимать дополнительные подключения, вызовите метод
close().Этот вызов метода освобождает серверный сокет и все его ресурсы, но не закрывает подключенный объект
BluetoothSocket, возвращенный методомaccept(). В отличие от TCP/IP, RFCOMM позволяет подключать только одного клиента на канал за раз, поэтому в большинстве случаев имеет смысл вызыватьclose()дляBluetoothServerSocketсразу после принятия подключенного сокета.
Поскольку вызов accept() является блокирующим, не выполняйте его в основном потоке UI. Выполнение в другом потоке гарантирует, что ваше приложение сможет реагировать на другие действия пользователя. Обычно все действия, связанные с BluetoothServerSocket или BluetoothSocket, выполняются в новом потоке, управляемом приложением. Чтобы прервать заблокированный вызов, например accept(), вызовите close() для BluetoothServerSocket или 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(), относящийся к приложению, предназначен для инициации потока передачи данных, о котором рассказывается в статье о передаче данных по Bluetooth.
Обычно BluetoothServerSocket следует закрывать, как только вы закончите прослушивание входящих подключений. В этом примере close() вызывается сразу после получения BluetoothSocket. Вы также можете предоставить в своем потоке общедоступный метод, который может закрывать частный BluetoothSocket в случае, если вам нужно прекратить прослушивание на этом серверном сокете.
Подключиться как клиент
Чтобы установить соединение с удаленным устройством, которое принимает подключения через открытый серверный сокет, сначала нужно получить объект BluetoothDevice, представляющий это устройство. Чтобы узнать, как создать BluetoothDevice, прочитайте статью Как найти устройства Bluetooth. Затем вам нужно использовать BluetoothDevice, чтобы получить BluetoothSocket и установить подключение.
Ниже описана основная процедура.
Используя
BluetoothDevice, получитеBluetoothSocket, вызвавcreateRfcommSocketToServiceRecord(UUID).Этот метод инициализирует объект
BluetoothSocket, который позволяет клиенту подключиться кBluetoothDevice. Переданный здесь UUID должен совпадать с UUID, который использовался серверным устройством при вызовеlistenUsingRfcommWithServiceRecord(String, UUID)для открытияBluetoothServerSocket. Чтобы использовать совпадающий UUID, задайте строку UUID в коде приложения, а затем укажите ссылку на нее в коде сервера и клиента.Чтобы начать подключение, вызовите
connect(). Обратите внимание, что этот метод является блокирующим вызовом.После того как клиент вызывает этот метод, система выполняет поиск SDP, чтобы найти удаленное устройство с соответствующим UUID. Если поиск выполнен успешно и удаленное устройство принимает подключение, оно предоставляет канал RFCOMM для использования во время подключения, и метод
connect()возвращает результат. Если подключение не удалось или время ожидания методаconnect()истекло (примерно через 12 секунд), метод выдает ошибкуIOException.
Поскольку connect() является блокирующим вызовом, процедуру подключения всегда следует выполнять в потоке, отдельном от основного потока активности (пользовательского интерфейса).
Пример
Ниже приведен простой пример клиентского потока, который инициирует подключение по Bluetooth:
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() вызывается до попытки подключения. Всегда вызывайте cancelDiscovery() перед connect(), особенно потому, что cancelDiscovery() выполняется успешно независимо от того, выполняется ли в данный момент обнаружение устройств. Если приложению нужно определить, выполняется ли поиск устройств, используйте метод isDiscovering().
Метод manageMyConnectedSocket(), относящийся к приложению, предназначен для инициации потока передачи данных, о котором рассказывается в разделе Передача данных по Bluetooth.
Когда вы закончите с BluetoothSocket, всегда звоните close(). Это приведет к немедленному закрытию подключенного сокета и освобождению всех связанных внутренних ресурсов.