Поиск по этому блогу

воскресенье, 20 августа 2017 г.

Пишем socket эхо сервер и клиент



Давно не приходилось работать с сокетами, а когда приходилось как-то небыло особо сложных задач которые бы выходили за пределы того что нельзя было нагуглить. Сегодня в статье я хочу расказать про элементарнейшее из работы с сокетами, сегодня я раскажу про получение эха с сервера каждый раз когда мы отправляем туда данные. Я написал небольшой скрипт который возвращает эхо нам на запрос, и написал небольшую программку которая отправляет сообщение соккету. Дальше если захотите, можно будет сделать уже чат по этому же принципу или же какую-то работу приложения через сокеты в real-time. 


Для начала мы начнем с написания сервера. Он у нас будет написан на NodeJS и хоститься будет на Heroku. Для начала нам нужно зарегистрироваться на heroku, сделать это можно тут. Дальше после регистрации вам откроется страница помощи, на которой вам будет предложено создать первый проект в этом сервисе, если же вы случайно пропустили его то вот детальная инструкция как настроить heroku для работы с NodeJS. Вся работа с heroku, заливка, деплой и т.д. происходит один в один как работа с git. Вы создаете репозиторий на heroku и потом заливаете туда ваши файлы, и сразу после заливки оно на автомате начинает деплоить то что вы залили.

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

После инсталяции и настройки всего этого добра давайте начнем педалить. Если вы прошли туториал после регистрации, у вас должны были остаться файлы package.json, index.js и index.html. Нам они нужны, в них мы будем писать наши команды и код. Для начала давайте рассмотрим index.js и index.html. Эти два файла у нас являются мозгом и лицом нашего сервера, в index.js мы будем принимать какое-то сообщение от клиента, и отправлять ответ ему же в виде эха — то есть то же сообщение что мы прислали. В index.html я хотел выводить то, что нам присылает сервер, но потом передумал ибо это надо сильно заморачиваться и писать много лишнего кода для рассинхронизации сокетов которые принимает сервер… По этому я оставил только эхо которое будет возвращаться обратно клиенту.

index.js
var WebSocketServer = require('ws').Server
  , http = require('http')
  , express = require('express')
  , app = express()
  , port = process.env.PORT || 5000;

app.use(express.static(__dirname + '/'));

var server = http.createServer(app);
server.listen(port);

var wss = new WebSocketServer({server: server});
wss.on('connection', function(ws) {
    ws.on('message', function(message) {
        ws.send("Server received: " + message, function() {  });
    });

    ws.on('close', function() {
        console.log('websocket connection close');
    });
});

Что же у нас тут написано? Ну для начала мы подключили библиотеку ws что расшифровывается как web socket, а нужен он нам для работы с сокетами, очевидно. Дальше мы подключили библиотеку http и express для того что бы наше приложение умело работать с http и express для создания слушателя определенного порта, в нашем случае это порт 5000. 

Ниже мы создали сервер который будет обеспечивать нам эхо и прописали наш порт который мы будем прослушивать для получения сообщений. Ниже создаем сокет и указываем какой сервер будем слушать, в нашем случае это наш heroku.

Дальше идет метод on в котором мы указываем параметер который говорит что нам делать по ws.on('connection'), я прописал в нем что мы должны принимать сообщения с помощью ws.on('message') — если они есть, и отправляем эхо обратно с приставкой «Server received:», и ws.on('close') — если мы закрыли страницу или вышли из приложения, мы отключаем в нем все наши таймеры и так далее.

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

index.html
<html>
  <body>
    <h1>It's just stub for Socket Example</h1>
  </body>
</html>

Тут все тривиально, думаю даже не стоит объяснять, просто выводим текст на странице.

Ну и дальше нас интересует package.json в котором у нас указывается что компилировать, где, с какой версией и т.д. Это такой, своеобразный файл настроек сервера для компиляции.

package.json
{
  "name": "socketio",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start" : "node index.js"
  },
  "author": "inampaki",
  "license": "ISC",
  "dependencies": {
    "express": "^4.13.3",
    "express-ws": "^0.2.6",
    "socket.io": "^1.3.7"
  }
}

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

Дальше загружаем это все на хероку.
git add .
git commit -m 'some comment to commit here'
git push heroku master

Дальше вы должны увидеть долгую и длинную простыню команд которые будут деплоить ваш код на сервере, и в итоге это все должно кончится и вы должны увидеть что-то на подобии такого.

После этого всего написав в командной строке heroku open, вы должны увидеть надпись «It's just stub for Socket Example» которая у нас была прописана в index.html. Если же этого нету значит где-то какая-то ошибка, подеплойте код на компютере, с помощью команды node index.js, оно покажет вам ошибки или же скомпилирует и запустит сервер.

Дальше нам нужно написать программу, она у нас будет очень простой — один экран на нем одно поле для ввода и кнопка для отправки сообщений, и ниже текстовое поле которое будет выводить эхо с сервера.

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

app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion '25.0.0'

    defaultConfig {
        applicationId "test.socket.app"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.1.0'

    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

    compile 'com.github.nkzawa:socket.io-client:0.3.0'
}

Вот такая фигня. Библиотеки мы подключили, теперь еще надо добавить в манифест пермишен для доступа в интернет.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="test.socket.app" >

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <activity android:name=".MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Тут как видно мы добавили два пермишена, первый для доступа в интернет, а второй на проверку доступа к сети в интернет.

Дальше давайте рассмотрим разметку нашей activity_main.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:onClick="onSendClick"
        android:text="send" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="Large Text"
        android:id="@+id/textView"
        android:layout_below="@+id/button"
        android:layout_centerHorizontal="true" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editText"
        android:layout_above="@+id/button"
        android:layout_centerHorizontal="true" />

</RelativeLayout>

Как я и писал выше, у нас тут будет три элемента, edittext, textview и кнопка. Далее опишем их работу этих элементов в нашей активити.

MainActivity.java
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.TextView;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

import java.net.URI;
import java.net.URISyntaxException;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity {

    private static final String HOST = "ws://stark-caverns-76076.herokuapp.com";

    @BindView(R.id.editText)
    EditText editText;
    @BindView(R.id.textView)
    TextView textView;

    private WebSocketClient mWebSocketClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        connectWebSocket();
    }

    private void connectWebSocket() {
        try {
            URI uri = new URI(HOST);
            mWebSocketClient = new WebSocketClient(uri) {
                @Override
                public void onOpen(ServerHandshake serverHandshake) {
                    mWebSocketClient.send("Connecting from " + Build.MANUFACTURER + " " + Build.MODEL);
                }

                @Override
                public void onMessage(String s) {
                    final String message = s;
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText(message);
                        }
                    });
                }

                @Override
                public void onClose(int i, String s, boolean b) {
                    Log.i("Websocket", "Connection Closed " + s);
                }

                @Override
                public void onError(Exception e) {
                    e.printStackTrace();
                }
            };
            mWebSocketClient.connect();
        } catch (URISyntaxException e) {
            e.printStackTrace();
            return;
        }
    }

    @OnClick(R.id.button)
    public void onSendClick() {
        if(!mWebSocketClient.getConnection().isClosed()) {
            mWebSocketClient.send(editText.getText().toString());
            editText.setText("");
        } else {
            connectWebSocket();
        }
    }
}

И что же мы тут имеем. Для начала мы указываем хост к которому будем подключаться для отправки сообщений и получения эха, он у нас вот такой — ws://stark-caverns-76076.herokuapp.com, я его менять в дальнейшем не буду, так что он будет я так думаю вечный, пока хероку не отключит нас.

В onCreate() мы проинициализировали layout, указали что запускаем сокет с помощью метода connectWebSocket(), в нем мы создали путь к которому будем стучаться, дальше создаем объект класса WebSocket и указываем путь, а дальше создаем колбеки которые будут нам возвращать какие-то состояния. 

В методе onOpen() по открытию сокета мы отправляем серверу что за девайс подключился и что за система у него. 
В onMessage() мы принимаем сообщения с сервера и отображаем в textView. 
В onClose() мы просто закрываем все что у нас открыто касательно сокетов.
Ну и onError() возвращает ошибки сервера.

Дальше мы вызываем метод подключения к серверу, и воаля, у нас есть все для подключения к серверу. Осталось написать метод отправки сообщений, а он у нас ниже в клике по кнопке — onSendClick(). 

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

Исходники:

GitHub