Pull to refresh
254.33

ServerSocket для IPC в Android и примеры межпроцессного взаимодействия

Level of difficultyMedium
Reading time6 min
Views2.2K

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

В следующей статье разберем общение между двумя приложениями на одном устройстве. Не переключайтесь!

Tags:
Hubs:
Total votes 11: ↑11 and ↓0+12
Comments3

Articles

Information

Website
l.tbank.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия