
Всем привет! Меня зовут Евгений Шувагин, я уже шесть лет занимаюсь Android‑разработкой, а последние два года работаю в команде Биометрических продуктов. Идея для статьи возникла из желания разобраться, как организовать общение между браузером и Android‑приложением для передачи данных. В поисках удобного решения я обратил внимание на ServerSocket
— простой и гибкий способ локального взаимодействия без лишних сложностей.
Прежде чем перейти к практической реализации, разберу основные сценарии использования ServerSocket
и межпроцессного взаимодействия в рамках одного процесса и приведу примеры рабочего кода.
Класс ServerSocket
Рассмотрим класс ServerSocket
. Он использовался нашими предшественниками в Java
и остается фундаментальным инструментом для создания серверных приложений, способных обрабатывать входящие сетевые запросы от клиентов.
Использовать ServerSocket
для общения между процессами в рамках одного приложения можно, когда стандартные механизмы IPC не подходят или когда требуется особый уровень изоляции и безопасности.
ServerSocket может использоваться:
в отладочных и тестировочных целях, когда нужно симулировать сетевое взаимодействие;
в KMM‑приложениях для связи между нативными процессами
Android
иiOS
, когда другие механизмы передачи данных недоступны;при обмене файлами между процессами, когда стандартные IPC‑механизмы неэффективны ввиду размера файлов.
Приведу реализацию упрощенного сценария на языке Kotlin
, в котором сервер (:server)
принимает подключение от клиента (:client)
, они обмениваются сообщениями и сервер завершает работу.
Код серверной части:
fun run() {
// (1) Создаем экземпляр класса ServerSocket
val server: ServerSocket = createServer()
// (2) Дожидаемся подключения клиента
val clientSocket: Socket = server.accept()
// (3) Обмениваемся сообщениями
handleClient(clientSocket)
}
Создание серверной части
Создание сервера:
val port = 65111
fun createServer(): ServerSocket {
return ServerSocket(
port = port,
backlog = 1,
bindAddr = InetAddress.getLoopbackAddress(),
)
}
Конструктор ServerSocket
на вход принимает три параметра.
Port — номер порта, на котором сервер будет прослушивать входящие соединения. Важно осмысленно подойти к выбору порта для сервера, так как использование уже занятого порта приведет к конфликту и выбрасыванию ошибки.
Есть возможность передать 0, тогда система выберет первый незанятый порт. Узнать, на каком порте запустился сервер, можно будет так:
val server = ServerSocket(0, 1, InetAddress.getLoopbackAddress())
val port = server.localPort
Backlog — максимальное количество клиентских соединений, которые могут ожидать обработки в очереди. Этот параметр позволяет ограничить количество клиентов, ожидающих соединения, чтобы предотвратить перегрузку сервера.
BindAddr — сетевой IP‑адрес, уникальный адрес устройства в рамках сети. Тут важно понимать, какой IP‑адрес использовать: от этого зависит, будет ли виден созданный нами сервер. Перед выбором ознакомимся с их разновидностями.
Для нашего примера выбрать первый незанятый порт будет недостаточно эффективно, так как для подключения номер этого порта придется передать клиенту. Самый надежный вариант — заранее выбрать порт, а с выбором нам поможет их классификация.
Есть три основные категории портов.
Системные (известные) порты. Диапазон от 0 до 1023 включает порты, которые обычно назначаются важнейшим сетевым службам и протоколам операционной системой. Такие как :443 — порт web-сервера, :21 — порт FTP-сервера.
Зарегистрированные (пользовательские) порты. Занимают диапазон от 1024 до 49 151. Эти порты не регулируются так строго, как системные, и используются для различных сервисов и приложений, когда требуется стабильный порт, не конфликтующий с системными портами и широко известными установленными сервисами.
Для зарегистрированных портов существует организация IANA (Internet Assigned Numbers Authority), которая отвечает за координацию и учет распределения этих портов, чтобы предотвратить их перекрытие.
Динамические (частные) порты. Диапазон портов — от 49 152 до 65 535, представляет собой порты для частного использования. Они часто используются для временных соединений, именно они и подойдут для нашей задачи.
Когда мы поднимаем сервер, важно понимать, кто и откуда сможет к нему подключаться. Это зависит от типа IP‑адреса, который он использует: localhost, приватный или публичный.
Loop‑back (localhost). Адреса начинаются на 127.* и используются для общения на одном физическом устройстве. Получить такой адрес можно через команду InetAddress.getLoopbackAddress(). Именно он нам и нужен для общения между двумя процессами в приложении.
Приватные. Адреса начинаются на 192.168.*, 10.*, 172.16.*, не имеют доступа к интернету и используются для общения в рамках одной сети. Узнать адрес устройства в сети можно через системный сервис ConnectivityManager.
Публичные. Это все остальные IP‑адреса, назначаются автоматически при подключении устройства к интернету и не могут быть присвоены другому устройству. Узнать адрес получится, только используя сторонние сервисы, например 2ip.ru.
С созданием сервера разобрались — теперь дожидаемся подключения клиента:
val clientSocket: Socket = server.accept()
Метод accept
блокирует поток и дожидается первого присоединившегося клиента. Как только клиент подключился, мы получаем Socket
, который состоит из двух потоков: InputStream
, из которого можно читать, и OutputStream
, в который можно писать.
И заключительный этап, на котором происходит обмен сообщениями:
fun handleClient(clientSocket: Socket) {
// Отправляем сообщение
val outputStream = clientSocket.getOutputStream()
val writer = PrintWriter(outputStream)
writer.println("Hello Client!")
writer.flush()
// Дожидаемся сообщения
val inputStream = InputStreamReader(clientSocket.getInputStream())
val reader = BufferedReader(inputStream)
val clientMessage = reader.readLine()
// Закрываем ресурсы
writer.close()
reader.close()
}
При отправке сообщения получаем выходной поток OutputStream
из сокета, оборачиваем его в PrintWriter
, чтобы было удобно записывать текстовые данные. Отправляемое сообщение должно заканчиваться символом новой строки \n
, используется для обозначения конца сообщения. Вызываем flush()
— он очищает буфер и немедленно отправляет сообщение.
Дожидаемся сообщения и получаем входной поток InputStream
, оборачиваем его в BufferedReader
для построчного чтения и дожидаемся, пока клиент пришлет нам сообщение. После чего освобождаем занятые ресурсы вызовом close-методов. На этом серверный код можно считать завершенным.
Создание клиентской части
Код клиентской части:
fun run() {
// (1) Подключаемся к серверу
val socket = connectToServer()
// (2) Обмениваемся сообщениями
handleServer(socket)
}
В коде клиентской части все попроще. Создаем сокет с параметрами созданного ранее сервера — указываем порт, на котором открыли сервер, и локальный адрес.
val port = 65111
fun connectToServer(): Socket {
return Socket(
address = InetAddress.getLoopbackAddress(),
port = port,
)
}
Читаем, отправляем приветственное сообщение серверу и освобождаем ресурсы.
fun handleServer(socket: Socket) {
// Получаем сообщение от сервера
val inputStreamReader = InputStreamReader(socket.getInputStream())
val reader = BufferedReader(inputStreamReader)
val messageFromServer = reader.readLine()
// Отправляем сообщение серверу
val outputStream = socket.getOutputStream()
val writer = PrintWriter(outputStream)
writer.println("Hello Server!")
// Закрываем ресурсы
reader.close()
socket.close()
}
Для визуализации прикручиваем UI и получаем что-то похожее на это:

Обмен сообщениями-компонентами на одном устройстве
Упомяну класс LocalServerSocket
, который похож на ServerSocket
, запущенный на Localhost, но использует более эффективный механизм взаимодействия внутри Android
.
Вместо IP-адресов и портов, как в ServerSocket
, LocalServerSocket
работает через файловую систему, создавая доменные сокеты. Это делает обмен данными внутри одного устройства быстрее и безопаснее. Вот пример отправки и получения сообщения.
Cерверная часть:
val name = "my_socket"
val serverSocket = LocalServerSocket(name)
val socket: LocalSocket = serverSocket.accept()
val writer = PrintWriter(socket.getOutputStream())
writer.println("Hello World!")
writer.flush()
writer.close()
Клиентская часть:
val name = "my_socket"
val clientSocket = LocalSocket()
clientSocket.connect(LocalSocketAddress(name))
val reader = BufferedReader(InputStreamReader(clientSocket.inputStream))
val message = reader.readLine()
Выводы
Мы посмотрели, как поднять ServerSocket
, выбрать порт и организовать обмен сообщениями между двумя процессами на устройстве. Это может приблизить понимание работы крупных фреймворков и откроет новые пути для решения задач межпроцессорного взаимодействия.
В следующей статье разберем общение между двумя приложениями на одном устройстве. Не переключайтесь!