Разработка приложений на основе Bluetooth API (JSR82) – часть 2
Автор: Беломойцев Д.Е.
В предыдущей части было дано общее описание программных компонент, которые необходимо иметь в системе для разработки приложений с использованием интерфейса методов JSR82 (JABWT). Настоящая часть содержит более подробное рассмотрение клиент-серверной архитектуры соединения bluetooth, а также аспекты использования протокола OBEX для двунаправленной передачи данных в рамках подобных соединений.
javax.bluetooth
Пакет javax.bluetooth содержит ряд классов
- - DiscoveryAgent
- - LocalDevice
- - RemoteDevice
- - UUID
- - DeviceClass
- - DataElement
и интерфейсов
- - DiscoveryListener
- - ServiceRecord
- - L2CAPConnection
- - L2CAPConnectionNotifier
Класс DiscoveryAgent предоставляет возможности по осуществлению поиска доступных устройств bluetooth и опросу их на предмет поддерживаемых сервисов. Этим целям служат следующие методы:
- boolean startInquiry(int accessCode, DiscoveryListener listener) , начинает поиск окружающих bluetooth передатчиков в зависимости от значения аргумента accessCode (DiscoveryAgent.GIAC или DiscoveryAgent.LIAC).
- RemoteDevice[] retrieveDevices(int option) , возвращает список заранее предопределенных устройств (аргумент option – DiscoveryAgent.PREKNOWN) или список устройств, найденных в процессе предыдущего поиска (аргумент option – DiscoveryAgent.CACHED).
- int searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, DiscoveryListener discListener) , вызывается сцелью поиска сервисов на конкретном устройстве-сервере.
Интерфейс DiscoveryListener, реализуемый клиентским классом, содержит ряд методов, например,
- void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) , вызываемый в случае обнаружения устройства bluetooth процедурой startInquiry класса DiscoveryAgent.
- void servicesDiscovered(int transID, ServiceRecord[] servRecord) , вызываемый в случае обнаружения сервисов в процессе работы метода searchServices класса DiscoveryAgent.
javax.obex
В javax.obex есть классы
- - ServerRequestHandler
- - ResponseCodes
- - PasswordAuthentication
и интерфейсы
- - ClientSession
- - HeaderSet
- - Operation
- - SessionNotifier
- - Authenticator
Серверный класс наследуется от ServerRequestHandler, поэтому в конкретной реализации возможно потребуется перегрузка нескольких методов, например,
- int onConnect(HeaderSet request, HeaderSet reply) , вызывается при инициировании клиентом процесса соединения с сервером и получении запроса CONNECT.
- int onGet(Operation op) , вызывается при получении запроса GET от клиента.
- int onPut(Operation op) , вызывается при получении запроса PUT от клиента.
Интерфейс HeaderSet определяет методы и поля данных, которыми снабжена структура обмена информацией при передаче пакетов OBEX. С помощью метода
- void getHeader(int headerID, java.lang.Object headerValue)
можно устанавливать поля, а метод
- java.lang.Object getHeader(int headerID)
предназначен для получения значений этих полей.
Так можно установить имя – поле HeaderSet.NAME (объект java.lang.String – строка символов unicode) или HeaderSet.LENGTH (объект java.lang.Long).
Интерфейс Operation позволяет работать с операциями PUT или GET. Он наследуется от интерфейса javax.microedition.io.ContentConnection, поэтому обладает методами по работе с потоками данных.
Кроме этого методы
- HeaderSet getReceivedHeaders()
- void sendHeaders(HeaderSet headers)
Серверный модуль
При реализации серверного модуля важно учесть ряд аспектов.
1. Серверный класс наследуется от ServerRequestHandler. Поэтому требуется перегрузка методов обработки запросов onPut и onGet, если требуется реализовать собственный механизм, иначе на запросы PUT и GET клиента будет возвращаться код ответа, обозначающий нереализованность соответствующих методов.
public int onPut(Operation op) {
InputStream netStream = null;
try {
HeaderSet headers = op.getReceivedHeaders();
netStream = op.openInputStream();
int fileLength = Integer.parseInt(((Long) headers.getHeader(HeaderSet.LENGTH)).toString());
receiveBuffer = new byte[ fileLength ];
int read = 0, curWrite = 0;
byte[] buffer = new byte[fileLength];
while ((read = netStream.read(buffer)) != -1) {
System.arraycopy( buffer, 0, receiveBuffer, curWrite, read );
curWrite+=read;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try{
netStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
return ResponseCodes.OBEX_HTTP_ACCEPTED;
}
В данной реализации метода, получив от объекта Operation объект HeaderSet, уже от него можно получить данные об объеме передаваемой информации. Под этот объем выделяется два буфера, в первый из которых (локальный) информация попадает непосредственно из потока, а во второй (член класса) – переписывается из первого. Поток открывается на ввод от объекта Operation. Данные из него забираются до того момента, пока метод read вместо количества прочитанных из потока байт не вернет -1. Затем происходит закрытие потока.
public int onGet(Operation op)
{
try{
HeaderSet hs2send = createHeaderSet();
hs2send.setHeader(HeaderSet.LENGTH, new Long(sendBuffer.length) );
op.sendHeaders(hs2send);
}
catch( java.io.IOException e )
{
}
DataOutputStream netStream = null;
try{
netStream = op.openDataOutputStream();
netStream.write(sendBuffer);
netStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try{
netStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
return ResponseCodes.OBEX_HTTP_ACCEPTED;
}
Приведенная выше реализация метода onGet создает объект HeaderSet, в который заносится объем данный, которые клиент получит от сервера. Далее этот объект передается методу sendHeaders объекта Operation для отсылки клиенту с ближайшим сообщением OBEX. Затем от объекта Operation создается выходной поток данных, куда методом write записывается информация из буфера. По окончании передачи поток закрывается.
2. Экземпляр класса сервера должен будет поддерживать сервис, который в свою очередь будет иметь URL, по которому клиент сможет поднять соединение.
String serviceURL ="btgoep://localhost:00B0D00154EF;name=DB_BT_OBEXServer";
В этой строке набор символов до первого знака ':' обозначает протокол, по которому идет обмен данными в соединении. «btgoep» соответствует OBEX, «btspp» - протоколу SPP, «btl2cap» - протоколу L2CAP. Далее, после описания протокола, идет адрес устройства, на котором поднимается сервис, и строка-идентификатор сервиса.
3. В рамках данного обзора не рассматриваются вопросы организации многопоточного соединения с несколькими клиентами (в этом случае будет необходимо добавить код для синхронизации работы между различными потоками). Инициализация сервера заключается в регистрации сервиса в базе данных и переходе в режим ожидания соединения.
Регистрация выполняется методом open класса javax.microedition.io.Connector:
Connection clientConn = Connector.open(serviceURL);
Далее необходимо установить режим, в котором будет находиться bluetooth устройство сервера, чтобы оно могло быть обнаружено клиентами. Это делается вызовом метода setDiscoverable класса LocalDevice с соответствующим аргументом:
LocalDevice localDevice = LocalDevice.getLocalDevice();
localDevice.setDiscoverable(DiscoveryAgent.GIAC);
Здесь аргументом могут быть ряд значений, в т.ч. DiscoveryAgent.GIAC (устройство переходит в доступный для обнаружения режим на продолжительное время, без каких-либо условий), DiscoveryAgent.LIAC (устройство переходит в доступный для обнаружения режим на период, определяемый каким-либо условиями или событиями), DiscoveryAgent.NOT_DISCOVERABLE (устройство недоступно для обнаружения).
Затем, в зависимости от того, какой протокол был выбран при регистрации службы, устройство переводится в режим ожидания соединения методом acceptAndOpen:
if (clientConn instanceof SessionNotifier) {
SessionNotifier session = (SessionNotifier) clientConn;
try {
session.acceptAndOpen(this);
} catch (IOException e) {
System.err.println(e);
}
}
Теперь при соединении с клиентом сервер будет обрабатывать запросы типа CONNECT, GET, PUT и др.
4. Корректное завершение работы сервера предполагает закрытие всех потоков, а также вызов метода close для объекта Connection.
Клиентский модуль
В отношении клиентского модуля также необходимо соблюсти ряд положений.
1. Клиентский класс должен реализовывать интерфейс DiscoveryListener. Поэтому необходимо включить в него коды методов:
- public void deviceDiscovered(RemoteDevice device, DeviceClass code) , вызывается при обнаружении устройства.
- public void servicesDiscovered(int transaction, ServiceRecord[] services) , вызывается при обнаружении служб на удаленном сервере в процессе поиска.
- public void serviceSearchCompleted(int transID, int responseCode) , вызывается при завершении процесса поиска служб.
- public void inquiryCompleted(int discoveryType) , вызывается по завершении процесса поиска устройств.
Реализация данных методов целиком зависит от задач конкретной разработки, однако бывает целесообразно предусмотреть сохранить результаты обнаружения устройств или сервисов для последующего использования, или выполнять определенную последовательность действий при вызове обработчика события.
public void deviceDiscovered(RemoteDevice device, DeviceClass code) {
startServiceDiscovery(device);
}
При обработке события нахождения устройства для него можно автоматически вызывать процедуру поиска поддерживаемых служб.
public void servicesDiscovered(int transaction, ServiceRecord[] services) {
for (int ctr = 0; ctr < services.length; ctr++) {
Connection conn = null;
try {
conn = Connector.open(services[ctr].getConnectionURL(
ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false));
if (conn instanceof ClientSession) {
ClientSession session = (ClientSession) conn;
}
} catch (IOException e) {
}
}
}
Обработчик события нахождения службы в данном случае автоматически запускает процедуру соединения с поддерживающим ее сервером. Для этого из объекта ServiceRecord методом getConnectionURL получается URL, с помощью которого методом open класса Connector инициируется процедура установления соединения.
Процедура поиска служб заключается в вызове метода searchServices класса DiscoveryAgent. При этом в качестве аргумента передается объект UUID, содержащий идентификатор сервиса.
UUID thatService = new UUID("00B0D00154EF ",false);
private void startServiceDiscovery(RemoteDevice device) {
UUID[] services = new UUID[1];
int[] args = null;
services[0] = thatService;
DiscoveryAgent agent = localDevice.getDiscoveryAgent();
try {
int transaction = agent.searchServices(args, services, device, this);
serviceSearchTransactions.put(new Integer(transaction), device);
} catch (BluetoothStateException e) {
}
}
2. Обмен данными в соединении осуществляется по инициативе клиента.
Для передачи на сервер необходимо создать объект типа HeaderSet, установить в нем поле HeaderSet.LENGTH, чтобы на стороне сервера было возможно проконтролировать объем полученной информации. Далее он передается объекту типа ClientSession, который был получен при установлении соединения методом open класса Connector, через метод put. Поток на передачу DataOutputStream открывается соответствующим методом интерфейса Operation. Вызовом метода write данные, которые передаются ему в качестве аргумента, направляются в поток. По окончании передачи поток и операция закрываются.
public void sendData( byte[] data ) {
DataOutputStream netStream = null;
Operation op = null;
try {
HeaderSet getHeader = ((ClientSession)connServer).createHeaderSet();
getHeader.setHeader(HeaderSet.LENGTH,new Long(data.length));
op = ((ClientSession)connServer).put(getHeader);
netStream = op.openDataOutputStream();
netStream.write(data);
netStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try{
netStream.close();
op.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
Для приема данных с сервера необходимо отправить объект типа HeaderSet методом get интерфейса ClientSession. От полученного объекта Operation методом getReceivedHeaders получается объект типа HeaderSet, содержащий ответ сервера с информацией о передаваемых данных. Затем открывается поток на прием, данные читаются методом read, пока он не вернет код -1 вместо количества принятых байт. По окончании передачи поток и операция закрываются.
public void receiveData( byte[] data )
{
InputStream netStream = null;
try {
HeaderSet getHeader = ((ClientSession)connServer).createHeaderSet();
Operation op = ((ClientSession)connServer).get(getHeader);
HeaderSet headers = op.getReceivedHeaders();
netStream = op.openInputStream();
int fileLength = Integer.parseInt(((Long) headers.getHeader(HeaderSet.LENGTH)).toString());
receiveBuffer = new byte[ fileLength ];
int read = 0;
byte[] buffer = new byte[fileLength];
int curWrite = 0;
while ((read = netStream.read(buffer)) != -1) {
System.arraycopy( buffer, 0, receiveBuffer, curWrite, read );
curWrite+=read;
}
op.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try{
netStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
data = receiveBuffer;
}
Заключение
В данной статье были рассмотрены вопросы использования протокола OBEX и классов пакетов javax.bluetooth и javax.obex для разработки приложений с использованием интерфейса JSR82. Перечислены наиболее часто используемые методы и параметры, приведены фрагменты программного кода с их использованием.
В следующей части планируется рассмотреть вопросы обеспечения безопасности соединений, а также усовершенствовать уже рассмотренные примеры для работы с несколькими серверами и потоками.