ServerSocket на Android в пределах одной сети

cbcbcf8b3ef686babbb9a5493e34feea.jpg

Продолжая серию статей, общения между процессами и между двумя приложениями, в заключительной части разберем примеры в пределах одной Wi-Fi‑сети.

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

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

Сервис Network Service Discovery

Раньше, если нам нужно было подключиться к серверу внутри локальной сети, мы просто указывали его IP‑адрес — и это работало, так как он был известен заранее. Но что, если мы хотим обмениваться данными с незнакомыми устройствами в одной сети? Решение — сервис Network Service Discovery (NSD).

Network Service Discovery — стандартный механизм в Android, который упрощает процесс регистрации сервера и его поиска клиентскими приложениями.

С его помощью приложения могут:

  • Публиковать свой сервер в сети (сервер говорит: «Я здесь!»).

  • Находить доступные серверы (клиенты видят сервер, даже не зная его IP).

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

Серверная часть

fun run() {
	// Получение IP-адреса устройства в локальной сети
	val ip = getIP()
	// Создаем cервер
	val server = ServerSocket(0,1,ip)
	// Задаем имя ПО для нашего сервера
	val serverName = "My Server”
	// Узнаем порт, на котором создался наш сервер
	val port = server.localPort
    // Регистрируем сервер в локальной сети
	val registeredServer = registerServerInDNS(serverName, port)
	// Дожидаемся подключения клиента
	val clientSocket = server.accept()
	// Начинаем общение
	runCommunication(clientSocket)
}

Для начала общения нам необходимо получить текущий IP-адрес в локальной сети, сделать это можно при помощи ConnectivityManager сервиса:

val manager = context.getSystemService(ConnectivityManager::class.java)
fun getIP(): String? {
	val properties = manager.getLinkProperties(manager.activeNetwork)
	return properties?.linkAddresses
    	?.first { adress -> adress.address.isSiteLocalAddress }
        ?.address?.hostAddress
}

IP‑адрес может быть null, если устройство не подключено к сети. Нужно зарегистрировать коллбэк ConnectivityManager.NetworkCallback, чтобы обработать события подключения и отключения и при каждых изменениях в сети получать актуальный IP‑адрес.

val manager = context.getSystemService(ConnectivityManager::class.java)
val networkStateCallback = object : ConnectivityManager.NetworkCallback() {
	override fun onAvailable(network: android.net.Network) {
    	getIP()
	}
}
val request = NetworkRequest.Builder()
	.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
	.build()
manager.registerNetworkCallback(request, networkStateCallback)

Создание сервера:

val server = ServerSocket(0, 1, ip) 

Указываем порт 0, чтобы получить первый незанятый порт в системе, а значением 1 указываем, сколько одновременно клиентов могут с нами работать. Последним аргументом передаем IP-адрес. Подробнее о создании можно почитать в прошлых частях.

Регистрация в сети: создаем объект NsdServiceInfo, который хранит информацию о сервере, который мы хотим зарегистрировать:

val manager = context.getSystemService(NsdManager::class.java)
    fun register(serviceName: String, port: Int) {   
        val nsdServiceInfo = NsdServiceInfo()   
        // Уникальное имя сервера в пределах сети
        nsdServiceInfo.serviceName = serviceName   
        // Порт, который использует сервер
        nsdServiceInfo.port = port   
        // Тип сервера
       nsdServiceInfo.serviceType = "_socket._tcp"
      manager.registerService(nsdServiceInfo, NsdManager.PROTOCOL_DNS_SD, listener)
} 
val listener = object : NsdManager.RegistrationListener {
       // Коллбек успешной регистрации
	override fun onServiceRegistered(info: NsdServiceInfo) {       
    	val registeredName = info.serviceName
	}
}

Строка  socket.tcp описывает тип сервера, она состоит из протокола (_http, websocket) и транспортной схемы (tcp, _udp), используемой сервисом. Она может быть какой угодно — важно только, чтобы все клиентские приложения, которые должны найти наш сервис, искали его именно под этим именем.

При успешной регистрации сервера в сети отработает коллбек onServiceRegistered. В нем можно будет найти обновленное имя сервера в случае, если оно было занято кем-то другим.

Клиентская часть

На клиентской части для поиска серверов используется NsdManager, который сообщает о появлении или исчезновении серверов в сети:

// Найденные серверы будет хранить в списке
val servers = mutableListOf()
val manager = context.getSystemService(NsdManager::class.java)

fun findServices() {
	val listener = object : NsdManager.DiscoveryListener {
            // Получаем информацию о сервере
    	override fun onServiceFound(service: NsdServiceInfo) {
        	    getInfo(service)
    	  }
       
    	override fun onServiceLost(service: NsdServiceInfo) {
        	// Когда сервер становится недоступным, удаляем его из списка
        	    servers.removeIf { it.serviceName == service.serviceName }
    	  }
	}
	// Запуск процесса обнаружения, указывая тип сервера, который мы ищем
	manager.discoverServices("_socket._tcp", NsdManager.PROTOCOL_DNS_SD, listener)
}

// Подключение
fun connect() {
     val server = servers.first()
     val port = server.port
     val host = server.host.hostAdress
    createServer(host, port)
}

Колбек onServiceFound сообщает, что сервер был найден и для получения IP-адреса, его порта и других параметров мы должны использовать метод resolveService, после чего можно установить обмен данными:

fun getInfo(service: NsdServiceInfo) {   
val manager = context.getSystemService(NsdManager::class.java)
val callback = object : NsdManager.ResolveListener {
    	override fun onServiceResolved(serviceWithInfo: NsdServiceInfo) {
        	// При успехе добавляем новый сервер в список
        	servers.add(serviceWithInfo)
    	}
	}
manager.resolveService(service, callback)
}

Колбэк onServiceLost уведомляет о том, что сервер стал недоступным. В этом случае его можно удалить из списка и разорвать соединение, если обмен данными уже был установлен.

Метод discoverServices у NsdManager запускает процесс обнаружения сервисов в сети, а строка »_socket._tcp» указывает на тип сервиса. Это та самая строка, которую мы ранее использовали при регистрации сервера в сети в серверной части.

У объекта NsdServiceInfo доступны свойства host.hostAddress(IP-адрес) и port, которые нужно использовать для подключения к найденному сервису. 

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

 клиент и сервер находят друг друга, устанавливают соединение, обмениваются сообщениями
 клиент и сервер находят друг друга, устанавливают соединение, обмениваются сообщениями

Полноценный сервер

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

Для подключения к серверу, запущенному на мобильном устройстве, используется его внешний IP-адрес, который часто меняется динамически. Чтобы его узнать, нужно воспользоваться внешним сервисом, но лучше взять у своего провайдера статический IP-адрес или придумать механизм уведомления об изменении адреса сервера (отправлять сообщение в телеграм-бот и т. п.)

Будущие ограничения

Local Network Protection (LNP) — новая функция в будущем Android, которая ограничит возможность приложений свободно работать в локальной сети. Теперь пользователи смогут сами контролировать, какие приложения получают доступ. В дальнейшем появится специальное Runtime Permission, но пока разработчики могут протестировать новое поведение с помощью shell-команд. Все это означает, что просто так поднять сервер без уведомления пользователя на его устройстве больше не получится.

Стоит упомянуть о Wi-Fi Direct и Wi-Fi Aware — это две технологии беспроводной связи, которые позволяют устройствам взаимодействовать без подключения к Wi-Fi-сети. Но они различаются по своему назначению и принципу работы.

Демонстрация прямого общение устройств
Демонстрация прямого общение устройств

Wi-Fi Direct создает прямое соединение между устройствами с высокой скоростью передачи данных. Технология уже применяется:

  • в принтерах для печати документов прямо с телефона;

  • в функциях типа Samsung Quick Share для быстрой передачи файлов;

  • в стриминге видео и музыки на телевизоры или проекторы.

Wi-Fi Aware позволяет устройствам обнаруживать друг друга и обмениваться данными в фоновом режиме и применяется:

  • в социальных приложениях, таких как Bumble или Facebook, для поиска пользователей поблизости;

  • в многопользовательских играх, таких как Spaceteam и BombSquad, для улучшения пользовательского опыта;

  • в умных домах для автоматического подключения устройств;

  • для отправки персонализированных сообщений, к примеру когда подходишь к витрине магазина или экспонату в музее.

Выводы

В этой серии статей я показал, как использовать ServerSocket для экзотического IPC на Android. Старался оставить рабочие примеры кода, которые представляют собой минимальную реализацию, готовую к использованию.

В зависимости от целей можно углубиться в изучение способов обезопасить свое общение с помощью шифрования, исследовать новые протоколы взаимодействия или изучить Wi-Fi Direct и Wi-Fi Aware.

Если остались вопросы или хотите поделиться своим опытом — добро пожаловать в комментарии!

© Habrahabr.ru