Android: Сетевые коммуникации с помощью Nearby (PlayServices API)

Совсем недавно Google предоставила мобильным разработчикам Android новую технологию сетевого обмена данными — Nearby. Мне она стала сразу интересна, так как позволяет устанавливать локальное соединение между Android устройствами без особых заморочек! Нет нужды заставлять пользователя вводить IP адрес и порт, он просто инициирует соединение, а клиенты к нему просто подключаются. На странице описывающей технологию указаны следующие варианты использования: — многопользовательские игры на индивидуальных экранах — игроки играют в сетевые игры каждый со своего устройства, которые объединены в сеть (классика жанра); — многопользовательские игры на общем экране — в данном случае в качестве сервера может выступать GoogleTV, на нём будет происходить основной игровой процесс, а все подключившиеся будут использовать свой телефон/планшет в качестве игрового контроллера (как на фото!); — и конечно для любого обмена данными между различными Android устройствами.bab28366163942e79b89e2da4c5a6286.pngУже сейчас вы можете пропробовать эту технологию в игре Beach Buggy Racing:[embedded content]После того как основной материал статьи был подготовлен, мне стало интересно на сколько хорошо система контролирует очерёдность доставляемых пакетов. Специально для этих целей я подготовил маленькое приложение для пересылки фотографий в виде текста. С одного устройства на другое пересылались десятки тысяч пакетов по 2048 символов каждый. Очерёдность не была нарушена, ни одного пакета не утеряно. За контроль очерёдности доставки пришлось заплатить временем доставки, оно увеличилось.

Рассмотрим принципы работы с Nearby.Дабы не создавать велосипед я взял оригинальный пример и рассмотрел его с переводом всех комментариев.Прежде всего удостоверьтесь что на вашем телефоне имеется последняя версия сервисов GooglePlay — https://play.google.com/store/apps/details? id=com.google.android.gms.Теперь перейдём к основным моментам проекта: В проект добавлена библиотека PlayServices (в файл «build.gradle»), именно она позволяет работать с Nearby:

dependencies { compile fileTree (dir: 'libs', include: ['*.jar']) compile 'com.android.support: appcompat-v7:22.0.0' compile 'com.google.android.gms: play-services:7.0.0' } Работу с Nearby можно разделить на следующие этапы:1) Создание главного объекта доступа — GoogleApiClient. Запуск клиента. Остановка клиента2) Запуск рекламации намерения стать точкой доступа3) Запуск поиска точек для соединения4) Присоединение к точке5) Обработка заявок на присоединение6) Контроль соединения7) Принятие и обработка сообщений от оппонента8) Отправка сообщенияРассмотрим всё по порядку.Создание главного объекта доступа — GoogleApiClient. Запуск клиента. Остановка клиента. Тут всё просто. В конструкторе активности создаём главный объект доступа к Nearby. При старте активности запускаем его, при остановке активности отключаемся от сети.

@Override protected void onCreate (Bundle savedInstanceState) { … mGoogleApiClient = new GoogleApiClient.Builder (this) .addConnectionCallbacks (this) .addOnConnectionFailedListener (this) .addApi (Nearby.CONNECTIONS_API) .build (); … } @Override public void onStart () { super.onStart (); Log.d (TAG, «onStart»); mGoogleApiClient.connect (); } @Override public void onStop () { super.onStop (); Log.d (TAG, «onStop»); if (mGoogleApiClient!= null) { mGoogleApiClient.disconnect (); } } Следующий этап — Запуск рекламации намерения стать точкой доступа, метод startAdvertising: private void startAdvertising () { debugLog («startAdvertising»); if (! isConnectedToNetwork ()) { debugLog («startAdvertising: not connected to WiFi network.»); return; } // Выделяем идентификатор приложения для активации возможности другим устройствам подключиться к данному. List appIdentifierList = new ArrayList<>(); appIdentifierList.add (new AppIdentifier (getPackageName ())); AppMetadata appMetadata = new AppMetadata (appIdentifierList); // Рекламация соединений. Запуск службы управления соединениями. При подключении нового устройства, произойдёт определение идентификатора устройства в понятном виде, например «LGE Nexus 5» String name = null; Nearby.Connections.startAdvertising (mGoogleApiClient, name, appMetadata, TIMEOUT_ADVERTISE, this).setResultCallback (new ResultCallback() { @Override public void onResult (Connections.StartAdvertisingResult result) { Log.d (TAG, «startAdvertising: onResult:» + result); if (result.getStatus ().isSuccess ()) { debugLog («startAdvertising: onResult: SUCCESS»); updateViewVisibility (STATE_ADVERTISING); } else { debugLog («startAdvertising: onResult: FAILURE »); // Если пользователь будет нажимать кнопку 'Advertise' несколько раз за таймаут, будет появляться сообщение 'STATUS_ALREADY_ADVERTISING' int statusCode = result.getStatus ().getStatusCode (); if (statusCode == ConnectionsStatusCodes.STATUS_ALREADY_ADVERTISING) { debugLog («STATUS_ALREADY_ADVERTISING»); } else { updateViewVisibility (STATE_READY); } } } }); } Если пользователь будет беспрестанно «жмахать» по кнопке «Advertise», он получит сообщение что мол всё работает нормально, расслабся :) — STATUS_ALREADY_ADVERTISINGТретий этап — Запуск поиска точек для соединения:

private void startDiscovery () { debugLog («startDiscovery»); if (! isConnectedToNetwork ()) { debugLog («startDiscovery: not connected to WiFi network.»); return; } // Поиск устройств с запущенным сервисом рекламации Nearby соединений по идентификатору приложения. String serviceId = getString (R.string.service_id); Nearby.Connections.startDiscovery (mGoogleApiClient, serviceId, TIMEOUT_DISCOVER, this) .setResultCallback (new ResultCallback() { @Override public void onResult (Status status) { if (status.isSuccess ()) { debugLog («startDiscovery: onResult: SUCCESS»); updateViewVisibility (STATE_DISCOVERING); } else { debugLog («startDiscovery: onResult: FAILURE»); // Если пользователь будет нажимать кнопку 'Discover' несколько раз за таймаут, то будет появляться сообщение 'STATUS_ALREADY_DISCOVERING' int statusCode = status.getStatusCode (); if (statusCode == ConnectionsStatusCodes.STATUS_ALREADY_DISCOVERING) { debugLog («STATUS_ALREADY_DISCOVERING»); } else { updateViewVisibility (STATE_READY); } } } }); } Всё очень прозрачно и понятно. Просто запуск поиска точек доступа.Теперь рассмотрим — Присоединение к точке обмена данными. Для этого сначала необходимо найти доступные точки доступа, а затем присоединяться к нужной. Метод onEndpointFound специально создан для того, чтобы сообщать о новой найденной точке:

@Override public void onEndpointFound (final String endpointId, String deviceId, String serviceId, final String endpointName) { Log.d (TAG, «onEndpointFound:» + endpointId + »:» + endpointName); // Найдены точки для подключения. Отображаем диалог для пользователя, с выбором конечных устройств для подключения. if (mMyListDialog == null) { // Configure the AlertDialog that the MyListDialog wraps AlertDialog.Builder builder = new AlertDialog.Builder (this) .setTitle («Endpoint (s) Found») .setCancelable (true) .setNegativeButton («Cancel», new DialogInterface.OnClickListener () { @Override public void onClick (DialogInterface dialog, int which) { mMyListDialog.dismiss (); } }); // Создание слушателя для диалога mMyListDialog = new MyListDialog (this, builder, new DialogInterface.OnClickListener () { @Override public void onClick (DialogInterface dialog, int which) { String selectedEndpointName = mMyListDialog.getItemKey (which); String selectedEndpointId = mMyListDialog.getItemValue (which); MainActivity.this.connectTo (selectedEndpointId, selectedEndpointName); mMyListDialog.dismiss (); } }); } mMyListDialog.addItem (endpointName, endpointId); mMyListDialog.show (); } В методе «connectTo» реализован диалог выбора точки к которой возможно подключиться. При выборе одного из варианта переходим к непосредственному подключению: /** * Отправка запроса на подключение к конечному устройству. * @param endpointId — идентификатор устройства к которому необходимо подключиться * @param endpointName — название конечной точки, к которой осуществляется подключение. Параметр используется для оповещения о статусе подключения. * */ private void connectTo (String endpointId, final String endpointName) { debugLog («connectTo:» + endpointId + »:» + endpointName); // Отправка запроса на подключение к удалённому устройству. String myName = null; byte[] myPayload = null; Nearby.Connections.sendConnectionRequest (mGoogleApiClient, myName, endpointId, myPayload, new Connections.ConnectionResponseCallback () { @Override public void onConnectionResponse (String endpointId, Status status, byte[] bytes) { Log.d (TAG, «onConnectionResponse:» + endpointId + »:» + status); if (status.isSuccess ()) { debugLog («onConnectionResponse:» + endpointName + » SUCCESS»); Toast.makeText (MainActivity.this, «Connected to » + endpointName, Toast.LENGTH_SHORT).show (); mOtherEndpointId = endpointId; updateViewVisibility (STATE_CONNECTED); } else { debugLog («onConnectionResponse:» + endpointName + » FAILURE»); } } }, this); } Если всё прошло успешно, то можно начинать обмен сообщениями.Для обработки заявок на присоединение предназначен метод onConnectionRequest:

@Override public void onConnectionRequest (final String endpointId, String deviceId, String endpointName, byte[] payload) { debugLog («onConnectionRequest:» + endpointId + »:» + endpointName); // Данное устройство является рекламирующим и оно получило запрос на подключение. Показываем диалоговое окно предлагающее пользователю принять заявку на подключение или отклонить запрос. mConnectionRequestDialog = new AlertDialog.Builder (this) .setTitle («Connection Request») .setMessage («Do you want to connect to » + endpointName + »?») .setCancelable (false) .setPositiveButton («Connect», new DialogInterface.OnClickListener () { @Override public void onClick (DialogInterface dialog, int which) { byte[] payload = null; Nearby.Connections.acceptConnectionRequest (mGoogleApiClient, endpointId, payload, MainActivity.this) .setResultCallback (new ResultCallback() { @Override public void onResult (Status status) { if (status.isSuccess ()) { debugLog («acceptConnectionRequest: SUCCESS»); mOtherEndpointId = endpointId; updateViewVisibility (STATE_CONNECTED); } else { debugLog («acceptConnectionRequest: FAILURE»); } } }); } }) .setNegativeButton («No», new DialogInterface.OnClickListener () { @Override public void onClick (DialogInterface dialog, int which) { Nearby.Connections.rejectConnectionRequest (mGoogleApiClient, endpointId); } }).create (); mConnectionRequestDialog.show (); } За контроль соединения отвечают ряд методов: onDisconnected — обработка разрыва связи; onConnected — обработка подключения; onEndpointLost — обработка разрыва связи; onConnectionSuspended — обработка прерывание соединения; onConnectionFailed — обработка неудачного соединения.Контроль за переподключением клиентов (например при разрыве связи при выходе пользователя из зоны действия WiFi) полностью ложится на разработчика.Для обработки приходящих сообщений необходимо переписать метод onMessageReceived:

@Override public void onMessageReceived (String endpointId, byte[] payload, boolean isReliable) { // Сообщение, полученное от подключённой точки debugLog («onMessageReceived:» + endpointId + »:» + new String (payload)); } Отправка сообщений осуществляется с помощью двух методов:1) Nearby.Connections.sendReliableMessage — отправка надёжных сообщений;2) Nearby.Connections.sendUnreliableMessage — отправка ненадёжных сообщений.При использовании первого метода, система сама контролирует правильность очерёдности доставляемых сообщений, во втором случае последовательность может нарушиться, так как контроля никакого нет. Зато второй метод быстрее, поэтому его лучше использовать когда требуется отправлять большое количество сообщений, например при отправке положения курсора на экране.В ресурсах необходимо указать идентификатор сервиса по которому будет происходить поиск и подключения клиентов.

Для разрешения рекламации приложения в манифесте необходимо прописать следующее: Если вы соберёте это приложение и запустите его на своих устройствах то сможете наблюдать следующее:[embedded content]При первом взгляде может показаться что использование API Nearby сложно и громоздко, но это только на первый взгляд. В итоге разработчик получает готовый, надёжный, контролируемый инструмент для сетевого обмена данными. Лично мне это решение очень понравилось, не надо больше контролировать очередность прихода пакетов с данными, просить пользователей ввести ip адрес и номер сокета, производить дополнительные настройки… Красота! Исходники с комментариямиОтдельно APK

Спасибо за помощь в подготовке материала inatale!

© Habrahabr.ru