В рамках проекта ChatGhost была разработана минималистичная система обмена сообщениями между несколькими клиентами Android, использующая протокол WebSocket. Главной особенностью решения стало отсутствие зависимости от сторонних облачных решений или сервисов, таких как Firebase или Telegram Bot API. Вся система работает в локальной сети, что обеспечивает полный контроль над данными, простоту настройки и высокую скорость передачи сообщений. Реализованно на базе Почтового приложения (временно находиться в MainActivity.java, в будущем будет добавлена отдельная кнопка для чата в режиме реального времени).
Что нужно для работы:
- Android Studio, OkHttp WebSocket API
- Node.js + WebSocket-библиотека ws
- Локальная 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» //только для локальных адресов
Запуск сервера
- Разрешаем входящие подключения, через PowerShell от имени администратора:
New-NetFirewallRule -DisplayName "Allow Node WebSocket" -Direction Inbound -LocalPort 8081 -Protocol TCP -Action Allow
- Устанавливаем и запускаем сервер
npm install ws
node server.js