Взаимодействие 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’ы.

ссылка на оригинал статьи http://habrahabr.ru/post/208132/