Передача данных по сети при помощи socket при разработке android приложения

4.845 (11)

При разработке различных приложений, зачастую стоят задачи о передаче данных с мобильного приложения на сервер или другое устройство. Это могут быть разные файлы, такие как фотография или видео, данные по датчикам или же любая другая информация.  

При такой задаче необходимо реализовать какой-то механизм обмена данными. Зачастую строятся архитектуры, при которых есть определенный заранее клиент и сервер. Ведь необходимо знать, куда мы будем отправлять данные и кто их будет принимать. 

Помимо выбора, кто будет сервером, а кто клиентом, необходимо еще знать (или определить), в каком виде будут поступать данные и как их можно интерпретировать.

В этой заметке мы попытаемся построить такой механизм передачи данных и написать приложения для телефона и компьютера, которые будут выступать в качестве сервера.

Первым делом, создаем пустое android приложение и даем ему права на использование сети для передачи. Для этого необходимо в манифесте (AndroidManifest.xml) добавить нужные права. 

<uses-permission android:name="android.permission.INTERNET" />

Теперь наше приложения имеет доступ к сети. Для передачи данных будем использовать некий socket. Что же это такое?   Socket - это механизм обмена данными между процессами, при этом эти процессы могут находится на различных машинах, которые соединены в одну сеть. Для того, чтобы мы могли общаться, нам необходимо знать адрес компьютера, на котором запущен процесс, и номер порта, по которому будет происходить обмен.
Создадим класс LaptopServer, в котором будем описывать нашу логику подключения. И сразу добавим в него поля для имени сервера и номера порта, а также самого сокета, через который мы будет производить общение:

	
public class LaptopServer {

   private static final String LOG_TAG = "myServerApp";

   // ip адрес сервера, который принимает соединения
   private String mServerName = "192.168.0.10";

   // номер порта, на который сервер принимает соединения 
   private int mServerPort = 6789;

   // сокет, через которий приложения общается с сервером
   private Socket mSocket = null;

   public LaptopServer() {}

}	

Для минимальной работы нам необходимо хотя бы три метода: открыть соединение, отправить данные и закрыть соединение.
Когда мы будем открывать соединения, мы должны убедится, что ранее не был открыт сокет, дабы не засорять ресурсы, и если он открыт - закрыть его.

/**
 * Открытие нового соединения. Если сокет уже открыт, то он закрывается.
 *
 * @throws Exception
 *      Если не удалось открыть сокет
 */
public void openConnection() throws Exception {

    /* Освобождаем ресурсы */
    closeConnection();

    try {
        /*
            Создаем новый сокет. Указываем на каком компютере и порту запущен наш процесс,
            который будет принамать наше соединение.
        */
        mSocket = new Socket(mServerName,mServerPort);

    } catch (IOException e) {
        throw new Exception("Невозможно создать сокет: "+e.getMessage());
    }
}	

Для закрытия соединения, у сокета необходимо вызвать метод close():

/**
 * Метод для закрытия сокета, по которому мы общались.
 */ 
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;
}	

Отправка данных происходит через OutputStream. Его можно обвернуть в различные обвертки, а можно и напрямую у сокета вызвать getOutputStream() метод write():

/**
 * Метод для отправки данных по сокету.
 *
 * @param data
 *          Данные, которые будут отправлены
 * @throws Exception
 *          Если невозможно отправить данные
 */
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());
    }
}	

Также нам необходимо переопределить метод finalize() и освободить ресурс: 

@Override
protected void finalize() throws Throwable {
    super.finalize();
    closeConnection();
}	

Отлично! У нас есть основа для передачи данных по сокету. Давайте теперь сделаем три кнопки и повесим на них обработку, для четкого понимания действий, которые необходимо выполнить. Этими кнопками будут «open», «send» и «close»
Для их создания откроем наш layout файл activity_main.xml:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" 
    tools:context=".MainActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/button_open_connection"
            android:layout_margin="10dp"
            android:text="open"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/button_send_connection"
            android:layout_margin="10dp"
            android:text="send"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/button_close_connection"
            android:layout_margin="10dp"
            android:text="close"/>
    </LinearLayout>
</RelativeLayout>	

Перейдем теперь к нашему Activity и создадим обработчики на наши кнопки. Добавим также маленькую особенность: при старте приложения будет активна только одна кнопка - «open», а две остальные - нет. Дальше увидите, зачем это сделано.

private Button mButtonOpen  = null;
private Button mButtonSend  = null;
private Button mButtonClose = null;
private LaptopServer mServer = null;	

В методе onCreate() добавляем следующее:

mButtonOpen = (Button) findViewById(R.id.button_open_connection);
mButtonSend = (Button) findViewById(R.id.button_send_connection);
mButtonClose = (Button) findViewById(R.id.button_close_connection);
mButtonSend.setEnabled(false);
mButtonClose.setEnabled(false);	

Теперь нам необходимо создать обработчик и повесить его на кнопку. Первым делом нужно открыть соединения с сервером. Для того, чтобы у нас все подключалось, вынесем подключения в отдельный поток. Но у нас есть маленький нюанс: две неактивные кнопки «send» и «close», и нам необходимо их активировать, когда у нас появится соединение с сервером. Но мы сейчас находимся в стороннем потоке от ui, а как известно обновлять интерфейс можно только в ui потоке, и поэтому нам необходимо использовать метод runOnUiThread(), для того, чтобы мы могли обновить элементы в нашем интерфейсе. 

mButtonOpen.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        /* создаем объект для работы с сервером*/
        mServer = new LaptopServer();
        /* Открываем соединение. Открытие должно происходить в отдельном потоке от ui */
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mServer.openConnection();
                    /*
                        устанавливаем активные кнопки для отправки данных
                        и закрытия соедиения. Все данные по обновлению интерфеса должны
                        обрабатывается в Ui потоке, а так как мы сейчас находимся в
                        отдельном потоке, нам необходимо вызвать метод  runOnUiThread()
                    */
                    runOnUiThread(new Runnable() {

                        @Override
                        public void run() {
                            mButtonSend.setEnabled(true);
                            mButtonClose.setEnabled(true);
                        }
                    });
                } catch (Exception e) {
                    Log.e(LOG_TAG, e.getMessage());
                    mServer = null;
                }
            }
        }).start();
    }
});	

Отправку данных также нужно вынести в отдельный поток. Для примера, будем просто отправлять текстовую строку.

mButtonSend.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (mServer == null) {
            Log.e(LOG_TAG, "Сервер не создан");
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    /* отправляем на сервер данные */
                    mServer.sendData("Send text to server".getBytes());
                } catch (Exception e) {
                    Log.e(LOG_TAG, e.getMessage());
                }
            }
        }).start();
    }
});	

И остается последняя кнопка для закрытия соединения:

mButtonClose.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
        /* Закрываем соединение */
        mServer.closeConnection();
        /* устанавливаем неактивными кнопки отправки и закрытия */
        mButtonSend.setEnabled(false);
        mButtonClose.setEnabled(false);
    }
});

На этом наш клиент готов, при запуске мы увидим наши три кнопочки. Однако работать он не будет, поскольку у нас еще нет сервера. Если мы нажмем «open»  - то увидим exception в logcat о том, что невозможно подсоединится к серверу.
Невозможно создать сокет: failed to connect to /192.168.0.10 (port 6789): connect failed: ECONNREFUSED (Connection refused)

Теперь нам необходимо написать наш сервер. Его задача будет заключаться в том, чтобы слушать определенный порт на сетевом интерфейсе и обрабатывать входящие соединения. Каждое из них необходимо обрабатывать в новом потоке, чтобы наш сервер мог спокойно реагировать на остальные входящие соединения. 
Для создания проекта мы будем использовать eclipse. Создадим пустой java проект и добавим в него пару классов.
Создадим те, которые будут реализовывать логику получения новых соединений:

public class Server implements Runnable {    

    /* 
     * Реалезация шаблона Singleton
     * {@link https://en.wikipedia.org/wiki/Singleton_pattern}      
     */
    private static volatile Server instane = null;    

    /* Порт, на который сервер принимает соеденения */
    private final int SERVER_PORT = 6789;   

    /* Сокет, который обрабатывает соединения на сервере */
    private ServerSocket serverSoket = 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 {
            /* Создаем серверный сокет, которые принимает соединения */
            serverSoket = new ServerSocket(SERVER_PORT);
            System.out.println("Start server on port: "+SERVER_PORT);            

            /*
             * старт приема соединений на сервер
             */
            while(true) {

                ConnectionWorker worker = null;               

                try {
                    /* ждем нового соединения  */
                    worker = new ConnectionWorker(serverSoket.accept());
                    System.out.println("Get client connection");                    

                    /* создается новый поток, в котором обрабатывается соединение */
                    Thread t = new Thread(worker);
                    t.start(); 

                } catch (Exception e) {
                    System.out.println("Connection error: "+e.getMessage());
                }  
            }
        } catch (IOException e) {
            System.out.println("Cant start server on port "+SERVER_PORT+":"+e.getMessage());
        } finally {
            /* Закрываем соединение */
            if (serverSoket != null) {
                try {
                    serverSoket.close();
                } catch (IOException e) {
                }
            }
        }
    }
}	

Давайте рассмотрим, что в этом классе реализовано. Основной метод - это run(), в котором запускается обработка новых соединений. Создается сокет, который в дальнейшем «отдает» новые соединения. 

Принимающие соединения обрабатываются в бесконечном цикле. Цикл блокируется на моменте 

worker = new ConnectionWorker(serverSoket.accept());	

и продолжает работать, когда будет новое соединение. После этого создается новый поток и ему передается обработка нового соединения.

Thread t = new Thread(worker);
t.start();	

Все последующие обработки уже совершаются в классе ConnectionWorker. На него положена задача обрабатывать входной поток данных с сокета.

public class ConnectionWorker implements Runnable {
    /* сокет, через который происходит обмен данными с клиентом*/
    private Socket clientSocket = null;   

    /* входной поток, через который получаем данные с сокета */
    private InputStream inputStream = null;   

    public ConnectionWorker(Socket socket) {
        clientSocket = socket;
    }    

    @Override
    public void run() {        

        /* получаем входной поток */
        try {
            inputStream = clientSocket.getInputStream();        
        } catch (IOException e) {
            System.out.println("Cant get input stream");
        }

        /* создаем буфер для данных */
        byte[] buffer = new byte[1024*4];       

        while(true) {

            try {
                /* 
                 * получаем очередную порцию данных
                 * в переменной count хранится реальное количество байт, которое получили
                 */
                int count = inputStream.read(buffer,0,buffer.length);

                /* проверяем, какое количество байт к нам прийшло */
                if (count > 0) {
                    System.out.println(new String(buffer,0,count));
                } else 
                    /* если мы получили -1, значит прервался наш поток с данными  */
                    if (count == -1 ) {
                        System.out.println("close socket");
                        clientSocket.close();  
                        break;
                    }
            } catch (IOException e) {
                System.out.println(e.getMessage()); 
            }
        } 
    }
}	

Теперь нам необходимо создать нашу main функцию, в которой будет запускаться наш сервер:

public class MainApp {
    public static void main(String[] args) {
        Server server = Server.getServer();
        Thread t = new Thread(server);
        t.start();
    }
}	

Нам остается запустить сервер и клиент. Убедитесь в том, что сервер и телефон подключены к одной сети.
Запускаем сервер, запускаем приложения на телефоне - и можем спокойно подключатся и отправлять данные.  

 

См. так же разработка Андроид приложений для IoT.

Комментарий (1)

Антон

04 Июня 2017 в 01:13

Здраввствуйте! Спасибо за замечательную статью! Целый день искал что-то подобное! Но возник вопрос, как в данном случае сравнивать данные с БД? Если у меня, например, на андроиде есть форма авторизации, я ее заполняю, отправляю данные на сервер, а он в свою очередь сравнивает эти данные в БД и, если логин-пароль совпадают, отображается второй Активити, если нет - говорится, что пользователь не найден. Кто-то упоминал о использовании Веб-сокетов. Какое принципиальное отличие их от вашего кода? Спасибо!

23/ 7
ответить

Войдите с помощью соцсетей:
или
введите свои данные: