Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров61K


Предположим, мы пишем игру для 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'ы.
Теги:
Хабы:
+27
Комментарии16

Публикации

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн