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

в 6:04, , рубрики: android, google play services, mobile development, nearby, разработка, Разработка под android

Совсем недавно Google предоставила мобильным разработчикам Android новую технологию сетевого обмена данными — Nearby. Мне она стала сразу интересна, так как позволяет устанавливать локальное соединение между Android устройствами без особых замарочек! Нет нужды заставлять пользователя вводить IP адрес и порт, он просто инициирует соединение, а клиенты к нему просто подключаются. На странице описывающей технологию указаны следующие варианты использования:
— многопользовательские игры на индивидуальных экранах – игроки играют в сетевые игры каждый со своего устройства, которые объединены в сеть (классика жанра);
— многопользовательские игры на общем экране – в данном случае в качестве сервера может выступать GoogleTV, на нём будет происходить основной игровой процесс, а все подключившиеся будут использовать свой телефон/планшет в качестве игрового контроллера (как на фото!);
— и конечно для любого обмена данными между различными Android устройствами.

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

Уже сейчас вы можете пропробовать эту технологию в игре Beach Buggy Racing:

После того как основной материал статьи был подготовлен, мне стало интересно на сколько хорошо система контролирует очерёдность доставляемых пакетов. Специально для этих целей я подготовил маленькое приложение для пересылки фотографий в виде текста :) С одного устройства на другое пересылались десятки тысяч пакетов по 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<AppIdentifier> 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<Connections.StartAdvertisingResult>() {
            @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<Status>() {
                    @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<Status>() {
                                    @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 – отправка ненадёжных сообщений.
При использовании первого метода, система сама контролирует правильность очерёдности доставляемых сообщений, во втором случае последовательность может нарушиться, так как контроля никакого нет. Зато второй метод быстрее, поэтому его лучше использовать когда требуется отправлять большое количество сообщений, например при отправке положения курсора на экране.

В ресурсах необходимо указать идентификатор сервиса по которому будет происходить поиск и подключения клиентов.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    ...
    <string name="service_id"><!-- идентификатор вашего сервиса, например имя.вашего.пакета--></string>
    ...
</resources>

Для разрешения рекламации приложения в манифесте необходимо прописать следующее:

<application>
  <meta-data android:name="com.google.android.gms.nearby.connection.SERVICE_ID"
            android:value="@string/service_id" />
  <activity>
      ...
  </activity>
</application>

Если вы соберёте это приложение и запустите его на своих устройствах то сможете наблюдать следующее:

При первом взгляде может показаться что использование API Nearby сложно и громоздко, но это только на первый взгляд. В итоге разработчик получает готовый, надёжный, контролируемый инструмент для сетевого обмена данными. Лично мне это решение очень понравилось, не надо больше контролировать очередность прихода пакетов с данными, просить пользователей ввести ip адрес и номер сокета, производить дополнительные настройки… Красота )))

Исходники с комментариями
Отдельно APK

Автор: SunSunSun

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js