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()
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.
Если остались вопросы или хотите поделиться своим опытом — добро пожаловать в комментарии!