Вступление
Уважаемые Хабрахабровцы, хочу поделиться с Вами своей разработкой для OS Android.
Данная статья ориентирована, во-первых, на новичков в андроид-разработке, во-вторых, на людей, которым интересна идея о безопасности общения по сети, в-третьих, просто на тех кому интересно.
Суть
Моя цель написать мессенджер, который позволил бы, в коей мере, уйти от всемирной слежки. Уйти? — спросите Вы. Да именно так я представляю себе, мое творение. Потому как общение клиента с сервером реализуется на сокетах, с применением ГОСТ-товского шифрования «МАГМА» (блочный симметричный).
Программный комплекс (назовем его комплексом, ибо он состоит из двух модулей, написанных собственными руками) имеет в своем составе следующие компоненты: клиентская часть, написанная в AndroidStudio и серверная часть, написанная в IntelliJ IDEA. Клиентов мы распространяем доступными нам способами: передачей APK по BlueTooth, WatsApp, PlayMarket, да и вообще как душа пожелает и как удобно Вашей аудитории. Сервер запускаем на своем ПК, можно конечно и арендовать какой-нибудь сторонний сервер, все равно данные там хранить мы не будем. Клиенты регистрируются, авторизуются и готовы для массовых переписок. (в перспективе реализую и индивидуальное общение ТЕТ-А-ТЕТ, а также шифрование).
Недоработки
Само собой, существует огромный список вещей, которые еще нужно доделать: нативный приятный дизайн, возможность обмена не только текстовыми сообщениями, но и фото, шифрование, дабы данные пользователей не передавались в открытом виде, уведомления, ну в общем много чего. Но моя основная мысль этой публикации — поделиться с Вами, дорогие мои, идеей о возможности практически абсолютно инкогнитого общения друг с другом.
Хватит лирики, перейдем к кодингу
Комментарии в коде писал для себя, поэтому думаю все будет понятно и по ним.
Начнем с сервера:
1. класс описания самого «ядра» сервера
package sample;
import java.io.IOException;
import java.net.*;
public class Server implements Runnable {
private static volatile Server instane = null;
//порт, который принимает соединения
private final int SERVER_PORT = 4444;
//создание сокета, который обрабатывает соединения на сервере
private ServerSocket serverSocket = null;
//пустой конструктор класса
private Server() {
}
public static Server getServer() {
if (instane == null) {
synchronized (Server.class) {
if (instane == null) {
instane = new Server();
}
}
}
return instane;
}
@Override
//метод, в котором запускается обработка новых соединений
public void run(){
try {
//создание серверного сокета, который принимает соединения
serverSocket = new ServerSocket(SERVER_PORT);
System.out.println("Сервер запущен на порту: " + SERVER_PORT);
//старт приема соединений на сервер
//в бесконечном цикле, который блокируется на
//моменте: worker = new ConnectionWorker(serverSocket.accept());
//и продолжает работать, когда будет новое соединение
//после создается новый поток, и ему передается обработка нового
//соединения: Thread t = new Thread(worker);
// t.start();
while (true){
ConnectionWorker worker = null;
try {
//ожидание нового соединения
worker = new ConnectionWorker(serverSocket.accept());
System.out.println("___________________");
System.out.println("Клиент подключился!");
//создание нового потока, в котором обрабатывается соединение
Thread t = new Thread(worker);
t.start();
}catch (Exception e){
System.out.println("Ошибка соединения: " + e.getMessage());
}
}
}catch (IOException e){
System.out.println("Невозможно запустить сервер на порту: " + SERVER_PORT + ":" + e.getMessage());
}finally {
//закрываем соединение
if(serverSocket != null){
try {
serverSocket.close();
}catch (IOException e){
}
}
}
}
}
2. класс описания работы со входящими/выходящими данными
package sample;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import java.io.IOException;
import java.io.*;
import java.net.Socket;
//этот класс обрабатывает входной поток данных с сокета
public class ConnectionWorker implements Runnable {
//сокет, через который происходит обмен данными с клиентом
private Socket clientSocket = null;
//входной поток, через который получаем данные с сокета (от клиента)
private InputStream inputStream = null;
//выходной поток, через который отдаем данные сокету (клиенту)
private OutputStream outputStream = null;
//конструктор класса,который принимает парамент типа Socket
public ConnectionWorker(Socket socket) {
clientSocket = socket;
}
@Override
public void run() {
try {
//получаем входной поток данных
inputStream = clientSocket.getInputStream();
outputStream = clientSocket.getOutputStream();
} catch (IOException e) {
System.out.println("Невозможно получить входящие данные!");
}
//создание буффера для данных
byte[] buffer = new byte[1024 * 4];
while (true) {
try {
//получение очередной порции данных
//в переменной count хранится реальное
//колличество полученных байт
int count = inputStream.read(buffer, 0, buffer.length);
//проверяем, какое колличество байт пришло
//выводим в консоль принятые данные
if (count > 0) {
//отправка через порт всем клиентам полученных данных
sendData((new String(buffer, 0, count)).getBytes());
//вывод на консоль полученных данных
System.out.println(new String(buffer, 0, count));
} else
//если получили -1, то поток с данными прервался
//закрываем сокет
if (count == -1) {
System.out.println("Клиент отключился!");
System.out.println("__________________");
//System.out.println("Close socket n" + "_________________________________");
//clientSocket.close();
break;
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
//функция отправки данных клиетам//////////////////
public void sendData(byte[] data) {
try {
outputStream.write(data);
outputStream.flush();
} catch (IOException e) {
}
}
}
3. главный класс — точка старта сервера
package sample;
//обработка соединений проходит в кажном новом потоке для
//того чтобы сервер мог обрабатывать каждое новое входящее соединение
public class MainApp {
//фуекция запуска сервера
public static void main(String[] args) {
Server server = Server.getServer();
Thread t = new Thread(server);
t.start();
}
}
Запускаем сервер, он слушаем порт, ждем подключений, информирует если таковые есть и передает потоки данных.
Далее, клиент, код фронт-энда показывать не буду, что бы не нагромождать, суть итак ясна будет:
1. первое что нужно — это регистрация (локальная, конечно, мы ведь за конфиденциальность).
Для этого нам нужна БД, которая хранит наши входные данные
package info.fandroid.sqlite;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
class DBHelper extends SQLiteOpenHelper {
final String LOG_TAG = "myLogs";
public DBHelper(Context context) {
// конструктор суперкласса
super(context, "logpassDB", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d(LOG_TAG, "--- onCreate database ---");
// создаем таблицу с полями
db.execSQL("create table tablelogpass ("
+ "id integer primary key autoincrement,"
+ "login text,"
+ "password text" + ");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
2. собственной персоной сама активити, реализующая процесс регистрации
package info.fandroid.sqlite;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
public class MainActivity extends Activity{
EditText userLogin, userPassword;
DBHelper dbHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//находим поля ввода логина и парля
userLogin = (EditText) findViewById(R.id.userLogin);
userPassword = (EditText) findViewById(R.id.userPassword);
// создаем объект для создания и управления версиями БД
dbHelper = new DBHelper(this);
///////проверка на наличие логина и пароля в БД, если да, то запуск активити авторизации/////
// подключаемся к БД
SQLiteDatabase db = dbHelper.getWritableDatabase();
// делаем запрос всех данных из таблицы mytable, получаем Cursor
Cursor c = db.query("tablelogpass", null, null, null, null, null, null);
// ставим позицию курсора на первую строку выборки
// если в выборке нет строк, вернется false
if (c.moveToFirst()) {
// определяем номера столбцов по имени в выборке
int nameColIndex = c.getColumnIndex("login");
int emailColIndex = c.getColumnIndex("password");
do {
// получаем значения по номерам столбцов и пишем все в лог
String checkLogin = c.getString(nameColIndex);
String checkPass = c.getString(emailColIndex);
if(checkLogin != null && checkPass != null){
Intent intent = new Intent(this, Main2Activity.class);
startActivity(intent);
finish();
}else {
}
// переход на следующую строку
// а если следующей нет (текущая - последняя), то false - выходим из цикла
} while (c.moveToNext());
}
//освобождаем ресурсы, занятые курсором
c.close();
//закрываем подключение к БД
dbHelper.close();
}
//////////функция регистрации логина и пароля/////////////////////////////////
public void add(View view) {
// создаем объект для данных
ContentValues cv = new ContentValues();
// получаем данные из полей ввода
String name = userLogin.getText().toString();
String email = userPassword.getText().toString();
// подключаемся к БД
SQLiteDatabase db = dbHelper.getWritableDatabase();
// подготовим данные для вставки в виде пар: наименование столбца - значение
cv.put("login", name);
cv.put("password", email);
// вставляем запись и получаем ее ID
db.insert("tablelogpass", null, cv);
Intent intent = new Intent(this, Main2Activity.class);
startActivity(intent);
finish();
}
}
3. после регистрации… правильно — авторизация
package info.fandroid.sqlite;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
public class Main2Activity extends AppCompatActivity {
EditText userLogin, userPassword;
TextView textView;
DBHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
///находим поля ввода логина и пароля
userLogin = (EditText) findViewById(R.id.userLogin);
userPassword = (EditText) findViewById(R.id.userPassword);
textView = (TextView) findViewById(R.id.textView);
// создаем объект для создания и управления версиями БД
dbHelper = new DBHelper(this);
/////////поток для установки в строку логина пользовательского логина/////////
new Thread(new Runnable() {
@Override
public void run() {
// подключаемся к БД
SQLiteDatabase db = dbHelper.getWritableDatabase();
// делаем запрос всех данных из таблицы mytable, получаем Cursor
Cursor c = db.query("tablelogpass", null, null, null, null, null, null);
// ставим позицию курсора на первую строку выборки
// если в выборке нет строк, вернется false
if (c.moveToFirst()) {
// определяем номера столбцов по имени в выборке
int nameColIndex = c.getColumnIndex("login");
do {
// получаем значения по номерам столбцов и пишем все в лог
String checkLogin = c.getString(nameColIndex);
userLogin.setText("Ваш логин: " + checkLogin);
userLogin.setEnabled(false);
// переход на следующую строку
// а если следующей нет (текущая - последняя), то false - выходим из цикла
} while (c.moveToNext());
}
//освобождаем ресурсы, занятые курсором
c.close();
//закрываем подключение к БД
dbHelper.close();
}
}).start();
}
///////функция проверки введенных логина и пароля/////////////////////////////
public void read(View view){
// подключаемся к БД
SQLiteDatabase db = dbHelper.getWritableDatabase();
// делаем запрос всех данных из таблицы mytable, получаем Cursor
Cursor c = db.query("tablelogpass", null, null, null, null, null, null);
// ставим позицию курсора на первую строку выборки
// если в выборке нет строк, вернется false
if (c.moveToFirst()) {
// определяем номера столбцов по имени в выборке
int emailColIndex = c.getColumnIndex("password");
do {
// получаем значения по номерам столбцов и пишем все в лог
String checkPass = c.getString(emailColIndex);
String pass = userPassword.getText().toString();
if(checkPass.equals(pass)){
textView.setText("Добро пожаловать!");
Intent intent = new Intent(this, Main3Activity.class);
startActivity(intent);
finish();
}else {
textView.setText("Вы ввели неверные данные!");
}
// переход на следующую строку
// а если следующей нет (текущая - последняя), то false - выходим из цикла
} while (c.moveToNext());
}
//освобождаем ресурсы, занятые курсором
c.close();
//закрываем подключение к БД
dbHelper.close();
}
}
4. серверная часть клиента
package info.fandroid.sqlite;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
//класс описания логики подключения///////////////////////////////////
public class LaptopServer {
private static final String LOG_TAG = "myServerApp";
//ip-адрес сервера, который принимает соединения
private String mServerName = "192.168.43.136";
//порт, который принимает соединения
private int mServerPort = 4444;
//сокет, через который приложения общаются с сервером
private Socket mSocket = null;
//пустой конструктор красса
public LaptopServer(){
}
//функция открытия нового соединения, если сокет уже открыт, то закрываем его/
public void openConnection() throws Exception{
//освобождаем ресурсы: закрываем сокет
closeConnection();
try{
//создание нового сокета, с указанием адреса сервера, и порта процесса
mSocket = new Socket(mServerName, mServerPort);
}catch (IOException e){
throw new Exception("Невозможно создать сокет: " + e.getMessage());
}
}
//функция отправления данных по сокету/////////////////////////////////////////
//переменная data - данные, которые отправляем/////////////////////////////////
public void sendData(byte[] data) throws Exception{
if(mSocket == null || mSocket.isClosed()){
throw new Exception("Невозможно отправить данные. Сокет не создан или закрыт");
}
try {
//отправка данных
mSocket.getOutputStream().write(data);
mSocket.getOutputStream().flush();
}catch (IOException e){
throw new Exception("Невозможно отправить данные: " + e.getMessage());
}
}
//функция закрытия соединения//////////////////////////////////////////////////
public void closeConnection(){
//проверяем сокет, если не закрыт, то закрываем его
if (mSocket != null && !mSocket.isClosed()){
try {
mSocket.close();
}catch (IOException e){
Log.e(LOG_TAG, "Невозможно закрыть сокет: " + e.getMessage());
}finally {
mSocket = null;
}
}
mSocket = null;
}
//функция получения сообщений от других клиентов////////////////////////////////
public void getData() throws IOException {
//создание буффера для данных
byte[] buffer = new byte[1024 * 4];
while (true){
//получаем входящие сообщения
try {
int count = mSocket.getInputStream().read(buffer,0,buffer.length);
String enterMessage = new String(buffer,0,count);
Main3Activity ma = new Main3Activity();
if(count > 1){
ma.adapter.add(enterMessage);
ma.adapter.notifyDataSetChanged();
}else {
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
//переопределяем метод finalize и освобождаем ресурсы
protected void finalize() throws Throwable {
super.finalize();
closeConnection();
}
}
5. реализация общения с сервером
package info.fandroid.sqlite;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
public class Main3Activity extends AppCompatActivity {
///////////////////переменные клиента////////////////////////////////////
private Button mButtonSend = null;
private Button mButtonRemove = null;
private EditText editMessage = null;
private LaptopServer mServer = null;//экземпляр класса LaptopServer
String message = "";
String name;
DBHelper dbHelper;
//////////////////////переменные list-view///////////////////////////////
ArrayList<String> messages = new ArrayList();
ArrayList<String> selectedMessages = new ArrayList();
ArrayAdapter<String> adapter;
ListView messagesList;
//private EditText editMessage = null; // уже есть в переменных клиента
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
mServer = new LaptopServer();
///////////////////list view/////////////////////////////////////////////
messagesList = (ListView) findViewById(R.id.messagesList);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_multiple_choice, messages);
messagesList.setAdapter(adapter);
// обработка установки и снятия отметки в списке
messagesList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
// получаем нажатый элемент
String msg = adapter.getItem(position);
if (messagesList.isItemChecked(position) == true) {
selectedMessages.add(msg);
} else selectedMessages.remove(msg);
}
});
// создаем объект для создания и управления версиями БД
dbHelper = new DBHelper(this);
/////////поток для установки в строку логина пользовательского логина/////////
new Thread(new Runnable() {
@Override
public void run() {
// подключаемся к БД
SQLiteDatabase db = dbHelper.getWritableDatabase();
// делаем запрос всех данных из таблицы mytable, получаем Cursor
Cursor c = db.query("tablelogpass", null, null, null, null, null, null);
// ставим позицию курсора на первую строку выборки
// если в выборке нет строк, вернется false
if (c.moveToFirst()) {
// определяем номера столбцов по имени в выборке
int nameColIndex = c.getColumnIndex("login");
do {
// получаем значения по номерам столбцов и пишем все в лог
name = c.getString(nameColIndex);
// переход на следующую строку
// а если следующей нет (текущая - последняя), то false - выходим из цикла
} while (c.moveToNext());
}
//освобождаем ресурсы, занятые курсором
c.close();
//закрываем подключение к БД
dbHelper.close();
}
}).start();
//поиск кнопок и EditText-ов для client socket///////////////////////////
mButtonSend = (Button) findViewById(R.id.button_send);
mButtonRemove = (Button) findViewById(R.id.button_remove);
editMessage = (EditText) findViewById(R.id.editMessage);
/////поток подключения к серверу и установки кнопок в нужное состояние/////////
new Thread(new Runnable() {
@Override
public void run() {
try {
mServer.openConnection();
//устанавливаем активные кнопки для отправки данных, закрытия соединения и т.д.
//все данные по обновлению интерфейса должны обработыватся в отдельном UI-потоке,
//т.к. мы сейчас в отдельном потоке, необходимо вызвать метод runOnUiThread()
runOnUiThread(new Runnable() {
@Override
public void run() {
mButtonSend.setEnabled(true);
mButtonRemove.setEnabled(true);
editMessage.setEnabled(true);
}
});
} catch (Exception e) {
mServer = null;
}
}
}).start();
//кнопка для отправки данных///////////////////////////////////////////////////
mButtonSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//получам текст из TextView-ов для имени и сообщения
message = editMessage.getText().toString();
Toast.makeText(getApplicationContext(), "Отправлено: " + message, Toast.LENGTH_LONG).show();
/////////////////////для list view///////////////////////////////////////
adapter.add("Я: " + message);
editMessage.setText("");
adapter.notifyDataSetChanged();
/////////////////////////////////////////////////////////////////////////
new Thread(new Runnable() {
@Override
public void run() {
try {
//отправляем данные на сервер
mServer.sendData((name + " : " + message).getBytes());
} catch (Exception e) {
}
}
}).start();
}
});
//кнопка для удаления данных из ListView///////////////////////////////////////
mButtonRemove.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//получаем и удаляем выделенные элементы
for (int i = 0; i < selectedMessages.size(); i++) {
adapter.remove(selectedMessages.get(i));
}
// снимаем все ранее установленные отметки
messagesList.clearChoices();
// очищаем массив выбраных объектов
selectedMessages.clear();
adapter.notifyDataSetChanged();
new Thread(new Runnable() {
@Override
public void run() {
try {
mServer.sendData((name + " удалил: " + selectedMessages).getBytes());
} catch (Exception e) {
}
}
}).start();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
mServer.closeConnection();
}
}
Дабы не быть голословным. Ниже приведены результаты работы!
1. Здесь мы видим запущенный сервер, который ждет клиентов:
"
2. Собственно, окно регистрации: пользователь придумывает себе логин и пароль, дальше вход в приложение только по этим данным:
3. В случае если клиент ввел данные неправильно, не страшно, можно попробовать еще:
4. В случае правильно введенных данных после авторизации попадаем на активити массовой переписки. Пишем сообщение, видим что сервер его получил:
5. И так далее, переписываемся, при необходимости можем отметить ненужные сообщения и удалить их:
Заключение
Вот собственно, друзья мои, данным проектом я занимаюсь в настоящее время. Конечно он еще очень «сырой», но думаю еще пару месяцев и все будет готово. Что можно сказать о перспективе… Штука на мой взгляд интересная, и даже может быть очень полезной, в зависимости от фантазии…
Автор: Пупсень и Вупсень