Взаимодействие 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'ы.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 16

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

      Пытались также использовать WiFi-direct, но он нормально так и не заработал у нас. Обнаружение устройств работало отлично. А вот если происходило несколько коннектов/дисконнектов то устройства в итоге переставали подключаться друг другу. В итоге перешли на использование jmdns
        0
        Нет ли у Вас какой-либо информации о поддержке/неподдержке мультикаста на различных девайсах?
        Ну, или хотя бы предполагаемый процент покрытия зоопарка девайсов при использовании мультикаста?

        Лично меня на данным момент интересует вопрос определения ip-адреса http-сервера в локальной сетке и посему информация о JmDNS весьма кстати.
          +1
          Я тестировал этот код на старых устройствах с Android 2.3 и везде все было нормально с Multicast DNS. Так что теоретически такой способ вполне неплохо подходит на роль универсального для определения IP-адреса сервера в локальной сети. Вероятно, есть некий процент устройств, на котором это не будет работать, но я думаю, что этот процент весьма небольшой. Да и все устройства, на которых это не будет работать, весьма старые.
            0
            тут есть приложение для теста. как я понимаю все зависит от производителя телефона
            cafbit.com/entry/testing_multicast_support_on_android
            0
            WiFi Direct вроде начиная с 4.1.
            Насколько я понял в процессе написания игрушки, JmDNS — единственное более-менее вменяемое решение данной задачи.
              0
                0
                Тогда прошу прощения.
                API 14 еще куда ни шло, но девайсы с Android 2.3 по-прежнему остаются в стороне.
            –1
            1. Android Network Service Discovery.
            После этого все остальное имеет лишь академический интерес. Дропнуть все старше 16 API коммерчески оправдано.
              +1
              Да что уж там, давайте только Kit Kat оставим.
              Только вот большинство потенциальных заказчиков вряд ли оценят подобный подход.
                –1
                Заказчики они такие заказчики :) У меня пару лет назад требовали поддержку АПИ 3.
                Я все таки говорю про реальный рынок. Там не сухие проценты, эти проценты имеют еще и качество (кто на Android 2.3 и кто на API 18)
                0
                Это оправдано, если есть возможность самостоятельно принимать решения о поддержке разных версий. Хотя и на 2.3.3 до сих пор довольно много активных пользователей (судя по статистике для моих приложений из google play).
                  0
                  Безусловно больше всегда лучше.
                  Но время берет свое, приложение планируем сегодня, делаем завтра, запускаем послезавтра. А тех пользователей все меньше и меньше. Кроме того, есть деликатный вопрос ценности пользователей. Обладатель свежего аппарата куда «денежнее», чем владелец галакси эйс 2011 года. И всегда есть предел допустимой переплаты за решение для всех.
                0
                Почему нельзя послать широковещательное сообщение на всю локальную сеть внутри Wi-Fi на которое ответят все устройства, поддерживающий нужный нам сервис?
                  0
                  Спасибо за статью. Ещё хотелось почитать про реальный опыт разработки игр с поддержкой локальной сети — насколько я представляю, там должно быть достаточно много проблем со скоростью работы (если говорить об экшн играх).
                    0
                    Опыта разработки именно экшн-игр у меня, увы, нет.
                    Вероятно, проблемы будут. Но все эти проблемы в общем-то вполне решаемы — вы же можете играть в какой-нибудь Call of Duty даже по интернету и не испытывать особых проблем.
                      0
                      Я имею ввиду, что при «классической» схеме мультиплеера по локальной сети — когда на одном устройстве запускается сервер и клиент, а на остальных — только клиенты, «серверу», возможно, будет не хватать производительности обрабатывать модель мира и все взаимодействия с клиентами. Ну это так, эвристика)

                  Only users with full accounts can post comments. Log in, please.