
Продолжая тему ServerSocket, в предыдущей части мы разобрали, как организовать общение между двумя процессами в рамках одного приложения. Теперь перейдем к более интересной задаче — взаимодействию между приложением и сайтом, открытым в браузере на одном устройстве.
Если бы оба участника обмена данными были Android‑приложениями, мы могли бы использовать стандартные механизмы IPC, такие как Intent для отправки сообщений, ContentProvider для доступа к данным или Binder для более сложных взаимодействий. Но в нашем случае одна из сторон — браузер, в котором работает веб‑приложение. Браузер не имеет доступа к этим механизмам, так как работает в своем песочном окружении и не может напрямую взаимодействовать с компонентами Android.
Именно поэтому ServerSocket становится удобным решением, позволяя Android‑приложению создать локальный сервер, к которому браузер может подключаться как к обычному веб‑серверу. Это дает возможность гибко передавать данные между приложением и сайтом, обходя ограничения стандартных средств IPC.
Сценарии использования
Такой подход позволит идентифицировать пользователя, который переходит из приложения в браузер на сайт, передавать данные в реальном времени, управлять воспроизведением контента на сайте, обновлять интерфейс браузера на основе событий в приложении, передавать файлы или содержимое буфера обмена.
В прошлый раз мы обменивались данными между процессами напрямую, без использования протоколов. Но в случае с браузером такой подход невозможен из‑за ограничений на низкоуровневые сетевые соединения. Браузер не позволяет устанавливать прямое TCP‑соединение с ServerSocket, поэтому нам необходимо использовать один из поддерживаемых протоколов — HTTP или WebSocket. В зависимости от выбора технологии серверная имплементация будет различаться.
Протокол HTTP
Для соединения по HTTP на клиентской стороне можно использовать такой скрипт на HTML:
<!DOCTYPE html> <html> <body> <p id="message"></p> <script> function read() { const message = document.getElementById("message"); fetch("http://localhost:65111/") .then(response => response.text()) .then(responseData => { message.innerHTML = `Server Message: ${responseData}`; }) } read() </script> </body> </html>
Этот скрипт выполняет HTTP‑запрос с браузера к локальному серверу, который работает на localhost:65111.
Создание серверной части
Серверный код будет такой:
fun run() { // Создаем экземпляр класса ServerSocket val server: ServerSocket = createServer() // Дожидаемся подключение клиента val clientSocket: Socket = server.accept() // Обмениваемся сообщениями handleClient(clientSocket) }
Создание сервера:
val port = 65111 fun createServer(): ServerSocket { // Задаем нужные параметры val server = ServerSocket( port = port, backlog = 100, bindAddr = InetAddress.getLoopbackAddress(), ) // Устанавливаем таймаут server.soTimeout = 60.seconds.inWholeMilliseconds.toInt() return server }
Задаем порт, о котором будет знать наш клиент. В этом случае можно подстраховаться и использовать массив портов, и тогда, когда один из портов окажется занятым, мы сможем переключится на другой. Код в этом случае будет выглядеть так:
val ports = listOf(65111, 65112, 65113, 65114, ...) fun createServer(): ServerSocket { for (port in ports) { try { return ServerSocket(port, …) } catch (e: BindException) { // log or do something } } error("Could not create ServerSocket") }
Для экономии ресурсов устройства устанавливаем таймаут на установку соединения. Если клиент в течение 60 секунд не подключится, сервер будет уничтожен, а точнее выбросится исключение SocketTimeoutException.
Обмениваемся сообщениями. Вспоминаем, что HTTP‑запрос — это текст, состоящий из стартовой строки и заголовков:
fun handleClient(clientSocket: Socket) { val reader = BufferedReader(InputStreamReader(clientSocket.getInputStream())) // Читаем начальную строку запроса val request = requestParts(reader) // Читаем заголовки val headers = readHeaders(reader) // Отправляем сообщение sendMessage(clientSocket) writer.close() clientSocket.close() }
Перед HTTP‑заголовками клиентский запрос начинается со стартовой строки запроса, которая содержит следующие компоненты:
-
метод запроса — определяет тип операции, например
GET,POST,PUT,DELETE; -
запрашиваемый ресурс — указывает на целевой ресурс, например
URLна сервере; -
версию
HTTP— версию протокола, используемую для запроса.
На основе этих данных можно выполнить внутреннюю логику для обработки запроса, сгенерировать соответствующий ответ и отправить его обратно клиенту. Для их получения нужно распарсить первую присылаемую строку:
fun requestParts(reader: BufferedReader): Map<String, String> = buildMap { val request = reader.readLine() val requestParts = request.split(" ") put("method", requestParts[0]) // Метод запроса (GET, POST и т. д.) put("resource", requestParts[1]) // Запрашиваемый ресурс (URL) put("httpVersion", requestParts[2]) // Версия протокола }
После начальной строки запроса следуют HTTP‑заголовки. Они предоставляют дополнительную информацию о запросе или клиенте. Например, информацию о браузере (User‑Agent), предпочтения по содержимому (Accept) и другое, что позволит более тонко настроить ответ.
Каждый заголовок состоит из имени заголовка, последующего двоеточия и значения и разделяется переводом строки:
fun readHeaders(reader: BufferedReader): MutableMap<String, String> { val headers = mutableMapOf<String, String>() var line = reader.readLine() while (line.isNotEmpty()) { val headerParts = line.split(":") if (headerParts.size >= 2) { val headerName = headerParts[0].trim() val headerValue = headerParts[1].trim() headers[headerName] = headerValue } line = reader.readLine() } return headers }
Для отправки HTTP‑ответа клиенту нужно отправить необходимые HTTP‑заголовки.
HTTP/1.1 200 OK — строка ответа, указывающая на версию HTTP‑протокола и статус ответа (200), который сообщает о том, что запрос был успешно обработан.
Content‑Type: text/plain — заголовок, определяющий MIME‑тип содержимого ответа. В данном случае сообщается, что ответ представлен в формате обычного текста.
Access‑Control‑Allow‑Origin:* — заголовок используется для указания, что ресурс может быть доступен с любого источника.
Content‑Length: ${response.length} — заголовок, указывающий на длину тела ответа в байтах.
fun sendMessage(clientSocket: Socket) { val writer = PrintWriter(clientSocket.getOutputStream(), true) val response = "Hello Client!" writer.println("HTTP/1.1 200 OK") writer.println("Content-Type: text/plain") writer.println("Access-Control-Allow-Origin: *") writer.println("Content-Length: ${response.length}") writer.println() writer.println(response) }
Завершается процедура закрытием потока вывода и самого сокета. На этом сервер готов отправить сообщение Hello World! каждому клиенту, который подключится.
Запустив сервер и открыв HTML‑страницу в браузере мобильного устройства, мы можем увидеть результат.

Протокол WebSocket
В случаях, когда требуется беспрерывное общение, стоит переключится на протокол WebSocket: он обеспечит двустороннюю связь в реальном времени и минимальную задержку. Преобразуем скрипт клиентской части в чат:
<!DOCTYPE html> <html> <body> <p></p> <font size="4"> <div id="messages"></div> </font> <input type="text" id="inputMessage" placeholder="Enter your message"> <button onclick="sendMessage()">Send</button> <script> function addMessage(message) { const logDiv = document.getElementById('messages'); const p = document.createElement('p'); p.textContent = message; logDiv.appendChild(p); } const socket = new WebSocket('ws://localhost: 65111'); socket.addEventListener('message', function (event) { addMessage('Server: ' + event.data); }); function sendMessage() { const inputMessage = document.getElementById("inputMessage").value; if (socket.readyState === WebSocket.OPEN) { socket.send(inputMessage); addMessage('You: ' + inputMessage); document.getElementById("inputMessage").value = ""; } else { addMessage('WebSocket is not open. Cannot send message.'); } } </script> </body> </html>
Изменится серверный код:
fun run() { // Создание сервера не изменится val server: ServerSocket = createServer() val clientSocket: Socket = server.accept() // Изменится установка соединения с клиентом handleClient(clientSocket) } fun handleClient(clientSocket: Socket) { val inputStream = clientSocket.getInputStream() val outputStream = clientSocket.getOutputStream() val reader = BufferedReader(InputStreamReader(inputStream)) val writer = PrintWriter(outputStream) // Считываем заголовки val headers = readHeaders(reader) // Рукопожатие handshake(writer, headers) // Обмен сообщениями runCommunication(inputStream, outputStream) }
Считывание заголовков остается неизменным. На этот раз они понадобятся нам для этапа Handshake.
Установление WebSocket‑соединения начинается с критического этапа — рукопожатия (handshake), которое обеспечивает переключение с HTTP‑протокола на WebSocket. Рассмотрим, как это реализуется в коде:
fun handshake(writer: PrintWriter, headers: Map<String, String>) { // Вычисляем ключ для подтверждения установки соединения val encodedKey = calculateWebSocketAccept(headers["Sec-WebSocket-Key"] // Отправляем заголовки на успешное рукопожатие, это константы, закрепленные в RFC для установки соединения по websocket writer.println("HTTP/1.1 101 Switching Protocols") writer.println("Upgrade: websocket") writer.println("Connection: Upgrade") // Ключ, подтверждающий успешное установление соединения writer.println("Sec-WebSocket-Accept: $encodedKey") writer.println() writer.flush() }
Метод calculateWebSocketAccept вычисляет ключ подтверждения для обеспечения безопасности соединения, принимает строку Sec-WebSocket-Key, которую клиент отправляет в запросе. Это значение преобразуется и отправляется обратно клиенту для завершения рукопожатия.
Процесс вычисления ключа описан в RFC 6455. А вот так выглядит реализация:
fun calculateWebSocketAccept(clientWebSocketKey: String): String { // Константа из RFС для вычисления ключа val WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" val concatenated = clientWebSocketKey + WEB_SOCKET_GUID val sha1 = MessageDigest.getInstance("SHA-1") val hashBytes = sha1.digest(concatenated.toByteArray()) val encodedBytes = Base64.getEncoder().encode(hashBytes) return String(encodedBytes) }
После успешного соединения с клиентом можем начать общение. Дожидаемся сообщения от клиента и отправляем ему ответ:
fun runCommunication(input: InputStream, output: OutputStream) { while (true) { // Сообщения, получаемые из сокета, присылаются в виде фрейма val receivedFrame = readSocketFrame(input) // Отправляем ответ на сообщение клиента sendText(output, "Hello Client!") } }
Функция readSocketFrame отвечает за получение фрейма сообщения от клиента. В WebSocket сообщения разбиваются на фреймы.
Фрейм — это последовательность байтов, где первые шесть байтов содержат служебную информацию, а оставшиеся байты — данные.
Как происходит чтение фрейма:
private fun readSocketFrame(input: InputStream): String { // Первый байт, биты в нем рассказывают о типе сообщения, для примера не важно, пропускаем описание каждого бита в этом байте val b1 = input.read() // Второй байт val b2 = input.read() // Последние 7 битов обозначают длину сообщения (максимальная длина сообщения — 125 байт, если нужно больше, придется разбираться, как это устроено) val messageLength = (b2 and 0b01111111).toLong() // Следующие 4 байта — это маска, которой зашифровано сообщение val maskKey = input.getBytes(4) // Все последующие байты — это наше сообщение val payload = input.getBytes(messageLength) // Расшифровываем сообщение unmaskedPayload(payload, maskKey) // Возвращаем результат return String(payload) } // Алгоритм из RFC для расшифровывания сообщения private fun unmaskedPayload(payload: ByteArray, maskKey: ByteArray) { for (index in payload.indices) { payload[index] = payload[index] xor maskKey[index % 4] } }
Отправляем ответ, где первые два байта — это заголовки и остальное тело сообщения:
private fun sendText(output: OutputStream, text: String) { val utf8Bytes = text.toByteArray() // Первый байт, говорящий, что мы отправляет текст output.write(0b10000001) // Второй байт — длина сообщения output.write(utf8Bytes.size) // И само сообщение output.write(utf8Bytes) output.flush() }
В примере я минимально рассмотрел протокол Websocket. Описанной функциональности хватит для банального общения текстом, когда не нужно дробить большие сообщения на маленькие кусочки. Вот что получилось:
В результате такой сервер может работать недолго, если приложение находится в фоновом режиме. Но в случае, когда пользователь переходит напрямую из вашего приложения на ваш сайт в браузере, этого должно быть достаточно.
Выводы
Мы рассмотрели, как организовать общение между двумя приложениями на одном устройстве, где клиентом выступает веб‑страница в браузере. Разобрали, какие существуют ограничения на формат общения, а также ограничения по времени работы из‑за политики OC управления фоновыми процессами.
В следующей статье расскажу, как организовать общение приложений в одной Wi-Fi‑сети.
ссылка на оригинал статьи https://habr.com/ru/articles/893890/
Добавить комментарий