Найти в Дзене
InnaTomeya

Реализация локального чата на Android через WebSocket без сторонних серверов

В рамках проекта ChatGhost была разработана минималистичная система обмена сообщениями между несколькими клиентами Android, использующая протокол WebSocket. Главной особенностью решения стало отсутствие зависимости от сторонних облачных решений или сервисов, таких как Firebase или Telegram Bot API. Вся система работает в локальной сети, что обеспечивает полный контроль над данными, простоту настройки и высокую скорость передачи сообщений. Реализованно на базе Почтового приложения (временно находиться в MainActivity.java, в будущем будет добавлена отдельная кнопка для чата в режиме реального времени). Что нужно для работы: Начнем с серверной части, а именно создадим WebSocket (Node.js) const WebSocket = require('ws'); const wss = new WebSocket.Server({ host: '0.0.0.0', port: 8081 }); console.log("WebSocket server running on ws://0.0.0.0:8081"); wss.on('connection', function connection(ws) { console.log("Client connected"); ws.on('message', function incoming(message) { const text = messa

В рамках проекта ChatGhost была разработана минималистичная система обмена сообщениями между несколькими клиентами Android, использующая протокол WebSocket. Главной особенностью решения стало отсутствие зависимости от сторонних облачных решений или сервисов, таких как Firebase или Telegram Bot API. Вся система работает в локальной сети, что обеспечивает полный контроль над данными, простоту настройки и высокую скорость передачи сообщений. Реализованно на базе Почтового приложения (временно находиться в MainActivity.java, в будущем будет добавлена отдельная кнопка для чата в режиме реального времени).

Что нужно для работы:

  1. Android Studio, OkHttp WebSocket API
  2. Node.js + WebSocket-библиотека ws
  3. Локальная Wi-Fi сеть (в пределах одной подсети

Начнем с серверной части, а именно создадим WebSocket (Node.js)

const WebSocket = require('ws');

const wss = new WebSocket.Server({ host: '0.0.0.0', port: 8081 });

console.log("WebSocket server running on ws://0.0.0.0:8081");

wss.on('connection', function connection(ws) {

console.log("Client connected");

ws.on('message', function incoming(message) {

const text = message.toString();

console.log("Received:", text);

// Рассылаем всем, кроме отправителя:

wss.clients.forEach(function each(client) {

if (client !== ws && client.readyState === WebSocket.OPEN) {

client.send(text);

}

});

});

ws.on('close', function () {

console.log("Client disconnected");

});

});

Где, подключается библиотека ws — это популярная и быстрая реализация WebSocket-сервера и клиента на Node.js.

host: '0.0.0.0' — слушаются все IP-интерфейсы (локальные и внешние)
используется порт 8081 (можно и 8080).

Создадим WebSocketManager.java для управления соединением

// WebSocketManager.java

package com.example.chatghost;

import android.util.Log;

import okhttp3.OkHttpClient;

import okhttp3.Request;

import okhttp3.WebSocket;

import okhttp3.WebSocketListener;

import okio.ByteString;

public class WebSocketManager {

private WebSocket webSocket;

private final String SERVER_URL = "ws://192.168.1.78:8081";

private WebSocketCallback callback;

public WebSocketManager(WebSocketCallback callback) {

this.callback = callback;

connect(); // вызывается один раз

}

private void connect() {

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder().url(SERVER_URL).build();

webSocket = client.newWebSocket(request, new WebSocketListener() {

@Override

public void onOpen(WebSocket ws, okhttp3.Response response) {

callback.onOpen();

}

@Override

public void onMessage(WebSocket ws, String text) {

callback.onMessage(text);

}

@Override

public void onFailure(WebSocket ws, Throwable t, okhttp3.Response response) {

callback.onError(t);

}

@Override

public void onClosing(WebSocket ws, int code, String reason) {

callback.onClose();

}

});

}

public void sendMessage(String message) {

if (webSocket != null) {

webSocket.send(message);

}

}

public void close() {

if (webSocket != null) {

webSocket.close(1000, "Closing");

}

}

public interface WebSocketCallback {

void onOpen();

void onMessage(String message);

void onClose();

void onError(Throwable t);

}

}

Где, чтобы подключить okhttp3 (в проекте выбераем File-Project Structure-Depedencies-app-добавляем через «+» Library com.squareup.okhttp3:okhttp:5.0.0)

webSocket — экземпляр WebSocket-соединения.

SERVER_URL — адрес сервера, к которому нужно подключиться (в локальной сети, порт 8081).

callback — интерфейс, чтобы уведомить внешние классы (например, MainActivity) о событиях WebSocket.

@Override
public void onMessage(WebSocket ws, String text) {
callback.onMessage(text);
} // Получено текстовое сообщение от сервера — оно передаётся наружу (например, чтобы отобразить его в чате).

@Override
public void onFailure(WebSocket ws, Throwable t, okhttp3.Response response) {
callback.onError(t);
} // Ошибка соединения (например, сервер недоступен). Это может быть сеть, таймаут, неверный адрес.

Меняем ActivityMain.java (напоминаю это временно, потом создадим отдельный Java Class.

package com.example.chatghost;

import android.os.Bundle;

import android.view.View;

import android.widget.Button;

import android.widget.EditText;

import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

private WebSocketManager webSocketManager;

private TextView chatView;

private EditText inputField;

private EditText nameField;

private Button sendButton;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

chatView = findViewById(R.id.chatView);

inputField = findViewById(R.id.inputField);

nameField = findViewById(R.id.nameField);

sendButton = findViewById(R.id.sendButton);

webSocketManager = new WebSocketManager(new WebSocketManager.WebSocketCallback() {

@Override

public void onOpen() {

runOnUiThread(() -> chatView.append("Connected to chat\n"));

}

@Override

public void onMessage(String message) {

runOnUiThread(() -> chatView.append(message + "\n"));

}

@Override

public void onClose() {

runOnUiThread(() -> chatView.append("Disconnected\n"));

}

@Override

public void onError(Throwable t) {

runOnUiThread(() -> chatView.append("Error: " + t.getMessage() + "\n"));

}

});

sendButton.setOnClickListener(view -> {

String msg = inputField.getText().toString();

String name = nameField.getText().toString();

if (!msg.isEmpty() && !name.isEmpty()) {

String fullMessage = name + ": " + msg;

webSocketManager.sendMessage(fullMessage);

runOnUiThread(() -> chatView.append(fullMessage + "\n")); // своё сообщение

inputField.setText("");

}

});

}

@Override

protected void onDestroy() {

super.onDestroy();

webSocketManager.close();

}

}

Меняем activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

android:padding="16dp"

android:layout_width="match_parent"

android:layout_height="match_parent">

<TextView

android:id="@+id/chatView"

android:layout_width="match_parent"

android:layout_height="0dp"

android:layout_weight="1"

android:textSize="16sp"

android:background="#eee"

android:padding="8dp"

android:scrollbars="vertical" />

<EditText

android:id="@+id/nameField"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:hint="Введите ваше имя" />

<EditText

android:id="@+id/inputField"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:hint="Введите сообщение" />

<Button

android:id="@+id/sendButton"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:text="Отправить" />

</LinearLayout>

Создаем файл для разрешения НЕзашифрованного трафика network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>

<network-security-config>

<domain-config cleartextTrafficPermitted="true">

<domain includeSubdomains="true">192.168.1.78</domain>

</domain-config>

</network-security-config>

ВНИМАНИЕ!!!

Не забываем про главный конфигурационный файл AndroidManifest.xml, добавляем

<application

android:allowBackup="true"

android:dataExtractionRules="@xml/data_extraction_rules"

android:fullBackupContent="@xml/backup_rules"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:roundIcon="@mipmap/ic_launcher_round"

android:supportsRtl="true"

android:theme="@style/Theme.ChatGhost"

tools:targetApi="31"

android:usesCleartextTraffic="true"

android:networkSecurityConfig="@xml/network_security_config"

>

Где, android:usesCleartextTraffic=»true» //Разрешает незашифрованный трафик
android:networkSecurityConfig=»@xml/network_security_config» //только для локальных адресов

Запуск сервера

  1. Разрешаем входящие подключения, через PowerShell от имени администратора:

New-NetFirewallRule -DisplayName "Allow Node WebSocket" -Direction Inbound -LocalPort 8081 -Protocol TCP -Action Allow

  1. Устанавливаем и запускаем сервер

npm install ws

node server.js

Ссылка на статью