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

от автора

Продолжая серию статей, общения между процессами и между двумя приложениями, в заключительной части разберем примеры в пределах одной 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<NsdServiceInfo>() 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-адрес или придумать механизм уведомления об изменении адреса сервера (отправлять сообщение в телеграм-бот и т. п.)

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

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.

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


ссылка на оригинал статьи https://habr.com/ru/articles/894650/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *