
Всем привет! Меня зовут Евгений Шувагин, я уже шесть лет занимаюсь 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, выбрать порт и организовать обмен сообщениями между двумя процессами на устройстве. Это может приблизить понимание работы крупных фреймворков и откроет новые пути для решения задач межпроцессорного взаимодействия.
В следующей статье разберем общение между двумя приложениями на одном устройстве. Не переключайтесь!
