Мобильный мессенджер обмена мгновенными сообщениями

Вступление


Уважаемые Хабрахабровцы, хочу поделиться с Вами своей разработкой для 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. И так далее, переписываемся, при необходимости можем отметить ненужные сообщения и удалить их:



Заключение


Вот собственно, друзья мои, данным проектом я занимаюсь в настоящее время. Конечно он еще очень «сырой», но думаю еще пару месяцев и все будет готово. Что можно сказать о перспективе… Штука на мой взгляд интересная, и даже может быть очень полезной, в зависимости от фантазии…
Метки:
android studio, java, socket