...

суббота, 4 января 2014 г.

Взаимодействие Android-устройств в локальной сети


Предположим, мы пишем игру для Android, которая подразумевает некое сетевое взаимодействие между устройствами. Причем наши устройства находятся в одной сети и мы хотим, чтобы взаимодействие между ними осуществлялось быстро, а значит вариант с обменом данными через интернет нам не подходит. Ах да, еще одна маленькая ложка дегтя — мы хотим охватить максимально возможную аудиторию, для чего нам необходимо поддерживать Android 2.3.

Что же нам делать? Давайте поговорим об этом, а заодно рассмотрим относительно новые возможности Android SDK для соединения двух и более устройств.



О чем это и для кого это?




Как-то раз, уйдя с предыдущего места работы и погрузившись в заслуженный отдых, я принялся писать сетевую игру, в которую могут играть люди, находящиеся в одной локальной сети. И сразу же столкнулся с тем, что для нормального функционирования подобной игры нам мало соорудить сетевое взаимодействие — нам нужно сделать нормальное и быстрое обнаружение устройств в сети. Собственно, в данной статье я поделюсь своим опытом в реализации решения для данной задачи.

Сразу оговорюсь, что статья предназначена в большей мере для тех, кто имеет опыт Android-разработки, написал несколько приложений и хочет расширить свой кругозор, а также улучшить профессиональные навыки.

Какие возможные способы решения существуют?





  1. Android Network Service Discovery. Простой и эффективный способ обнаружения устройств. На Android Developer есть пошаговое руководство по подключению NSD, есть пример NsdChat, который можно скачать там же. Но есть один существенный минус — данный метод поддерживается только начиная с API Level 16, то есть с Android 4.1 Jelly Bean;

  2. Второе решение, предлагаемое нам на сайте Android Developer — Wi-Fi Peer-to-Peer. Проблема этого метода та же самая — поддерживается он только начиная с API Level 16;

  3. Есть странное решение, которое предлагается некоторыми программистами на Stack Overflow — самостоятельно сканировать локальную сеть на предмет наличия сервера. То есть проходить по всем адресам сети. Это уже сейчас звучит как странный велосипед, а теперь представьте, что порт нашего сервера назначается автоматически. Таким образом, сканирование даже самую небольшой сети становится достаточно долгой и трудоемкой задачей;

  4. Наконец, мы можем обратить внимание на Java-библиотеки и написать что-нибудь с их использованием. Например, JmDNS.




Последний способ выглядит вполне адекватным и, кажется, может обеспечить нас требуемой скоростью и удобством обнаружения устройств в сети для конечного пользователя.

Итак...




Я вооружился JmDNS и решил попробовать соорудить несколько классов, которые по максимуму упростят написание описанных выше приложений. Но для начала пришлось немного повырезать дубликаты .class-файлов из jar-пакета JmDNS (проблема описана здесь):

mkdir unjar
cd unjar
jar xf ../jmdns.jar
jar cfm ../jmdns.jar META-INF/MANIFEST.MF javax/


Далее я взял исходный код NsdChat с Android Developer и изменил его служебный класс, который отвечает за инициализацию сокетов и организацию сетевого взаимодействия. Также я написал wrapper для JmDNS



import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.util.Log;

import java.io.IOException;
import java.net.InetAddress;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;

/**
* @author alwx
* @version 1.0
*/
public class NetworkDiscovery {
private final String DEBUG_TAG = NetworkDiscovery.class.getName();
private final String TYPE = "_alwx._tcp.local.";
private final String SERVICE_NAME = "LocalCommunication";

private Context mContext;
private JmDNS mJmDNS;
private ServiceInfo mServiceInfo;
private ServiceListener mServiceListener;
private WifiManager.MulticastLock mMulticastLock;

public NetworkDiscovery(Context context) {
mContext = context;
try {
WifiManager wifi = (WifiManager) mContext.getSystemService(android.content.Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifi.getConnectionInfo();
int intaddr = wifiInfo.getIpAddress();

byte[] byteaddr = new byte[]{
(byte) (intaddr & 0xff),
(byte) (intaddr >> 8 & 0xff),
(byte) (intaddr >> 16 & 0xff),
(byte) (intaddr >> 24 & 0xff)
};
InetAddress addr = InetAddress.getByAddress(byteaddr);
mJmDNS = JmDNS.create(addr);
} catch (IOException e) {
Log.d(DEBUG_TAG, "Error in JmDNS creation: " + e);
}
}

/**
* starts server with defined names on given port
*
* @param port server port
*/
public void startServer(int port) {
try {
wifiLock();
mServiceInfo = ServiceInfo.create(TYPE, SERVICE_NAME, port, SERVICE_NAME);
mJmDNS.registerService(mServiceInfo);
} catch (IOException e) {
Log.d(DEBUG_TAG, "Error in JmDNS initialization: " + e);
}
}

/**
* performs servers discovery
*
* @param listener listener, that will be called after successful discovery
* (see {@link me.alwx.localcommunication.connection.NetworkDiscovery.OnFoundListener}
*/
public void findServers(final OnFoundListener listener) {
mJmDNS.addServiceListener(TYPE, mServiceListener = new ServiceListener() {
@Override
public void serviceAdded(ServiceEvent serviceEvent) {
ServiceInfo info = mJmDNS.getServiceInfo(serviceEvent.getType(), serviceEvent.getName());
listener.onFound(info);
}

@Override
public void serviceRemoved(ServiceEvent serviceEvent) {
}

@Override
public void serviceResolved(ServiceEvent serviceEvent) {
mJmDNS.requestServiceInfo(serviceEvent.getType(), serviceEvent.getName(), 1);
}
});
}

/**
* closes connection & unregisters all services
*/
public void reset() {
if (mJmDNS != null) {
if (mServiceListener != null) {
mJmDNS.removeServiceListener(TYPE, mServiceListener);
mServiceListener = null;
}
mJmDNS.unregisterAllServices();
}
if (mMulticastLock != null && mMulticastLock.isHeld()) {
mMulticastLock.release();
}
}

/**
* accuires Wi-Fi lock
*/
private void wifiLock() {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(android.content.Context.WIFI_SERVICE);
mMulticastLock = wifiManager.createMulticastLock(SERVICE_NAME);
mMulticastLock.setReferenceCounted(true);
mMulticastLock.acquire();
}

public interface OnFoundListener {
void onFound(ServiceInfo info);
}
}


Здесь размещены 4 основные функции для работы Network Discovery:



  1. startServer для создания сервера и регистрации соответствующего сервиса в локальной сети;

  2. findServers для поиска серверов;

  3. reset для окончания работы с Network Discovery и последующего освобождения ресурсов;

  4. wifiLock для запроса блокировки Wi-Fi.


В завершении я написал универсальный класс ConnectionWrapper для полноценной организации обнаружения, а также обмена сообщениями в локальной сети. Таким образом, создание сервера в конечном приложении выглядит следующим образом:



getConnectionWrapper().startServer();
getConnectionWrapper().setHandler(mServerHandler);




А вот и mServerHandler, использующийся для приема и обработки сообщений:

private Handler mServerHandler = new MessageHandler(MainActivity.this) {
@Override
public void onMessage(String type, JSONObject message) {
try {
if (type.equals(Communication.Connect.TYPE)) {
final String deviceFrom = message.getString(Communication.Connect.DEVICE);
Toast.makeText(getApplicationContext(), "Device: " + deviceFrom, Toast.LENGTH_SHORT).show();
}
} catch (JSONException e) {
Log.d(DEBUG_TAG, "JSON parsing exception: " + e);
}
}
};




Отправка сообщений еще проще:

getConnectionWrapper().send(
new HashMap<String, String>() {{
put(Communication.MESSAGE_TYPE, Communication.ConnectSuccess.TYPE);
}}
);




И, наконец, метод для обнаружения и подключения к серверу:

private void connect() {
getConnectionWrapper().findServers(new NetworkDiscovery.OnFoundListener() {
@Override
public void onFound(javax.jmdns.ServiceInfo info) {
if (info != null && info.getInet4Addresses().length > 0) {
getConnectionWrapper().stopNetworkDiscovery();
getConnectionWrapper().connectToServer(
info.getInet4Addresses()[0],
info.getPort(),
mConnectionListener
);
getConnectionWrapper().setHandler(mClientHandler);
}
}
});
}




Как видите, все очень просто. А главное, все это работает в любой версии Android для максимум двух устройств. Но сделать так, чтобы это работало для условно неограниченного числа устройств очень легко, и очевидное решение придет к вам почти сразу после детального изучения класса Connection. Пусть это будет в качестве домашнего задания.

Ах, да, весь код доступен для изучения и использования всеми желающими в моем репозитории на GitHub.. И, конечно, не исключаю то, что некоторые вещи можно сделать лучше и проще, поэтому не стесняйтесь форкать и делать pull request'ы.

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий