Совсем недавно Google предоставила мобильным разработчикам Android новую технологию сетевого обмена данными — Nearby. Мне она стала сразу интересна, так как позволяет устанавливать локальное соединение между Android устройствами без особых заморочек! Нет нужды заставлять пользователя вводить IP адрес и порт, он просто инициирует соединение, а клиенты к нему просто подключаются. На странице описывающей технологию указаны следующие варианты использования:
— многопользовательские игры на индивидуальных экранах – игроки играют в сетевые игры каждый со своего устройства, которые объединены в сеть (классика жанра);
— многопользовательские игры на общем экране – в данном случае в качестве сервера может выступать GoogleTV, на нём будет происходить основной игровой процесс, а все подключившиеся будут использовать свой телефон/планшет в качестве игрового контроллера (как на фото!);
— и конечно для любого обмена данными между различными Android устройствами.
Уже сейчас вы можете пропробовать эту технологию в игре Beach Buggy Racing:
После того как основной материал статьи был подготовлен, мне стало интересно на сколько хорошо система контролирует очерёдность доставляемых пакетов. Специально для этих целей я подготовил маленькое приложение для пересылки фотографий в виде текста. С одного устройства на другое пересылались десятки тысяч пакетов по 2048 символов каждый. Очерёдность не была нарушена, ни одного пакета не утеряно. За контроль очерёдности доставки пришлось заплатить временем доставки, оно увеличилось.
Рассмотрим принципы работы с Nearby.
Дабы не создавать велосипед я взял оригинальный пример и рассмотрел его с переводом всех комментариев.
Прежде всего удостоверьтесь что на вашем телефоне имеется последняя версия сервисов GooglePlay — https://play.google.com/store/apps/details?id=com.google.android.gms.
Теперь перейдём к основным моментам проекта:
В проект добавлена библиотека PlayServices (в файл «build.gradle»), именно она позволяет работать с Nearby:
Работу с Nearby можно разделить на следующие этапы:
1) Создание главного объекта доступа – GoogleApiClient. Запуск клиента. Остановка клиента
2) Запуск рекламации намерения стать точкой доступа
3) Запуск поиска точек для соединения
4) Присоединение к точке
5) Обработка заявок на присоединение
6) Контроль соединения
7) Принятие и обработка сообщений от оппонента
8) Отправка сообщения
Рассмотрим всё по порядку.
Создание главного объекта доступа – GoogleApiClient. Запуск клиента. Остановка клиента. Тут всё просто. В конструкторе активности создаём главный объект доступа к Nearby. При старте активности запускаем его, при остановке активности отключаемся от сети.
Следующий этап — Запуск рекламации намерения стать точкой доступа, метод startAdvertising:
Если пользователь будет беспрестанно «жмахать» по кнопке “Advertise”, он получит сообщение что мол всё работает нормально, расслабся :) — STATUS_ALREADY_ADVERTISING
Третий этап — Запуск поиска точек для соединения:
Всё очень прозрачно и понятно. Просто запуск поиска точек доступа.
Теперь рассмотрим — Присоединение к точке обмена данными. Для этого сначала необходимо найти доступные точки доступа, а затем присоединяться к нужной. Метод onEndpointFound специально создан для того, чтобы сообщать о новой найденной точке:
В методе “connectTo” реализован диалог выбора точки к которой возможно подключиться. При выборе одного из варианта переходим к непосредственному подключению:
Если всё прошло успешно, то можно начинать обмен сообщениями.
Для обработки заявок на присоединение предназначен метод onConnectionRequest:
За контроль соединения отвечают ряд методов:
onDisconnected – обработка разрыва связи;
onConnected – обработка подключения;
onEndpointLost – обработка разрыва связи;
onConnectionSuspended – обработка прерывание соединения;
onConnectionFailed – обработка неудачного соединения.
Контроль за переподключением клиентов (например при разрыве связи при выходе пользователя из зоны действия WiFi) полностью ложится на разработчика.
Для обработки приходящих сообщений необходимо переписать метод onMessageReceived:
Отправка сообщений осуществляется с помощью двух методов:
1) Nearby.Connections.sendReliableMessage – отправка надёжных сообщений;
2) Nearby.Connections.sendUnreliableMessage – отправка ненадёжных сообщений.
При использовании первого метода, система сама контролирует правильность очерёдности доставляемых сообщений, во втором случае последовательность может нарушиться, так как контроля никакого нет. Зато второй метод быстрее, поэтому его лучше использовать когда требуется отправлять большое количество сообщений, например при отправке положения курсора на экране.
В ресурсах необходимо указать идентификатор сервиса по которому будет происходить поиск и подключения клиентов.
Для разрешения рекламации приложения в манифесте необходимо прописать следующее:
Если вы соберёте это приложение и запустите его на своих устройствах то сможете наблюдать следующее:
При первом взгляде может показаться что использование API Nearby сложно и громоздко, но это только на первый взгляд. В итоге разработчик получает готовый, надёжный, контролируемый инструмент для сетевого обмена данными. Лично мне это решение очень понравилось, не надо больше контролировать очередность прихода пакетов с данными, просить пользователей ввести ip адрес и номер сокета, производить дополнительные настройки… Красота!
Исходники с комментариями
Отдельно APK
Спасибо за помощь в подготовке материала inatale!
— многопользовательские игры на индивидуальных экранах – игроки играют в сетевые игры каждый со своего устройства, которые объединены в сеть (классика жанра);
— многопользовательские игры на общем экране – в данном случае в качестве сервера может выступать GoogleTV, на нём будет происходить основной игровой процесс, а все подключившиеся будут использовать свой телефон/планшет в качестве игрового контроллера (как на фото!);
— и конечно для любого обмена данными между различными Android устройствами.
Уже сейчас вы можете пропробовать эту технологию в игре 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
Спасибо за помощь в подготовке материала inatale!
NikolayRyabkov
А в чем разница между этим подходом и Network Service Discovery? Ощущение, что делается одно и то же, только тут фиксируется способ получения данных не через сокет.
SunSunSun Автор
«Nearby» является текущим этапом развития сетевых коммуникаций для Android приложений (особенно игровых), поэтому частично технологии перекликаются с ранее созданными.
Контролирует ли технология «Network Service Discovery» очерёдность обработки пересылаемых пакетов?
NikolayRyabkov
Она выдает TCP сокет. А там уже сам TCP. У меня просто был негативный опыт с NSD (глючило и падало). Вот ищу замену.
SunSunSun Автор
Попробуйте Nearby, я глюков не наблюдал. Единственно в режиме контроля доставки пакетов увеличивается время доствки за счёт контроля очерёдности.