Pull to refresh

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

Вступление


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



Заключение


Вот собственно, друзья мои, данным проектом я занимаюсь в настоящее время. Конечно он еще очень «сырой», но думаю еще пару месяцев и все будет готово. Что можно сказать о перспективе… Штука на мой взгляд интересная, и даже может быть очень полезной, в зависимости от фантазии…
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.